# DynamicsiMulation ![img](Dynamicsimulation.assets/v2-706ab0c301d8ec162aa7b8ffae238e45_b.jpg) 目前(2018年)在游戏中,通常使用韦尔莱积分做动力学模拟。使用韦尔莱积分可以模拟大部分物体的运动。布料,绳子,弹簧,软体,棍子都可以模拟。但是神奇的是,国内几乎找不到什么资料,找到稀少的仅有几篇看了也做不出东西或者不理解。本节将由浅入深一步一步实现各种动力学模拟的效果。 本人只是一个美术,如有错误还请各路大神斧正。 本文的实现环境是:虚幻4引擎 4.19 和VS2017 下面都是c++代码,翻译成c#在unity里也同样适用。 下面是本文的小结顺序: (1)韦尔莱积分的原理及推导 (2)我们的第一个动力学小球 (3)棍子模拟 (4)三角形稳定的几何结构模拟 (5)约束 (6)布料模拟 (7)发散总结 ------ 【1】韦尔莱积分的原理和推导 这里有一篇纯数学的推导 关于verlet算法,有人可以简单地讲解下吗? www.zhihu.com![图标](https://zhstatic.zhihu.com/assets/zhihu/editor/zhihu-card-default.svg) 但是我看了以后还是感觉有点迷,所以我再来按照我的思路再推导一次 首先先考虑一个质点。一个质点的运动在游戏里其实每帧是位置的改变,我们有一个球,在时间 t 的时候在A位置,在时间 t'’ 的时候在B位置。我们需要按照一定的方法把它从A位置移动到B位置即可,这就是我们的核心诉求。为了模拟真实世界,所以我们的移动方式需要遵循物理定律,这时候我们就把牛顿老爷子的运动三定律请出来了,我们使用运动三定律来移动我们的质点。 先来一张S-T图,以一个简单的平抛运动为例(规定向上为S正方向) ![img](Dynamicsimulation.assets/v2-bbb71b872995e18130cbdc7ce71d29ff_hd.jpg)图(1) 设: 时间为 ![t](Dynamic simulation.assets/equation.svg) 路程为 ![r](Dynamic simulation.assets/equation.svg) 速度为 ![v](Dynamic simulation.assets/equation.svg) ![t+dt](Dynamic simulation.assets/equation.svg) 时刻的位置为 ![r(t + dt)](Dynamic simulation.assets/equation.svg) ​ 此时刻的速度为 ![ v(t + dt)](Dynamic simulation.assets/equation.svg) ![r ,v](Dynamic simulation.assets/equation.svg) 均为向量 将 ![ r(t + dt)](Dynamic simulation.assets/equation.svg) 用泰勒展开式展开 ![r(Dynamic simulation.assets/equation.svg)=\frac{r(t)}{0!} + \frac{r^{'}(t)(t+dt)}{1!} + \frac{r^{''}(t)(t+dt-2)}{2!} + 余项](https://www.zhihu.com/equation?tex=r%28t%2Bdt%29%3D%5Cfrac%7Br%28t%29%7D%7B0%21%7D+%2B+%5Cfrac%7Br%5E%7B%27%7D%28t%29%28t%2Bdt%29%7D%7B1%21%7D+%2B+%5Cfrac%7Br%5E%7B%27%27%7D%28t%29%28t%2Bdt-2%29%7D%7B2%21%7D+%2B+%E4%BD%99%E9%A1%B9) 由图1所示,速度是s-t函数的一阶导数,加速度是s-t的二阶导数,所以: ![v = r^{'}(Dynamic simulation.assets/equation.svg)](https://www.zhihu.com/equation?tex=v+%3D+r%5E%7B%27%7D%28t%29) ![a = r^{''}(Dynamic simulation.assets/equation.svg)](https://www.zhihu.com/equation?tex=a+%3D+r%5E%7B%27%27%7D%28t%29) 又由牛顿第二定律: ![f = ma](Dynamic simulation.assets/equation.svg) f为力,m为质量,a为加速度 所以等式可以化简为 ![r(Dynamic simulation.assets/equation.svg)=r(t)+v(t)dt+\frac{f(t)dt^{2}}{2m} + 余项](https://www.zhihu.com/equation?tex=r%28t%2Bdt%29%3Dr%28t%29%2Bv%28t%29dt%2B%5Cfrac%7Bf%28t%29dt%5E%7B2%7D%7D%7B2m%7D+%2B+%E4%BD%99%E9%A1%B9) 同理 ![r(t - dt)](Dynamic simulation.assets/equation.svg) 可以化简为: ![r(Dynamic simulation.assets/equation.svg)=r(t)-v(t)dt+\frac{f(t)dt^{2}}{2m}+余项](https://www.zhihu.com/equation?tex=r%28t-dt%29%3Dr%28t%29-v%28t%29dt%2B%5Cfrac%7Bf%28t%29dt%5E%7B2%7D%7D%7B2m%7D%2B%E4%BD%99%E9%A1%B9) 由 (1)+(2)可得: ![r(Dynamic simulation.assets/equation.svg)\approx 2r(t)-r(t-dt)+\frac{f(t)dt^{2}}{m}](https://www.zhihu.com/equation?tex=r%28t%2Bdt%29%5Capprox+2r%28t%29-r%28t-dt%29%2B%5Cfrac%7Bf%28t%29dt%5E%7B2%7D%7D%7Bm%7D) 这里为什么可以省略约等呢,因为我们的精度要求没那么高,余项的值其实很小可以忽略。 再整理一下 ![r(Dynamic simulation.assets/equation.svg)](https://www.zhihu.com/equation?tex=r%28t+%2B+dt%29) 为质点新的位置 ![NewPosition](Dynamic simulation.assets/equation.svg) ![r(Dynamic simulation.assets/equation.svg) ](https://www.zhihu.com/equation?tex=r%28t%29+) 为质点现在的位置 ![CurPosition](Dynamic simulation.assets/equation.svg) ![r(Dynamic simulation.assets/equation.svg)](https://www.zhihu.com/equation?tex=r%28t+-+dt%29) 为质点老的位置 ![OldPosition](Dynamic simulation.assets/equation.svg) 所以我们的代码就可以写成: ![NewPosition = CurPosition + (CurPosition - OldPosition) + \frac{force *(Dynamic simulation.assets/equation.svg)^2}{m}](https://www.zhihu.com/equation?tex=NewPosition+%3D+CurPosition+%2B+%EF%BC%88CurPosition+-+OldPosition%EF%BC%89+%2B+%5Cfrac%7Bforce+%2A%28dt%29%5E2%7D%7Bm%7D) 代码如下: ![img](Dynamicsimulation.assets/v2-21e87b808f2bc17b6fd1c1a2483dfab6_hd.jpg) ------ 【2】我们第一个动力学小球 现在我们有了移动一个质点的物理学方法了,但是这还仅仅不够,我们仅仅有了一个移动质点的方法,我们现在还需要一个质点莱给我们移动,还需要显示它,还需要时间来驱动这个公式。 下面第一步,我们要先有一个质点,所以我们定义了一个质点类型 ![img](Dynamicsimulation.assets/v2-b995690ec4f22e3168ef4c5d616e2945_hd.jpg) 然后定义了一个组件 ![img](Dynamicsimulation.assets/v2-e695734a4abb1eaa88b8a59f173b07cb_hd.jpg) 在这个组件中,声明一个质点数TestParticles数组和一个力ForceDir ![img](Dynamicsimulation.assets/v2-ea4f7d6cc61e479826d504e5dcd5ef31_hd.jpg) 然后是我们的Velet函数,这个函数就是运行我们韦尔莱积分的函数了。为了不让我们的质点掉到无限远处,所以我用一个SolveConstrain函数来限定它,当它到达我定义的地面高度时,新的位置 = 老的位置,这样就相当于它停下不动了。 在组件的构造函数中出事话这些成员变量 ![img](Dynamicsimulation.assets/v2-09cca002ee0b1ae081d5c4cf6286b85e_hd.jpg) 然后在Tick函数中每帧执行我们的函数 ![img](Dynamicsimulation.assets/v2-91efaa8394d85a7634cbe1872dfe55e1_hd.jpg) 这里面这个ShowPoint是一个数组 ![img](Dynamicsimulation.assets/v2-48444e9105992a3c543df9ee9d9d156b_hd.jpg) 为了让编辑器里能方便拿到我们质点的位置,这样我们就能用这个数组在蓝图脚本里drawdebugpoint了,来辅助我们观察,让我们能看到我们的质点。 在引擎里创建一个Actor,然后把我们写的这个组件加到Actor里,然后在tick函数里每帧以Shwopoints数组draw一个DebugSphere,那么你将会看到如下效果: ![img](Dynamicsimulation.assets/v2-1087cf48ca51f0d13aefb30d737f6197_b.jpg) 我们的第一个运动的球就出来啦! 完整代码如下: h文件 ```text #pragma once #include "CoreMinimal.h" #include "UObject/ObjectMacros.h" #include "Components/MeshComponent.h" #include "VCubeComponent.generated.h" class UStaticMesh; struct FVParticle { FVParticle(): OldPos(0,0,0), CurPos(0,0,0) {} FVector OldPos; FVector CurPos; }; //This is a mesh effect component UCLASS(hidecategories = (Object, LOD, Physics, Collision), editinlinenew, meta = (BlueprintSpawnableComponent), ClassGroup = Rendering, DisplayName = "VCubeComponent") class VCUBE_API UVCubeComponent : public UMeshComponent { GENERATED_UCLASS_BODY() public: UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = VCubeComponent) TArray ShowPoints; private: //------------------------------------------------------// TArrayTestParticles; FVector forcedir; void Velet() { const float SubstepTimeSqr = 0.1; // Calc overall force //const FVector ParticleForce = FVector(0, 0, -1); for (int32 i = 0; i < TestParticles.Num(); i++) { // Find vel const FVector Vel = TestParticles[i].CurPos - TestParticles[i].OldPos; // Update position const FVector NewPosition = TestParticles[i].CurPos + Vel + (SubstepTimeSqr * forcedir); TestParticles[i].OldPos = TestParticles[i].CurPos; TestParticles[i].CurPos = NewPosition; } } void SolveConstrian() { for (int32 i = 0; i < TestParticles.Num(); i++) { FVector velocity = (TestParticles[i].CurPos - TestParticles[i].OldPos) / (TestParticles[i].CurPos - TestParticles[i].OldPos).Size(); if ((TestParticles[i].CurPos).Size() >= 800) { TestParticles[i].CurPos = TestParticles[i].OldPos; } } } }; ``` cpp文件 ```text #include "VCubeComponent.h" ////////////////////////////////////////////////////////////////////////// UVCubeComponent::UVCubeComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; bTickInEditor = true; bAutoActivate = true; ShowPoints.Reset(); TestParticles.Reset(); ShowPoints.AddUninitialized(2); TestParticles.AddUninitialized(2); forcedir = FVector(0, 0, -1); } void UVCubeComponent::OnRegister() { Super::OnRegister(); TestParticles[0].CurPos = GetOwner()->GetActorLocation(); TestParticles[1].CurPos = FVector(0, 0, 0); MarkRenderDynamicDataDirty(); } void UVCubeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Velet(); SolveConstrian(); ShowPoints[0] = TestParticles[0].CurPos; ShowPoints[1] = TestParticles[1].CurPos; // Need to send new data to render thread MarkRenderDynamicDataDirty(); UpdateComponentToWorld(); } ``` ------ 【3】棍子模拟 我们有了一个球以后,我们可以尝试再在TestParticles数组里加入第二个质点,然后蓝图里以这个质点再画一次DebugSphere。 我们现在只有了棍子的两端,这时候我们需要一个方式来让棍子的两端不碰在一起 ![img](Dynamicsimulation.assets/v2-7d36311c3a688642b89babd08fee8991_hd.jpg) 然后在Tick函数中和注册函数中: ![img](Dynamicsimulation.assets/v2-c9c23418dfa46de609d458a70d673288_hd.jpg) 于是你就能看到下面的效果了: ![img](Dynamicsimulation.assets/v2-7579a0e9fa818460dd605d798a44306e_b.jpg) ------ 【4】三角形稳定结构模拟 有了一根棍子,我们再加两根棍子,就是个闭合三角形 效果如下: ![img](Dynamicsimulation.assets/v2-387b28bab8077fe93e9317b1cefd0f70_b.jpg) 我们在constrain里指认一下就可以了 ![img](Dynamicsimulation.assets/v2-a17491b7b777d38d5f19aa7eb270e49f_hd.jpg) 我们还可以加一个点模拟四面体 ![img](Dynamicsimulation.assets/v2-79e6f342197bba2cb91bde594e7435be_b.jpg) ------ 【5】约束 目前约束有以下几种方法 (1)棍子约束 ![img](Dynamicsimulation.assets/v2-20de30a86bd91e2336c6f1177b7ad344_hd.jpg) (2)软棍子约束 软棍子约束就是,假设有A--B--C三个质点,我先对AB用棍子约束,在对AC用棍子约束多step一次 (3)钉子约束 就是让粒子不运动,给个bool判断一下是否bFree就可以了 ![img](Dynamicsimulation.assets/v2-749b7b1a46809d3ed1b8a182dfb64931_hd.jpg) (4)角度约束(群里一哥们儿的代码,他写了我就不想写了) ![img](Dynamicsimulation.assets/v2-b4a40122422d42dfbdf78b82977d2a92_hd.jpg) 然后你就能做各种约束啦 ![img](Dynamicsimulation.assets/v2-2dd6e23d0d79ae420408c7f15d1e1b82_b.jpg) ------ 【6】布料模拟 主要是约束的构建比较麻烦,需要质点构建横竖两个方向的约束,我的方法是先构建横着的,再构建竖着的。 ![img](Dynamicsimulation.assets/v2-954c9c7dce3a51789cfc7bf7f7e91548_hd.jpg) ![img](Dynamicsimulation.assets/v2-23901741ff2e0e5f4936401b86c5be57_b.jpg) ![img](Dynamicsimulation.assets/v2-daf2ef8cdb2d4886fd64e0de16acc0b6_b.jpg) ![img](Dynamicsimulation.assets/v2-967309a1c3fcd816ae7f9c7fb6546e9e_b.jpg) 然后我们就能做出布料啦! ------ 【7】发散 以上还没有使用角度约束,我们还可以用韦尔来积分制作橡胶棒,弯曲树木的躯干等等效果。还可以制作软体。 布料demo核心代码: 粒子类型和约束类型部分: ```text struct FVParticle { FVParticle(): bFree(true), OldPos(0,0,0), CurPos(0,0,0) {} bool bFree; FVector OldPos; FVector CurPos; }; struct FVConstrain { FVConstrain(){} FVParticle* particleA; FVParticle* particleB; void BuildConstrain(FVParticle& A, FVParticle& B) { particleA = &A; particleB = &B; } void SolveDistance() { if (particleA == nullptr || particleB == nullptr) return; float DisierDistance = 49; FVector Delta = particleB->CurPos - particleA->CurPos; float CurrentDistance = Delta.Size(); float ErrorFactor = (CurrentDistance - DisierDistance) / CurrentDistance; if (particleA->bFree && particleB->bFree) { particleA->CurPos += ErrorFactor * 0.5f * Delta; particleB->CurPos -= ErrorFactor * 0.5f * Delta; } else if (particleA->bFree) { particleA->CurPos += ErrorFactor * Delta; } else if (particleB->bFree) { particleB->CurPos -= ErrorFactor * Delta; } //Simple collision if ((particleA->CurPos).Size() >= 700) { particleA->CurPos = particleA->OldPos; } if ((particleB->CurPos).Size() >= 700) { particleB->CurPos = particleB->OldPos; } } void DrawDebugConstrain(UWorld* world) { DrawDebugCylinder(world, particleA->CurPos, particleB->CurPos, 1.0, 6, FColor::Blue, false, -1, 0, 1.0); } }; ``` 组件头文件部分: ```text TArrayTestParticles; TArrayConstrains; FVector forcedir; void Velet() { const float SubstepTimeSqr = 0.1; // Calc overall force //const FVector ParticleForce = FVector(0, 0, -1); for (int32 i = 0; i < TestParticles.Num(); i++) { if (TestParticles[i].bFree) { // Find vel const FVector Vel = TestParticles[i].CurPos - TestParticles[i].OldPos; // Update position const FVector NewPosition = TestParticles[i].CurPos + Vel + (SubstepTimeSqr * forcedir); TestParticles[i].OldPos = TestParticles[i].CurPos; TestParticles[i].CurPos = NewPosition; } } } void SolveDistance(FVParticle& particleA, FVParticle& particleB) { float DisierDistance = 300; FVector Delta = particleB.CurPos - particleA.CurPos; float CurrentDistance = Delta.Size(); float ErrorFactor = (CurrentDistance - DisierDistance) / CurrentDistance; if (particleA.bFree && particleB.bFree) { particleA.CurPos += ErrorFactor * 0.5f * Delta; particleB.CurPos -= ErrorFactor * 0.5f * Delta; } else if (particleA.bFree) { particleA.CurPos += ErrorFactor * Delta; } else if (particleB.bFree) { particleB.CurPos -= ErrorFactor * Delta; } //Simple collision if ((particleA.CurPos).Size() >= 800) { particleA.CurPos = particleA.OldPos; } if ((particleB.CurPos).Size() >= 800) { particleB.CurPos = particleB.OldPos; } } void SolveConstrian() { for (int32 i = 0; i < Constrains.Num(); i++) { Constrains[i].SolveDistance(); } } ``` 组件cpp部分 ```text UVCubeComponent::UVCubeComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; bTickInEditor = true; bAutoActivate = true; forcedir = FVector(0, 0, -1); } void UVCubeComponent::OnRegister() { Super::OnRegister(); int32 XSideNum = 10; int32 YSideNum = 10; int32 TestParticleNum = XSideNum * YSideNum; int32 ConstrainNum = (4 * TestParticleNum - 2 * XSideNum - 2 * YSideNum) * 0.5; ShowPoints.Reset(); TestParticles.Reset(); Constrains.Reset(); ShowPoints.AddUninitialized(TestParticleNum); TestParticles.AddUninitialized(TestParticleNum); Constrains.AddUninitialized(ConstrainNum); for (int32 Y = 0; Y < YSideNum; Y++) { for (int32 X = 0; X < XSideNum; X ++) { TestParticles[Y * XSideNum + X].CurPos = FVector(X * 50, Y * 50, 0); if ((Y == 0 && X ==0) || (Y == 0 && X == XSideNum - 1)) { //TestParticles[Y * XSideNum + X].bFree = false; } } } // @---@---@---@ constrain int32 XXSideConstrainNum = XSideNum - 1; int32 XYSideConstrainNum = YSideNum; for (int32 Y = 0; Y < XYSideConstrainNum; Y++) { for (int32 X = 0; X < XXSideConstrainNum; X++) { Constrains[Y * XXSideConstrainNum + X].BuildConstrain(TestParticles[Y * XSideNum + X], TestParticles[Y * XSideNum + X + 1]); } } // @||@||@||@ constrain int32 YXSideConstrainNum = XSideNum; int32 YYSideConstrainNum = YSideNum - 1; for (int32 Y = 0; Y < YYSideConstrainNum; Y++) { for (int32 X = 0; X < YXSideConstrainNum; X++) { Constrains[Y * YXSideConstrainNum + X + XXSideConstrainNum * XYSideConstrainNum].BuildConstrain(TestParticles[Y * YXSideConstrainNum + X], TestParticles[Y * YXSideConstrainNum + X + XSideNum]); } } MarkRenderDynamicDataDirty(); } void UVCubeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Velet(); SolveConstrian(); for (int32 i = 0; i < TestParticles.Num(); i++) { ShowPoints[i] = TestParticles[i].CurPos; } for (auto cons : Constrains) { cons.DrawDebugConstrain(GetOwner()->GetWorld()); } // Need to send new data to render thread MarkRenderDynamicDataDirty(); UpdateComponentToWorld(); } ``` 我的环境模拟篇还有更深层次的运用 YivanLee:虚幻4渲染编程(环境模拟篇)【第五卷:可交互物理植被模拟 - 上】 zhuanlan.zhihu.com![图标](Dynamic simulation.assets/v2-d37731435c7e3c4dd055d943130118fc_180x120.jpg) Enjoy it ! 最后展示一下我拿PhysicsX制作的物理吊桥: ![img](Dynamicsimulation.assets/v2-7af98dc4b8ee1518817c8c323b7cd54d_b.jpg) 编辑于 2019-05-31