WPF中的简单水动画

原文 https://stuff.seans.com/2008/08/21/simple-water-animation-in-wpf/

很多年前(80年代中期),我在一家拥有Silicon Graphics工作站的公司工作。在旨在展示SGI机器高端图形的少数演示中,有一个模拟了一个小线框网格中的波传播。通过更改网格中的点的高度然后让模拟运行来玩游戏非常有趣。并且SGI机器足够快,结果动画只是令人着迷。

在WPF中重新创建这个水模拟似乎是一个很好的方式来学习WPF中的3D图形。(最终结果在这里)。

第一步是找到一种模拟水中波传播的算法。事实证明,有一种非常简单的算法可以简单地通过获取相邻点的平均高度来实现期望的效果。在2D Water上的文章中详细描述了基本算法。“ 水效应解释”中也描述了相同的算法。

下一步是设置3D视口及其组成元素。我使用了两种不同的定向灯,在水面上创造了更多的对比度,同时为水面定义了漫反射和镜面反射材料特性。

这是相关的XAML。请注意,meshMain是包含水面的网格。


1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

40

41

<Viewport3D Name="viewport3D1" Margin="0,8.181,0,0" Grid.Row="1">

    <Viewport3D.Camera>

        <PerspectiveCamera x:Name="camMain" Position="48 7.8 41" LookDirection="-48 -7.8 -41" FarPlaneDistance="100" UpDirection="0,1,0" NearPlaneDistance="1" FieldOfView="70">

        </PerspectiveCamera>

    </Viewport3D.Camera>

    <ModelVisual3D x:Name="vis3DLighting">

        <ModelVisual3D.Content>

            <DirectionalLight x:Name="dirLightMain" Direction="2, -2, 0"/>

        </ModelVisual3D.Content>

    </ModelVisual3D>

    <ModelVisual3D>

        <ModelVisual3D.Content>

            <DirectionalLight Direction="0, -2, 2"/>

        </ModelVisual3D.Content>

    </ModelVisual3D>

    <ModelVisual3D>

        <ModelVisual3D.Content>

            <GeometryModel3D x:Name="gmodMain">

                <GeometryModel3D.Geometry>

                    <MeshGeometry3D x:Name="meshMain" >

                    </MeshGeometry3D>

                </GeometryModel3D.Geometry>

                <GeometryModel3D.Material>

                    <MaterialGroup>

                        <DiffuseMaterial x:Name="matDiffuseMain">

                            <DiffuseMaterial.Brush>

                                <SolidColorBrush Color="DarkBlue"/>

                            </DiffuseMaterial.Brush>

                        </DiffuseMaterial>

                        <SpecularMaterial SpecularPower="24">

                            <SpecularMaterial.Brush>

                                <SolidColorBrush Color="LightBlue"/>

                            </SpecularMaterial.Brush>

                        </SpecularMaterial>

                    </MaterialGroup>

                </GeometryModel3D.Material>

            </GeometryModel3D>

        </ModelVisual3D.Content>

    </ModelVisual3D>

</Viewport3D>

接下来,我们创建一个WaveGrid类,实现上述基本算法。基本思想是我们维护两个独立的网格数据缓冲区 - 一个表示水的当前状态,一个表示先前状态。  WaveGrid将此数据存储在两个Point3DCollection对象中。在我们运行模拟时,我们交替使用哪个缓冲区并将我们的MeshGeometry3D.Positions属性附加到最新的缓冲区。请注意,我们只是改变点的垂直高度 - 即Y值。

WaveGrid还建立了对网格的三角形索引,在Int32Collection这也将可以连接到我们的MeshGeometry3D。

所有有趣的东西都发生在ProcessWater中。这是我们实现文章中描述的平滑算法的地方。由于我想要对网格中的每个点进行完全动画处理,因此我不仅处理了具有四个相邻点的内部点,而且还处理了网格边缘上的点。当我们添加相邻点的高度值时,我们会跟踪我们找到的邻居数量,以便我们可以正确地进行平均。

每个点的最终值是平滑(邻居的平均高度)和“速度”的函数,它基本上是 - 在最后一次迭代期间距离均衡的距离是多少?然后我们还应用阻尼因子,因为波将逐渐失去其幅度。

