在本文的小案例中,将展示如何拓展UE的渲染系统以及物理系统,来实现一个由弹簧质点系统驱动的软体模拟demo。

首先对vibe coder一句话总结思路:对模型凸包加四面体化创建控制点以及四面体笼,每个顶点根据其在四面体笼内的重心坐标烘焙出索引以及权重。在UE中导入该数据,在模型VS中获取索引权重数据并混合得到各个顶点的世界空间位置。随后在运行时,给每个四面体的每个控制点创建一个刚体,四面体每条边由弹簧链接。其中弹簧在Chaos引擎底层对应约束FRigidSpringConstraint ,这东西没有Game Thread的接口,因此需要手动拓展Chaos引擎的接口开放给GT调用。

本文的案例基于UE 5.4.2版本的代码。

1. 模型渲染

这部分不是重点,就快速过一下思路。

首先是模型来源的问题,笔者之前尝试用tripo免费额度直出一个乃龙模型,在网站上看着效果还可以,不过基本没法直接拿到blender中编辑,同时导入UE中似乎也会有些奇奇怪怪的问题,所以最后还是在网上找了个免费模型…

tripo还有个智能拓扑模式,生成的模型我只能说,目前指望一次性直出还不现实…

首先是创建一个可以让模型自由形变的骨骼。思路很简单:让模型放在四面体cage中,每个顶点计算其在四面体内的重心坐标,并对四个顶点做线性插值,从而实现随着四面体的形变移动的效果。为了保证所有顶点都在一个四面体内部,一个简单办法是对原始几何体计算凸包,然后对这个凸包做四面体分割。

以这个思路为提示词叫AI vibe了一个python小demo出来。效果如下图所示。AI做快速原型的能力实在是恐怖。

这个方法其实挺糙的,对于存在四肢的类人形生物会在头到胳膊之间创造大量空白区域。可能需要在算凸包之前分析几何体的结构(对这方面不了解,可能会让demo做的很复杂)

这个程序会导出一个json文件,假设有N个顶点的模型创建了M个四面体,这些四面体共有Q个控制点,bones_rest代表了在闲置状态下Q个控制点的局部空间坐标(Qx3的数组),tets代表了M个四面体顶点到控制点的索引(Mx4数组),vertex bones和weights则分别代表了各个模型原顶点分别对应哪些控制点以及控制点以及权重(Nx4的数组)。

image-20260930164731367

在UE中落地的思路很简单:在VS中(UE中对应VertexFactory)传入骨骼的索引权重数据以及世界空间位置,根据VertexID取出对应顶点数据,混合后,得到形变后的顶点世界空间坐标。这里我直接在LocalVertexFactory.ush的基础上做修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/** Intermediates that are common to all supported vertex factory input types */
struct FVertexFactoryIntermediatesCommon
{
...
// 骨骼混合的权重和顶点位置
float4x4 TetRiggingWorldPosition;
float4 TetRiggingWeights;
...
};



void FetchTetRiggingVertexData(int VertexId, inout FVertexFactoryIntermediatesCommon Common)
{
int4 BoneIndices = TetRigging.TetBoneIndices[VertexId];

Common.TetRiggingWorldPosition[0] = float4(TetRigging.TetBoneTransforms[BoneIndices.x], 1.0f);
Common.TetRiggingWorldPosition[1] = float4(TetRigging.TetBoneTransforms[BoneIndices.y], 1.0f);
Common.TetRiggingWorldPosition[2] = float4(TetRigging.TetBoneTransforms[BoneIndices.z], 1.0f);
Common.TetRiggingWorldPosition[3] = float4(TetRigging.TetBoneTransforms[BoneIndices.w], 1.0f);

Common.TetRiggingWeights = TetRigging.TetBoneWeights[VertexId];
}

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
...
FetchTetRiggingVertexData(Input.VertexId, Intermediates.Common);
...
}

float4 CalcWorldPosition(FVertexFactoryIntermediatesCommon Intermediates, float4 Position, FDFMatrix LocalToWorld)
{
...

// 我们想要混合的是每一行的加权求和而非每列的加权求和
// 因此要让行向量乘上矩阵
float4 BlendWorldPosition = mul(Intermediates.TetRiggingWeights, Intermediates.TetRiggingWorldPosition);

// UE5为了支持大世界渲染,一般不会直接传WorldToView矩阵,而是会传入double精度的世界空间到相机空间平移变换的位置差值
// 我们必须在完成世界空间计算后在投影计算开始前做这样一次手动转换, 而且是用这些DF开头的double浮点数计算专用的函数
// 函数明明是叫CalcWorldPosition但计算结果却不是世界空间坐标, UE很神奇吧...
float3 BlendTranslatedPosition = DFFastLocalSubtractDemote(BlendWorldPosition.xyz, DFNegate(ResolvedView.PreViewTranslation));
return float4(BlendTranslatedPosition, 1.0f);
...
}

为了便捷,这里的做法完全无视了法线,渲染生成的法线和真实法线是对不上的,想要在实际项目里落地可能要考虑在cpu端处理法线计算或单独弄个compute shader pass计算法线。

随后就是CPU这边的绑定代码,VertexFactory的定义和uniform buffer的声明,VertexFactory直接继承自FLocalVertexFactory,在绑定时只需附加上我们新加的TetRigging相关buffer的绑定逻辑即可。(UE这里设计的太狗屎了,本来很多活反射可以做的,现在不得不写一大堆乱七八糟的代码来绑定资源)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
...
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FTetRiggingVertexFactoryParameters,)
SHADER_PARAMETER_SRV(StructuredBuffer<float3>, TetBoneTransforms)
SHADER_PARAMETER_SRV(StructuredBuffer<int4>, TetBoneIndices)
SHADER_PARAMETER_SRV(StructuredBuffer<float4>, TetBoneWeights)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

