之前一直做的是物理模拟相关,所以选择从这个入手,附上参考教程的链接:https://zhuanlan.zhihu.com/p/36990803
首先,要找到CableComponent的源代码。直接google一下,点开API文档就能得到在哪个目录层级下。还有个更简单的方法,直接在VS的Solution Explorer里搜就好了(在此吐槽自己,我真是太菜了
开始看源代码了。
留个疑问:(第一次先说明一下,在学习过程中,会遇到各种各样的问题,有的是会直接阻碍后面的学习需要马上解决的,有的不那么重要不会阻碍后续学习可以以后再细细研究的。第二类疑惑会记录下来并且标注上“留个疑问”。以后得到答案后,再来在后面贴上问题的答案。
- UE 中的这些类或者结构体的名称,开头的大写字母是代表什么意思?FCableParticle的F,UCableComponent的U
- UE里面的各种宏,比如说在UCableComponent上方的UCLASS, 这些宏什么意思,怎么用。。
Cable是什么?简单的说就是由distance constraint 连接起来的点,可以用来实现绳索、链条一类的东西。
留个疑问:
- 可以实现更复杂的,比如布料、头发吗
- 可以实现主动运动的,比如蛇吗
其实光靠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