这是WaveGrid类的完整代码:


1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Media;

using System.Windows.Media.Media3D;

namespace WaveSim

{

    class WaveGrid

    {

        // Constants

        const int MinDimension = 5;    

        const double Damping = 0.96;

        const double SmoothingFactor = 2.0;     // Gives more weight to smoothing than to velocity

        // Private member data

        private Point3DCollection _ptBuffer1;

        private Point3DCollection _ptBuffer2;

        private Int32Collection _triangleIndices;

        private int _dimension;

        // Pointers to which buffers contain:

        //    - Current: Most recent data

        //    - Old: Earlier data

        // These two pointers will swap, pointing to ptBuffer1/ptBuffer2 as we cycle the buffers

        private Point3DCollection _currBuffer;

        private Point3DCollection _oldBuffer;

        /// <summary>

        /// Construct new grid of a given dimension

        /// </summary>

        ///

<param name="Dimension"></param>

        public WaveGrid(int Dimension)

        {

            if (Dimension < MinDimension)

                throw new ApplicationException(string.Format("Dimension must be at least {0}", MinDimension.ToString()));

            _ptBuffer1 = new Point3DCollection(Dimension * Dimension);

            _ptBuffer2 = new Point3DCollection(Dimension * Dimension);

            _triangleIndices = new Int32Collection((Dimension - 1) * (Dimension - 1) * 2);

            _dimension = Dimension;

            InitializePointsAndTriangles();

            _currBuffer = _ptBuffer2;

            _oldBuffer = _ptBuffer1;

        }

        /// <summary>

        /// Access to underlying grid data

        /// </summary>

        public Point3DCollection Points

        {

            get { return _currBuffer; }

        }

        /// <summary>

        /// Access to underlying triangle index collection

        /// </summary>

        public Int32Collection TriangleIndices

        {

            get { return _triangleIndices; }

        }

        /// <summary>

        /// Dimension of grid--same dimension for both X & Y

        /// </summary>

        public int Dimension

        {

            get { return _dimension; }

        }

        /// <summary>

        /// Set center of grid to some peak value (high point).  Leave

        /// rest of grid alone.  Note: If dimension is even, we‘re not

        /// exactly at the center of the grid--no biggie.

        /// </summary>

        ///

<param name="PeakValue"></param>

        public void SetCenterPeak(double PeakValue)

        {

            int nCenter = (int)_dimension / 2;

            // Change data in oldest buffer, then make newest buffer

            // become oldest by swapping

            Point3D pt = _oldBuffer[(nCenter * _dimension) + nCenter];

            pt.Y = (int)PeakValue;

            _oldBuffer[(nCenter * _dimension) + nCenter] = pt;

            SwapBuffers();

        }

        /// <summary>

        /// Leave buffers in place, but change notation of which one is most recent

        /// </summary>

        private void SwapBuffers()

        {

            Point3DCollection temp = _currBuffer;

            _currBuffer = _oldBuffer;

            _oldBuffer = temp;

        }

        /// <summary>

        /// Clear out points/triangles and regenerates

        /// </summary>

        ///

<param name="grid"></param>

        private void InitializePointsAndTriangles()

        {

            _ptBuffer1.Clear();

            _ptBuffer2.Clear();

            _triangleIndices.Clear();

            int nCurrIndex = 0;     // March through 1-D arrays

            for (int row = 0; row < _dimension; row++)

            {

                for (int col = 0; col < _dimension; col++)

                {

                    // In grid, X/Y values are just row/col numbers

                    _ptBuffer1.Add(new Point3D(col, 0.0, row));

                    // Completing new square, add 2 triangles

                    if ((row > 0) && (col > 0))

                    {

                        // Triangle 1

                        _triangleIndices.Add(nCurrIndex - _dimension - 1);

                        _triangleIndices.Add(nCurrIndex);

                        _triangleIndices.Add(nCurrIndex - _dimension);

                        // Triangle 2

                        _triangleIndices.Add(nCurrIndex - _dimension - 1);

                        _triangleIndices.Add(nCurrIndex - 1);

                        _triangleIndices.Add(nCurrIndex);

                    }

                    nCurrIndex++;

                }

            }

            // 2nd buffer exists only to have 2nd set of Z values

            _ptBuffer2 = _ptBuffer1.Clone();

        }