// 在GetElementShaderBindings过程中会用到引用buffer的指针
struct FTetRiggingVertexFactoryUserData
{
TUniformBufferRef<FTetRiggingVertexFactoryParameters> RiggingUniformBuffer;
};
...

class FTetRiggingVertexFactory : public FLocalVertexFactory
{
DECLARE_VERTEX_FACTORY_TYPE_API(FTetRiggingVertexFactory, TETRIGGING_API)

FTetRiggingVertexFactory(ERHIFeatureLevel::Type InFeatureLevel, const char* InDebugName = "FTetRiggingVertexFactory")
: FLocalVertexFactory(InFeatureLevel, InDebugName)
{
}

TETRIGGING_API virtual void InitRHI(FRHICommandListBase& RHICmdList) override;
virtual void ReleaseRHI() override;

// 从LocalVertexFactory拷贝所有LocalVertexFactory的属性
void CopyFromLocalVertexFactory(const FLocalVertexFactory& LocalVertexFactory);

void UpdateRiggingUniformBuffer(FRHICommandListBase& RHICmdList, FRHIShaderResourceView* TetBoneTransformsSRV
, FRHIShaderResourceView* TetBoneIndicesSRV, FRHIShaderResourceView* TetBoneWeightsSRV);

public:
TUniformBufferRef<FTetRiggingVertexFactoryParameters> GetRiggingUniformBuffer() {return RiggingUniformBuffer;}
const FTetRiggingVertexFactoryUserData* GetUserData() const {return &UserData;}

private:
TUniformBufferRef<FTetRiggingVertexFactoryParameters> RiggingUniformBuffer;
FTetRiggingVertexFactoryUserData UserData;
};
...
// 自定义VertexFactory的绑定规则
class FTetRiggingVertexFactoryShaderParameters : public FVertexFactoryShaderParameters
{
DECLARE_TYPE_LAYOUT(FTetRiggingVertexFactoryShaderParameters, NonVirtual);
public:
void Bind(const FShaderParameterMap& ParameterMap);

void GetElementShaderBindings(
const class FSceneInterface* Scene,
const FSceneView* InView,
const class FMeshMaterialShader* Shader,
const EVertexInputStreamType InputStreamType,
ERHIFeatureLevel::Type FeatureLevel,
const FVertexFactory* VertexFactory,
const FMeshBatchElement& BatchElement,
class FMeshDrawSingleShaderBindings& ShaderBindings,
FVertexInputStreamArray& VertexStreams
) const;
};
...
//将VertexFactory绑定到对应的Shader上
IMPLEMENT_VERTEX_FACTORY_TYPE(FTetRiggingVertexFactory, "/Plugin/TetRigging/Private/TetRiggingVertexFactory.ush",
EVertexFactoryFlags::UsedWithMaterials
| EVertexFactoryFlags::SupportsStaticLighting
| EVertexFactoryFlags::SupportsDynamicLighting
| EVertexFactoryFlags::SupportsPositionOnly
| EVertexFactoryFlags::SupportsCachingMeshDrawCommands
| EVertexFactoryFlags::SupportsPrimitiveIdStream
| EVertexFactoryFlags::SupportsPSOPrecaching
| EVertexFactoryFlags::SupportsManualVertexFetch
);
IMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE(FTetRiggingVertexFactory, SF_Vertex, FTetRiggingVertexFactoryShaderParameters);
IMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE(FTetRiggingVertexFactory, SF_Pixel, FTetRiggingVertexFactoryShaderParameters);

IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FTetRiggingVertexFactoryParameters, "TetRigging");
IMPLEMENT_TYPE_LAYOUT(FTetRiggingVertexFactoryShaderParameters);

void FTetRiggingVertexFactoryShaderParameters::GetElementShaderBindings(const class FSceneInterface* Scene, const FSceneView* InView, const class FMeshMaterialShader* Shader,const EVertexInputStreamType InputStreamType, ERHIFeatureLevel::Type FeatureLevel, const FVertexFactory* VertexFactory, const FMeshBatchElement& BatchElement, class FMeshDrawSingleShaderBindings& ShaderBindings, FVertexInputStreamArray& VertexStreams) const
{
const FLocalVertexFactory* LocalVertexFactory = static_cast<const FLocalVertexFactory*>(VertexFactory);

// 手动顶点获取(Manual Vertex Fetch)或 GPUScene 时,绑定 LocalVF 的 uniform buffer,
// 与 FLocalVertexFactoryShaderParametersBase::GetElementShaderBindingsBase 保持一致。
if (LocalVertexFactory->SupportsManualVertexFetch(FeatureLevel) || UseGPUScene(GMaxRHIShaderPlatform, FeatureLevel))
{
ShaderBindings.Add(Shader->GetUniformBufferParameter<FLocalVertexFactoryUniformShaderParameters>(), LocalVertexFactory->GetUniformBuffer());
}

// 绑定自定义 rigging uniform buffer(TetBoneTransforms / TetBoneIndices / TetBoneWeights)
const FTetRiggingVertexFactoryUserData* UserData = static_cast<const FTetRiggingVertexFactoryUserData*>(BatchElement.UserData);
if (UserData)
{
ShaderBindings.Add(Shader->GetUniformBufferParameter<FTetRiggingVertexFactoryParameters>(), UserData->RiggingUniformBuffer);
}
}

关于Proxy应该在StaticMeshSceneProxy 基础上继承还是继承UPrimitiveSceneProxy 我也纠结了一下,最终还是感觉明明是带动画的对象还叫Static Mesh语义上有点奇怪,而且类似功能的Skeletal Mesh也不是在Static Mesh的基础上继承的。这部分代码比较繁琐就不展示了。

