断断续续花了一个月,终于把这本书的一二两章啃了下来,理解流体模拟的理论似乎不难,无论是《Fluid Simulation for Computer Graphics》还是《计算流体力学基础及其应用》都能很好帮助程序员去理解这些原理,可在缺乏实践情况下,这种对原理的理解其实跟死记硬背没什么区别。《Fluid Engine Development》提供了一个实现完成的流体模拟引擎以及它的编程实现原理,充分帮助程序员通过编程实现流体动画引擎,以此完成流体模拟学习的第一步。这不,早在今年一月就嚷嚷研究学习流体模拟却苦苦挣扎无法入门的我,在抄着作者的代码看着作者的书的情况,终于实现了一个流体模拟引擎。我终于可以自信地说自己已经入门了流体模拟o(╥﹏╥)o。
流体模拟我学习流体模拟的本质原因,是因为迷恋上了复杂的流体动画,渴望通过编程来创造它们。流体模拟主要是指结合流体模拟的物理现象、方程和计算机图形学的方法来模拟海面、海浪、烟雾等场景。
流体模拟的基本方法可分为三类: 基于纹理变换的流体模拟、基于二维高度场网格的流体模拟以及基于真实物理方程的流体模拟。
基于纹理变换的流体模拟只需要对水面纹理进行法向扰动后、绘制水面的倒影(反射)以及绘制水底的情况(透射)即可绘制出一般的水面效果。 但这种方法由于其根本上没有水面网格,所以水面起伏的绘制效果不明显。
基于二维高度场的网格流体模拟方法把水面表示成为一个连续的平面网格,然后生成一系列对应于这张网络的连续的高度纹理-称为高度图。接着每个网格顶点对应于一个高度图的像素,作为水面高度,从而表示出整个水面。
以上2者方法相对简单,但真实度不高,本质上只是hack,基于真实物理方程的流体模拟才能制作目前所能呈现在计算机上最真实的流体动画。尽管这种方法,受限于过高的计算量,一直处于离线渲染领域,很难在实时游戏中使用,但随着实时流体模拟技术的进步以及硬件条件的进步,在游戏中使用这种方法并不遥远,实际上,这甚至已经成为了现实。看看这个视频,实时流体模拟已经能做得很好了。
流体模拟的基本方法主要有三种,一种使用粒子(拉格朗日方法),一种使用网格(欧拉方法),第三种则是2者的混合。本文主要讲基于粒子的流体模拟。
基于物理的动画流体动画首先是动画,动画的本质是在给定时间序列播放一系列图像,由此我们可以编写帧的结构体和动画的抽象类。我们创建新的动画时,只要编写类继承Animation,再重写onUpdate函数,在指定帧更新图像即可。
struct Frame final { public: int index = 0; double timeIntervalInSeconds = 1.0 / 60.0; double GetTimeInSeconds() const; void Advance(); void Advance(int delta); }; class Animation{ public: void Update(const Frame& frame); protected: //********************************************** //the function is called from Animation:Update(); //this function and implement its logic for updating the animation state. //********************************************** virtual void onUpdate(const Frame& frame) = 0; }; void Animation::Update(const Frame & frame){ onUpdate(frame); } double Frame::GetTimeInSeconds() const{ return index * timeIntervalInSeconds; } void Frame::Advance(){ ++index; } void Frame::Advance(int delta){ index += delta; }流体动画属于基于物理的动画,它不同于正常的动画,正常的动画并不依赖外界的输入以及其他帧的数据和状态,它们大多是根据时间状态的改变来播放不同的图像,或是通过以时间为输入的函数(例如sin(t)图像)来切换状态。基于物理的动画正好相反,当前帧的数据和状态完全是由上一帧决定的。由此编写了PhysicsAnimation类
class PhysicsAnimation : public Animation{ public: double GetCurrentTime() const { return _currentTime; } private: void initialize(); void timeStep(double timeIntervalInSeconds); Frame _currentFrame; double _currentTime = 0.0; protected: virtual void onUpdate(const Frame& frame) override final; //********************************************** //the function is called from Animation:timeStep(double); //This function is called for each time-step; //********************************************** virtual void onTimeStep(double timeIntervalInSeconds) = 0; //********************************************** //the function is called from Animation:initialize(); //Called at frame 0 to initialize the physics state.; //********************************************** virtual void onInitialize() = 0; }; void PhysicsAnimation::initialize(){ onInitialize(); } void PhysicsAnimation::onUpdate(const Frame & frame){ if (frame.index > _currentFrame.index) { if (_currentFrame.index < 0) { initialize(); } int numberOfFrames = frame.index - _currentFrame.index; for (int i = 0; i < numberOfFrames; ++i) { timeStep(frame.timeIntervalInSeconds); } _currentFrame = frame; } }我们可以看到PhysicsAnimation重写了onUpdate函数,采用渐近更新每一帧
粒子动画系统我们使用粒子模拟流体,那就不可避免的要使用粒子系统