虚幻4渲染编程(图元汇编篇)【第五卷:游戏中的动力学模拟】

我的专栏目录

小IVan:专题概述及目录​zhuanlan.zhihu.com图标

还是先上效果吧

v2-706ab0c301d8ec162aa7b8ffae238e45_b.gif

目前(2018年)在游戏中,通常使用韦尔莱积分做动力学模拟。使用韦尔莱积分可以模拟大部分物体的运动。布料,绳子,弹簧,软体,棍子都可以模拟。但是神奇的是,国内几乎找不到什么资料,找到稀少的仅有几篇看了也做不出东西或者不理解。本节将由浅入深一步一步实现各种动力学模拟的效果。

本人只是一个美术,如有错误还请各路大神斧正。

本文的实现环境是:虚幻4引擎 4.19 和VS2017

下面都是c++代码,翻译成c#在unity里也同样适用。

下面是本文的小结顺序:

(1)韦尔莱积分的原理及推导

(2)我们的第一个动力学小球

(3)棍子模拟

(4)三角形稳定的几何结构模拟

(5)约束

(6)布料模拟

(7)发散总结


【1】韦尔莱积分的原理和推导

这里有一篇纯数学的推导

关于verlet算法,有人可以简单地讲解下吗?​www.zhihu.com图标

但是我看了以后还是感觉有点迷,所以我再来按照我的思路再推导一次

首先先考虑一个质点。一个质点的运动在游戏里其实每帧是位置的改变,我们有一个球,在时间 t 的时候在A位置,在时间 t'’ 的时候在B位置。我们需要按照一定的方法把它从A位置移动到B位置即可,这就是我们的核心诉求。为了模拟真实世界,所以我们的移动方式需要遵循物理定律,这时候我们就把牛顿老爷子的运动三定律请出来了,我们使用运动三定律来移动我们的质点。

先来一张S-T图,以一个简单的平抛运动为例(规定向上为S正方向)

v2-bbb71b872995e18130cbdc7ce71d29ff_b.jpg
图(1)

设:

时间为 t

路程为 r

速度为 v

t+dt 时刻的位置为 r(t + dt)

此时刻的速度为 v(t + dt)

r ,v 均为向量

r(t + dt) 用泰勒展开式展开