最后,每帧从物理线程获取顶点数据后只要更新Uniform Buffer中的TetBoneTransforms 这个buffer即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void UTetRiggingComponent::PushRenderData()
{
...
TArray<FVector3f> Data = Component->GetCurrentBoneTransforms();
ENQUEUE_RENDER_COMMAND(TetRiggingSendDynamicData)(
[InSceneProxy, Component, InData = MoveTemp(Data)](FRHICommandListImmediate& RHICmdList)
{
if (Component.IsValid())
{
InSceneProxy->UpdateDynamicData(InData, RHICmdList);
}
}
);
}
...
void FTetRiggingSceneProxy::UpdateDynamicData(const TArray<FVector3f>& Position, FRHICommandListBase& CmdList)
{
if (!bHasValidRiggingData)
{
return;
}

check(Position.Num() == CachedPositions.Num());
CachedPositions = Position;

TransformBuffer.UpdateDynamicData(CmdList, CachedPositions, RLM_WriteOnly);
}
...
void FTetRiggingTransformBuffer::UpdateDynamicData(FRHICommandListBase& RHICmdList, const TArray<FVector3f>& Positions, EResourceLockMode LockMode)
{
check(Positions.Num() == NumBoneCount);

void* Data = RHICmdList.LockBuffer(VertexBufferRHI, 0, GetResourceSize(), LockMode);
if (Data)
{
FMemory::Memcpy(Data, Positions.GetData(), GetResourceSize());
}
RHICmdList.UnlockBuffer(VertexBufferRHI);
}

2. 物理模拟

整个系统的物理模拟本文选择用弹簧质点系统驱动—这玩意看名字很容易理解,就是由一大堆由弹簧链接的质点组成的系统。

在UE中,质点可以由关闭了渲染的UPrimitiveComponent来表示—每个质点对应一个UPrimitiveComponent。笔者尝试过用更底层的FBodyInstance 作为质点,但这个东西似乎和UPrimitiveComponent耦合的比较深,单独创建容易出问题,所以放弃了(不得不感概UE的代码写的真狗屎,UPrimitiveComponent把渲染,物理,网络功能全部耦合到一起了,分都分不开。想要类似unity的rigid body component这样干净的对象可能得从FSingleParticleProxy这些更底层的接口上封装了)。

而对于弹簧,我们首先简单介绍一下PBD弹簧的理论。

在PBD中,物体之间的交互行为被建模成约束 ,PBD通过修正物体的位置 来满足约束进而反推物体的速度。

而弹簧可以视作被软化了的等距离约束 , 这种约束的 (其中 为粒子i的质量的倒数)。由于弹簧被软化了,因此在求解约束的时候不强制要求 ,而是加入缩放参数 (也就是Stiffness)控制软化的程度: 。

幸运的是UE已经实现了基于PBD的刚体弹簧约束以及求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FPBDRigidSpringConstraints : public TPBDIndexedConstraintContainer<FPBDRigidSpringConstraints>
{
public:
...
void ApplyPositionConstraints(const TArrayView<int32>& ConstraintIndices, const FReal Dt, const int32 It, const int32 NumIts);
...
};

// 求解Rigid Spring约束用到的Solver
// 在FPBDRigidSpringConstraints的基础上做了简单封装
template<typename ConstraintContainerType>
class TIndexedConstraintContainerSolver : public FConstraintContainerSolver
{
public:
...
// Chaos的PBD求解器在求解过程中会调用各个Solver的ApplyPositionConstraints接口来实现位置修正(对应公式中p'=p+dp)
virtual void ApplyPositionConstraints(const FReal Dt, const int32 It, const int32 NumIts) override final
{
ConstraintContainer.ApplyPositionConstraints(ConstraintIndices, Dt, It, NumIts);
}
...
};

