UE5破碎系统浅析

场与破碎阈值

Field

场可以造成物体破碎,也可以用于固定物体等

UE中使用AFieldSystemActor来管理场,AFieldSystemActor中的FieldSystemComponent用于创建场。从蓝图的角度看,我们会创建一个继承自AFieldSystemActor的蓝图类来自定义场,如官方示例中的FS_AnchorField_GenericFS_MasterField以及FS_SleepDisable_Generic

场分为三种

  • Transient Field:瞬时场。最常用的场,可以通过BeginPlay,Tick或者是Event的形式生成并生效
  • Persistent Field:持久场
  • Construction Field:构造场。需要在构造函数中创建,并需要在Geometry Collection中注册。用途可以参照官方的FS_SleepDisable_Generic

AGeometryCollectionActor

ue5 lua脚本 ue5 chaos_数据

通常为摆在场景中,代表一个可破坏的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时,会将数据拷贝至AGeometryCollectionActorUGeometryCollectionComponent中,此方法通过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结算的底层起始被称作Strain

template<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,以造成不同等级的物体破碎效果的

ue5 lua脚本 ue5 chaos_API_02

// 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的继承结构大致如下图所示

ue5 lua脚本 ue5 chaos_ue5 lua脚本_03

/**
* 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::ApplyPhysicsFieldUFieldSystemComponent::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::EFieldTypeEFieldPhysicsType,对先前创建的节点的类型进行区分
// 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) {}

ue5 lua脚本 ue5 chaos_API_04

  • 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,以锚点场为例

  • AFieldSystemActorConstructionScript中初始化FieldNode并调用"Add Construction Field"
  • AFieldSystemActorOnConstruction中将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单词错别字