        /// <summary>

        /// Determine next state of entire grid, based on previous two states.

        /// This will have the effect of propagating ripples outward.

        /// </summary>

        public void ProcessWater()

        {

            // Note that we write into old buffer, which will then become our

            //    "current" buffer, and current will become old. 

            // I.e. What starts out in _currBuffer shifts into _oldBuffer and we

            // write new data into _currBuffer.  But because we just swap pointers,

            // we don‘t have to actually move data around.

            // When calculating data, we don‘t generate data for the cells around

            // the edge of the grid, because data smoothing looks at all adjacent

            // cells.  So instead of running [0,n-1], we run [1,n-2].

            double velocity;    // Rate of change from old to current

            double smoothed;    // Smoothed by adjacent cells

            double newHeight;

            int neighbors;

            int nPtIndex = 0;   // Index that marches through 1-D point array

            // Remember that Y value is the height (the value that we‘re animating)

            for (int row = 0; row < _dimension ; row++)

            {

                for (int col = 0; col < _dimension; col++)

                {

                    velocity = -1.0 * _oldBuffer[nPtIndex].Y;     // row, col

                    smoothed = 0.0;

                    neighbors = 0;

                    if (row > 0)    // row-1, col

                    {

                        smoothed += _currBuffer[nPtIndex - _dimension].Y;

                        neighbors++;

                    }

                    if (row < (_dimension - 1))   // row+1, col

                    {

                        smoothed += _currBuffer[nPtIndex + _dimension].Y;

                        neighbors++;

                    }

                    if (col > 0)          // row, col-1

                    {

                        smoothed += _currBuffer[nPtIndex - 1].Y;

                        neighbors++;

                    }

                    if (col < (_dimension - 1))   // row, col+1

                    {

                        smoothed += _currBuffer[nPtIndex + 1].Y;

                        neighbors++;

                    }

                    // Will always have at least 2 neighbors

                    smoothed /= (double)neighbors;

                    // New height is combination of smoothing and velocity

                    newHeight = smoothed * SmoothingFactor + velocity;

                    // Damping

                    newHeight = newHeight * Damping;

                    // We write new data to old buffer

                    Point3D pt = _oldBuffer[nPtIndex];

                    pt.Y = newHeight;   // row, col

                    _oldBuffer[nPtIndex] = pt;

                    nPtIndex++;

                }

            }

            SwapBuffers();

        }

    }

}

[/sourcecode]

Finally, we need to hook everything up.  When our main window fires up, we create an instance of <strong>WaveGrid </strong>and set the center point in the grid to some peak value.  When we start the animation, this higher point will fall and trigger the waves.

We do all of the animation in the <strong>CompositionTarget.Rendering </strong>event handler.  This is the recommended spot to do custom animations in WPF, as opposed to doing the animation in some timer Tick event.  (<em>Windows Presentation Foundation Unleashed</em>, Nathan, pg 470).

When you attach a handler to the <strong>Rendering </strong>event, WPF just continues rendering frames indefinitely.  One problem is that the handler will get called for every frame rendered, which turns out to be too fast for our water animation.  To get the water to look right, we keep track of the time that we last rendered a frame and then wait a specified number of milliseconds before rendering another.

Here is the full source code for Window1.xaml.cs:

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Media3D;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

using System.Windows.Threading;

namespace WaveSim

{

    /// <summary>

    /// Interaction logic for Window1.xaml

    /// </summary>

    public partial class Window1 : Window

    {

        private Vector3D zoomDelta;

        private WaveGrid _grid;

        private bool _rendering;

        private double _lastTimeRendered;

        private double _firstPeak = 6.5;

        // Values to try:

        //   GridSize=20, RenderPeriod=125

        //   GridSize=50, RenderPeriod=50

        private const int GridSize = 50;   

        private const double RenderPeriodInMS = 50;   

        public Window1()