求解的代码如下所示,基本与前文介绍的公式能一一对应上,与公式的对应关系已在注释中给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 求解弹簧约束的主要代码路径
void FPBDRigidSpringConstraints::ApplyPositionConstraints(const TArrayView<int32>& ConstraintIndices, const FReal Dt, const int32 It, const int32 NumIts)
{
for (int32 ConstraintIndex : ConstraintIndices)
{
ApplyPhase1Single(Dt, ConstraintIndex);
}
}
...
FVec3 FPBDRigidSpringConstraints::GetDelta(int32 ConstraintIndex, const FVec3& WorldSpaceX1, const FVec3& WorldSpaceX2) const
{
FSolverBody& Body0 = *ConstraintSolverBodies[ConstraintIndex][0];
FSolverBody& Body1 = *ConstraintSolverBodies[ConstraintIndex][1];

if (!Body0.IsDynamic() && !Body1.IsDynamic())
{
return FVec3(0);
}

// p_0 - p_1
const FVec3 Difference = WorldSpaceX2 - WorldSpaceX1;
const FReal Distance = Difference.Size();
if (Distance < FReal(1e-7))
{
return FVec3(0);
}

// (p_0 - p_1) / |p_0 - p_1|
const FVec3 Direction = Difference / Distance;
// (|p_0 - p_1| - d_rest) * (p_0 - p_1) / |p_0 - p_1|
const FVec3 Delta = (Distance - SpringSettings[ConstraintIndex].RestLength) * Direction;
const FReal CombinedInvMass = Body0.InvM() + Body1.InvM();

// delta = k * (1 / (w_0 + w_1)) * (|p_0 - p_1| - d_rest) * (p_0 - p_1) / |p_0 - p_1|
return SpringSettings[ConstraintIndex].Stiffness * Delta / CombinedInvMass;
}
...
void FPBDRigidSpringConstraints::ApplyPhase1Single(const FReal Dt, int32 ConstraintIndex) const
{
check(ConstraintSolverBodies[ConstraintIndex][0] != nullptr);
check(ConstraintSolverBodies[ConstraintIndex][1] != nullptr);
FSolverBody& Body0 = *ConstraintSolverBodies[ConstraintIndex][0];
FSolverBody& Body1 = *ConstraintSolverBodies[ConstraintIndex][1];
const FVec3 BodyP0 = Body0.CorrectedP();
const FRotation3 BodyQ0 = Body0.CorrectedQ();
const FVec3 BodyP1 = Body1.CorrectedP();
const FRotation3 BodyQ1 = Body1.CorrectedQ();

if (!Body0.IsDynamic() && !Body1.IsDynamic())
{
return;
}

const FVec3 WorldSpaceX1 = BodyQ0.RotateVector(Distances[ConstraintIndex][0]) + BodyP0;
const FVec3 WorldSpaceX2 = BodyQ1.RotateVector(Distances[ConstraintIndex][1]) + BodyP1;
const FVec3 Delta = GetDelta(ConstraintIndex, WorldSpaceX1, WorldSpaceX2);

if (Body0.IsDynamic())
{
const FVec3 Radius = WorldSpaceX1 - BodyP0;
// w_0 * delta = k * (w_0 / (w_0 + w_1)) * (|p_0 - p_1| - d_rest) * (p_0 - p_1) / |p_0 - p_1|
const FVec3 DX = Body0.InvM() * Delta;
const FVec3 DR = Body0.InvI() * FVec3::CrossProduct(Radius, Delta);
Body0.ApplyTransformDelta(DX, DR);
Body0.UpdateRotationDependentState();
}

if (Body1.IsDynamic())
{
const FVec3 Radius = WorldSpaceX2 - BodyP1;
//- w_1 * delta = - k * (w_1 / (w_0 + w_1)) * (|p_0 - p_1| - d_rest) * (p_0 - p_1) / |p_0 - p_1|
const FVec3 DX = Body1.InvM() * -Delta;
const FVec3 DR = Body1.InvI() * FVec3::CrossProduct(Radius, -Delta);
Body1.ApplyTransformDelta(DX, DR);
Body1.UpdateRotationDependentState();
}
}

然而这部分代码并没有开放上层Game Thread接口调用,因此,我们不得不自己手动扩展Chaos物理引擎,创建对应的接口。

整个Chaos物理引擎底层的求解器与上层Game Thread之间关键类的逻辑层级结构如下图所示:

对于每个物理对象,Chaos都会创建一个Physics Proxy, Physics Proxy处于游戏线程和物理线程之间的物理接口层,负责在两个线程之间转发数据和状态。在游戏逻辑层,Component会持有Proxy对应的游戏层Handle并通过Handle操作Proxy。而在物理层, 每个Proxy持有物理层对象Handle,物理层的真实对象则会由其Container持有(对于粒子Container是FPBDRigidSOAs)并由Solver维护。

对于这个流程,目前Rigid Spring约束还缺这些工作:

  1. 在底层,需要将FPBDRigidSpringConstraints注册到Solver中,确保其能被物理线程驱动求解。
  2. 需要创建对应的Proxy类型,负责在物理线程和游戏线程之间交换数据
  3. 创建一个能被游戏线程引用的Constraint Handle
  4. 在接口层创建对应的Create和Destroy接口

