这是这个系列的第二卷也是最后一卷。第一卷的传送门在这里:
【1】CableComponent的原理
首先我们要知道cablecomponent的实现方法。下面先来看这张图

首先我们需要一堆粒子,此粒子非彼粒子(不要想得那么高端)。这里说的粒子可不是特效的粒子系统。这里的粒子其实就是一堆数据你可以理解为一堆数据位置标记。一个粒子里包含了粒子的新老位置信息喝是否可自由移动信息。我们利用各种公式去解算这些“隐形的”粒子,来重新拓扑整个模型。打开虚幻的源码,你可以看到定义粒子的结构体。

从上面可以发现,第一步我们定义一个Cableparticle的数组就可以了,你能在虚幻的CableComponent.h里找到这个数组。




class FPrimitiveSceneProxy;
/** Struct containing information about a point along the 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;
};
//~ Begin UActorComponent Interface.
virtual void OnRegister() override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
virtual void SendRenderDynamicData_Concurrent() override;
virtual void CreateRenderState_Concurrent() override;
//~ Begin UActorComponent Interface.
//~ Begin USceneComponent Interface.
virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
virtual void QuerySupportedSockets(TArray<FComponentSocketDescription>& OutSockets) const override;
virtual bool HasAnySockets() const override;
virtual bool DoesSocketExist(FName InSocketName) const override;
virtual FTransform GetSocketTransform(FName InSocketName, ERelativeTransformSpace TransformSpace = RTS_World) const override;
//~ Begin USceneComponent Interface.
//~ Begin UPrimitiveComponent Interface.
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
//~ End UPrimitiveComponent Interface.
//~ Begin UMeshComponent Interface.
virtual int32 GetNumMaterials() const override;
//~ End UMeshComponent Interface.
/**
* 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;
/** 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<FVector>& Locations) const;
/** 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;
private:
/** 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<FCableParticle> Particles;
friend class FCableSceneProxy;
后面这些就是一些函数和数组的声明了。这些函数和数组会在后面一一用到。
打开CableCompoent.cpp,找到OnRegister函数。它是Component的初始化函数
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<NumParticles; ParticleIdx++)
{
FCableParticle& Particle = Particles[ParticleIdx];
const float Alpha = (float)ParticleIdx/(float)NumSegments;
const FVector InitialPosition = CableStart + (Alpha * Delta);
Particle.Position = InitialPosition;
Particle.OldPosition = InitialPosition;
Particle.bFree = true; // default to free, will be fixed if desired in TickComponent
}
}
这里主要做了初始化操作,先确定粒子的数量,粒子的数量时绳子的段数+1。
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();
};
判断绳子最前端和最后端的粒子时锁定状态还是自由状态,然后在PerformSubstep(UseSubstep, Gravity);种执行韦尔莱算法更新particles的位置,方便给渲染线程重构顶点位置。如果你激活了物理模拟,它还会计算物理状态。
void UCableComponent::PerformSubstep(float InSubstepTime, const FVector& Gravity)
{
SCOPE_CYCLE_COUNTER(STAT_Cable_SimTime);
VerletIntegrate(InSubstepTime, Gravity);
SolveConstraints();
if (bEnableCollision)
{
PerformCableCollision();
}
}
这里还是很清晰的。先执行韦尔莱运算,然后对粒子进行约束,然后如果激活了物理,再计算物理碰撞。
/** 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::CreateRenderState_Concurrent()
{
Super::CreateRenderState_Concurrent();
SendRenderDynamicData_Concurrent();
}
void UCableComponent::SendRenderDynamicData_Concurrent()
{
if(SceneProxy)
{
// Allocate cable dynamic data
FCableDynamicData* DynamicData = new FCableDynamicData;
// Transform current positions from particles into component-space array
const FTransform& ComponentTransform = GetComponentTransform();
int32 NumPoints = NumSegments+1;
DynamicData->CablePoints.AddUninitialized(NumPoints);
for(int32 PointIdx=0; PointIdx<NumPoints; PointIdx++)
{
DynamicData->CablePoints[PointIdx] = ComponentTransform.InverseTransformPosition(Particles[PointIdx].Position);
}
// Enqueue command to send to render thread
ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
FSendCableDynamicData,
FCableSceneProxy*,CableSceneProxy,(FCableSceneProxy*)SceneProxy,
FCableDynamicData*,DynamicData,DynamicData,
{
CableSceneProxy->SetDynamicData_RenderThread(DynamicData);
});
}
}
/** Dynamic data sent to render thread */
struct FCableDynamicData
{
/** Array of points */
TArray<FVector> CablePoints;
};


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public struct Particle
{
public bool bFree;
public Vector3 NewPosition;
public Vector3 OldPosition;
}
[ExecuteInEditMode]
public class CS_DynamicRope : MonoBehaviour
{
public bool bDoubleSide = false;
public bool bAttachStart = true;
public bool bAttachEnd = true;
public Vector3 EndPosition;
public Vector3 StartPosition;
public Vector3 CableForce;
public float CableLength = 10.0f;
public float CableWidth = 2.0f;
public int NumSegments = 2;
public float SubstepTime = 1;
public int SolverIterations = 1;
public float GravityScale = 1;
public int tillingNum = 1;
public Material CableMaterial;
private Particle[] Particles;
private float SecondTime;
private bool bAttached = false;
private Vector3 AttachedPos = new Vector3(0, 0, 0);
void CreateMesh(Mesh meshval)
{
//边数默认为2
int NumOfSides = 2;
//构建面片带模型
int NumOfPoints = Particles.Length;
int NumOfVertex = NumOfPoints * NumOfSides;
float cableYOffset = CableWidth / 2;
Vector3[] NewVertexes = new Vector3[NumOfVertex];
Vector2[] NewUVs = new Vector2[NumOfVertex];
int vertexindexnum = 0;
for (int PointIndex = 0; PointIndex < NumOfPoints; PointIndex++)
{
int CurrentIndex = Mathf.Max(0, PointIndex);
int NextIndex = Mathf.Min(PointIndex + 1, NumOfPoints - 1);
Vector3 LineDir = Vector3.Normalize(Particles[NextIndex].NewPosition - Particles[CurrentIndex].NewPosition);
Vector3 LineForward = gameObject.transform.TransformVector(new Vector3(0, 0, 1));
Vector3 LineUp = Vector3.Cross(LineDir, LineForward);
for (int Sides = 0; Sides < NumOfSides; Sides++)
{
if (Sides == 0)
{
NewVertexes[vertexindexnum] = Particles[PointIndex].NewPosition - LineUp * cableYOffset;
NewUVs[vertexindexnum] = new Vector2(1.0f / (float)NumOfPoints * PointIndex * tillingNum, 0);
}
else
{
NewVertexes[vertexindexnum] = Particles[PointIndex].NewPosition + LineUp * cableYOffset;
NewUVs[vertexindexnum] = new Vector2(1.0f / (float)NumOfPoints * PointIndex * tillingNum, 1);
}
vertexindexnum++;
}
}
meshval.vertices = NewVertexes;
meshval.uv = NewUVs;
//构建三角形
if(bDoubleSide)
{
int NumOfTriangles = NumSegments * NumOfSides * 3 * 2;
int[] newTriangles = new int[NumOfTriangles];
int tiangleindexnum = 0;
for (int SecIndex = 0; SecIndex < NumSegments; SecIndex++)
{
int BL = 0; // S1---TL-----TR-------X---
int TL = 0; // | | |
int BR = 0; // | N | N+1 |
int TR = 0; // | | |
// S0---BL------BR------X---
BL = SecIndex * 2;
TL = SecIndex * 2 + 1;
BR = (SecIndex + 1) * 2;
TR = (SecIndex + 1) * 2 + 1;
//正面第一个三角形
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
//正面第二个三角形
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
//背面第一个三角形
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
//背面第二个三角形
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
}
meshval.triangles = newTriangles;
}
else
{
int NumOfTriangles = NumSegments * NumOfSides * 3;
int[] newTriangles = new int[NumOfTriangles];
int tiangleindexnum = 0;
for (int SecIndex = 0; SecIndex < NumSegments; SecIndex++)
{
int BL = 0; // S1---TL-----TR-------X---
int TL = 0; // | | |
int BR = 0; // |N | |
int TR = 0; // | | |
// S0---BL------BR------X---
BL = SecIndex * 2;
TL = SecIndex * 2 + 1;
BR = (SecIndex + 1) * 2;
TR = (SecIndex + 1) * 2 + 1;
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TL;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = TR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BR;
tiangleindexnum++;
newTriangles[tiangleindexnum] = BL;
tiangleindexnum++;
}
meshval.triangles = newTriangles;
}
}
void DrawMesh()
{
//清空它
Mesh mesh = GetComponent<MeshFilter>().sharedMesh;
mesh.Clear();
//重画它
float newGravity = GravityScale * -0.918f;
VerletIntergrate(SubstepTime, new Vector3(0, newGravity, 0));
SolveConstrains();
CreateMesh(mesh);
}
void BuildPoints()
{
//只会在开始时执行一次
float SecLength = CableLength / NumSegments;
for(int pointIndex = 0; pointIndex<Particles.Length; pointIndex++)
{
Particles[pointIndex].NewPosition = new Vector3(SecLength * pointIndex, 0, 0);
Particles[pointIndex].bFree = true;
}
if (bAttachStart ==true)
{
Particles[0].bFree = false;
Particles[0].NewPosition = StartPosition;
}
else
Particles[0].bFree = true;
if (bAttachEnd ==true)
{
Particles[Particles.Length - 1].bFree = false;
Particles[Particles.Length - 1].NewPosition = EndPosition;
}
else
Particles[Particles.Length - 1].bFree = true;
}
// Use this for initialization
void Start ()
{
gameObject.AddComponent<MeshFilter>();
gameObject.AddComponent<MeshRenderer>();
gameObject.GetComponent<MeshRenderer>().material = CableMaterial;
Particles = new Particle[NumSegments + 1];
BuildPoints();
DrawMesh();
}
void VerletIntergrate (float InSubstepTime, Vector3 Gravity)
{
int NumParticles = NumSegments + 1;
float SubSteptimeSqr = InSubstepTime * InSubstepTime;
for(int ParticleIndex = 0; ParticleIndex < NumParticles; ParticleIndex++)
{
if (Particles[ParticleIndex].bFree)
{
Vector3 PartocleForce = Gravity + CableForce;
Vector3 Vel = Particles[ParticleIndex].NewPosition - Particles[ParticleIndex].OldPosition;
Vector3 NewPosition = Particles[ParticleIndex].NewPosition + Vel + (SubSteptimeSqr * PartocleForce);
Particles[ParticleIndex].OldPosition = Particles[ParticleIndex].NewPosition;
Particles[ParticleIndex].NewPosition = NewPosition;
}
}
}
void SolveConstrains()
{
float Segmentlength = CableLength / NumSegments;
for(int IterationIdx = 0; IterationIdx < SolverIterations; IterationIdx++)
{
// Solve distance constraint for each segment
for (int SegIdx = 0; SegIdx < NumSegments; SegIdx++)
{
// Find current vector between particles
Vector3 Delta = Particles[SegIdx + 1].NewPosition - Particles[SegIdx].NewPosition;
//
float CurrentDistance = Delta.magnitude;
float ErrorFactor = (CurrentDistance - Segmentlength) / CurrentDistance;
// Only move free particles to satisfy constraints
if (Particles[SegIdx].bFree && Particles[SegIdx + 1].bFree)
{
Particles[SegIdx].NewPosition += ErrorFactor * 0.5f * Delta;
Particles[SegIdx + 1].NewPosition -= ErrorFactor * 0.5f * Delta;
}
else if (Particles[SegIdx].bFree)
{
Particles[SegIdx].NewPosition += ErrorFactor * Delta;
}
else if (Particles[SegIdx + 1].bFree)
{
Particles[SegIdx + 1].NewPosition -= ErrorFactor * Delta;
}
}
}
}
// Update is called once per frame
void Update ()
{
float UseSubStep = Mathf.Max(SubstepTime,0.005f);
SecondTime += Time.deltaTime;
if(bAttached == true)
{
Particles[Particles.Length - 1].NewPosition = AttachedPos;
}
else
{
Particles[Particles.Length - 1].NewPosition = new Vector3(0, 0, 3);
}
//BuildPoints()
DrawMesh();
}
//和外部交互逻辑交互的函数,如果只是想要一个单纯的绳子脚本,请删除这里
public void AttachEndStart(Vector3 EndPos)
{
bAttached = true;
AttachedPos = transform.worldToLocalMatrix * new Vector4(EndPos.x, EndPos.y, EndPos.z, 1);
}
public void DettachEndStart()
{
bAttached = false;
AttachedPos = new Vector3(0, 0, 0);
}
}