很多年前(80年代中期),我在一个公司,硅谷图形工作站的工作。其中少数炫耀的SGI机器的高端图形演示是在一个小线框网格模拟波的传播。这是很好玩通过改变网格点的高度和然后又让模拟运行。和SGI的机器不够快,由此产生的动画只是如痴如醉。
在WPF再造水面模拟似乎是一个很好的方式,多一点了解WPF中的三维图形。 (最终的结果是在这里)。
第一步是要找到一种算法,通过水模拟波传播。原来,是一个非常简单的算法,简单地以邻近点的平均身高达到预期的效果。在这篇文章中描述的基本算法详细的二维水面。相同的算法,也描述了在水面解释的影响。
下一步是成立的3D视图,其构成要素。我用两个不同的方向灯,以创造更多的水面,以及确定水面漫反射和镜面材料的性能对比。
以下是有关XAML。请注意,meshMain网将包含水面。
1 <Viewport3D Name="viewport3D1" Margin="0,8.181,0,0" Grid.Row="1"> 2 <Viewport3D.Camera> 3 <PerspectiveCamera x:Name="camMain" Position="48 7.8 41" LookDirection="-48 -7.8 -41" FarPlaneDistance="100" UpDirection="0,1,0" NearPlaneDistance="1" FieldOfView="70"> 4 5 </PerspectiveCamera> 6 </Viewport3D.Camera> 7 <ModelVisual3D x:Name="vis3DLighting"> 8 <ModelVisual3D.Content> 9 <DirectionalLight x:Name="dirLightMain" Direction="2, -2, 0"/> 10 </ModelVisual3D.Content> 11 </ModelVisual3D> 12 <ModelVisual3D> 13 <ModelVisual3D.Content> 14 <DirectionalLight Direction="0, -2, 2"/> 15 </ModelVisual3D.Content> 16 </ModelVisual3D> 17 <ModelVisual3D> 18 <ModelVisual3D.Content> 19 <GeometryModel3D x:Name="gmodMain"> 20 <GeometryModel3D.Geometry> 21 <MeshGeometry3D x:Name="meshMain" > 22 </MeshGeometry3D> 23 </GeometryModel3D.Geometry> 24 <GeometryModel3D.Material> 25 <MaterialGroup> 26 <DiffuseMaterial x:Name="matDiffuseMain"> 27 <DiffuseMaterial.Brush> 28 <SolidColorBrush Color="DarkBlue"/> 29 </DiffuseMaterial.Brush> 30 </DiffuseMaterial> 31 <SpecularMaterial SpecularPower="24"> 32 <SpecularMaterial.Brush> 33 <SolidColorBrush Color="LightBlue"/> 34 </SpecularMaterial.Brush> 35 </SpecularMaterial> 36 </MaterialGroup> 37 </GeometryModel3D.Material> 38 </GeometryModel3D> 39 </ModelVisual3D.Content> 40 </ModelVisual3D> 41 </Viewport3D>
下一步,我们创建了一个WaveGrid类,实现以上所述的基本算法。其基本思路是,我们保持两个单独的缓冲区网的数据,表示当前状态的水和一个以前的状态。 WaveGrid这个数据存储两个Point3DCollection对象。当我们运行仿真,我们轮流缓冲区,我们正在编写和我们MeshGeometry3D.Positions属性附加到最近的缓冲区。请注意,我们只有改变点,垂直高度为Y值。
WaveGrid还建立了三角形网格指数,这也将连接到我们的MeshGeometry3D Int32Collection。
所有有趣的东西在ProcessWater发生。这是我们实现在文章中描述的平滑算法。因为我想充分动画网格中的每一点,我处理不只是内部点有四个相邻点,但沿网格的边缘点,以及。正如我们在邻近点的高度值,我们保持多少邻居,我们发现,使我们能够做的平均正确的轨道。
每个点的最终值是平滑(你的邻居的平均身高)和“速度”,这是基本上如何远离平衡点,在最后一次迭代的功能?我们也应用阻尼系数,因为波将逐渐失去它们的振幅。
这里的为WaveGrid的源代码
WaveGrid还建立了三角形网格指数,这也将连接到我们的MeshGeometry3D Int32Collection。
所有有趣的东西在ProcessWater发生。这是我们实现在文章中描述的平滑算法。因为我想充分动画网格中的每一点,我处理不只是内部点有四个相邻点,但沿网格的边缘点,以及。正如我们在邻近点的高度值,我们保持多少邻居,我们发现,使我们能够做的平均正确的轨道。
每个点的最终值是平滑(你的邻居的平均身高)和“速度”,这是基本上如何远离平衡点,在最后一次迭代的功能?我们也应用阻尼系数,因为波将逐渐失去它们的振幅。
这里的为WaveGrid的源代码
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Windows.Media; 6 using System.Windows.Media.Media3D; 7 8 namespace WaveSim 9 { 10 class WaveGrid 11 { 12 // Constants 13 const int MinDimension = 5; 14 const double Damping = 0.96; 15 const double SmoothingFactor = 2.0; // Gives more weight to smoothing than to velocity 16 17 // Private member data 18 private Point3DCollection _ptBuffer1; 19 private Point3DCollection _ptBuffer2; 20 private Int32Collection _triangleIndices; 21 22 private int _dimension; 23 24 // Pointers to which buffers contain: 25 // - Current: Most recent data 26 // - Old: Earlier data 27 // These two pointers will swap, pointing to ptBuffer1/ptBuffer2 as we cycle the buffers 28 private Point3DCollection _currBuffer; 29 private Point3DCollection _oldBuffer; 30 31 /// <summary> 32 /// Construct new grid of a given dimension 33 /// </summary> 34 /// 35 <param name="Dimension"></param> 36 public WaveGrid(int Dimension) 37 { 38 if (Dimension < MinDimension) 39 throw new ApplicationException(string.Format("Dimension must be at least {0}", MinDimension.ToString())); 40 41 _ptBuffer1 = new Point3DCollection(Dimension * Dimension); 42 _ptBuffer2 = new Point3DCollection(Dimension * Dimension); 43 _triangleIndices = new Int32Collection((Dimension - 1) * (Dimension - 1) * 2); 44 45 _dimension = Dimension; 46 47 InitializePointsAndTriangles(); 48 49 _currBuffer = _ptBuffer2; 50 _oldBuffer = _ptBuffer1; 51 } 52 53 /// <summary> 54 /// Access to underlying grid data 55 /// </summary> 56 public Point3DCollection Points 57 { 58 get { return _currBuffer; } 59 } 60 61 /// <summary> 62 /// Access to underlying triangle index collection 63 /// </summary> 64 public Int32Collection TriangleIndices 65 { 66 get { return _triangleIndices; } 67 } 68 69 /// <summary> 70 /// Dimension of grid--same dimension for both X & Y 71 /// </summary> 72 public int Dimension 73 { 74 get { return _dimension; } 75 } 76 77 /// <summary> 78 /// Set center of grid to some peak value (high point). Leave 79 /// rest of grid alone. Note: If dimension is even, we're not 80 /// exactly at the center of the grid--no biggie. 81 /// </summary> 82 /// 83 <param name="PeakValue"></param> 84 public void SetCenterPeak(double PeakValue) 85 { 86 int nCenter = (int)_dimension / 2; 87 88 // Change data in oldest buffer, then make newest buffer 89 // become oldest by swapping 90 Point3D pt = _oldBuffer[(nCenter * _dimension) + nCenter]; 91 pt.Y = (int)PeakValue; 92 _oldBuffer[(nCenter * _dimension) + nCenter] = pt; 93 94 SwapBuffers(); 95 } 96 97 /// <summary> 98 /// Leave buffers in place, but change notation of which one is most recent 99 /// </summary> 100 private void SwapBuffers() 101 { 102 Point3DCollection temp = _currBuffer; 103 _currBuffer = _oldBuffer; 104 _oldBuffer = temp; 105 } 106 107 /// <summary> 108 /// Clear out points/triangles and regenerates 109 /// </summary> 110 /// 111 <param name="grid"></param> 112 private void InitializePointsAndTriangles() 113 { 114 _ptBuffer1.Clear(); 115 _ptBuffer2.Clear(); 116 _triangleIndices.Clear(); 117 118 int nCurrIndex = 0; // March through 1-D arrays 119 120 for (int row = 0; row < _dimension; row++) 121 { 122 for (int col = 0; col < _dimension; col++) 123 { 124 // In grid, X/Y values are just row/col numbers 125 _ptBuffer1.Add(new Point3D(col, 0.0, row)); 126 127 // Completing new square, add 2 triangles 128 if ((row > 0) && (col > 0)) 129 { 130 // Triangle 1 131 _triangleIndices.Add(nCurrIndex - _dimension - 1); 132 _triangleIndices.Add(nCurrIndex); 133 _triangleIndices.Add(nCurrIndex - _dimension); 134 135 // Triangle 2 136 _triangleIndices.Add(nCurrIndex - _dimension - 1); 137 _triangleIndices.Add(nCurrIndex - 1); 138 _triangleIndices.Add(nCurrIndex); 139 } 140 141 nCurrIndex++; 142 } 143 } 144 145 // 2nd buffer exists only to have 2nd set of Z values 146 _ptBuffer2 = _ptBuffer1.Clone(); 147 } 148 149 /// <summary> 150 /// Determine next state of entire grid, based on previous two states. 151 /// This will have the effect of propagating ripples outward. 152 /// </summary> 153 public void ProcessWater() 154 { 155 // Note that we write into old buffer, which will then become our 156 // "current" buffer, and current will become old. 157 // I.e. What starts out in _currBuffer shifts into _oldBuffer and we 158 // write new data into _currBuffer. But because we just swap pointers, 159 // we don't have to actually move data around. 160 161 // When calculating data, we don't generate data for the cells around 162 // the edge of the grid, because data smoothing looks at all adjacent 163 // cells. So instead of running [0,n-1], we run [1,n-2]. 164 165 double velocity; // Rate of change from old to current 166 double smoothed; // Smoothed by adjacent cells 167 double newHeight; 168 int neighbors; 169 170 int nPtIndex = 0; // Index that marches through 1-D point array 171 172 // Remember that Y value is the height (the value that we're animating) 173 for (int row = 0; row < _dimension ; row++) 174 { 175 for (int col = 0; col < _dimension; col++) 176 { 177 velocity = -1.0 * _oldBuffer[nPtIndex].Y; // row, col 178 smoothed = 0.0; 179 180 neighbors = 0; 181 if (row > 0) // row-1, col 182 { 183 smoothed += _currBuffer[nPtIndex - _dimension].Y; 184 neighbors++; 185 } 186 187 if (row < (_dimension - 1)) // row+1, col 188 { 189 smoothed += _currBuffer[nPtIndex + _dimension].Y; 190 neighbors++; 191 } 192 193 if (col > 0) // row, col-1 194 { 195 smoothed += _currBuffer[nPtIndex - 1].Y; 196 neighbors++; 197 } 198 199 if (col < (_dimension - 1)) // row, col+1 200 { 201 smoothed += _currBuffer[nPtIndex + 1].Y; 202 neighbors++; 203 } 204 205 // Will always have at least 2 neighbors 206 smoothed /= (double)neighbors; 207 208 // New height is combination of smoothing and velocity 209 newHeight = smoothed * SmoothingFactor + velocity; 210 211 // Damping 212 newHeight = newHeight * Damping; 213 214 // We write new data to old buffer 215 Point3D pt = _oldBuffer[nPtIndex]; 216 pt.Y = newHeight; // row, col 217 _oldBuffer[nPtIndex] = pt; 218 219 nPtIndex++; 220 } 221 } 222 223 SwapBuffers(); 224 } 225 } 226 }
最终的结果是相当令人满意的一系列的涟漪,从初始扰动传播的一个很好的流畅的动画效果。你可以安装和运行的模拟点击这里。请注意您可以使用鼠标滚轮放大/缩小。
最后,我们需要挂钩一切。当我们的主窗口触发,我们创建一个WaveGrid实例和一些峰值设定在网格的中心点。当我们开始动画,这个高点回落,引发的海浪。
我们在做CompositionTarget.Rendering事件处理程序的所有动画。这是推荐的当场做WPF中的自定义动画,而不是做一些计时器的Tick事件中的动画。 (Windows Presentation Foundation中如虎添翼,弥敦道,470页)。
当你渲染事件的处理程序,WPF只是继续无限期地渲染帧。一个问题是,该处理程序将调用每一个渲染帧,原来是太快为我们的水动画。为了获得水权看,我们保持的时间,我们最后呈现了一个框架,然后等待指定的毫秒数,前呈现另一个轨道。
在这里是Window1.xaml.cs完整的源代码:
最终的结果是相当令人满意的一系列的涟漪,从初始扰动传播的一个很好的流畅的动画效果。你可以安装和运行的模拟点击这里。请注意您可以使用鼠标滚轮放大/缩小。
我们在做CompositionTarget.Rendering事件处理程序的所有动画。这是推荐的当场做WPF中的自定义动画,而不是做一些计时器的Tick事件中的动画。 (Windows Presentation Foundation中如虎添翼,弥敦道,470页)。
当你渲染事件的处理程序,WPF只是继续无限期地渲染帧。一个问题是,该处理程序将调用每一个渲染帧,原来是太快为我们的水动画。为了获得水权看,我们保持的时间,我们最后呈现了一个框架,然后等待指定的毫秒数,前呈现另一个轨道。
在这里是Window1.xaml.cs完整的源代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Linq; 5 using System.Text; 6 using System.Windows; 7 using System.Windows.Controls; 8 using System.Windows.Data; 9 using System.Windows.Documents; 10 using System.Windows.Input; 11 using System.Windows.Media; 12 using System.Windows.Media.Media3D; 13 using System.Windows.Media.Imaging; 14 using System.Windows.Navigation; 15 using System.Windows.Shapes; 16 using System.Windows.Threading; 17 18 namespace WaveSim 19 { 20 /// <summary> 21 /// Interaction logic for Window1.xaml 22 /// </summary> 23 public partial class Window1 : Window 24 { 25 private Vector3D zoomDelta; 26 27 private WaveGrid _grid; 28 private bool _rendering; 29 private double _lastTimeRendered; 30 private double _firstPeak = 6.5; 31 32 // Values to try: 33 // GridSize=20, RenderPeriod=125 34 // GridSize=50, RenderPeriod=50 35 private const int GridSize = 50; 36 private const double RenderPeriodInMS = 50; 37 38 public Window1() 39 { 40 InitializeComponent(); 41 42 _grid = new WaveGrid(GridSize); // 10x10 grid 43 slidPeakHeight.Value = _firstPeak; 44 _grid.SetCenterPeak(_firstPeak); 45 meshMain.Positions = _grid.Points; 46 meshMain.TriangleIndices = _grid.TriangleIndices; 47 48 // On each WheelMouse change, we zoom in/out a particular % of the original distance 49 const double ZoomPctEachWheelChange = 0.02; 50 zoomDelta = Vector3D.Multiply(ZoomPctEachWheelChange, camMain.LookDirection); 51 } 52 53 private void Window_MouseWheel(object sender, MouseWheelEventArgs e) 54 { 55 if (e.Delta > 0) 56 // Zoom in 57 camMain.Position = Point3D.Add(camMain.Position, zoomDelta); 58 else 59 // Zoom out 60 camMain.Position = Point3D.Subtract(camMain.Position, zoomDelta); 61 Trace.WriteLine(camMain.Position.ToString()); 62 } 63 64 // Start/stop animation 65 private void btnStart_Click(object sender, RoutedEventArgs e) 66 { 67 if (!_rendering) 68 { 69 _grid = new WaveGrid(GridSize); // 10x10 grid 70 _grid.SetCenterPeak(_firstPeak); 71 meshMain.Positions = _grid.Points; 72 73 _lastTimeRendered = 0.0; 74 CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering); 75 btnStart.Content = "Stop"; 76 slidPeakHeight.IsEnabled = false; 77 _rendering = true; 78 } 79 else 80 { 81 CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering); 82 btnStart.Content = "Start"; 83 slidPeakHeight.IsEnabled = true; 84 _rendering = false; 85 } 86 } 87 88 void CompositionTarget_Rendering(object sender, EventArgs e) 89 { 90 RenderingEventArgs rargs = (RenderingEventArgs)e; 91 if ((rargs.RenderingTime.TotalMilliseconds - _lastTimeRendered) > RenderPeriodInMS) 92 { 93 // Unhook Positions collection from our mesh, for performance 94 // (see http://blogs.msdn.com/timothyc/archive/2006/08/31/734308.aspx) 95 meshMain.Positions = null; 96 97 // Do the next iteration on the water grid, propagating waves 98 _grid.ProcessWater(); 99 100 // Then update our mesh to use new Z values 101 meshMain.Positions = _grid.Points; 102 103 _lastTimeRendered = rargs.RenderingTime.TotalMilliseconds; 104 } 105 } 106 107 private void slidPeakHeight_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) 108 { 109 _firstPeak = slidPeakHeight.Value; 110 _grid.SetCenterPeak(_firstPeak); 111 } 112 } 113 }