【UE学习记录】初次尝试:CableComponent

之前一直做的是物理模拟相关,所以选择从这个入手,附上参考教程的链接:https://zhuanlan.zhihu.com/p/36990803

首先,要找到CableComponent的源代码。直接google一下,点开API文档就能得到在哪个目录层级下。还有个更简单的方法,直接在VS的Solution Explorer里搜就好了(在此吐槽自己,我真是太菜了




开始看源代码了。

image.png

留个疑问:(第一次先说明一下,在学习过程中,会遇到各种各样的问题,有的是会直接阻碍后面的学习需要马上解决的,有的不那么重要不会阻碍后续学习可以以后再细细研究的。第二类疑惑会记录下来并且标注上“留个疑问”。以后得到答案后,再来在后面贴上问题的答案。

  1. UE 中的这些类或者结构体的名称,开头的大写字母是代表什么意思?FCableParticle的F,UCableComponent的U
  2. UE里面的各种宏,比如说在UCableComponent上方的UCLASS, 这些宏什么意思,怎么用。。

Cable是什么?简单的说就是由distance constraint 连接起来的点,可以用来实现绳索、链条一类的东西。

留个疑问:

  1. 可以实现更复杂的,比如布料、头发吗
  2. 可以实现主动运动的,比如蛇吗

其实光靠cable那种简单的结构并不可以。。。。cable只是个简单的线性结构,constraint也很简单,只有attach和distance。想实现更复杂的,需要对cable好好改造才行。

struct FCableParticle
{
    FCableParticle()
    : bFree(true)
    , Position(0,0,0)
    , OldPosition(0,0,0)
    {}

    /** If this point is free (simulating) or fixed to something */
    bool bFree;
    /** Current position of point */
    FVector Position;
    /** Position of point on previous iteration */
    FVector OldPosition;
};

先看FCableParticle, 这个结构体是粒子的一些信息:当前位置、之前位置和是否Free.
我们要模拟cable的动态,还需要什么?

  • 一个绳索里有哪些粒子
  • 这些粒子怎么连接
  • 解算参数:连接的强度、timestep等等
  • 受到什么样的外力和内力、具体的求解和积分以计算出下个时间点的位置

再来看看UCableComponent。下面贴的源码有修改,主要是为了方便看和解说。

/** Component that allows you to specify custom triangle mesh geometry */
UCLASS(hidecategories=(Object, Physics, Activation, "Components|Activation"), editinlinenew, meta=(BlueprintSpawnableComponent), ClassGroup=Rendering)
class CABLECOMPONENT_API UCableComponent : public UMeshComponent
{
    GENERATED_UCLASS_BODY()
private:
/**
先来看private部分,这部分保存了一个绳索的主体部分:粒子们的信息保存在Particles数组里,还有timestep的信息:TimeRemainder。
还有一些私有方法,每一步的解算、处理碰撞。
*/

    /** Solve the cable spring constraints */
    void SolveConstraints();
    /** Integrate cable point positions */
    void VerletIntegrate(float InSubstepTime, const FVector& Gravity);
    /** Perform collision traces for particles */
    void PerformCableCollision();
    /** Perform a simulation substep */
    void PerformSubstep(float InSubstepTime, const FVector& Gravity);
    /** Get start and end position for the cable */
    void GetEndPositions(FVector& OutStartPosition, FVector& OutEndPosition);
    /** Amount of time 'left over' from last tick */
    float TimeRemainder;
    /** Array of cable particles */
    TArray  Particles;// 粒子struct保存在一个数组里。
    friend class FCableSceneProxy;
public:
//public里面是暴露的接口和参数,之后再看
};

void UCableComponent::PerformSubstep(float InSubstepTime, const FVector& Gravity)
{
    SCOPE_CYCLE_COUNTER(STAT_Cable_SimTime);
    VerletIntegrate(InSubstepTime, Gravity);
    SolveConstraints();
    if (bEnableCollision){
        PerformCableCollision();
    }
}

每个substep的流程:
Verlet积分、solveConstraint、如果开了碰撞就PerformCableCollision.

接着就顺藤摸瓜看这些函数具体做了什么。

void UCableComponent::VerletIntegrate(float InSubstepTime, const FVector& Gravity)
{
    SCOPE_CYCLE_COUNTER(STAT_Cable_IntegrateTime);

    const int32 NumParticles = NumSegments+1;
    const float SubstepTimeSqr = InSubstepTime * InSubstepTime;

    for(int32 ParticleIdx=0; ParticleIdx

在这个积分中,对于每个粒子,如果这个粒子是free的,
就计算出所受的外力,(重力+CableForce)(这里写的是force,更像是加速度)
然后由速度、加速度更新位置(OldPosition和Position)

留个疑问:
这感觉和PBD很像啊

/** Solve a single distance constraint between a pair of particles */
static void SolveDistanceConstraint(FCableParticle& ParticleA, FCableParticle& ParticleB, float DesiredDistance)
{
    // Find current vector between particles
    FVector Delta = ParticleB.Position - ParticleA.Position;
    // 
    float CurrentDistance = Delta.Size();
    float ErrorFactor = (CurrentDistance - DesiredDistance)/CurrentDistance;

    // Only move free particles to satisfy constraints
    if(ParticleA.bFree && ParticleB.bFree)
    {
        ParticleA.Position += ErrorFactor * 0.5f * Delta;
        ParticleB.Position -= ErrorFactor * 0.5f * Delta;
    }
    else if(ParticleA.bFree)
    {
        ParticleA.Position += ErrorFactor * Delta;
    }
    else if(ParticleB.bFree)
    {
        ParticleB.Position -= ErrorFactor * Delta;
    }
}

void UCableComponent::SolveConstraints()
{
    SCOPE_CYCLE_COUNTER(STAT_Cable_SolveTime);

    const float SegmentLength = CableLength/(float)NumSegments;

    // For each iteration..
    for (int32 IterationIdx = 0; IterationIdx < SolverIterations; IterationIdx++)
    {
        // Solve distance constraint for each segment
        for (int32 SegIdx = 0; SegIdx < NumSegments; SegIdx++)
        {
            FCableParticle& ParticleA = Particles[SegIdx];
            FCableParticle& ParticleB = Particles[SegIdx + 1];
            // Solve for this pair of particles
            SolveDistanceConstraint(ParticleA, ParticleB, SegmentLength);
        }

        // If desired, solve stiffness constraints (distance constraints between every other particle)
        if (bEnableStiffness)
        {
            for (int32 SegIdx = 0; SegIdx < NumSegments-1; SegIdx++)
            {
                FCableParticle& ParticleA = Particles[SegIdx];
                FCableParticle& ParticleB = Particles[SegIdx + 2];
                SolveDistanceConstraint(ParticleA, ParticleB, 2.f*SegmentLength);
            }
        }
    }
}

看了这个solver, 我更确信了,这不就是PBD吗?
每个segment求解一下distance constraint,distance constraint 的求解就是挪动点的位置,使两点距离满足距离约束。

在bEnableStiffness部分,是每隔一段求解一下距离约束。这让我想起了PBD中的long range constraint长距离约束。不过这里处理的很糙,仅仅适合用在绳索这样的长条物体里,如果布料那样的网状结构,这样简单的处理是会出问题的。
还有一点就是,求解distance constraint 中,看里面的这种写法,是默认每个particle都是相同的质量,这样写一般没有什么问题,只是有可能不能满足一些特殊需求。。。。

void UCableComponent::PerformCableCollision()
{
    SCOPE_CYCLE_COUNTER(STAT_Cable_CollisionTime);

    UWorld* World = GetWorld();
    // If we have a world, and collision is not disabled
    if (World && GetCollisionEnabled() != ECollisionEnabled::NoCollision)
    {
        // Get collision settings from component
        FCollisionQueryParams Params(SCENE_QUERY_STAT(CableCollision));

        ECollisionChannel TraceChannel = GetCollisionObjectType();
        FCollisionResponseParams ResponseParams(GetCollisionResponseToChannels());

        // Iterate over each particle
        for (int32 ParticleIdx = 0; ParticleIdx < Particles.Num(); ParticleIdx++)
        {
            FCableParticle& Particle = Particles[ParticleIdx];
            // If particle is free
            if (Particle.bFree)
            {
                // Do sphere sweep
                FHitResult Result;
                bool bHit = World->SweepSingleByChannel(Result, Particle.OldPosition, Particle.Position, FQuat::Identity, TraceChannel, FCollisionShape::MakeSphere(0.5f * CableWidth), Params, ResponseParams);
                // If we got a hit, resolve it
                if (bHit)
                {
                    if (Result.bStartPenetrating)
                    {
                        Particle.Position += (Result.Normal * Result.PenetrationDepth);
                    }
                    else
                    {
                        Particle.Position = Result.Location;
                    }

                    // Find new velocity, after fixing collision
                    FVector Delta = Particle.Position - Particle.OldPosition;
                    // Find component in normal
                    float NormalDelta = Delta | Result.Normal;
                    // Find component in plane
                    FVector PlaneDelta = Delta - (NormalDelta * Result.Normal);

                    // Zero out any positive separation velocity, basically zero restitution
                    Particle.OldPosition += (NormalDelta * Result.Normal);

                    // Apply friction in plane of collision if desired
                    if (CollisionFriction > KINDA_SMALL_NUMBER)
                    {
                        // Scale plane delta  by 'friction'
                        FVector ScaledPlaneDelta = PlaneDelta * CollisionFriction;

                        // Apply delta to old position reduce implied velocity in collision plane
                        Particle.OldPosition += ScaledPlaneDelta;
                    }
                }
            }
        }
    }
}

处理碰撞,检测和处理是写在一起的。
对于每个free particle, 检测从old position到position, 有没有hit something, 如果有,得到一个穿插深度和法向量。
先修正position,简单方法就是把粒子沿着法向量移动一个穿插深度,从碰撞物中拉出来。
然后修改速度,这里是通过修改old position来间接修改速度的。
总之,将速度消除掉和Nomal平行的分量。
同时还有对摩擦力的处理,思路是速度消除掉一部分和Normal垂直的分量。

这个碰撞处理是真的简单粗暴,碰撞物需要是一个简单的凸物体,一个粒子一次只会考虑一个碰撞物,哪怕和几个物体同时碰撞。这里修正好穿插以后,有可能之前算的distance constraint不再满足,这里也不再处理。
毕竟是游戏,速度最重要,一小点穿插什么的不算什么。如果没有大形变大速度,这么处理还是OK的。


上面是cable的解算内核的分析,接下来再看public部分

/**
     *  Should we fix the start to something, or leave it free.
     *  If false, component transform is just used for initial location of start of cable
     */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cable")
    bool bAttachStart;

    /** 
     *  Should we fix the end to something (using AttachEndTo and EndLocation), or leave it free. 
     *  If false, AttachEndTo and EndLocation are just used for initial location of end of cable
     */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cable")
    bool bAttachEnd;

    /** Actor or Component that the defines the end position of the cable */
    UPROPERTY(EditAnywhere, Category="Cable")
    FComponentReference AttachEndTo;

    /** Socket name on the AttachEndTo component to attach to */
    UPROPERTY(EditAnywhere, Category = "Cable")
    FName AttachEndToSocketName;

    /** End location of cable, relative to AttachEndTo (or AttachEndToSocketName) if specified, otherwise relative to cable component. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cable", meta=(MakeEditWidget=true))
    FVector EndLocation;

/** Rest length of the cable */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cable", meta=(ClampMin = "0.0", UIMin = "0.0", UIMax = "1000.0"))
    float CableLength;

    /** How many segments the cable has */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Cable", meta=(ClampMin = "1", UIMin = "1", UIMax = "20"))
    int32 NumSegments;

    /** Controls the simulation substep time for the cable */
    UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category="Cable", meta=(ClampMin = "0.005", UIMin = "0.005", UIMax = "0.1"))
    float SubstepTime;

    /** The number of solver iterations controls how 'stiff' the cable is */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cable", meta=(ClampMin = "1", ClampMax = "16"))
    int32 SolverIterations;

    /** Add stiffness constraints to cable. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Cable")
    bool bEnableStiffness;

    /** 
     *  EXPERIMENTAL. Perform sweeps for each cable particle, each substep, to avoid collisions with the world. 
     *  Uses the Collision Preset on the component to determine what is collided with.
     *  This greatly increases the cost of the cable simulation.
     */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Cable")
    bool bEnableCollision;

    /** If collision is enabled, control how much sliding friction is applied when cable is in contact. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Cable", meta = (ClampMin = "0.0", ClampMax = "1.0", EditCondition = "bEnableCollision"))
    float CollisionFriction;

    /** Force vector (world space) applied to all particles in cable. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cable Forces")
    FVector CableForce;

    /** Scaling applied to world gravity affecting this cable. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cable Forces")
    float CableGravityScale;

    /** How wide the cable geometry is */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cable Rendering", meta=(ClampMin = "0.01", UIMin = "0.01", UIMax = "50.0"))
    float CableWidth;

    /** Number of sides of the cable geometry */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Cable Rendering", meta=(ClampMin = "1", ClampMax = "16"))
    int32 NumSides;

    /** How many times to repeat the material along the length of the cable */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cable Rendering", meta=(UIMin = "0.1", UIMax = "8"))
    float TileMaterial;

这些是暴露给editor的参数,
两端要不要attach,attach到哪个位置或者attach到哪个物体上;
cable长度、段数、宽度、还有一些关于形状、材质的参数
重力、其他外力、摩擦、要不要加碰撞、要不要EnableStiffness,
substepTime和求解迭代次数。

    /** Attaches the end of the cable to a specific Component **/
    UFUNCTION(BlueprintCallable, Category = "Cable")
    void SetAttachEndToComponent(USceneComponent* Component, FName SocketName = NAME_None);

    /** Attaches the end of the cable to a specific Component within an Actor **/
    UFUNCTION(BlueprintCallable, Category = "Cable")
    void SetAttachEndTo(AActor* Actor, FName ComponentProperty, FName SocketName = NAME_None);
    
    /** Gets the Actor that the cable is attached to **/
    UFUNCTION(BlueprintCallable, Category = "Cable")
    AActor* GetAttachedActor() const;

    /** Gets the specific USceneComponent that the cable is attached to **/
    UFUNCTION(BlueprintCallable, Category = "Cable")
    USceneComponent* GetAttachedComponent() const;

    /** Get array of locations of particles (in world space) making up the cable simulation. */
    UFUNCTION(BlueprintCallable, Category = "Cable")
    void GetCableParticleLocations(TArray& Locations) const;

一些GET、SET函数,跳过
看重要的函数:初始化、TickComponent

UCableComponent::UCableComponent( const FObjectInitializer& ObjectInitializer )
    : Super( ObjectInitializer )
{
    PrimaryComponentTick.bCanEverTick = true;
    bTickInEditor = true;
    bAutoActivate = true;

    bAttachStart = true;
    bAttachEnd = true;
    CableWidth = 10.f;
    NumSegments = 10;
    NumSides = 4;
    EndLocation = FVector(100.f,0,0);
    CableLength = 100.f;
    SubstepTime = 0.02f;
    SolverIterations = 1;
    TileMaterial = 1.f;
    CollisionFriction = 0.2f;
    CableGravityScale = 1.f;

    SetCollisionProfileName(UCollisionProfile::PhysicsActor_ProfileName);
}

void UCableComponent::OnRegister()
{
    Super::OnRegister();

    const int32 NumParticles = NumSegments+1;

    Particles.Reset();
    Particles.AddUninitialized(NumParticles);

    FVector CableStart, CableEnd;
    GetEndPositions(CableStart, CableEnd);

    const FVector Delta = CableEnd - CableStart;

    for(int32 ParticleIdx=0; ParticleIdx

看到这里的初始化函数,我才发现,cable原来是这种东西。。。
其实就是在start和end两个位置之间,等距离切个多少段,particle就是节点。整个就是个线性的结构。


void UCableComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    const FVector Gravity = FVector(0, 0, GetWorld()->GetGravityZ()) * CableGravityScale;

    // Update end points
    FVector CableStart, CableEnd;
    GetEndPositions(CableStart, CableEnd);

    FCableParticle& StartParticle = Particles[0];

    if (bAttachStart)
    {
        StartParticle.Position = StartParticle.OldPosition = CableStart;
        StartParticle.bFree = false;
    }
    else
    {
        StartParticle.bFree = true;
    }

    FCableParticle& EndParticle = Particles[NumSegments];
    if (bAttachEnd)
    {
        EndParticle.Position = EndParticle.OldPosition = CableEnd;
        EndParticle.bFree = false;
    }
    else
    {
        EndParticle.bFree = true;
    }

    // Ensure a non-zero substep
    float UseSubstep = FMath::Max(SubstepTime, 0.005f);

    // Perform simulation substeps
    TimeRemainder += DeltaTime;
    while(TimeRemainder > UseSubstep)
    {
        PerformSubstep(UseSubstep, Gravity);
        TimeRemainder -= UseSubstep;
    }

    // Need to send new data to render thread
    MarkRenderDynamicDataDirty();

    // Call this because bounds have changed
    UpdateComponentToWorld();
};

一个tick,执行一次performSubStep,
然后将数据送到render里。


关于cableComponent的解算逻辑已经看完了,
总结一下:
onRegister初始化,包括创建particle数组。
每个TickComponent中,根据各种受力和约束计算更新粒子的速度,并且将数据送给渲染线程。
至于怎么将cable的数据送去render thread的。这个以后在渲染管线再看吧。

最后加个cable如何使用的文档:
https://docs.unrealengine.com/en-US/Basics/Components/Rendering/CableComponent/index.html

你可能感兴趣的:(【UE学习记录】初次尝试:CableComponent)