[图形学] 实时流体模拟中的数学方法

reference: 《Mathematics for 3D Game Programming and Computer Graphics》


关于这一章节我用OpenGL写了一个可运行的demo,需要的话可以看这里:[OpenGL] 动态的水面模拟


目录

1.波动方程
2.近似导数
3.求表面位移
4.实现
5.代码

        许多游戏里表达的世界里都有被流体表面覆盖的区域。要么是水泊,要么是一桶强酸,或者是熔岩坑,我们想要流体表面能够表现得和物理现实世界一样。我们可以通过模拟流体以波的形式起伏来实现这些效果。在这一章节中,我们介绍著名的波动方程,并将它应用到实时流体表面仿真中。

 

波动方程


        波动方程是一个偏微分方程,它描述了恒张力下一维弦或是二维表面上每个点的运动状态。通过考虑一根端点固定在x轴上的弹性绳,我们可以推导出一维波方程。我们假设该绳子密度均匀分布,并且受到切线方向的恒力F。

        
        让函数z(x,t)表示这根绳子在水平位置x处以及时间t时的垂直位置。当绳子向z方向移动时,绳子上的每个点受到张力并产生加速度。牛顿第二定律指出对于处在x = s和x = s+△x区间的绳子在任一时间t受到的力F(x,t)等于它的质量乘以其加速度a(x,t)。由于绳子的线性密度是,这一小段绳子的质量是△x,所以我们有:

        
 
        正如下图所示,我们可以把分布在x=s到x=s+△x之间每个点受到的力分解成水平和竖直分量,分别是H(x,t)和V(x,t)。让theta表示绳子在x=s处的切线与x轴之间的夹角。由于张力T沿着切线方向,它的水平分量和竖直分量可以写作:

        

        


        让表示在x = s+△x处的切线与x轴之间的夹角。在这一端点的张力水平分量H(s+△x,t)以及竖直分量V(s+△x,t)可以写作:

        


        对于较细微的运动,我们假设力的水平分量为0,那么这一段的加速度仅在竖直方向上。因此,对于处在x=s和x=s+△x间的一小段绳子而言,有:

        

        由于函数H与x无关,我们可以直接写成H(t),而不是H(x,t)。

        作用于x = s到x = s + △x区域的垂直力将会产生一个由

       

        的z分量给定的加速度。由于垂直的加速度等于位置函数z(x,t)的二次导数,我们有:

        

        两边同乘密度并且令极限△x趋向于0:

        

        方程的右式等价于V的x分量在s处的偏导,所以我们可以把它重写成

        (*)

        使用

       

        中给出的H(t)和V(s,t) 的值,我们可以用H(t)来这样表达V(s,t):

       

        由于是绳的切线与x轴的夹角,tan就等于函数z(x,t)在s处的斜率,因此:

        

        我们把这个式子代入(*),得到:

        

        又因为H(t)与x无关,我们写成

        

       对于细微的运动而言,接近于1,所以我们用张力T来近似表达H(t).令,我们现在可以得到一维波方程:

        

        二维波方程可以通过扩展维度得到:

        

        常数c表达了单位时间内的距离,所以我们可以把它表达为速度。关于c,我们有一个没有验证的事实,它实际上是波沿着绳子或者从一个表面上产生的速度。由于波的速度随着介质所受张力增大而增大,并随着介质密度的减小而减小,这将是有意义的。

        上式除了表面张力没有考虑到其它的力。因此,表面波的平均振幅不会像现实世界的流体一样衰减。我们可以在等式后面加一个和表面点速度方向相反的粘性阻尼力:

        

        其中,非负的常量表达了流体的黏度,的值控制了表面波要花多久时间停下来。一个小的值可以让波持续很长一段时间,但是一个大的值可以让波迅速的消失,正如稠密的油一样。


