c# 模拟登陆 webbrowser 抓取_虚幻4渲染编程(图元汇编篇)【第五卷:游戏中的动力学模拟】...

17a9829efed4ddc77fadbbc8d891f84d.png

MY BLOG DIRECTORY:

小IVan:专题概述及目录​zhuanlan.zhihu.com
c52f9ffc3d8505cd4d306f9add94fa50.png

INTRODUCTION:

d3a82c5b82a5ccc374e156652a5178cf.gif

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

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

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

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

下面是本文的小结顺序:

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

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

(3)棍子模拟

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

(5)约束

(6)布料模拟

(7)发散总结


MAIN CONTENT:

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

这里有一篇纯数学的推导

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

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

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

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

11b97eabab280b540b73890f4ec713f8.png
图(1)

设:

时间为

路程为

速度为

时刻的位置为

此时刻的速度为

均为向量

用泰勒展开式展开

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

又由牛顿第二定律:

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

所以等式可以化简为

同理

可以化简为:

由 (1)+(2)可得:

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

再整理一下

为质点新的位置

为质点现在的位置

为质点老的位置

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

代码如下:

49771edd1616567672c4983bef8e8ad1.png

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

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

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

ef4e54441e98bd3c2346e4349798ba6b.png

然后定义了一个组件

31fe983ce4239845e6b9a1fbfc5a89ac.png

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

4e0bd8e5fbfb7b212c78775549835381.png

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

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

e11639b33d2cbe1a07d25a16f00b9d3d.png

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

35cf01d624d520cb3c53bbe0c01d63a8.png

这里面这个ShowPoint是一个数组

6f1ffb0d5e7d35a87bea8dac85172a12.png

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

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

220dcf872e1f560e01262ec9c4e3cca6.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。

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

f0db35454fbd9613837957ba598a7dc1.png

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

b67e28b245853cd1e9dfd2a8442017a0.png

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

7d272c54a16b9c2ee0a7053283ba7764.gif

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

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

效果如下:

70711bbc12d36bf7223df455f260a2ee.gif

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

fee3ce0f441f3e92f4b64486ce755a9c.png

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

2a839e8109c3532c3330a0dc72bfdf53.gif

【5】约束

目前约束有以下几种方法

(1)棍子约束

379def92e05bbe5122e1e3f5c1fd99c5.png

(2)软棍子约束

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

(3)钉子约束

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

8ea9b40228dd47a39976c82f550f1a4d.png

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

3bedc40349f68d437a83996c4f50c76e.png

然后你就能做各种约束啦

2452c5f7ec0ce2afeb7786fb60f3fd27.gif

【6】布料模拟

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

829b8112676200cf8a560623e273a653.png

6733edd8475a96cd7268acf7b06ee903.gif

69bcaa479de3b19f5654125a8d8302a1.gif

090131ff0cfe253a84959e3d9703be61.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();
}

SUMMARY AND OUTLOOK:

我的环境模拟篇还有更深层次的运用

YivanLee:虚幻4渲染编程(环境模拟篇)【第五卷:可交互物理植被模拟 - 上】​zhuanlan.zhihu.com
9c79296fb92db5b3c169fd39b8ac59da.png

Enjoy it !

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

64766d55e1571b887f371b6a25697969.png
https://www.zhihu.com/video/1194050466986700800

NEXT:

(图元汇编篇)【第六卷:与场景交互的软体果冻】​zhuanlan.zhihu.com
f002c1e20c62d2288aab22447c282ddc.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值