在本文的小案例中,将展示如何拓展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的数组)。
在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 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.0 f); Common.TetRiggingWorldPosition[1 ] = float4(TetRigging.TetBoneTransforms[BoneIndices.y], 1.0 f); Common.TetRiggingWorldPosition[2 ] = float4(TetRigging.TetBoneTransforms[BoneIndices.z], 1.0 f); Common.TetRiggingWorldPosition[3 ] = float4(TetRigging.TetBoneTransforms[BoneIndices.w], 1.0 f); 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); float3 BlendTranslatedPosition = DFFastLocalSubtractDemote(BlendWorldPosition.xyz, DFNegate(ResolvedView.PreViewTranslation)); return float4(BlendTranslatedPosition, 1.0 f); ... }
为了便捷,这里的做法完全无视了法线,渲染生成的法线和真实法线是对不上的,想要在实际项目里落地可能要考虑在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 () 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 ; 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; }; ... 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 ;}; ... 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); if (LocalVertexFactory->SupportsManualVertexFetch (FeatureLevel) || UseGPUScene (GMaxRHIShaderPlatform, FeatureLevel)) { ShaderBindings.Add (Shader->GetUniformBufferParameter <FLocalVertexFactoryUniformShaderParameters>(), LocalVertexFactory->GetUniformBuffer ()); } 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) ; ... }; template <typename ConstraintContainerType>class TIndexedConstraintContainerSolver : public FConstraintContainerSolver{ public : ... 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 ); } const FVec3 Difference = WorldSpaceX2 - WorldSpaceX1; const FReal Distance = Difference.Size (); if (Distance < FReal (1e-7 )) { return FVec3 (0 ); } const FVec3 Direction = Difference / Distance; const FVec3 Delta = (Distance - SpringSettings[ConstraintIndex].RestLength) * Direction; const FReal CombinedInvMass = Body0.InvM () + Body1.InvM (); 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; 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; 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约束还缺这些工作:
在底层,需要将FPBDRigidSpringConstraints注册到Solver中,确保其能被物理线程驱动求解。
需要创建对应的Proxy类型,负责在物理线程和游戏线程之间交换数据
创建一个能被游戏线程引用的Constraint Handle
在接口层创建对应的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 : ... FORCEINLINE FPBDRigidSpringConstraints& GetSpringConstraints () { return SpringConstraints; ... private : ... 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 { ... RigidSpringConstraintType = 5 , ... }; ... enum class EPhysicsProxyType : uint32{ ... RigidSpringProxy, ... }; ... 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) ; void CHAOS_API DestroyOnPhysicsThread (FPBDRigidsSolver* InSolver) ; void CHAOS_API DestroyOnGameThread () ; private : FRigidSpringConstraint* Constraint_GT; 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{ ... CHAOS_API void RegisterObject (FRigidSpringConstraint* GTConstraint, const FRigidSpringConstraintParams& Params) ; CHAOS_API void UnregisterObject (FRigidSpringConstraint* GTConstraint) ; ... }; ... void FPBDRigidsSolver::RegisterObject (FRigidSpringConstraint* GTConstraint, const FRigidSpringConstraintParams& Params) { FRigidSpringConstraintPhysicsProxy* ConstraintProxy = new FRigidSpringConstraintPhysicsProxy (GTConstraint, Params); ConstraintProxy->SetSolver (this ); AddDirtyProxy (ConstraintProxy); } void FPBDRigidsSolver::UnregisterObject (FRigidSpringConstraint* GTConstraint) { FRigidSpringConstraintPhysicsProxy* ConstraintProxy = GTConstraint->GetProxy <FRigidSpringConstraintPhysicsProxy>(); check (ConstraintProxy); RemoveDirtyProxy (ConstraintProxy); GTConstraint->GetProxy ()->MarkDeleted (); GTConstraint->SetProxy (static_cast <FCharacterGroundConstraintProxy*>(nullptr )); ConstraintProxy->DestroyOnGameThread (); 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 ()) { ... 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){ ... DirtyProxiesData->ForEachProxy ([this , Manager, RewindData](int32 DataIdx, FDirtyProxy& Dirty) { if (Dirty.Proxy->GetIgnoreDataOnStep_Internal () != CurrentFrame) { switch (Dirty.Proxy->GetType ()) { ... 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 : ... 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) ; ... }; ... 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; } 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 ; } } } }
至此,我们已经完成了所有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 void UTetRiggingComponent::OnCreatePhysicsState () { RecreateControlPointBodySetups (); 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 ; } for (FPhysicsConstraintHandle& ConstraintHandle : SpringHandles) { if (ConstraintHandle.IsValid ()) { FPhysicsInterface::ReleaseConstraintSpring (ConstraintHandle); } } SpringHandles.Empty (); 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 ; } ControlPointBodies.Reserve (BodyCount); for (int32 BodyIndex = 0 ; BodyIndex < BodyCount; BodyIndex++) { USphereComponent* ControlPointBody = NewObject <USphereComponent>(this ); ControlPointBody->SetSphereRadius (ControlPointRadius); ControlPointBody->SetVisibility (false , true ); ControlPointBody->SetHiddenInGame (true ); ControlPointBody->SetCastShadow (false ); ControlPointBody->SetMobility (EComponentMobility::Movable); ControlPointBody->SetCollisionEnabled (ECollisionEnabled::QueryAndPhysics); ControlPointBody->SetCollisionProfileName (UCollisionProfile::PhysicsActor_ProfileName); ControlPointBody->SetGenerateOverlapEvents (false ); ControlPointBody->SetupAttachment (this ); ControlPointBody->SetRelativeTransform (GetControlPointRestTransform (BodyIndex).GetRelativeTransform (GetComponentTransform ())); ControlPointBody->RegisterComponent (); ControlPointBody->SetSimulatePhysics (true ); ControlPointBody->SetEnableGravity (true ); if (Mass > 0.0f ) { ControlPointBody->SetMassOverrideInKg (NAME_None, Mass, true ); } ControlPointBodies.Add (ControlPointBody); } { 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 (); 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 ; struct KeyFuncs { using KeyType = FTetSpringEdge; using KeyInitType = const FTetSpringEdge&; using ElementInitType = const FTetSpringEdge&; enum { bAllowDuplicateKeys = false }; static KeyInitType GetSetKey (ElementInitType Edge) { return Edge; } 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); } } } ... 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引擎相关的代码,目前只是验证了原理,缺乏打磨。当前主要还存在这些问题:
需要修正法线计算,在模拟时法线随着顶点位置更新。现在的demo通过将材质的漫反射和高光项置0,只设置纯色自发光来规避了这点。
在开启SSGI和Lumen时,间接光渲染会出问题,还需要修正。
用高速子弹撞击质点的时候会导致弹簧的方向翻转进而导致软体会永久性凹陷。
目前只有控制点处的刚体有碰撞,刚体之间存在缝隙,会穿模。
UE实现的PBD弹簧Stiffness没有物理意义,调参纯凭感觉。这个问题已经在后续的XPBD中解决了。可以考虑用XPBD改进这个算法(不过看起来UE在实现Chaos的时候没给XPBD留接口,可能需要对底层接口有较大改动)