近似导数


        考虑黏性阻力的二维波方程可以通过分离变量法解出。但是这个解的形式非常复杂,对于实时模拟而言,它的计算量太大了。因此,我们选择用数值分析的方法来模拟流体表面波的运动。

        假设我们的流体表面是由均匀分布在n x m规则网格上的三角形组成的,如下图。设邻接点在x和y方向上的距离均为d,令连续的流体状态计算的时间间隔为t。我们用z(,i,j,k)来表达网格上的一个顶点,其中i,j是满足0<=i<n和0<=j<m的整数,它们表达的是空间坐标点。k是一个表达临时坐标值得非负整数。也就说,z(i,j,k)等价于时间kt时,在坐标点(id,jd)处的顶点位移。

        

        我们利用边界条件推断出处在网格边界上的点位移为0,内部点的位置可以利用前面给出的二维波方程求出,而近似导数可以利用邻接点位移差求出。正如下图所显示的,我们可以通过计算顶点和它x方向的邻接点之间△z和△x的平均比例,来估计顶点(i,j)处沿x轴方向的切线向量。使用这一技术,并且利用△x = d这一事实,我们可以这样表达导数

        

        

        

 

        我们用类似的计算顶点与y方向邻接点的△z与△y的平均比例方法,来定义点(i,j)处的导数。和x方向一样,△y = d,所以我们有:


        


        我们可以通过计算某一点当前时间的位移和前一时间位移的差值,以及当前时间和接下来时间的估计位移之间的差值,来定义时间导数。时间间隔为t,所以△z与三△t比例的平均值为:


        


        二次导数的估计可以用和一次导数一样的方法,这是通过计算一次导数与空间或时间坐标之一的差值求得的。为了进一步解释这一点,我们考虑在顶点(i,j)处关于x的二次导数。该顶点一阶导数的平均差值


        


        我们把前面求得的一阶导数的值代入这个方程,得到:


        


        除以d我们得到与△x的比值,这样我们就能定义二次导数:


        


        这个公式要求我们使用与当前点隔了两个点距离的邻近点的位移来计算。幸运的是,我们并未使用邻接点,所以我们可以缩放点(i,j)处的坐标为原来的一半。使用最近的邻接点并且把距离缩减为一半,我们得到了关于x的二阶导数的公式:


        


        类似的关于y以及关于t的二阶导数公式如下:


       


求表面位移


        使用关于t的一阶导数以及我们推导出的三个关于x,y,t的二阶导数,在点(i,j)处有黏性阻力的二维波方程可以被写成这样的形式:


        


        我们想要决定在时间t后的下一个位移z(i,j,k+1),如果我们已经知道当前位移z(i,j,k)以及前一次位移z(i,j,k-1)。z(i,j,k+1)的方程为:


        

        这恰恰是我们想要的。这个公式每一项前面的常量都可以预先计算,每次计算网格顶点时,只剩下三次乘法以及四次加法。

        如果波动速率c太快,或者时间间隔t太长,那么我们的迭代方程将会发散到无穷。为了保证位移是有限的,我们需要决定一个确切的坐标值,使得方程在这个值之下保持稳定。为了保证收敛,我们要求从水平表面离开的顶点应当沿着释放时的表面移动。

        假设我们有一个nxm的顶点数组,除了坐标为(i0,j0)的顶点,其余顶点都有 z(i,j,0) = 0以及z(i,j,1) = 0。我们令(i0,j0)处的点处在某一位置,使得z(i0,j0,0) = h并且z(i0,j0,1)=h,其中h是一个非0的位移。现在假设在(i0,j0)的点在时间2t时释放,那么计算z(i0,j0,2)的值时,方程的第三项为0,因而我们有:

        

        对于沿着水平表面移动的顶点,它在2t时的位移一定比在t时的位移要小,因此,我们有

        

        把z(i0,j0,2)的值代入,我们得到

        

        所以:

        

        分离c,我们得到

        

        这告诉我们对于任意邻接点间给定距离d以及方程迭代的任意连续时间间隔t,波的速度c一定比这个方程现实的最大值要小。

        或者说,我们可以在给定距离d和波速c的轻快下算出最大时间间隔t。然后方程两边同乘并化简:

        


        左边的不等式仅仅要求t>0,这是一个自然成立的条件。而右边的不等式可以转化为:

        

        使用二次方程,这个多项式的根为:

        

        由于方程中的二次项系数为正,所以对应的抛物线开口1向上,因此当t分布在根的两边时,多项式是负的。又因为根中根号里的式子大于,所以两个根中较小的那个是复数,所以可以被舍去。我们现在可以这样表达时间t的取值范围:

        

        使用处在范围外的波速c,或者使用处在落在范围外的时间,都会导致顶点位移的指数爆炸增长。


