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:


<Window x:Class="LearnAvalonCS.FluidWindow" xmlns="http://schemas.microsoft.com/winfx/avalon/2005"

xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005" Title="LearnAvalonCS" Loaded="WindowLoaded">

   <DockPanel>
      
<
StackPanel Width="200" Orientation="Vertical" DockPanel.Dock="Right">
         
<
StackPanel.Background>
            
<
LinearGradientBrush>
               
<
LinearGradientBrush.GradientStops>
                  
<
GradientStop Color="AntiqueWhite" Offset="0.0"/>
                  
<
GradientStop Color="Silver" Offset="0.3"/>
                  
<
GradientStop Color="Silver" Offset="0.7"/>
                  
<
GradientStop Color="AntiqueWhite" Offset="1.0"/>
               
</
LinearGradientBrush.GradientStops>
            
</
LinearGradientBrush>
         
</
StackPanel.Background>
         
         
<
Button Click="RestartFluid">Restart</Button>
         
<
Button Click="StopFluid">Stop</Button>
         
<
GroupBox Header="Camera">
            
<
StackPanel>
               
<
Slider x:Name="CameraX" Value="0" Maximum="3" Minimum="-3" Slider.TickPlacement="None"></Slider>
               
<
Slider x:Name="CameraY" Value="0" Maximum="3" Minimum="-3" Slider.TickPlacement="None"></Slider>
               
<
Slider x:Name="CameraZ" Value="3" Maximum="10" Minimum="0" Slider.TickPlacement="None"></Slider>
            
</
StackPanel>
         
</
GroupBox>

         
<
CheckBox x:Name="ShowWireframe">
            
<
TextBlock>Show Wireframe</TextBlock>
         
</
CheckBox>

         
<
GroupBox Header="Wave Parameters">
         
   <
StackPanel>
            
   <
TextBlock>Viscosity</TextBlock>
               
<
Slider x:Name="Viscosity" Maximum="1.0" Minimum="0.01" Value="0.2"></Slider>
               
<
TextBlock>Time Interval</TextBlock>
               
<
Slider x:Name="TimeInterval" Maximum="1.0" Minimum="0.01" Value="0.3"></Slider>
               
<
TextBlock>Wave Velocity</TextBlock>
               
<
Slider x:Name="WaveVelocity" Maximum="1.0" Minimum="0.01" Value="0.7"></Slider>
               
<
TextBlock>Distance between verts</TextBlock>
               
<
Slider x:Name="VertDistance" Maximum="1.0" Minimum="0.01" Value="0.5"></Slider>
            
</
StackPanel>
         
</
GroupBox>
      
</
StackPanel>
      
<
Viewport3D Name="myViewport">
         
<
ModelVisual3D>
            
<
ModelVisual3D.Content>
               
<
Model3DGroup>
                  
<
DirectionalLight Color="#FFFFFFFF" Direction="-3,-4,-5" />
                  
<
Model3DGroup x:Name="myModelGroup"/>
               
</
Model3DGroup>
            
</
ModelVisual3D.Content>
         
</
ModelVisual3D>
         
<
Viewport3D.Camera>
            
<
PerspectiveCamera x:Name="myCamera" FarPlaneDistance="20" LookAtPoint="0,0,0" Up="0,0,1" Position="2,0,3" NearPlaneDistance="1" FieldOfView="45"/>
         
</
Viewport3D.Camera>
      
</
Viewport3D>
   
</
DockPanel>
</
Window>


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Media.Media3D;

namespace LearnAvalonCS
{

/// <summary>
/// Interaction logic for FluidWindow.xaml
/// </summary>

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();
   
}

   private void StopFluid(object sender, RoutedEventArgs e)
   
{
      
fluid.Stop();
   
}

}
}


using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Media.Media3D;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using System.Diagnostics;

namespace LearnAvalonCS
{

public class FluidDynamics
{
   
private DispatcherTimer timer;
   
private MeshGeometry3D mesh;
   
private Point3D[][] buffer = new Point3D[2][];
   
private uint frameCount = 0;

   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);
   }
}

public static class MeshBuilder
{
   
public static Geometry3D BuildPlane(double w, int m, int n)
   
{
      
MeshGeometry3D mesh = new MeshGeometry3D();
      
int pointOffset = 0;
      
for (int j = 0; j < m; j++)   
      
{
         
double x = ((double)j) * w;   
         
for (int k = 0; k < n; k++, pointOffset++)
         
{
            
mesh.Positions.Add(
new Point3D(x, ((double)k) * w, 0.0));
            
mesh.Normals.Add(
new Vector3D(0.0, 0.0, 2.0*w));
            
if(0 == j || 0 == k)
               
continue;

            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(new Point(0.0, w * (double)n));
      
mesh.TextureCoordinates.Add(
new Point(w * (double)m, w * (double)n));
      
mesh.TextureCoordinates.Add(
new Point(0.0, 0.0));
      
mesh.TextureCoordinates.Add(
new Point(w * (double)m, 0.0));

      return mesh;
   }

   public static void UpdateWireframe(Model3DGroup wireframe, MeshGeometry3D mesh)
   
{
      
for(int i = 0; i < wireframe.Children.Count; i++)
      
{
         
ScreenSpaceLines3D wire = wireframe.Children[i] as ScreenSpaceLines3D;
         
int a = mesh.TriangleIndices[i*3];
         
int b = mesh.TriangleIndices[i*3 + 1];
         
int c = mesh.TriangleIndices[i*3 + 2];
         
Point3D pa = mesh.Positions[a];
         
Point3D pb = mesh.Positions[b];
         
Point3D pc = mesh.Positions[c];

         wire.Points[0] = pa;
         
wire.Points[1] = pb;
         
wire.Points[2] = pc;
         
wire.Points[3] = pa;
      
}
   
}

   public static Model3DGroup CreateWireframe(MeshGeometry3D mesh)
   
{
      
Model3DGroup group = new Model3DGroup();
      
for (int i = 0; i < mesh.TriangleIndices.Count; i += 3)
      
{
         
int a = mesh.TriangleIndices[i];
         
int b = mesh.TriangleIndices[i + 1];
         
int c = mesh.TriangleIndices[i + 2];

         
Point3D pa = mesh.Positions[a];
         
Point3D pb = mesh.Positions[b];
         
Point3D pc = mesh.Positions[c];

         ScreenSpaceLines3D wireframe = new ScreenSpaceLines3D();
         
wireframe.Color =
Colors.AliceBlue;
         
wireframe.Points.Add(pa);
         
wireframe.Points.Add(pb);
         
wireframe.Points.Add(pc);
         
wireframe.Points.Add(pa);
         
group.Children.Add(wireframe);
      
}
      
return group;
   
}
}
}