r(t+dt)=\frac{r(t)}{0!} + \frac{r^{'}(t)(t+dt)}{1!} + \frac{r^{''}(t)(t+dt-2)}{2!} + 余项

由图1所示,速度是s-t函数的一阶导数,加速度是s-t的二阶导数,所以:

v = r^{'}(t)

a = r^{''}(t)

又由牛顿第二定律:

f = ma

f为力,m为质量,a为加速度

所以等式可以化简为

r(t+dt)=r(t)+v(t)dt+\frac{f(t)dt^{2}}{2m} + 余项

同理 r(t - dt) 可以化简为:

r(t-dt)=r(t)-v(t)dt+\frac{f(t)dt^{2}}{2m}+余项

由 (1)+(2)可得:

r(t+dt)\approx 2r(t)-r(t-dt)+\frac{f(t)dt^{2}}{m}

这里为什么可以省略约等呢,因为我们的精度要求没那么高,余项的值其实很小可以忽略。

再整理一下

r(t + dt) 为质点新的位置 NewPosition

r(t) 为质点现在的位置 CurPosition

r(t - dt) 为质点老的位置 OldPosition

所以我们的代码就可以写成:

NewPosition = CurPosition + (CurPosition - OldPosition) + \frac{force *(dt)^2}{m}

代码如下:

v2-21e87b808f2bc17b6fd1c1a2483dfab6_b.jpg

【2】我们第一个动力学小球

现在我们有了移动一个质点的物理学方法了,但是这还仅仅不够,我们仅仅有了一个移动质点的方法,我们现在还需要一个质点莱给我们移动,还需要显示它,还需要时间来驱动这个公式。

下面第一步,我们要先有一个质点,所以我们定义了一个质点类型

v2-b995690ec4f22e3168ef4c5d616e2945_b.jpg

然后定义了一个组件

v2-e695734a4abb1eaa88b8a59f173b07cb_b.jpg

在这个组件中,声明一个质点数TestParticles数组和一个力ForceDir

v2-ea4f7d6cc61e479826d504e5dcd5ef31_b.jpg

然后是我们的Velet函数,这个函数就是运行我们韦尔莱积分的函数了。为了不让我们的质点掉到无限远处,所以我用一个SolveConstrain函数来限定它,当它到达我定义的地面高度时,新的位置 = 老的位置,这样就相当于它停下不动了。

在组件的构造函数中出事话这些成员变量

v2-09cca002ee0b1ae081d5c4cf6286b85e_b.jpg

然后在Tick函数中每帧执行我们的函数

v2-91efaa8394d85a7634cbe1872dfe55e1_b.jpg

这里面这个ShowPoint是一个数组

v2-48444e9105992a3c543df9ee9d9d156b_b.jpg

为了让编辑器里能方便拿到我们质点的位置,这样我们就能用这个数组在蓝图脚本里drawdebugpoint了,来辅助我们观察,让我们能看到我们的质点。

在引擎里创建一个Actor,然后把我们写的这个组件加到Actor里,然后在tick函数里每帧以Shwopoints数组draw一个DebugSphere,那么你将会看到如下效果:

v2-1087cf48ca51f0d13aefb30d737f6197_b.gif

我们的第一个运动的球就出来啦!

完整代码如下:

h文件

#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<FVector> ShowPoints;

private:
	//------------------------------------------------------//

	TArray<FVParticle>TestParticles;

	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文件

#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。

我们现在只有了棍子的两端,这时候我们需要一个方式来让棍子的两端不碰在一起

v2-7d36311c3a688642b89babd08fee8991_b.jpg

然后在Tick函数中和注册函数中:

v2-c9c23418dfa46de609d458a70d673288_b.jpg

于是你就能看到下面的效果了:

v2-7579a0e9fa818460dd605d798a44306e_b.gif

【4】三角形稳定结构模拟

有了一根棍子,我们再加两根棍子,就是个闭合三角形

效果如下:

v2-387b28bab8077fe93e9317b1cefd0f70_b.gif

我们在constrain里指认一下就可以了

v2-a17491b7b777d38d5f19aa7eb270e49f_b.jpg

我们还可以加一个点模拟四面体

v2-79e6f342197bba2cb91bde594e7435be_b.gif

【5】约束

目前约束有以下几种方法

(1)棍子约束

v2-20de30a86bd91e2336c6f1177b7ad344_b.jpg

(2)软棍子约束

软棍子约束就是,假设有A--B--C三个质点,我先对AB用棍子约束,在对AC用棍子约束多step一次

(3)钉子约束

就是让粒子不运动,给个bool判断一下是否bFree就可以了

v2-749b7b1a46809d3ed1b8a182dfb64931_b.jpg

(4)角度约束(群里一哥们儿的代码,他写了我就不想写了)

v2-b4a40122422d42dfbdf78b82977d2a92_b.jpg

然后你就能做各种约束啦

v2-2dd6e23d0d79ae420408c7f15d1e1b82_b.gif

【6】布料模拟

主要是约束的构建比较麻烦,需要质点构建横竖两个方向的约束,我的方法是先构建横着的,再构建竖着的。

v2-954c9c7dce3a51789cfc7bf7f7e91548_b.jpg
v2-23901741ff2e0e5f4936401b86c5be57_b.gif
v2-daf2ef8cdb2d4886fd64e0de16acc0b6_b.gif
v2-967309a1c3fcd816ae7f9c7fb6546e9e_b.gif

然后我们就能做出布料啦!


【7】发散

以上还没有使用角度约束,我们还可以用韦尔来积分制作橡胶棒,弯曲树木的躯干等等效果。还可以制作软体。

布料demo核心代码:

粒子类型和约束类型部分:

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);
	}

};

组件头文件部分:

	TArray<FVParticle>TestParticles;

	TArray<FVConstrain>Constrains;

	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部分

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图标

Enjoy it !

最后展示一下我拿PhysicsX制作的物理吊桥:

v2-7af98dc4b8ee1518817c8c323b7cd54d_b.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpongo11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值