        {

            InitializeComponent();

            _grid = new WaveGrid(GridSize);        // 10x10 grid

            slidPeakHeight.Value = _firstPeak;

            _grid.SetCenterPeak(_firstPeak);

            meshMain.Positions = _grid.Points;

            meshMain.TriangleIndices = _grid.TriangleIndices;

            // On each WheelMouse change, we zoom in/out a particular % of the original distance

            const double ZoomPctEachWheelChange = 0.02;

            zoomDelta = Vector3D.Multiply(ZoomPctEachWheelChange, camMain.LookDirection);

        }

        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)

        {

            if (e.Delta > 0)

                // Zoom in

                camMain.Position = Point3D.Add(camMain.Position, zoomDelta);

            else

                // Zoom out

                camMain.Position = Point3D.Subtract(camMain.Position, zoomDelta);

            Trace.WriteLine(camMain.Position.ToString());

        }

        // Start/stop animation

        private void btnStart_Click(object sender, RoutedEventArgs e)

        {

            if (!_rendering)

            {

                _grid = new WaveGrid(GridSize);        // 10x10 grid

                _grid.SetCenterPeak(_firstPeak);

                meshMain.Positions = _grid.Points;

                _lastTimeRendered = 0.0;

                CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);

                btnStart.Content = "Stop";

                slidPeakHeight.IsEnabled = false;

                _rendering = true;

            }

            else

            {

                CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);

                btnStart.Content = "Start";

                slidPeakHeight.IsEnabled = true;

                _rendering = false;

            }

        }

        void CompositionTarget_Rendering(object sender, EventArgs e)

        {

            RenderingEventArgs rargs = (RenderingEventArgs)e;

            if ((rargs.RenderingTime.TotalMilliseconds - _lastTimeRendered) > RenderPeriodInMS)

            {

                // Unhook Positions collection from our mesh, for performance

                // (see http://blogs.msdn.com/timothyc/archive/2006/08/31/734308.aspx)

                meshMain.Positions = null;

                // Do the next iteration on the water grid, propagating waves

                _grid.ProcessWater();

                // Then update our mesh to use new Z values

                meshMain.Positions = _grid.Points;

                _lastTimeRendered = rargs.RenderingTime.TotalMilliseconds;

            }

        }

        private void slidPeakHeight_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)

        {

            _firstPeak = slidPeakHeight.Value;

            _grid.SetCenterPeak(_firstPeak);

        }

    }

}

最终的结果非常令人满意 - 从最初的干扰中传播出来的一系列涟漪的平滑动画。您可以单击此处安装并运行模拟。请注意,您可以使用鼠标滚轮放大/缩小。

我们可以通过几种不同的方式扩展这个例子:

  • 以更逼真的方式渲染水面 - 例如玻璃状,带有反射。
  • 添加简单控件以更改视点或旋转网格本身
  • 添加旋钮用于玩Damping和SmoothingFactor之类的东西
  • 添加使用鼠标“抓取”网格中的点并手动向上/向下移动它们的功能
  • 雨滴模拟 - 只需添加定时器,引入新的随机峰值,代表雨滴
  • 抗锯齿 - 也将对角相邻的点视为邻居,但在平均时通过加权因子进行调整

原文地址:https://www.cnblogs.com/lonelyxmas/p/10799485.html

时间: 2024-08-28 05:11:29

WPF中的简单水动画的相关文章

《深入浅出WPF》笔记——绘画与动画

<深入浅出WPF>笔记——绘画与动画 本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一下学习的过程.在本记录中,为了更好的理解绘画与动画,多数的例子还是在VS里面敲出来的.好了,不废话了,现在开始. 一.WPF绘画 1.1基本图形 在WPF中可以绘制矢量图,不会随窗口或图型的放大或缩小出现锯齿或变形,除此之外,XAML绘制出来的图有个好处就是便于修改,当图不符合要求的时间,通常改某些属性就可以完成

WPF中的动画——(一)基本概念

