很多年前(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>
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 }