This week I thought I would test the waters with 3D in WPF. After seeing this video on Channel 9. I thought ... "Wow, that's a cool water effect". Well, I waited and waited for the code to be posted, but to no avail. So I've started my own little project to attempt this. Eventually I'll map real windows onto this mesh, but this is just the first run. Yeah, yeah ... I know mac did it first ... I wonder how many lines of code it took them?
This first step is just to get the water effect. I had to turn to fluid dynamics to figure out how to do this. Mathematics for 3D Game Programming had a lot of useful information for simulating the water. I would highly recommend this book to understand 3D principles. Here is a screen shot of the first draft:
I learned a lot from this first draft. Most importantly, it is about a thousand times faster not to recreate the mesh every frame. This site had some great information on morphing the mesh, as well as some great 3D samples (not all that pretty ... but solid fundamentals). ScreenSpaceLine3D's are pretty cool for creating a wireframe. The code I included in MeshBuilder should allow you to easily create wireframes of your 3D meshes. Unfortunately performance goes to $#!% when I turn on wireframe, so if anyone has some performance optimizations, let me know!
Additionally, I would have liked to use databinding for the camera sliders, but I had difficulty binding 3 sliders to one property on the camera. I found the MultiBinding class today, I'm going to see if this will let me solve that problem.
And the code:
<
using
namespace
public partial class FluidWindow : Window{ private FluidDynamics fluid; private Model3DGroup wireframe;
public FluidWindow() { InitializeComponent(); }
// To use Loaded event put Loaded="WindowLoaded" attribute in root element of .xaml file. private void WindowLoaded(object sender, RoutedEventArgs e) { CameraX.Value = myCamera.Position.X; CameraY.Value = myCamera.Position.Y; CameraZ.Value = myCamera.Position.Z;
CameraX.ValueChanged += new RoutedPropertyChangedEventHandler<double>(CameraPositionChanged); CameraY.ValueChanged += new RoutedPropertyChangedEventHandler<double>(CameraPositionChanged); CameraZ.ValueChanged += new RoutedPropertyChangedEventHandler<double>(CameraPositionChanged);
SetupScene(); }
private void SetupScene() { myModelGroup.Children.Clear(); if (null != fluid) fluid.Stop();
fluid = new FluidDynamics(50, 50, VertDistance.Value, TimeInterval.Value, WaveVelocity.Value, Viscosity.Value); fluid.ModelChanged += new EventHandler(fluid_ModelChanged);
MaterialGroup materials = new MaterialGroup(); Brush b = Brushes.DarkBlue.Copy(); b.Opacity = 60; b.Freeze(); materials.Children.Add(new DiffuseMaterial(b)); materials.Children.Add(new SpecularMaterial(b, 0.05)); GeometryModel3D model = new GeometryModel3D(fluid.Mesh, materials); wireframe = MeshBuilder.CreateWireframe(fluid.Mesh);
if (ShowWireframe.IsChecked) { myModelGroup.Children.Add(wireframe); }
//add the mesh myModelGroup.Children.Add(model); myModelGroup.Transform = new TranslateTransform3D(new Vector3D(-1, -1, 0)); fluid.Start(); fluid.Disturb(); }
void fluid_ModelChanged(object sender, EventArgs e) { if (ShowWireframe.IsChecked) { MeshBuilder.UpdateWireframe(wireframe, fluid.Mesh); } }
void CameraPositionChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { myCamera.Position = new Point3D(CameraX.Value, CameraY.Value, CameraZ.Value); }
private void RestartFluid(object sender, RoutedEventArgs e) { SetupScene(); }
}}
int width; int height;
private double k1, k2, k3;
public event EventHandler ModelChanged;
public FluidDynamics(int n, int m, double d, double t, double c, double mu) { this.mesh = MeshBuilder.BuildPlane(2.0/(double)n, n, m) as MeshGeometry3D; this.width = n; this.height = m;
Debug.Assert(n * m == mesh.Positions.Count);
buffer[0] = new Point3D[mesh.Positions.Count]; buffer[1] = new Point3D[mesh.Positions.Count];
double f1 = c * c * t * t / (d * d); double f2 = 1.0 / (mu * t + 2.0); k1 = (4.0 - 8.0 * f1) * f2; k2 = (mu * t - 2) * f2; k3 = 2.0 * f1 * f2; mesh.Positions.CopyTo(buffer[0], 0); mesh.Positions.CopyTo(buffer[1], 0); }
public MeshGeometry3D Mesh { get { return this.mesh; } }
private void OnModelChanged(EventArgs e) { if (this.ModelChanged != null) { this.ModelChanged(this, e); } }
public void Start() { timer = new DispatcherTimer(DispatcherPriority.Render); timer.Interval = TimeSpan.FromMilliseconds(200.0); timer.Tick += new EventHandler(DrawGeometry); timer.Start(); }
public void Stop() { if (null != timer) timer.Stop(); }
public void Disturb() { double factor = (this.mesh.Bounds.SizeX + this.mesh.Bounds.SizeY)/4; double secondary = factor * 0.85; double tercary = secondary * 0.65; int pi = this.buffer[0].Length/2 + width/2; this.buffer[0][pi].Z = factor; this.buffer[1][pi].Z = factor; this.buffer[0][pi+1].Z = secondary; this.buffer[1][pi + 1].Z = secondary; this.buffer[0][pi - 1].Z = secondary; this.buffer[1][pi - 1].Z = secondary; this.buffer[0][pi + this.height].Z = secondary; this.buffer[1][pi + this.height].Z = secondary; this.buffer[0][pi + this.height + 1].Z = tercary ; this.buffer[1][pi + this.height + 1].Z = tercary; this.buffer[0][pi + this.height - 1].Z = tercary; this.buffer[1][pi + this.height - 1].Z = tercary; this.buffer[0][pi - this.height].Z = secondary; this.buffer[1][pi - this.height].Z = secondary; this.buffer[0][pi - this.height + 1].Z = tercary; this.buffer[1][pi - this.height + 1].Z = tercary; this.buffer[0][pi - this.height - 1].Z = tercary; this.buffer[1][pi - this.height - 1].Z = tercary; }
void DrawGeometry(object sender, EventArgs e) { Random rand = new Random(); Point3D[] currentBuffer = buffer[1 - (frameCount % 2)]; Point3D[] previousBuffer = buffer[frameCount % 2];
//stop the timer while we draw timer.Stop(); for (int j = 1; j < this.height - 1; j++) { int p = j * height; for (int i = 1; i < this.width - 1; i++) { previousBuffer[p + i].Z = k1 * currentBuffer[p + i].Z + k2 * previousBuffer[p + i].Z + k3 * (currentBuffer[p + i + 1].Z + currentBuffer[p + i - 1].Z + currentBuffer[p + i + height].Z + currentBuffer[p + i - height].Z); } }
for (int alpha = 0; alpha < previousBuffer.Length; alpha++) { this.mesh.Positions[alpha] = previousBuffer[alpha]; }
//Compute new normals. for (int j = 1; j < height - 1; j++) { int offset = j*width; for (int i = 1; i < width - 1; i++) { Vector3D normal = new Vector3D(previousBuffer[offset + i - 1].Z - previousBuffer[offset + i - 1].Z, previousBuffer[offset + i - width].Z - previousBuffer[offset + i + width].Z, this.mesh.Normals[offset + i].Z); this.mesh.Normals[offset + i] = normal; } }
frameCount++;
//restart the timer. timer.Start();
OnModelChanged(EventArgs.Empty); }}
mesh.TriangleIndices.Add(pointOffset - n - 1); mesh.TriangleIndices.Add(pointOffset - 1); mesh.TriangleIndices.Add(pointOffset - n); mesh.TriangleIndices.Add(pointOffset); mesh.TriangleIndices.Add(pointOffset - n); mesh.TriangleIndices.Add(pointOffset - 1); } }
mesh.TextureCoordinates.Add(
wire.Points[0] = pa; wire.Points[1] = pb; wire.Points[2] = pc; wire.Points[3] = pa; } }