WPF的一个特点就是支持动画,我们可以非常容易的实现漂亮大方的界面.首先,我们来复习一下动画的基本概念.计算机中的动画一般是定格动画,也称之为逐帧动画,它通过每帧不同的图像连续播放,从而欺骗眼和脑产生动画效果.其原理在维基百科上有比较详尽的解释,这里就不多介绍了. 也就是说,我们要产生动画,只需要连续刷新界面即可.例如,我们要实现一个宽度变化的按钮的动画,可以用如下方式来实现: private void MainWindow_Loaded(object sender, RoutedEventAr

WPF 3D:简单的Point3D和Vector3D动画创造一个旋转的正方体

原文:WPF 3D:简单的Point3D和Vector3D动画创造一个旋转的正方体 运行结果: 事实上很简单,定义好一个正方体,处理好纹理.关于MeshGeometry3D的正确定义和纹理这里就不多讲了,可以参考我以前写过的一些文章: WPF 3D: MeshGeometry3D纹理坐标的正确定义 WPF 3D:MeshGeometry3D的定义和光照 接下来就是怎样让它动起来.我们通过3D点动画来改变照相机(Camera类型)的位置(Position属性)从而使正方体动起来(这样的话实际上正方

WPF中的动画——(三)时间线(TimeLine)

时间线(TimeLine)表示时间段. 它提供的属性可以让控制该时间段的长度.开始时间.重复次数.该时间段内时间进度的快慢等等.在WPF中内置了如下几种TimeLine: AnimationTimeline?:前面已经介绍过,主要用于属性的过渡,这种是最常见的动画. MediaTimeline:用于控制媒体文件播放的时间线. ParallelTimeline:ParallelTimeline?是一种可对其他时间线进行分组的时间线,可用于实现较复杂的动画. Storyboard?:一种特殊的?Pa

WPF中的动画——(五)路径动画

原文:WPF中的动画--(五)路径动画 路径动画是一种专门用于将对象按照指定的Path移动的动画,虽然我们也可以通过控制动画的旋转和偏移实现对象的移动,但路径动画更专业,它的实现更加简洁明了. 路径动画中最常用的是MatrixAnimationUsingPath,它通常用于控制对象的MatrixTransform,一个简单的例子如下: 1 <Canvas > 2 <Canvas.Resources> 3 <PathGeometry x:Key="path"

WPF中的动画——(三)时间线(TimeLine)(转)

WPF中的动画——(三)时间线(TimeLine) 时间线(TimeLine)表示时间段. 它提供的属性可以让控制该时间段的长度.开始时间.重复次数.该时间段内时间进度的快慢等等.在WPF中内置了如下几种TimeLine: AnimationTimeline :前面已经介绍过,主要用于属性的过渡,这种是最常见的动画. MediaTimeline:用于控制媒体文件播放的时间线. ParallelTimeline:ParallelTimeline 是一种可对其他时间线进行分组的时间线,可用于实现较复

WPF中的动画——(二)From/To/By 动画(二)

WPF中的动画——(二)From/To/By 动画 我们所实现的的动画中,很大一部分是让一个属性在起始值和结束值之间变化,例如,我在前文中实现的改变宽度的动画: var widthAnimation = new DoubleAnimation()    {        From = 0,        To = 320,        Duration = TimeSpan.FromSeconds(2),        RepeatBehavior = RepeatBehavior.Forev

Android中的补间动画(tween)的简单使用

相对帧动画,补间动画(tween)可以这么理解:我们不必像帧动画一样指定动画的每一帧,只需定义一个动画的开始和结束关键帧,而中间变化的帧由系统帮我们计算. tween动画可以分为下面几种: AlphaAnimation(透明渐变动画): 示例:res/anim/alpha.xml <?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.andr

简单的介绍下WPF中的MVVM框架

最近在研究学习Swift,苹果希望它迅速取代复杂的Objective-C开发,引发了一大堆热潮去学它,放眼望去各个培训机构都已打着Swift开发0基础快速上手的招牌了.不过我觉得,等同于无C++基础上手学习C#一样,即使将来OC被淘汰,那也是N年之后的事情,如果真的要做IOS开发,趁现在Swift才刚开始,花那么几个月去了解一下OC绝对是一件有帮助的事情. 扯远了,我前几天刚接触到一个叫做mvvm的框架,发现很有意思,带着学习的态度来写点东西,不足之处一起研究.还有一个很重要的原因,我发现不少同