Loading

UE5-ChaosDestruction

场与破碎阈值

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

通常为摆在场景中,代表一个可破坏的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,以造成不同等级的物体破碎效果的

// 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::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) {}
    

  • 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单词错别字

资料

posted @ 2022-07-12 15:41  _FeiFei  阅读(1717)  评论(0编辑  收藏  举报