UE5破碎系统浅析
场与破碎阈值
Field
场可以造成物体破碎,也可以用于固定物体等
UE中使用AFieldSystemActor
来管理场,AFieldSystemActor
中的FieldSystemComponent
用于创建场。从蓝图的角度看,我们会创建一个继承自AFieldSystemActor
的蓝图类来自定义场,如官方示例中的FS_AnchorField_Generic
,FS_MasterField
以及FS_SleepDisable_Generic
场分为三种
-
Transient Field
:瞬时场。最常用的场,可以通过BeginPlay,Tick或者是Event的形式生成并生效 -
Persistent Field
:持久场 -
Construction Field
:构造场。需要在构造函数中创建,并需要在Geometry Collection
中注册。用途可以参照官方的FS_SleepDisable_Generic
AGeometryCollectionActor
通常为摆在场景中,代表一个可破坏的Geometry
class GEOMETRYCOLLECTIONENGINE_API AGeometryCollectionActor: public AActor
{
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UGeometryCollectionComponent> GeometryCollectionComponent;
};
UGeometryCollectionComponent
中维护了一个UGeometryCollection
,以及各项Destruction相关的数据
class GEOMETRYCOLLECTIONENGINE_API UGeometryCollectionComponent : public UMeshComponent, public IChaosNotifyHandlerInterface
{
UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "ChaosPhysics")
TObjectPtr<const UGeometryCollection> RestCollection;
// 等等和碰撞有关的数据...
};
UGeometryCollection
是一个UObject
,其中也维护了和UGeometryCollectionComponent
中重复的Destruction数据。在生成AGeometryCollectionActor
时,会将数据拷贝至AGeometryCollectionActor
的UGeometryCollectionComponent
中,此方法通过ActorFactory实现
UActorFactoryGeometryCollection::UActorFactoryGeometryCollection(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
DisplayName = LOCTEXT("GeometryCollectionDisplayName", "GeometryCollection");
NewActorClass = AGeometryCollectionActor::StaticClass();
}
void UActorFactoryGeometryCollection::PostSpawnActor(UObject* Asset, AActor* NewActor)
{
Super::PostSpawnActor(Asset, NewActor);
// ...
// Set configured clustering properties.
NewGeometryCollectionActor->GetGeometryCollectionComponent()->EnableClustering = GeometryCollection->EnableClustering;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->ClusterGroupIndex = GeometryCollection->ClusterGroupIndex;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->MaxClusterLevel = GeometryCollection->MaxClusterLevel;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->SetPhysMaterialOverride(GEngine->DefaultDestructiblePhysMaterial);
// ...
}
DamageThreshold
在UGeometryCollectionComponent
中,DamageThreshold
控制了物体不同的损坏层级
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<float> DamageThreshold;
array index | value |
0 | 2000 |
1 | 1000 |
2 | 1500 |
假设物体的损坏层级和数组的大小相同,那么当"Damage"处于0 - 2000时,物体处于原状,当2000 - (2000 + 1000) 时,物体处于损坏一级,当 (2000+1000) - (2000 + 1000 + 1500) 时,处于损坏二级,以此类推
这里UE虽然取名为
DamageThreshold
,但它与AActor::TakeDamage
并无关系,它在UE结算的底层起始被称作Straintemplate<class T, int d> class TPBDRigidClusteredParticles : public TPBDRigidParticles<T, d> { const auto& Strains(int32 Idx) const { return MStrains[Idx]; } };
引擎对DamageThreshold的计算方式与上述有些出入,但上述描述更易于理解,具体可见以下代码
TSet<FPBDRigidParticleHandle*> FRigidClustering::ReleaseClusterParticlesImpl( FPBDRigidClusteredParticleHandle* ClusteredParticle, const TMap<FGeometryParticleHandle*, Chaos::FReal>* ExternalStrainMap, bool bForceRelease, bool bCreateNewClusters)
场与DamageThreshold
以瞬态场为例,设为该场添加了RadialFalloff FieldNode
,下面剖析RadialFalloff
是如何影响到场景中物体的DamageThreshold
,以造成不同等级的物体破碎效果的
// Display As "Add Transient Field" In Blueprint
void UFieldSystemComponent::ApplyPhysicsField(bool Enabled, EFieldPhysicsType Target, UFieldSystemMetaData* MetaData, UFieldNodeBase* Field)
{
BuildFieldCommand(Enabled, Target, MetaData, Field, true);
}
构建一个场关键注意两个参数
UFieldNodeBase
:用于评估Field作用域的节点,其中承载了各项评估数据。上图中的Field Magnitude
是用于累加于Strain上,并以此来评估DamageThreshold
的
UCLASS()
class FIELDSYSTEMENGINE_API UFieldNodeBase : public UActorComponent
{
GENERATED_BODY()
public:
virtual ~UFieldNodeBase() {}
virtual FFieldNodeBase::EFieldType Type() const { return FFieldNodeBase::EFieldType::EField_None; }
virtual bool ResultsExpector() const { return false; }
// 把UFieldNodeBase Component中的数据 拷贝到对应的new出来的数据类FFieldNodeBase 用于后续评估
virtual FFieldNodeBase* NewEvaluationGraph(TArray<const UFieldNodeBase*>& Nodes) const { return nullptr; }
};
UE使用UFieldNodeBase
作为可视化组件编辑,FFieldNodeBase
作为数据载体进行后续运算,FFieldNodeBase
的继承结构大致如下图所示
/**
* FieldNode<T>
*
* Typed field nodes are used for the evaluation of specific types of data arrays.
* For exampe, The FFieldNode<FVector>::Evaluate(...) will expect resutls
* of type TFieldArrayView<FVector>, and an example implementation is the UniformVectorField.
*
*/
template<class T>
class FFieldNode : public FFieldNodeBase
{
public:
virtual ~FFieldNode() {}
// 评估计算
virtual void Evaluate(FFieldContext&, TFieldArrayView<T>& Results) const = 0;
static EFieldType StaticType();
virtual EFieldType Type() const { return StaticType(); }
};
template<> inline FFieldNodeBase::EFieldType FFieldNode<int32>::StaticType() { return EFieldType::EField_Int32; }
template<> inline FFieldNodeBase::EFieldType FFieldNode<float>::StaticType() { return EFieldType::EField_Float; }
template<> inline FFieldNodeBase::EFieldType FFieldNode<FVector>::StaticType() { return EFieldType::EField_FVector; }
EFieldPhysicsType
:在底层用作判断不同UFieldNodeBase*
类型的枚举值(compare and static_cast)
瞬态场与持久场
- 调用
UFieldSystemComponent::ApplyPhysicsField
或UFieldSystemComponent::AddPersistentField
构建场 - 调用
FFieldObjectCommands::CreateFieldCommand
创建FFieldSystemCommand
,其中包含了TUniquePtr<FFieldNodeBase>
以及其他数据,然后将该指令Dispatch到物理线程 - 物理线程中循环调用
FPerSolverFieldSystem::FieldParameterUpdateCallback
,若发现有待执行的指令,则处理
void FPerSolverFieldSystem::FieldParameterUpdateCallback(
Chaos::FPBDRigidsSolver* InSolver,
Chaos::FPBDPositionConstraints& PositionTarget,
TMap<int32, int32>& TargetedParticles)
{
if (InSolver && !InSolver->IsShuttingDown())
{
FieldParameterUpdateInternal(InSolver, PositionTarget, TargetedParticles, TransientCommands, true);
FieldParameterUpdateInternal(InSolver, PositionTarget, TargetedParticles, PersistentCommands, false);
}
}
- 根据指令中记录的
FFieldNodeBase::EFieldType
与EFieldPhysicsType
,对先前创建的节点的类型进行区分
// FPerSolverFieldSystem::FieldParameterUpdateInternal
if (FieldCommand.RootNode->Type() == FFieldNodeBase::EFieldType::EField_Int32) {}
else if (FieldCommand.RootNode->Type() == FFieldNodeBase::EFieldType::EField_Float) {}
else if (FieldCommand.RootNode->Type() == FFieldNodeBase::EFieldType::EField_FVector) {}
- 以
EFieldPhysicsType::Field_ExternalClusterStrain
为例
if (FieldCommand.PhysicsType == EFieldPhysicsType::Field_ExternalClusterStrain) {
TMap<Chaos::FGeometryParticleHandle*, Chaos::FReal> ExternalStrain;
// 评估 获得要施加破碎力的采样点
static_cast<const FFieldNode<float>*>(FieldCommand.RootNode.Get())->
Evaluate(FieldContext, ResultsView);
for (const FFieldContextIndex& Index : FieldContext.GetEvaluatedSamples()) {
if (ResultsView[Index.Result] > 0) {
ExternalStrain.Add(ParticleHandles[Index.Sample], ResultsView[Index.Result]);
}
}
// 收集完力后 进一步计算模型的破碎情况
UpdateSolverBreakingModel(RigidSolver, ExternalStrain);
}
最终计算是否产生破碎效果的代码为
// FRigidClustering::BreakingMode
AllActivatedChildren.Add(ClusteredParticle, ReleaseClusterParticles(ClusteredParticle, ExternalStrainMap));
// FRigidClustering::ReleaseClusterParticles
if (ChildStrain >= Child->Strain() || bForceRelease) {}
构造场
场Actor的基类是AFieldSystemActor
,以锚点场为例
- 在
AFieldSystemActor
的ConstructionScript
中初始化FieldNode并调用"Add Construction Field"
- 在
AFieldSystemActor
的OnConstruction
中将UFieldNodeBase
等信息记录在记录在ConstructionCommands
数组中
蓝图中
ConstructionScript
的执行先与代码中的OnConstruction
/** * Construction script, the place to spawn components and do other setup. * @note Name used in CreateBlueprint function */ UFUNCTION(BlueprintImplementableEvent, meta=(BlueprintInternalUseOnly = "true", DisplayName = "Construction Script")) void UserConstructionScript(); /** * Called when an instance of this class is placed (in editor) or spawned. * @param Transform The transform the actor was constructed at. */ virtual void OnConstruction(const FTransform& Transform) {}
UGeometryCollectionComponent
在Register中(runtime)拿出InitializationFields
中记录的数据,进行遍历并构造FFieldSystemCommand
,最后传递给物理线程处理
UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "ChaosPhysics")
TArray<TObjectPtr<const AFieldSystemActor>> InitializationFields;
PhysicsState
对于可破碎的物体来讲,在Editor模式下进行切割后,需要在运行时设置好各种物理状态,以在以后的物理循环中进行模拟
主要看UGeometryCollectionComponent这个类
- 在组件执行完注册后,开始创建物理状态(UE会根据是否需要生成overlap事件等因素来判断是否需要延迟创建物理状态,这里不讨论)
void UActorComponent::ExecuteRegisterEvents(FRegisterComponentContext* Context) {
// ...
OnRegister();
// ...
CreatePhysicsState(/*bAllowDeferral=*/true);
}
- 创建物理状态
void UGeometryCollectionComponent::OnCreatePhysicsState()
{
UActorComponent::OnCreatePhysicsState();
// ...
TManagedArray<int32> & DynamicState = DynamicCollection->DynamicState;
// if this code is changed you may need to account for bStartAwake
EObjectStateTypeEnum LocalObjectType =
(ObjectType != EObjectStateTypeEnum::Chaos_Object_Sleeping) ? ObjectType :
EObjectStateTypeEnum::Chaos_Object_Dynamic;
// 如果不是Chaos_Object_UserDefined用户自定义的ObjectState
if (LocalObjectType != EObjectStateTypeEnum::Chaos_Object_UserDefined)
{
if (RestCollection && (LocalObjectType == EObjectStateTypeEnum::Chaos_Object_Dynamic))
{
TManagedArray<int32>& InitialDynamicState =
RestCollection->GetGeometryCollection()->InitialDynamicState;
// 设置每一个Particles的状态
for (int i = 0; i < DynamicState.Num(); i++) {
DynamicState[i] = (InitialDynamicState[i] ==
static_cast<int32>(Chaos::EObjectStateType::Uninitialized)) ?
static_cast<int32>(LocalObjectType) : InitialDynamicState[i];
}
}
else
{
for (int i = 0; i < DynamicState.Num(); i++)
{
DynamicState[i] = static_cast<int32>(LocalObjectType);
}
}
}
// ...
// 初始化PhysicsProxy并传递到物理线程
if (BodyInstance.bSimulatePhysics) {
RegisterAndInitializePhysicsProxy();
}
}
- 初始化PhysicsProxy
void UGeometryCollectionComponent::RegisterAndInitializePhysicsProxy()
{
FSimulationParameters SimulationParameters;
// 初始化SimulationParameters中各项数据...
// ...
// 获构造场中的命令 将在物理线程Tick前被使用
GetInitializationCommands(SimulationParameters.InitializationCommands);
// 将PhysicsProxy添加至物理线程中
PhysicsProxy = new FGeometryCollectionPhysicsProxy
(this, *DynamicCollection, SimulationParameters, InitialSimFilter, InitialQueryFilter);
FPhysScene_Chaos* Scene = GetInnerChaosScene();
Scene->AddObject(this, PhysicsProxy);
RegisterForEvents();
SetAsyncPhysicsTickEnabled(GetIsReplicated());
}
粒子数据获取
ObjectState
锚点场的本质就是将GeometryCollection中的部分粒子设置为Static或Kismet的状态,以此来进行固定的作用。当物体处于静态或者刚体状态时,就无法再对非物理模拟的刚体进行碰撞响应(如动画)。在Gameplay中有多种方式对GeometryCollection中粒子的状态进行设置
- 重写
OnCreatePhysicsState
if (FGeometryDynamicCollection* GeometryDynamicCollection = const_cast<FGeometryDynamicCollection*>(GetDynamicCollection()); bSetRootGeometryStatic && GeometryDynamicCollection)
{
// 将ObjectState设置为Chaos_Object_UserDefined后 就可以通过DynamicState数组来控制粒子的初始状态
TManagedArray<int32>& DynamicState = GeometryDynamicCollection->DynamicState;
if (DynamicState.Num() > 0)
{
DynamicState[0] = static_cast<int32>(Chaos::EObjectStateType::Static);
}
}
Super::OnCreatePhysicsState();
- 读取PhysicsProxy,直接设置
if (const FGeometryCollectionPhysicsProxy* PhysicsProxy = GeometryCollectionComponent->GetPhysicsProxy())
{
const TArray<FGeometryCollectionPhysicsProxy::FClusterHandle*>& ClusterHandleArray =
PhysicsProxy->GetParticles();
Chaos::FPBDRigidsSolver* Solver = PhysicsProxy->GetSolver<Chaos::FPBDRigidsSolver>();
for (FGeometryCollectionPhysicsProxy::FClusterHandle* ClusterHandle : ClusterHandleArray)
{
// 需要进行判空 避免物理线程上的对象还未初始化
if (ClusterHandle)
{
// 设置为Dynamic
Solver->GetEvolution()->SetParticleObjectState(ClusterHandle, Chaos::EObjectStateType::Dynamic);
}
}
}
ClusterHandle
匀速场中力的作用是通过是直接拿到Handle
,然后修改其速度实现的(修改发生在物理线程中)
Chaos::FPBDRigidParticleHandle* RigidHandle = ParticleHandles[Index.Sample]->CastToRigidParticle();
if (RigidHandle && RigidHandle->ObjectState() == Chaos::EObjectStateType::Dynamic)
{
RigidHandle->V() += ResultsView[Index.Result];
}
下面演示如何在AGeometryCollectionActor
本身中获取他所管理的粒子(Gameplay主线程中获取)
FString ParticlesDebugMessage = GetName() += " ParticlesInfo:\n";
// 获取当前对象所有模拟中的粒子
if (const FGeometryCollectionPhysicsProxy* PhysicsProxy = GeometryCollectionComponent->GetPhysicsProxy())
{
const TArray<FGeometryCollectionPhysicsProxy::FClusterHandle*>& ClusterHandleArray = PhysicsProxy->GetParticles();
for (FGeometryCollectionPhysicsProxy::FClusterHandle* ClusterHandle : ClusterHandleArray)
{
if (ClusterHandle == nullptr)
{
continue;
}
if (bHideDebugInfoWhenZeroVelocity && (ClusterHandle->V() == FVector::Zero() || ClusterHandle->W() == FVector::Zero()))
{
continue;
}
ParticlesDebugMessage += "Linear Velocity: " + ClusterHandle->V().ToString() + " " +
"Angular Velocity:" + ClusterHandle->W().ToString() + " ";
ParticlesDebugMessage += "CollisionImpulse: " + FString::SanitizeFloat(ClusterHandle->CollisionImpulse()) + " ";
ParticlesDebugMessage += "Stain: " + FString::SanitizeFloat(ClusterHandle->Strain()) + " ";
ParticlesDebugMessage += "Mass: " + FString::SanitizeFloat(ClusterHandle->M()) + " ";
// 将Chaos::EObjectStateType转换为带反射的EObjectStateTypeEnum
ParticlesDebugMessage += "ObjectState: " + StaticEnum<EObjectStateTypeEnum>()->GetNameByValue(static_cast<int64>(ClusterHandle->ObjectState())).ToString() + "\n";
}
}
GEngine->AddOnScreenDebugMessage(-1, 0, FColor::Red, ParticlesDebugMessage);
Proxy
下次一定
Bug汇总
- 在工厂中只对部分属性进行复制,这也意味着配置在UObject上的部分数据无法得到使用
void UActorFactoryGeometryCollection::PostSpawnActor(UObject* Asset, AActor* NewActor)
{
Super::PostSpawnActor(Asset, NewActor);
// ...
// Set configured clustering properties.
NewGeometryCollectionActor->GetGeometryCollectionComponent()->EnableClustering = GeometryCollection->EnableClustering;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->ClusterGroupIndex = GeometryCollection->ClusterGroupIndex;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->MaxClusterLevel = GeometryCollection->MaxClusterLevel;
NewGeometryCollectionActor->GetGeometryCollectionComponent()->SetPhysMaterialOverride(GEngine->DefaultDestructiblePhysMaterial);
// 如DamageThreshold数据就没有进行拷贝
// ...
}
UFieldSystemComponent::ApplyStayDynamicField
方法无法对Cluster Level大于等于2的物体生效- 返回非引用的const对象
// GeometryCollectionPhysicsProxy.h
const TArray<FClusterHandle*> GetParticles() const {
return SolverParticleHandles;
}
- 在设置粒子的DisableSleep阈值速度时,将线速度和角速度混用(但也有可能就是这么设计的)
UpdateMaterialSleepingThreshold
// FieldSystemProxyHelper.h UpdateMaterialDisableThreshold
if (ResultThreshold != InstanceMaterial->DisabledLinearThreshold)
{
InstanceMaterial->DisabledLinearThreshold = ResultThreshold;
InstanceMaterial->DisabledAngularThreshold = ResultThreshold;
}
EFieldPhysicsType::Field_AngularVelociy
单词错别字