WPF double gradient for color picker - wpf

My goal is to implement a custom color picker. I don't want to use existing implementations such as the xceed one, among other reasons because I'm working in a RGBA FP32^4 space. I know that WPF works only in a int8^4 space for display, but the coordinates I'm working with are in FP32^4 space. It will interop with a DirectX12 asset with 10 bits display.
One thing I need is to implement the double gradient luminosity vs saturation graph, from the {Hue Saturation Luminosity} space.
I could not figure out how to to this with double gradients in a Rectangle; so I thought that one way to do this is to have a single gradient eg.,
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="{Binding HueColor}" Offset="1"/>
<GradientStop Color="#00000000" Offset="0"/>
</LinearGradientBrush>
</Rectangle.Fill>
then add an horizontal saturation gradient filter that would overlap.
Using an opacity mask is not helping here:
<Rectangle.OpacityMask>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="#00000000" Offset="0"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.OpacityMask>
I could not find a way to achieve this in WPF. I wanted to avoid using cuda or directx12, that would be kind of overkill, even though accurate.

I had to implement manually the bitmap creation with 2 classes.
public class HSLfloat
{
public float H { get; set; }
public float S { get; set; }
public float L { get; set; }
public HSLfloat(RGBAfloat rgbafloat)
{
var tol = 0.000001;
var rgb = rgbafloat.ToArrayRGB();
var rgbTuple = rgbafloat.ToArrayTuple();
var Cmax = rgb.Max();
var Cmin = rgb.Min();
var delta = Cmax - Cmin;
L = delta / 2;
var s = Math.Abs(delta) < tol ? 0 : delta / (1 - Math.Abs(2 * L - 1));
var CmaxName = rgbTuple.Where(o => Math.Abs(o.Item1 - Cmax) < tol).Select(o => o.Item2).First();
if (Math.Abs(delta) < tol)
H = 0;
else
{
switch (CmaxName)
{
case 'R':
H = 60 * ((rgbafloat.G - rgbafloat.B) / delta % 6);
break;
case 'G':
H = 60 * ((rgbafloat.B - rgbafloat.R) / delta + 2);
break;
case 'B':
H = 60 * ((rgbafloat.R - rgbafloat.G) / delta + 4);
break;
}
}
}
public HSLfloat(float h, float s, float l)
{
H = h;
S = s;
L = l;
}
public RGBAfloat ToRGBAfloat() => new RGBAfloat(this);
public (float H, float S, float L) ToTuple() => (H, S, L);
}
and for RGBA space:
public class RGBAfloat
{
public float R { get; set; }
public float G { get; set; }
public float B { get; set; }
public float A { get; set; }
public RGBAfloat()
{
}
public RGBAfloat(double r, double g, double b, double a = 1d)
{
R = (float)r;
G = (float)g;
B = (float)b;
A = (float)a;
}
public RGBAfloat(float r, float g, float b, float a= 1f)
{
R = r;
G = g;
B = b;
A = a;
}
public RGBAfloat(RGBAfloatStruct colorFloatStruct)
{
R = colorFloatStruct.R;
G = colorFloatStruct.G;
B = colorFloatStruct.B;
A = colorFloatStruct.A;
}
public RGBAfloat(HSLfloat hslfloat)
{
var c = (1 - Math.Abs(2 * hslfloat.L - 1)) * hslfloat.S;
var x = c * (1 - Math.Abs((hslfloat.H / 60) % 2 - 1));
var m = hslfloat.L - c / 2;
var quadrant = (int)(hslfloat.H % 360 / 60); // [0-5]
switch (quadrant)
{
default:
case 0:
R = c + m;
G = x + m;
B = 0f + m;
break;
case 1:
R = x + m;
G = c + m;
B = 0f + m;
break;
case 2:
R = 0f + m;
G = c + m;
B = x + m;
break;
case 3:
R = 0f + m;
G = x + m;
B = c + m;
break;
case 4:
R = x + m;
G = 0f + m;
B = c + m;
break;
case 5:
R = c + m;
G = 0f + m;
B = x + m;
break;
}
}
public RGBAfloatStruct ToRGBAFloatStruct() => new RGBAfloatStruct {A = A, R = R, G = G, B = B};
public float[] ToArrayRGB() => new[] {R, G, B};
public float[] ToArray() => new[] {R, G, B, A};
public (float,char)[] ToArrayTuple() => new[] { (R, 'R'), (G, 'G'), (B, 'B') };
public (float R, float G, float B) ToTupleRGB() => (R, G, B);
public (float R, float G, float B, float A) ToTuple() => (R, G, B, A);
public HSLfloat ToHSLfloat() => new HSLfloat(this);
public int ToRGBAint()
{
var rr = (int)(R * 255);
var gg = (int)(G * 255);
var bb = (int)(B * 255);
int color = rr << 16;
color |= gg << 8;
color |= bb << 0;
return color;
}
public WriteableBitmap ToWriteableBitmap(int size = 200)
{
var (h, s, l) = ToHSLfloat().ToTuple();
var writeableBitmap = new WriteableBitmap(size, size, 96, 96, PixelFormats.Bgr32, null);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
s = (float)x / size;
l = (float)(size - y) / size;
var intColor = new HSLfloat(h, s, l).ToRGBAfloat().ToRGBAint();
unsafe
{
var ptr = writeableBitmap.BackBuffer;
ptr += y * writeableBitmap.BackBufferStride;
ptr += x * 4;
*((IntPtr*)ptr) = (IntPtr)intColor;
}
}
return writeableBitmap;
}
}
Result, SL map with (r,g,b)= (0.8, 0.5, 0.3):