实现


        作为流体表面方程

        

        的一个应用,我们需要存储两个缓冲区,每个都包含nxm个顶点位置信息。在每一帧,其中一个缓冲区包含了当前的顶点位置,另一个缓冲区包含了前一个顶点位置。当我们计算新的位移时,对于每个顶点,我们用包含新顶点位置的缓冲区替代旧顶点位置的缓冲区。包含当前顶点信息的缓冲区就转换成了包含之前顶点位置的缓冲区。所以我们事实上交换的是当前用于渲染帧的缓冲区。
        为了进行光照计算,我们需要知道每个顶点的正确法线向量,以及每个顶点正确的切线向量。对于坐标为(i,j)的顶点,它(未标准化的)沿x轴的切线向量 T和沿y轴的切线向量 B可以表达为

        

  
        我们用

        
        

        
        来替换上面的偏导数,得到:
        
        

        (同样未标准化的)法线向量 N可以简单由 N = T X B计算得到,它可以这样表达

        

        用2d来乘向量 T,B,和 N,不会改变这些向量指向的方向,但是却可以消除分母:
  
        

        下面的代码演示了一个流体表面模拟的实例。认识到两次流体位移计算的时间间隔必须为常数是很重要的。大多数游戏的帧率变化较大,所以我们需要一些手段来保证表面的点仅在高帧率过后的一段时间后才更新。

代码

//============================================================================
//
// Listing 15.1
//
// Mathematics for 3D Game Programming and Computer Graphics, 3rd ed.
// By Eric Lengyel
//
// The code in this file may be freely used in any software. It is provided
// as-is, with no warranty of any kind.
//
//============================================================================


#include "VectorClasses.h"


class Fluid
{
	private:
		
		long			width;
		long			height;
		
		Vector3D		*buffer[2];
		long			renderBuffer;
		
		Vector3D		*normal;
		Vector3D		*tangent;
		float			k1, k2, k3;
	
	public:
		
		Fluid(long n, long m, float d, float t, float c, float mu);
		~Fluid();
		
		void Evaluate(void);
};

Fluid::Fluid(long n, long m, float d, float t, float c, float mu)
{
	width = n;
	height = m;
	long count = n * m;
	
	buffer[0] = new Vector3D[count];
	buffer[1] = new Vector3D[count];
	renderBuffer = 0;
	
	normal = new Vector3D[count];
	tangent = new Vector3D[count];
	
	// Precompute constants for Equation (15.25).
	float f1 = c * c * t * t / (d * d);
	float f2 = 1.0F / (mu * t + 2);
	k1 = (4.0F - 8.0F * f1) * f2;
	k2 = (mu * t - 2) * f2;
	k3 = 2.0F * f1 * f2;
	
	// Initialize buffers.
	long a = 0;
	for (long j = 0; j < m; j++)
	{
		float y = d * j;
		for (long i = 0; i < n; i++)
		{
			buffer[0][a].Set(d * i, y, 0.0F);
			buffer[1][a] = buffer[0][a];
			normal[a].Set(0.0F, 0.0F, 2.0F * d);
			tangent[a].Set(2.0F * d, 0.0F, 0.0F);
			a++;
		}
	}
}

Fluid::~Fluid()
{
	delete[] tangent;
	delete[] normal;
	delete[] buffer[1];
	delete[] buffer[0];
}