首先是在Solver中注册对应的Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class FPBDRigidsEvolutionGBF : public FPBDRigidsEvolutionBase
{
public:
...
// 新加获取对应Container的接口
FORCEINLINE FPBDRigidSpringConstraints& GetSpringConstraints() { return SpringConstraints;
...
private:
...
// 新加SpringConstraints Container
FPBDRigidSpringConstraints SpringConstraints;
...
};

FPBDRigidsEvolutionGBF::FPBDRigidsEvolutionGBF(
FPBDRigidsSOAs& InParticles,
THandleArray<FChaosPhysicsMaterial>& SolverPhysicsMaterials,
const TArray<ISimCallbackObject*>* InMidPhaseModifiers,
const TArray<ISimCallbackObject*>* InCCDModifiers,
const TArray<ISimCallbackObject*>* InStrainModifiers,
const TArray<ISimCallbackObject*>* InCollisionModifiers,
bool InIsSingleThreaded)
...
{
...
// 将弹簧质点约束注册到系统中
AddConstraintContainer(SpringConstraints, ChaosSolverSpringConstraintPriority);
...
}

class FPBDRigidsSolver : public FPhysicsSolverBase
{
public:
...
// 暴露相关接口
FPBDRigidSpringConstraints& GetSpringConstraints() { return MEvolution->GetSpringConstraints(); }
const FPBDRigidSpringConstraints& GetSpringConstraints() const { return MEvolution->GetSpringConstraints(); }
...
};

这可以保证FPBDRigidsEvolutionGBF 在求解过程中调用到ApplyPositionConstraints 。

接着,我们需要定义弹簧约束对应的Proxy以及Game Thread持有的Handle,并在Proxy中处理弹簧的初始化逻辑和部分数据更新逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
enum class EConstraintType
{
...
// 注册新类型到ConstraintType中
RigidSpringConstraintType = 5,
...
};
...
enum class EPhysicsProxyType : uint32
{
...
// 注册新的Proxy类型
RigidSpringProxy,
...
};
...
// GameThread访问的数据,提供给Game Thread访问Physics Thread一些变量的状态
class FRigidSpringConstraint : public FConstraintBase
{
public:
...
};


struct FRigidSpringConstraintParams
{
TVector<FVector, 2> Locations;
FReal Stiffness;
FReal Damping;
FReal RestLength;
};
...
// 刚体弹簧约束的物理线程代理
class FRigidSpringConstraintPhysicsProxy : public IPhysicsProxyBase
{
public:
...
FRigidSpringConstraintPhysicsProxy(FRigidSpringConstraint* InConstraint, const FRigidSpringConstraintParams& Params, UObject* InOwner = nullptr);
// 创建因约束初始化时需要调用的接口
void CHAOS_API InitializeOnPhysicsThread(FPBDRigidsSolver* InSolver, FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData);
// 向游戏线程更新物理线程的数据
void CHAOS_API PushStateOnGameThread(FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData);
// 向物理线程更新游戏线程的数据
void CHAOS_API PushStateOnPhysicsThread(FPBDRigidsSolver* InSolver, const FDirtyPropertiesManager& Manager, int32 DataIdx, const FDirtyChaosProperties& RemoteData);
// 销毁Proxy在物理线程的副本
void CHAOS_API DestroyOnPhysicsThread(FPBDRigidsSolver* InSolver);
// 销毁Proxy在游戏线程的副本
void CHAOS_API DestroyOnGameThread();

private:
// 约束在游戏线程对应对象的引用
FRigidSpringConstraint* Constraint_GT;
// 约束在物理线程对应对象的引用,FPBDRigidSpringConstraintHandle由Constraint Container的AddConstraint接口创建
FPBDRigidSpringConstraintHandle* Constraint_PT;

// 约束的几个属性
TVector<FVector, 2> Locations;
FReal Stiffness;
FReal Damping;
FReal RestLength;
};
...

FRigidSpringConstraintPhysicsProxy::FRigidSpringConstraintPhysicsProxy(FRigidSpringConstraint* InConstraint, const FRigidSpringConstraintParams& Params,
UObject* InOwner)
:IPhysicsProxyBase(EPhysicsProxyType::RigidSpringProxy, InOwner, MakeShared<FProxyTimestampBase>()),
Constraint_GT(InConstraint),
RestLength(Params.RestLength),
Locations(Params.Locations),
Stiffness(Params.Stiffness),
Damping(Params.Damping)
{
check(Constraint_GT != nullptr);
Constraint_GT->SetProxy(this);
}

void FRigidSpringConstraintPhysicsProxy::InitializeOnPhysicsThread(FPBDRigidsSolver* InSolver, FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData)
{
auto& Handles = InSolver->GetParticles().GetParticleHandles();
if (Handles.Size())
{
auto& SpringConstraints = InSolver->GetSpringConstraints();
if(const FPhysicsObjectPairProperty* BodyPairs = RemoteData.FindRigidSpringObjects(Manager, DataIdx))
{
FGeometryParticleHandle* Particle0 = BodyPairs->PhysicsBodies[0]->GetParticle<Chaos::EThreadContext::Internal>();
FGeometryParticleHandle* Particle1 = BodyPairs->PhysicsBodies[1]->GetParticle<Chaos::EThreadContext::Internal>();
FPBDRigidSpringConstraints::FConstrainedParticlePair ParticlePair{Particle0, Particle1};
if (Particle0 && Particle1)
{
Constraint_PT = SpringConstraints.AddConstraint(
ParticlePair,Locations,
Stiffness, Damping, RestLength
);

Particle0->AddConstraintHandle(Constraint_PT);
Particle1->AddConstraintHandle(Constraint_PT);

FGenericParticleHandle(Particle0)->SetInertiaConditioningDirty();
FGenericParticleHandle(Particle1)->SetInertiaConditioningDirty();
}
}
}
}

void FRigidSpringConstraintPhysicsProxy::PushStateOnGameThread(FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData)
{
if (Constraint_GT != nullptr)
{
Constraint_GT->SyncRemoteData(Manager, DataIdx, RemoteData);
}
}

void FRigidSpringConstraintPhysicsProxy::PushStateOnGameThread(FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData)
{
if (Constraint_GT != nullptr)
{
Constraint_GT->SyncRemoteData(Manager, DataIdx, RemoteData);
}
}

void FRigidSpringConstraintPhysicsProxy::PushStateOnPhysicsThread(FPBDRigidsSolver* InSolver, const FDirtyPropertiesManager& Manager, int32 DataIdx, const FDirtyChaosProperties& RemoteData)
{

}

void FRigidSpringConstraintPhysicsProxy::DestroyOnPhysicsThread(FPBDRigidsSolver* InSolver)
{
if (Constraint_PT)
{
TVec2<FGeometryParticleHandle*> Particles = Constraint_PT->GetConstrainedParticles();

if(Particles[0])
{
Particles[0]->RemoveConstraintHandle(Constraint_PT);
}

if(Particles[1])
{
Particles[1]->RemoveConstraintHandle(Constraint_PT);
}

if(Constraint_PT->IsInConstraintGraph())
{
InSolver->GetEvolution()->RemoveConstraintFromConstraintGraph(Constraint_PT);
}

FPBDRigidsSolver::FJointConstraints& JointConstraints = InSolver->GetJointConstraints();
InSolver->GetSpringConstraints().RemoveConstraint(Constraint_PT->GetConstraintIndex());
Constraint_PT = nullptr;
}
}

void FRigidSpringConstraintPhysicsProxy::DestroyOnGameThread()
{
delete Constraint_GT;
Constraint_GT = nullptr;
}

接着,模仿其它Proxy的创建和销毁流程,我们照葫芦画瓢,完成FRigidSpringConstraintPhysicsProxy 的初始化和销毁逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class FPBDRigidsSolver : public FPhysicsSolverBase
{
...
// 创建GameThread的约束后,调用RegisterObject创建对应的Proxy以及ConstraintHandle
CHAOS_API void RegisterObject(FRigidSpringConstraint* GTConstraint, const FRigidSpringConstraintParams& Params);
// 在销毁GameThread的Constraint时同时销毁物理线程的Constraint Handle
CHAOS_API void UnregisterObject(FRigidSpringConstraint* GTConstraint);
...
};
...
void FPBDRigidsSolver::RegisterObject(FRigidSpringConstraint* GTConstraint, const FRigidSpringConstraintParams& Params)
{
// 注册过程中创建对应Proxy并将其添加到DirtyProxy等待队列中等待系统更新
// RegisterObject一般都是在GameThread中执行,因此需要将Proxy的更新逻辑放到队列中延迟更新
FRigidSpringConstraintPhysicsProxy* ConstraintProxy = new FRigidSpringConstraintPhysicsProxy(GTConstraint, Params);
ConstraintProxy->SetSolver(this);

AddDirtyProxy(ConstraintProxy);
}

void FPBDRigidsSolver::UnregisterObject(FRigidSpringConstraint* GTConstraint)
{
// 销毁对应Proxy,并添加Physics Thread Command在物理线程调用DestroyOnPhysicsThread销毁Proxy物理线程的Handle
FRigidSpringConstraintPhysicsProxy* ConstraintProxy = GTConstraint->GetProxy<FRigidSpringConstraintPhysicsProxy>();
check(ConstraintProxy);

RemoveDirtyProxy(ConstraintProxy);

// mark proxy timestamp so we avoid trying to pull from sim after deletion
GTConstraint->GetProxy()->MarkDeleted();
GTConstraint->SetProxy(static_cast<FCharacterGroundConstraintProxy*>(nullptr));

ConstraintProxy->DestroyOnGameThread(); //destroy the game thread portion of the proxy

// Finish de-registration on the physics thread...
EnqueueCommandImmediate([ConstraintProxy, this]()
{

ConstraintProxy->DestroyOnPhysicsThread(this);
RigidSpringConstraintPhysicsProxies_Internal.RemoveSingle(ConstraintProxy);
delete ConstraintProxy;
});
}
...
// 从物理线程向游戏线程同步数据
void FPBDRigidsSolver::PushPhysicsState(const FReal DeltaTime, const int32 NumSteps, const int32 NumExternalSteps)
{
...
DirtyProxiesData->ParallelForEachProxy([this, DynamicsWeight, Manager, ShapeDirtyData](int32 DataIdx, FDirtyProxy& Dirty)
{
switch(Dirty.Proxy->GetType())
{
...
// 调用PushStateOnGameThread向游戏线程同步数据
case EPhysicsProxyType::RigidSpringProxy:
{
auto Proxy = static_cast<FRigidSpringConstraintPhysicsProxy*>(Dirty.Proxy);
Proxy->PushStateOnGameThread(*Manager, DataIdx, Dirty.PropertyData);
Proxy->ResetDirtyIdx();
break;
}
...
}
}
...
}
...
void FPBDRigidsSolver::ProcessSinglePushedData_Internal(FPushPhysicsData& PushData)
{
...
// 遍历需要创建持久化Handle的Proxy
DirtyProxiesData->ForEachProxy([this, Manager, RewindData](int32 DataIdx, FDirtyProxy& Dirty)
{
if (Dirty.Proxy->GetIgnoreDataOnStep_Internal() != CurrentFrame)
{
switch (Dirty.Proxy->GetType())
{
...
// 调用InitializeOnPhysicsThread初始化对应的PhysicsThread Handle
case EPhysicsProxyType::RigidSpringProxy:
{
auto ConstraintProxy = static_cast<FRigidSpringConstraintPhysicsProxy*>(Dirty.Proxy);
const bool bIsNew = !ConstraintProxy->IsInitialized();
if (bIsNew)
{
RigidSpringConstraintPhysicsProxies_Internal.Add(ConstraintProxy);
ConstraintProxy->InitializeOnPhysicsThread(this, *Manager, DataIdx, Dirty.PropertyData);
ConstraintProxy->SetInitialized(GetCurrentFrame());
}
ConstraintProxy->PushStateOnPhysicsThread(this, *Manager, DataIdx, Dirty.PropertyData);
break;
}
}
}
});
...
}

最后,我们只需要在GameThread开放约束的创建以及销毁接口即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class FPhysInterface_Chaos : public FGenericPhysicsInterface, public FChaosEngineInterface
{
public:
...
// 用于添加弹簧质点约束的扩展API
static ENGINE_API FPhysicsConstraintHandle CreateConstraintSpring(const FPhysicsActorHandle& InActorRef1,const FPhysicsActorHandle& InActorRef2, FVector AttachPoint0, FVector AttachPoint1, float Stiffness, float Damping);
static ENGINE_API void ReleaseConstraintSpring(FPhysicsConstraintHandle& InConstraintHandle);
...
};
...
// 创建FRigidSpringConstraint并调用Solver的RegisterObject,注册该Constraint
FPhysicsConstraintHandle FPhysInterface_Chaos::CreateConstraintSpring(const FPhysicsActorHandle& InActorRef1, const FPhysicsActorHandle& InActorRef2, FVector AttachPoint0, FVector AttachPoint1, float Stiffness, float Damping)
{
Chaos::FPhysicsObject* Body1 = InActorRef1 ? InActorRef1->GetPhysicsObject() : nullptr;
Chaos::FPhysicsObject* Body2 = InActorRef2 ? InActorRef2->GetPhysicsObject() : nullptr;
if (!Body1 || !Body2)
{
return FPhysicsConstraintHandle{};
}

Chaos::FPhysicsSolver* Solver = Chaos::FPhysicsObjectInterface::GetSolver({&Body1, 1});
if (!Solver)
{
return FPhysicsConstraintHandle{};
}

FPhysicsConstraintHandle ConstraintHandle;
Chaos::FRigidSpringConstraint* SpringConstraint = new Chaos::FRigidSpringConstraint();
ConstraintHandle.Constraint = SpringConstraint;

SpringConstraint->SetPhysicsBodies({Body1, Body2});
Chaos::FRigidSpringConstraintParams ConstraintParams;
ConstraintParams.Locations[0] = AttachPoint0;
ConstraintParams.Locations[1] = AttachPoint1;
ConstraintParams.Stiffness = Stiffness;
ConstraintParams.Damping = Damping;
ConstraintParams.RestLength = (AttachPoint1 - AttachPoint0).Length();

Solver->RegisterObject(SpringConstraint, ConstraintParams);

return ConstraintHandle;
}

// 调用Solver的UnregisterObject接口,销毁Constraint
void FPhysInterface_Chaos::ReleaseConstraintSpring(FPhysicsConstraintHandle& InConstraintHandle)
{
if (InConstraintHandle.IsValid())
{
check(InConstraintHandle.Constraint->IsType(Chaos::EConstraintType::SpringConstraintType));
if (Chaos::FRigidSpringConstraint* Constraint = static_cast<Chaos::FRigidSpringConstraint*>(InConstraintHandle.Constraint))
{
if (Chaos::FRigidSpringConstraintPhysicsProxy* Proxy = Constraint->GetProxy<Chaos::FRigidSpringConstraintPhysicsProxy>())
{
Chaos::FPhysicsSolver* Solver = Proxy->GetSolver<Chaos::FPhysicsSolver>();
Solver->UnregisterObject(Constraint);

InConstraintHandle.Constraint = nullptr; // freed by the joint constraint physics proxy
}
}
}
}

至此,我们已经完成了所有Chaos引擎的拓展工作。

随后在UTetRiggingComponent中实现弹簧质点系统创建的逻辑即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// 覆写UPrimitiveComponent的OnCreatePhysicsState和OnDestroyPhysicsState,处理物理状态创建相关的逻辑
void UTetRiggingComponent::OnCreatePhysicsState()
{
RecreateControlPointBodySetups();

// 调用Super::OnCreatePhysicsState会初始化Component自身的FBodyInstance的初始化
// 这个Component自身不应该有刚体,因此我们应该跳过UPrimitiveComponent的OnCreatePhysicsState直接调用UActorComponent::OnCreatePhysicsState
UActorComponent::OnCreatePhysicsState();
OnComponentPhysicsStateChanged.Broadcast(this, EComponentPhysicsStateChange::Created);
}

void UTetRiggingComponent::OnDestroyPhysicsState()
{
ReleaseControlPointBodySetups();

UActorComponent::OnDestroyPhysicsState();
OnComponentPhysicsStateChanged.Broadcast(this, EComponentPhysicsStateChange::Destroyed);
}
...
void UTetRiggingComponent::ReleaseControlPointBodySetups()
{
if (!HasValidRiggingData())
{
UE_LOG(LogTemp, Error, TEXT("[%s, %d] TetRigging data is invalid"), UTF8_TO_TCHAR(__FUNCTION__), __LINE__);
return;
}

// 先销毁弹簧约束(其引用了刚体的 ActorHandle)
for (FPhysicsConstraintHandle& ConstraintHandle : SpringHandles)
{
if (ConstraintHandle.IsValid())
{
// 调用我们添加的ReleaseConstraintSpring接口销毁弹簧
FPhysicsInterface::ReleaseConstraintSpring(ConstraintHandle);
}
}
SpringHandles.Empty();

// 再销毁控制点刚体 Component
for (TObjectPtr<UPrimitiveComponent>& ControlPointBody : ControlPointBodies)
{
if (ControlPointBody)
{
ControlPointBody->DestroyComponent();
}
}
ControlPointBodies.Empty();
}
...
void UTetRiggingComponent::RecreateControlPointBodySetups()
{
if (!HasValidRiggingData())
{
UE_LOG(LogTemp, Error, TEXT("[%s, %d] TetRigging data is invalid"), UTF8_TO_TCHAR(__FUNCTION__), __LINE__);
return;
}

ReleaseControlPointBodySetups();

const int32 BodyCount = TetRiggingData->GetNumBones();

UWorld* World = GetWorld();
if (!World || !World->GetPhysicsScene())
{
UE_LOG(LogTemp, Error, TEXT("[%s, %d] Physics Scene is nullptr"),UTF8_TO_TCHAR(__FUNCTION__), __LINE__);
return;
}

// 每个控制点用一个独立的 UPrimitiveComponent 承载刚体(不参与渲染),
// 通过 UPrimitiveComponent::BodyInstance.ActorHandle 创建弹簧约束。
ControlPointBodies.Reserve(BodyCount);
for (int32 BodyIndex = 0; BodyIndex < BodyCount; BodyIndex++)
{
USphereComponent* ControlPointBody = NewObject<USphereComponent>(this);
ControlPointBody->SetSphereRadius(ControlPointRadius);

// 控制点仅用于物理,不参与渲染
ControlPointBody->SetVisibility(false, /*bPropagateToChildren=*/true);
ControlPointBody->SetHiddenInGame(true);
ControlPointBody->SetCastShadow(false);

ControlPointBody->SetMobility(EComponentMobility::Movable);
ControlPointBody->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
ControlPointBody->SetCollisionProfileName(UCollisionProfile::PhysicsActor_ProfileName);
ControlPointBody->SetGenerateOverlapEvents(false);

// 必须在注册前确定初始 Transform,物理体才会在正确的静止位置生成
ControlPointBody->SetupAttachment(this);
ControlPointBody->SetRelativeTransform(GetControlPointRestTransform(BodyIndex).GetRelativeTransform(GetComponentTransform()));

ControlPointBody->RegisterComponent();

// 参与仿真并受重力影响
ControlPointBody->SetSimulatePhysics(true);
ControlPointBody->SetEnableGravity(true);

// 设置控制点质量,Mass <= 0 时保留自动计算的质量
if (Mass > 0.0f)
{
ControlPointBody->SetMassOverrideInKg(NAME_None, Mass, /*bOverrideMass=*/true);
}

ControlPointBodies.Add(ControlPointBody);
}

// 生成对应数量的Constraint
{
auto CreateTetEdgeSpring = [&](int32 BodyIndex0, int32 BodyIndex1)
{
if (!ControlPointBodies.IsValidIndex(BodyIndex0) || !ControlPointBodies.IsValidIndex(BodyIndex1))
{
return;
}

UPrimitiveComponent* Body0 = ControlPointBodies[BodyIndex0];
UPrimitiveComponent* Body1 = ControlPointBodies[BodyIndex1];
if (!Body0 || !Body1)
{
return;
}

FVector Location0 = GetControlPointRestTransform(BodyIndex0).GetLocation();
FVector Location1 = GetControlPointRestTransform(BodyIndex1).GetLocation();
// 调用我们添加的CreateConstraintSpring接口创建弹簧
FPhysicsConstraintHandle Handle = FPhysicsInterface::CreateConstraintSpring(Body0->BodyInstance.ActorHandle,
Body1->BodyInstance.ActorHandle,
Location0, Location1, Stiffness, Damping);
if (Handle.IsValid())
{
SpringHandles.Add(Handle);
}
};

struct FTetSpringEdge
{
int32 BodyIndex0 = 0;
int32 BodyIndex1 = 0;

// TSet 的 KeyFuncs 需要提供 KeyType / KeyInitType / ElementInitType /
// bAllowDuplicateKeys / GetSetKey / GetKeyHash / Matches。
struct KeyFuncs
{
using KeyType = FTetSpringEdge;
using KeyInitType = const FTetSpringEdge&;
using ElementInitType = const FTetSpringEdge&;

enum { bAllowDuplicateKeys = false };

static KeyInitType GetSetKey(ElementInitType Edge) { return Edge; }

// 边是无向的:(a,b) 与 (b,a) 是同一条边,所以哈希也必须与顺序无关。
static uint32 GetKeyHash(KeyInitType Edge)
{
const int32 MinIndex = FMath::Min(Edge.BodyIndex0, Edge.BodyIndex1);
const int32 MaxIndex = FMath::Max(Edge.BodyIndex0, Edge.BodyIndex1);
return HashCombine(GetTypeHash(MinIndex), GetTypeHash(MaxIndex));
}

static bool Matches(KeyInitType A, KeyInitType B)
{
return (A.BodyIndex0 == B.BodyIndex0 && A.BodyIndex1 == B.BodyIndex1)
|| (A.BodyIndex0 == B.BodyIndex1 && A.BodyIndex1 == B.BodyIndex0);
}
};
};
TSet<FTetSpringEdge, FTetSpringEdge::KeyFuncs> Edges;

// 避免有重复边
for (int32 TetIndex = 0; TetIndex < TetRiggingData->GetNumTets(); TetIndex++)
{
FIntVector4 TetIndices = TetRiggingData->Tets[TetIndex];

Edges.Add(FTetSpringEdge{ TetIndices[0], TetIndices[3] });
Edges.Add(FTetSpringEdge{ TetIndices[1], TetIndices[3] });
Edges.Add(FTetSpringEdge{ TetIndices[2], TetIndices[3] });
Edges.Add(FTetSpringEdge{ TetIndices[0], TetIndices[2] });
Edges.Add(FTetSpringEdge{ TetIndices[1], TetIndices[2] });
Edges.Add(FTetSpringEdge{ TetIndices[0], TetIndices[1] });
}

for (auto& Edge : Edges)
{
CreateTetEdgeSpring(Edge.BodyIndex0, Edge.BodyIndex1);
}
}
}
...
// 在运行时,直接读取各个控制点的COMPosition获取其世界空间位置
// 这些数据随后通过PushRenderData发送到渲染线程并更新buffer
TArray<FVector3f> UTetRiggingComponent::GetCurrentBoneTransforms()
{
TArray<FVector3f> CurrentBoneTransforms;
CurrentBoneTransforms.AddUninitialized(TetRiggingData->GetNumBones());
if (ControlPointBodies.Num())
{
for (int32 BodyIndex = 0; BodyIndex < ControlPointBodies.Num(); BodyIndex++)
{
UPrimitiveComponent* ControlPointBody = ControlPointBodies[BodyIndex];
if (ControlPointBody)
{
CurrentBoneTransforms[BodyIndex] = FVector3f(ControlPointBody->BodyInstance.GetCOMPosition());
}
}
}
else
{
for (int32 BodyIndex = 0; BodyIndex < TetRiggingData->GetNumBones(); BodyIndex++)
{
CurrentBoneTransforms[BodyIndex] = FVector3f(GetControlPointRestTransform(BodyIndex).GetLocation());
}
}
return CurrentBoneTransforms;
}

3. 后续改进

做这个demo主要目的还是想梳理UE Chaos引擎相关的代码,目前只是验证了原理,缺乏打磨。当前主要还存在这些问题:

  1. 需要修正法线计算,在模拟时法线随着顶点位置更新。现在的demo通过将材质的漫反射和高光项置0,只设置纯色自发光来规避了这点。
  2. 在开启SSGI和Lumen时,间接光渲染会出问题,还需要修正。
  3. 用高速子弹撞击质点的时候会导致弹簧的方向翻转进而导致软体会永久性凹陷。
  4. 目前只有控制点处的刚体有碰撞,刚体之间存在缝隙,会穿模。
  5. UE实现的PBD弹簧Stiffness没有物理意义,调参纯凭感觉。这个问题已经在后续的XPBD中解决了。可以考虑用XPBD改进这个算法(不过看起来UE在实现Chaos的时候没给XPBD留接口,可能需要对底层接口有较大改动)