Try using a visual brush. You can bind your F32 Hue color and use a converter to convert between color spaces (as you can see in line 11). You might even get rid of the visual brush and use the linear gradient and opacity mask directly in your rectangle, though I didn't try that myself.
<Rectangle Width="200" Height="200">
<Rectangle.Fill>
<VisualBrush TileMode="None">
<VisualBrush.Visual>
<Canvas Background="Black" Width="1" Height="1" SnapsToDevicePixels="True">
<Rectangle Width="1" Height="1" SnapsToDevicePixels="True">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0" />
<GradientStop Color="{Binding HueColor, Converter={StaticResource color4Color}}" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#00FFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.OpacityMask>
</Rectangle>
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>

Related

Why is the Binding of the Point3DCollection not working?

Hey there,
the generation of my Point3DCollection works fine and i get the Mesh in the Viewport3D (https://imgur.com/5Hzitw2), but the calculateNoise() doesnt update my bound Positions in the Viewport3D even though the OnPropertyChanged Method in my ViewModel get's called. What am I doing wrong here?
Thanks in advance,
KonstIT
XAML:
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camera"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="White" Direction="-1, -1, -3" />
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="{Binding Positions}" TriangleIndices="{Binding TriangleIndices}"/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="White"/>
</GeometryModel3D.Material>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
ViewportViewModel:
internal class ViewportViewModel : INotifyPropertyChanged
{
private int _terrainSize;
private Point3DCollection _positions;
private Int32Collection _triangleIndices;
private Random random = new Random();
public Point3DCollection Positions
{
get
{
return _positions;
}
set
{
_positions = value;
OnPropertyChanged("Positions");
}
}
public Int32Collection TriangleIndices
{
get
{
return _triangleIndices;
}
set
{
_triangleIndices = value;
OnPropertyChanged("TriangleIndices");
}
}
public ViewportViewModel()
{
_terrainSize = 56;
_positions = new Point3DCollection();
_triangleIndices = new Int32Collection();
GeneratePositions();
GenerateTriangleIndices();
CalculateNoiseCommand = new CalculateNoiseCommand(this);
}
private void GeneratePositions()
{ //Works }
private void GenerateTriangleIndices()
{ //Works }
//Handling
...
internal void CalculateNoise()
{
for (int i = 0; i < _terrainSize * _terrainSize; i++)
{
Point3D point = new Point3D();
point = Positions[i];
point.Y = random.Next(10) / 10.0;
Positions[i] = point;
}
OnPropertyChanged("Positions");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
Console.WriteLine("ViewportViewModel: " + propertyName);
}
}
#endregion
}
The Code to replace the current Point3DCollection. It's the same as the GeneratePositions() which is called at the constructor, just with the Positions.Clear() method at the beginning. It's called via the CalculateNoise().
private void ReplacePositions()
{
if(Positions.Count > 0)
{
Positions.Clear();
}
var column = 0;
var row = 0;
Point3D point = new Point3D();
for (int i = 0; i < _terrainSize * _terrainSize; i++)
{
for (int j = 0; j < 3; j++)
{
if (j == 0)
point.X = ((float)column / ((float)_terrainSize - 1) - 0.5) * 2;
if (j == 1)
point.Y = 0;
if (j == 2)
point.Z = ((float)row / ((float)_terrainSize - 1) - 0.5) * 2;
}
Positions.Add(point);
// Calculate next row & column
column++;
if (column % _terrainSize == 0)
{
row++;
}
column %= _terrainSize;
}
}
internal void CalculateNoise()
{
ReplacePositions();
OnPropertyChanged("Positions");
}
public class GeometryTips : ViewModelBase
{
#region " Private Fields "
private int _terrainSize = 56;
private Random random = new Random();
#endregion
#region " Public Properties "
#region " MeshGeo "
private MeshGeometry3D _MeshGeo;
public MeshGeometry3D MeshGeo
{
get { return _MeshGeo; }
set { this.Set(ref _MeshGeo, value); }
}
#endregion
#endregion
#region " Commands "
#region " Create3DCommand "
public RelayCommand Create3DCommand { get; private set; }
private void Create3D()
{
for (int i = 0; i < _terrainSize * _terrainSize; i++)
{
Point3D point = new Point3D();
point = MeshGeo.Positions[i];
point.Y = random.Next(10) / 10.0;
MeshGeo.Positions[i] = point;
}
}
#endregion
#endregion
#region " Constructor "
/// <summary>
/// GeometryTips
/// </summary>
public GeometryTips()
{
this.Create3DCommand = new RelayCommand(Create3D);
this._MeshGeo = new MeshGeometry3D();
GeneratePositions();
GenerateTriangleIndices();
}
#endregion
#region " Private Functions "
#region " GeneratePositions "
/// <summary>
/// GeneratePositions
/// </summary>
private void GeneratePositions()
{
var column = 0;
var row = 0;
Point3D point = new Point3D();
for (int i = 0; i < _terrainSize * _terrainSize; i++)
{
for (int j = 0; j < 3; j++)
{
if (j == 0)
point.X = ((float)column / ((float)_terrainSize - 1) - 0.5) * 2;
if (j == 1)
point.Y = 0;
if (j == 2)
point.Z = ((float)row / ((float)_terrainSize - 1) - 0.5) * 2;
}
_MeshGeo.Positions.Add(point);
// Calculate next row & column
column++;
if (column % _terrainSize == 0)
{
row++;
}
column %= _terrainSize;
}
}
#endregion
#region " GenerateTriangleIndices "
/// <summary>
/// GenerateTriangleIndices
/// </summary>
private void GenerateTriangleIndices()
{
var value = 0;
for (int i = 0; i < _terrainSize * _terrainSize - _terrainSize; i++)
{
for (int triangle = 0; triangle < 3; triangle++)
{
if (i % _terrainSize == 0)
{
break;
}
if (triangle == 0)
{
value = i;
}
else if (triangle == 1)
{
value = i + triangle + _terrainSize - 2;
}
else if (triangle == 2)
{
value = i + triangle + _terrainSize - 2;
}
_MeshGeo.TriangleIndices.Add(value);
}
for (int triangle = 0; triangle < 3; triangle++)
{
if (i > 0 && ((i + 1) % _terrainSize) == 0)
{
break;
}
if (triangle == 0)
{
value = i + triangle;
}
else if (triangle == 1)
{
value = i + triangle + _terrainSize - 1;
}
else if (triangle == 2)
{
value = i + triangle - 1;
}
_MeshGeo.TriangleIndices.Add(value);
}
}
}
#endregion
#endregion
}
I created this viewmodel with MVVM Light.
I use a MeshGeometry3D class to bind against.
Here is my xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="10"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="10"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="10"></RowDefinition>
</Grid.RowDefinitions>
<Viewport3D Grid.Column="1" Grid.Row="1">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camera"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Direction="-1, -1, -3"/>
<GeometryModel3D Geometry="{Binding MeshGeo}">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="White"/>
</GeometryModel3D.Material>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<Button Grid.Column="1" Grid.Row="2" Content="Handle" Command="{Binding Create3DCommand}"></Button>
</Grid>
This was working for me, hope it helps...

Scale to fit contents to Canvas when factor is smaller than 1

With code below every seems to be working fine when the scaleX and scaleY are larger than 1 (e.g. this.Width becomes 100 -> 100/83.25 = 1.20. But when these values go below 1 (e.g. this.Width becomes 50 -> scaleX = 50/83.25 = 0.600) the presented sphere is larger than excpected.
Is there any formula to calculate the correct scaling value when the size becomes lower than the actual bound of the canvas contents?
Given the following XAML file:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:xxx" x:Class="xxx.MainTile"
Title="Easy uploader" SizeToContent="WidthAndHeight" ResizeMode="NoResize" ShowInTaskbar="False" Icon="quercis.ico" Topmost="True" WindowStyle="None" Loaded="Window_Loaded" SizeChanged="Window_SizeChanged">
<Grid>
<local:Tile HorizontalAlignment="Left" VerticalAlignment="Top" x:Name="Tile" >
</local:Tile>
</Grid>
</Window>
And the corresponding Tile class:
public partial class Tile : Canvas
{
public static int MinTileSize = 50;
private TileSize tileSize = TileSize.Large;
public delegate void TileSizeChangedDelegate();
public event TileSizeChangedDelegate TileSizeChanged;
public TileSize TileSize
{
get { return tileSize; }
set
{
TileSize orgTileSize = tileSize;
tileSize = value;
//TileCanvas.Width = TileCanvas.Height = 80;
switch (tileSize)
{
case EasyUploader.TileSize.Small: this.Width = MinTileSize; this.Height = MinTileSize; break;
case EasyUploader.TileSize.Medium: this.Width = 2 * MinTileSize; this.Height = 2 * MinTileSize; break;
case EasyUploader.TileSize.Wide: this.Width = 4 * MinTileSize; this.Height = 2 * MinTileSize; break;
case EasyUploader.TileSize.Large: this.Width = 4 * MinTileSize; this.Height = 4 * MinTileSize; break;
}
if (this.Parent.GetType() == typeof(MainTile))
{
Registry.SetCurrentUserValue("MainTileSize", value.ToString());
}
if ((orgTileSize != tileSize) && (TileSizeChanged != null))
TileSizeChanged();
double scaleX = this.Width / canvasBounds.Width; //if (scaleX < 1) scaleX = -1 * (canvasBounds.Width / this.Width);
double scaleY = this.Height / canvasBounds.Height; //if (scaleY < 1) scaleY = -1 * (canvasBounds.Height / this.Height);
this.RenderTransform = new ScaleTransform(scaleX, scaleY);
}
}
private Rectangle canvasBounds = new Rectangle() { Width = 0, Height = 0 };
public Tile()
{
this.Background = Brushes.Red;
string xaml = "<Ellipse Name=\"Ellipse\" Width=\"80\" Height=\"80\">" +
" <Ellipse.Fill>" +
" <RadialGradientBrush RadiusX=\"0.675\" RadiusY=\"0.675\" Center=\"0.644144,0.355856\" GradientOrigin=\"0.644144,0.355856\">" +
" <RadialGradientBrush.GradientStops>" +
" <GradientStop Color=\"#FFFFFFFF\" Offset=\"0\"/>" +
" <GradientStop Color=\"#FF153FC4\" Offset=\"1\"/>" +
" </RadialGradientBrush.GradientStops>" +
" <RadialGradientBrush.RelativeTransform>" +
" <TransformGroup/>" +
" </RadialGradientBrush.RelativeTransform>" +
" </RadialGradientBrush>" +
" </Ellipse.Fill>" +
"</Ellipse>";
Canvas canvasFromXaml = (Canvas)XamlReader.Parse("<Canvas xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">" + xaml + "</Canvas>");
double maxHeight = 0;
double maxWidth = 0;
while (canvasFromXaml.Children.Count > 0)
{
UIElement e = canvasFromXaml.Children[0];
canvasFromXaml.Children.Remove(e);
if (double.IsNaN(Canvas.GetLeft(e))) Canvas.SetLeft(e, 0);
if (double.IsNaN(Canvas.GetTop(e))) Canvas.SetTop(e, 0);
Canvas.SetTop(e, 0);
this.Children.Add(e);
maxWidth = Math.Max((double)e.GetValue(Canvas.WidthProperty) + (double)e.GetValue(Canvas.LeftProperty), maxWidth);
maxHeight = Math.Max((double)e.GetValue(Canvas.HeightProperty) + (double)e.GetValue(Canvas.TopProperty), maxHeight);
}
canvasBounds = new Rectangle() { Width = maxWidth, Height = maxHeight };
}
}
public enum TileSize
{
Small, // 50x50
Medium, // 100x100
Wide, // 200x100
Large // 200x200
}

Oxyplot Plotview resize window for good radial plot

I don't know how to describe my problem so I try to show you:
These ellipses are actually circles. When I resize the window I can get that:
Like you see, now I have a circles, but sometimes despite the window resize, I can't get it. If anybody know what I need to change that I don't need to resize anymore?
This is my plot window:
<Window x:Class="View.Views.PlotWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="http://oxyplot.org/wpf"
Title="PlotWindow" MinHeight="600" MinWidth="800" WindowStartupLocation="CenterScreen">
<oxy:PlotView Model="{Binding PlotModel}" />
</Window>
and methods creating plot model.
public override PlotModel DrawPlot(Data data)
{
ObservableCollection<Radial> radials = ((RdData)data).Radials;
if (data.Records[0].Items.Count > 2)
return null;
PlotModel model = new PlotModel();
int i = 0;
foreach (var r in radials)
{
var rad = generateRadialFunc(r);
var series = new FunctionSeries();
foreach (var p in rad)
{
series.Points.Add(new DataPoint(p[0], p[1]));
}
series.MarkerSize = 2;
series.MarkerStrokeThickness = 1;
series.Title = "Radial (" + string.Format("{0:N2}", r.centerCoordinates.Items[0].Value) + ';' + string.Format("{0:N2}", r.centerCoordinates.Items[1].Value) + ')';
i++;
model.Series.Add(series);
}
scatterAndAxis(model, data);
return model;
}
protected double[][] generateRadialFunc(Radial radial)
{
double[][] rad = new double[3600][];
int j = 0;
for (double i = 0.0; i <= 360.0; i+=0.1)
{
rad[j] = new double[2];
double angle = i * Math.PI / 180;
rad[j][0] = radial.centerCoordinates.Items[0].Value + radial.R * Math.Cos(angle);
rad[j][1] = radial.centerCoordinates.Items[1].Value + radial.R * Math.Sin(angle);
j++;
}
return rad;
}

RGB Parade in WPF

I'm supposed to create this RBG parade project in WPF, but I have no idea where to start. It should handle images in 720p settings (1280x720). Any idea on where to start, I'm completely clueless. I don't want the whole project just a few pointers.
Thank you.
Ok, this is what I came up with, I'm sure there's a better solution but it works:
public ObservableCollection<double> RPercentPerColumn { get; set; }
public ObservableCollection<double> GPercentPerColumn { get; set; }
public ObservableCollection<double> BPercentPerColumn { get; set; }
private int[] GetImagePixels(ref int imageHeight, ref int imageWidth)
{
BitmapImage image = new BitmapImage(new Uri(this.CurrentImageFile, UriKind.Absolute));
WriteableBitmap bmp = new WriteableBitmap(image);
int rows = bmp.PixelHeight;
int columns = bmp.PixelWidth;
int[] pixels = new int[bmp.PixelWidth * bmp.PixelHeight];
bmp.CopyPixels(pixels, columns * 4, 0);
imageHeight = bmp.PixelHeight;
imageWidth = bmp.PixelWidth;
return pixels;
}
private void ClearAllData()
{
this.RPercentPerColumn.Clear();
this.GPercentPerColumn.Clear();
this.BPercentPerColumn.Clear();
}
public void ProcessRGBParade()
{
if (string.IsNullOrEmpty(this.CurrentImageFile))
return;
ClearAllData();
int columns = 0;
int rows = 0;
int[] pixels = GetImagePixels(ref rows, ref columns);
int column = 0;
int row = 0;
int redColumnTotal = 0;
int greenColumnTotal = 0;
int blueColumnTotal = 0;
int currentPixel = 0;
double totalColorInColumn = 0;
double redIntensity = 0;
double greenIntensity = 0;
double blueIntensity = 0;
int r = 0;
int g = 0;
int b = 0;
// logic to calculate intensity
for (int i = 0; i < pixels.Length; i ++)
{
row++;
r = (pixels[currentPixel] & 0x00FF0000) >> 16;
g = (pixels[currentPixel] & 0x0000FF00) >> 8;
b = (pixels[currentPixel] & 0x000000FF);
redColumnTotal += r;
greenColumnTotal += g;
blueColumnTotal += b;
totalColorInColumn += r + g + b;
if (row == rows)
{
row = 0;
column++;
currentPixel = column;
redIntensity = (redColumnTotal / totalColorInColumn) * 100;
greenIntensity = (greenColumnTotal / totalColorInColumn) * 100;
blueIntensity = (blueColumnTotal / totalColorInColumn) * 100;
RPercentPerColumn.Add(double.IsNaN(redIntensity) ? 0 : redIntensity);
GPercentPerColumn.Add(double.IsNaN(greenIntensity) ? 0 : greenIntensity);
BPercentPerColumn.Add(double.IsNaN(blueIntensity) ? 0 : blueIntensity);
redColumnTotal = 0;
greenColumnTotal = 0;
blueColumnTotal = 0;
totalColorInColumn = 0;
}
else
{
currentPixel += columns;
}
}
}

WPF and 3D to create a bowl effect

How would one go about either using the 3D components of WPF or using a pseudo 3D effect to create a "Bowl" effect, where the user is looking down on a bowl and can drag around rectangles and have the rectangles perspective change so that it looks like they move up, down and around the bowl? I'm not after any gravity effects or anything, just when the items move, I need their perspective to be adjusted...
EDIT: I have been looking into the actual 3D effects available in WPF which do seem very very powerful, so maybe someone could help in getting a half sphere on my app and then binging some 3D meshes (rectangles) to its surface?
Any thoughts?
Thanks,
Mark
Ahh right, here you go then - you can drag the red rectangle around the bowl now - enjoy!
<Window x:Class="wpfbowl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseUp="Window_MouseUp" >
<Window.Resources>
<Transform3DGroup x:Key="WorldTrans">
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,0,1" Angle="{Binding RotationLeftRight}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="myAngleRotation2" Axis="1,0,0" Angle="{Binding RotationUpDown}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
</Window.Resources>
<StackPanel>
<Viewport3D Name="mainViewport" ClipToBounds="True" HorizontalAlignment="Stretch" Margin="0" Height="500" >
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection="0,5,0"
UpDirection="0,0,1"
Position="0,-10,0"
/>
</Viewport3D.Camera>
<ModelVisual3D >
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<PointLight Position="0,-10,0" Range="150" Color="White" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</ModelVisual3D>
<ModelVisual3D Transform="{StaticResource WorldTrans}">
<ModelVisual3D Content="{Binding Models}">
</ModelVisual3D>
</ModelVisual3D>
<ModelVisual3D >
<ModelVisual3D Content="{Binding BowlModel}">
</ModelVisual3D>
</ModelVisual3D>
</Viewport3D>
</StackPanel>
</Window>
and the code behind ...
using System;
using System.ComponentModel;
using System.Timers;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Input;
namespace wpfbowl
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : INotifyPropertyChanged
{
public Window1()
{
InitModels();
InitializeComponent();
}
private Model3DGroup _cube;
private bool _cubeSelected;
private bool _cubeMoving;
private Point3D _startPoint;
private Point3D _currentPoint;
public void InitModels()
{
const int bowlQuality = 20;
Models = new Model3DGroup();
BowlModel = new Model3DGroup();
_cube = GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 2.6, 0), new Size3D(1.5, 0.2, 2));
Models.Children.Add(_cube);
var bowl = CreateBowl(new Point3D(0, 0, 0), 3, bowlQuality, bowlQuality, GetSurfaceMaterial(Colors.Green));
BowlModel.Children.Add(bowl);
}
private readonly Timer _timer;
public Model3DGroup Models { get; set; }
public Model3DGroup BowlModel { get; set; }
private double _rotationLeftRight;
public double RotationLeftRight
{
get { return _rotationLeftRight; }
set
{
if (_rotationLeftRight == value) return;
_rotationLeftRight = value;
OnPropertyChanged("RotationLeftRight");
}
}
private double _rotationUpDown;
public double RotationUpDown
{
get { return _rotationUpDown; }
set
{
if (_rotationUpDown == value) return;
_rotationUpDown = value;
OnPropertyChanged("RotationUpDown");
}
}
public static Model3DGroup CreateBowl(Point3D center, double radius, int u, int v, MaterialGroup materialGroup)
{
var bowl = new Model3DGroup();
if (u < 2 || v < 2) return null;
var pts = new Point3D[u, v];
for (var i = 0; i < u; i++)
{
for (var j = 0; j < v; j++)
{
pts[i, j] = GetPosition(radius, i * 180 / (u - 1), j * 360 / (v - 1));
pts[i, j] += (Vector3D)center;
}
}
var p = new Point3D[4];
for (var i = 0; i < (u /2) - 1; i++)
{
for (var j = 0; j < v - 1; j++)
{
p[0] = pts[i, j];
p[1] = pts[i + 1, j];
p[2] = pts[i + 1, j + 1];
p[3] = pts[i, j + 1];
bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[1], p[2]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[1], p[0]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[3], p[0]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[3], p[2]));
}
}
return bowl;
}
private static Model3DGroup CreateTriangleModel(Material material, Point3D p0, Point3D p1, Point3D p2)
{
var mesh = new MeshGeometry3D();
mesh.Positions.Add(p0);
mesh.Positions.Add(p1);
mesh.Positions.Add(p2);
mesh.TriangleIndices.Add(0);
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(2);
var normal = CalculateNormal(p0, p1, p2);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
var model = new GeometryModel3D(mesh, material);
var group = new Model3DGroup();
group.Children.Add(model);
return group;
}
private static Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)
{
var v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
var v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);
return Vector3D.CrossProduct(v0, v1);
}
private static Point3D GetPosition(double radius, double theta, double phi)
{
var pt = new Point3D();
var snt = Math.Sin(theta * Math.PI / 180);
var cnt = Math.Cos(theta * Math.PI / 180);
var snp = Math.Sin(phi * Math.PI / 180);
var cnp = Math.Cos(phi * Math.PI / 180);
pt.X = radius * snt * cnp;
pt.Y = radius * cnt;
pt.Z = -radius * snt * snp;
return pt;
}
public static MaterialGroup GetSurfaceMaterial(Color colour)
{
var materialGroup = new MaterialGroup();
var emmMat = new EmissiveMaterial(new SolidColorBrush(colour));
materialGroup.Children.Add(emmMat);
materialGroup.Children.Add(new DiffuseMaterial(new SolidColorBrush(colour)));
var specMat = new SpecularMaterial(new SolidColorBrush(Colors.White), 30);
materialGroup.Children.Add(specMat);
return materialGroup;
}
public static Model3DGroup GetCube(MaterialGroup materialGroup, Point3D point, Size3D size)
{
var farPoint = new Point3D(point.X - (size.X / 2), point.Y - (size.Y / 2), point.Z - (size.Z / 2));
var nearPoint = new Point3D(point.X + (size.X / 2), point.Y + (size.Y / 2), point.Z + (size.Z / 2));
var cube = new Model3DGroup();
var p0 = new Point3D(farPoint.X, farPoint.Y, farPoint.Z);
var p1 = new Point3D(nearPoint.X, farPoint.Y, farPoint.Z);
var p2 = new Point3D(nearPoint.X, farPoint.Y, nearPoint.Z);
var p3 = new Point3D(farPoint.X, farPoint.Y, nearPoint.Z);
var p4 = new Point3D(farPoint.X, nearPoint.Y, farPoint.Z);
var p5 = new Point3D(nearPoint.X, nearPoint.Y, farPoint.Z);
var p6 = new Point3D(nearPoint.X, nearPoint.Y, nearPoint.Z);
var p7 = new Point3D(farPoint.X, nearPoint.Y, nearPoint.Z);
//front side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p3, p2, p6));
cube.Children.Add(CreateTriangleModel(materialGroup, p3, p6, p7));
//right side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p1, p5));
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p5, p6));
//back side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p1, p0, p4));
cube.Children.Add(CreateTriangleModel(materialGroup, p1, p4, p5));
//left side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p0, p3, p7));
cube.Children.Add(CreateTriangleModel(materialGroup, p0, p7, p4));
//top side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p7, p6, p5));
cube.Children.Add(CreateTriangleModel(materialGroup, p7, p5, p4));
//bottom side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p3, p0));
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p0, p1));
return cube;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var mousePos = e.GetPosition(mainViewport);
var hitParams = new PointHitTestParameters(mousePos);
VisualTreeHelper.HitTest(mainViewport, null, ResultCallback, hitParams);
}
public HitTestResultBehavior ResultCallback(HitTestResult result)
{
// Did we hit 3D?
var rayResult = result as RayHitTestResult;
if (rayResult != null)
{
// Did we hit a MeshGeometry3D?
var rayMeshResult = rayResult as RayMeshGeometry3DHitTestResult;
if (rayMeshResult != null)
{
if (_cubeSelected)
{
_cubeMoving = true;
_currentPoint = rayMeshResult.PointHit;
RotationLeftRight = (_startPoint.X - _currentPoint.X) * 15;
RotationUpDown = (_currentPoint.Z -_startPoint.Z)*15;
}
else
{
var model = rayMeshResult.ModelHit;
foreach (var c in _cube.Children)
{
if (c.GetType() != typeof(Model3DGroup)) continue;
var model3DGroup = (Model3DGroup)c;
foreach (var sc in model3DGroup.Children)
{
if (model != sc) continue;
_cubeSelected = true;
_startPoint = rayMeshResult.PointHit;
}
}
}
}
}
return HitTestResultBehavior.Continue;
}
private void Window_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (!_cubeSelected) return;
var mousePos = e.GetPosition(mainViewport);
var hitParams = new PointHitTestParameters(mousePos);
VisualTreeHelper.HitTest(mainViewport, null, ResultCallback, hitParams);
}
private void Window_MouseUp(object sender, MouseButtonEventArgs e)
{
if (!_cubeSelected) return;
_cubeSelected = false;
_cubeMoving = false;
}
}
}
Here you go, I wasn't quite sure what you meant about the rectangles so I've just added four red rectangles around the opening of a green bowl.
Cheers,
Andy
Xaml first ...
<Window x:Class="wpfbowl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<Transform3DGroup x:Key="WorldTrans">
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,0,1" Angle="{Binding Rotation}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
</Window.Resources>
<StackPanel>
<Viewport3D Name="mainViewport" ClipToBounds="True" HorizontalAlignment="Stretch" Margin="0" Height="500" >
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection="0,5,0"
UpDirection="0,0,1"
Position="0,-10,0"
/>
</Viewport3D.Camera>
<ModelVisual3D >
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<PointLight Position="0,-10,0" Range="150" Color="White" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</ModelVisual3D>
<ModelVisual3D Transform="{StaticResource WorldTrans}">
<ModelVisual3D Content="{Binding Models}">
</ModelVisual3D>
</ModelVisual3D>
</Viewport3D>
</StackPanel>
</Window>
... and heres the code behind ...
using System;
using System.ComponentModel;
using System.Timers;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
namespace wpfbowl
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : INotifyPropertyChanged
{
public Window1()
{
InitModels();
InitializeComponent();
_timer = new Timer(100);
_timer.Elapsed += TimerElapsed;
_timer.Enabled = true;
}
void TimerElapsed(object sender, ElapsedEventArgs e)
{
Dispatcher.Invoke(DispatcherPriority.Normal, new Action<double>(Transform), 2);
}
private void Transform(double value)
{
Rotation += value;
}
public void InitModels()
{
const int bowlQuality = 20;
Models = new Model3DGroup();
var sphere = CreateBowl(new Point3D(0, 0, 0), 3, bowlQuality, bowlQuality, GetSurfaceMaterial(Colors.Green));
Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(3, 0, 0), new Size3D(1.5, 0.2, 2)));
Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(-3, 0, 0), new Size3D(1.5, 0.2, 2)));
Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 0, 3), new Size3D(1.5, 0.2, 2)));
Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 0, -3), new Size3D(1.5, 0.2, 2)));
Models.Children.Add(sphere);
}
private readonly Timer _timer;
public Model3DGroup Models { get; set; }
private double _rotation;
public double Rotation
{
get { return _rotation; }
set
{
if (_rotation == value) return;
_rotation = value;
OnPropertyChanged("Rotation");
}
}
public static Model3DGroup CreateBowl(Point3D center, double radius, int u, int v, MaterialGroup materialGroup)
{
var bowl = new Model3DGroup();
if (u < 2 || v < 2) return null;
var pts = new Point3D[u, v];
for (var i = 0; i < u; i++)
{
for (var j = 0; j < v; j++)
{
pts[i, j] = GetPosition(radius, i * 180 / (u - 1), j * 360 / (v - 1));
pts[i, j] += (Vector3D)center;
}
}
var p = new Point3D[4];
for (var i = 0; i < (u /2) - 1; i++)
{
for (var j = 0; j < v - 1; j++)
{
p[0] = pts[i, j];
p[1] = pts[i + 1, j];
p[2] = pts[i + 1, j + 1];
p[3] = pts[i, j + 1];
bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[1], p[2]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[1], p[0]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[3], p[0]));
bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[3], p[2]));
}
}
return bowl;
}
private static Model3DGroup CreateTriangleModel(Material material, Point3D p0, Point3D p1, Point3D p2)
{
var mesh = new MeshGeometry3D();
mesh.Positions.Add(p0);
mesh.Positions.Add(p1);
mesh.Positions.Add(p2);
mesh.TriangleIndices.Add(0);
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(2);
var normal = CalculateNormal(p0, p1, p2);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
var model = new GeometryModel3D(mesh, material);
var group = new Model3DGroup();
group.Children.Add(model);
return group;
}
private static Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)
{
var v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
var v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);
return Vector3D.CrossProduct(v0, v1);
}
private static Point3D GetPosition(double radius, double theta, double phi)
{
var pt = new Point3D();
var snt = Math.Sin(theta * Math.PI / 180);
var cnt = Math.Cos(theta * Math.PI / 180);
var snp = Math.Sin(phi * Math.PI / 180);
var cnp = Math.Cos(phi * Math.PI / 180);
pt.X = radius * snt * cnp;
pt.Y = radius * cnt;
pt.Z = -radius * snt * snp;
return pt;
}
public static MaterialGroup GetSurfaceMaterial(Color colour)
{
var materialGroup = new MaterialGroup();
var emmMat = new EmissiveMaterial(new SolidColorBrush(colour));
materialGroup.Children.Add(emmMat);
materialGroup.Children.Add(new DiffuseMaterial(new SolidColorBrush(colour)));
var specMat = new SpecularMaterial(new SolidColorBrush(Colors.White), 30);
materialGroup.Children.Add(specMat);
return materialGroup;
}
public static Model3DGroup GetCube(MaterialGroup materialGroup, Point3D point, Size3D size)
{
var farPoint = new Point3D(point.X - (size.X / 2), point.Y - (size.Y / 2), point.Z - (size.Z / 2));
var nearPoint = new Point3D(point.X + (size.X / 2), point.Y + (size.Y / 2), point.Z + (size.Z / 2));
var cube = new Model3DGroup();
var p0 = new Point3D(farPoint.X, farPoint.Y, farPoint.Z);
var p1 = new Point3D(nearPoint.X, farPoint.Y, farPoint.Z);
var p2 = new Point3D(nearPoint.X, farPoint.Y, nearPoint.Z);
var p3 = new Point3D(farPoint.X, farPoint.Y, nearPoint.Z);
var p4 = new Point3D(farPoint.X, nearPoint.Y, farPoint.Z);
var p5 = new Point3D(nearPoint.X, nearPoint.Y, farPoint.Z);
var p6 = new Point3D(nearPoint.X, nearPoint.Y, nearPoint.Z);
var p7 = new Point3D(farPoint.X, nearPoint.Y, nearPoint.Z);
//front side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p3, p2, p6));
cube.Children.Add(CreateTriangleModel(materialGroup, p3, p6, p7));
//right side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p1, p5));
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p5, p6));
//back side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p1, p0, p4));
cube.Children.Add(CreateTriangleModel(materialGroup, p1, p4, p5));
//left side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p0, p3, p7));
cube.Children.Add(CreateTriangleModel(materialGroup, p0, p7, p4));
//top side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p7, p6, p5));
cube.Children.Add(CreateTriangleModel(materialGroup, p7, p5, p4));
//bottom side triangles
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p3, p0));
cube.Children.Add(CreateTriangleModel(materialGroup, p2, p0, p1));
return cube;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
}

Resources