void Fluid::Evaluate(void)
{
	// Apply Equation (15.25).
	for (long j = 1; j < height - 1; j++)
	{
		const Vector3D *crnt = buffer[renderBuffer] + j * width;
		Vector3D *prev = buffer[1 - renderBuffer] + j * width;
		
		for (long i = 1; i < width - 1; i++)
		{
			prev[i].z = k1 * crnt[i].z + k2 * prev[i].z +
				k3 * (crnt[i + 1].z + crnt[i - 1].z +
				crnt[i + width].z + crnt[i - width].z);
		}
	}
	
	// Swap buffers.
	renderBuffer = 1 - renderBuffer;
	
	// Calculate normals and tangents.
	for (long j = 1; j < height - 1; j++)
	{
		const Vector3D *next = buffer[renderBuffer] + j * width;
		Vector3D *nrml = normal + j * width;
		Vector3D *tang = tangent + j * width;
		
		for (long i = 1; i < width - 1; i++)
		{
			nrml[i].x = next[i - 1].z - next[i + 1].z;
			nrml[i].y = next[i - width].z - next[i + width].z;
			tang[i].z = next[i + 1].z - next[i - 1].z;
		}
	}
}


  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: MATLAB流体力学代码是基于MATLAB软件的工具,用于模拟和分析液体和气体的力学行为。它可以实现流体动力学、热传递、质量传递的求解,通过求解Navier-Stokes方程组来模拟流体流动,了解流体行为,这对于工程师和科学家来说非常有用。 MATLAB流体力学代码的主要功能包括:建模、网格生成、边界条件设置、求解流体方程、可视化流场等。建模是指将物理模型数字化为计算机模型,通过几何模型和物理方程描述流体流动的过程。网格生成是把物理模型离散化为有限的网格,在数值算法上更易求解。边界条件设置是规定流场边界速度、温度和压强等信息。求解流体方程是求解Navier-Stokes方程组、传热方程和质量方程,以求得流场的状态参数。可视化流场是以图形方式表示流动过程,可以更好地了解流体的行为。 MATLAB流体力学代码在许多领域有着广泛的应用,包括燃气轮机设计、水力学研究、航空航天工程等。因为MATLAB流体力学代码具有易于使用、灵活性和可移植性等优点,在各种科学和工程领域得到了广泛应用,有助于科研工作的开展和工程问题的解决。 ### 回答2: MATLAB是一种功能强大的编程语言和数值计算环境,广泛应用于各个领域的科学计算和工程应用,包括流体力学研究。流体力学是研究流体力学行为以及与固体的相互作用的学科。通过使用MATLAB编写流体力学代码,可以模拟和分析各种流体流动现象,例如流体的速度场、压力场以及阻力等。 在MATLAB,可以使用不同的数值方法和数值算法来求解流体力学方程,例如Navier-Stokes方程和Bernoulli方程。此外,还可以使用有限元法、有限差分法和有限体积法等常用的数值方法来进行离散化和数值求解。 编写MATLAB流体力学代码的过程主要包括以下几个步骤: 1. 建立模型:根据具体的流体力学问题,建立相应的数学模型,例如Navier-Stokes方程。 2. 离散化:将连续的流体力学方程离散化为离散的代数方程,通常使用有限差分法、有限元法或有限体积法等方法进行离散化。 3. 数值求解:根据离散化后的代数方程,使用适当的数值方法求解流体力学方程,得到速度场、压力场等结果。 4. 后处理:对求解结果进行分析和处理,例如绘制流速矢量图、压力分布图等,以便更好地理解和解释流体流动的行为。 5. 验证和优化:通过与实验数据进行比较,验证模型的准确性和精度,并根据需要进行代码的优化和改进,以提高计算效率和精度。 总之,MATLAB流体力学代码的编写可以帮助研究者更好地理解和分析流体流动问题,为流体力学研究提供有力支持。同时,由于MATLAB具有丰富的工具箱和开发环境,使得流体力学代码的编写和求解变得更加简单和高效。 ### 回答3: MATLAB是一种常用的科学计算软件,它也被广泛应用于流体力学领域的建模和仿真工作。用户可以利用MATLAB提供的各种函数库和工具箱来编写流体力学相关的代码。 在编写流体力学代码时,首先需要了解流体力学的基本原理和数学模型。然后,根据所需的具体问题和研究目的,选择合适的数值方法和算法进行求解。常用的数值方法包括有限差分法、有限元法和有限体积法等。MATLAB提供了丰富的数值计算函数和工具箱,可以方便地实现这些数值方法。 在编写流体力学代码时,通常需要借助MATLAB的数值计算函数进行向前差分、向后差分和心差分等运算。同时,还可以利用MATLAB提供的线性代数函数进行矩阵运算和线性方程组求解。此外,MATLAB还提供了可视化函数和工具,可以将数值计算结果以图表的方式展示出来,便于用户理解和分析。 编写流体力学代码时,需要注意代码的可读性和可复用性。可以将一些常用的操作和数值计算过程封装成函数或脚本,以便后续的重复使用。同时,需要添加适当的注释和文档,方便其他用户理解和使用代码。 总结而言,MATLAB是一种强大的科学计算软件,可以用于编写流体力学代码,实现流体力学模型的数值求解和仿真。通过MATLAB提供的函数和工具箱,用户可以方便地实现各种数值方法和算法,并通过可视化函数将数值计算结果直观地展示出来。编写流体力学代码时,需要考虑代码的可读性和可复用性,并添加适当的注释和文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值