这是这个系列的第二卷也是最后一卷。第一卷的传送门在这里:
虚幻4 渲染探索【1】引擎渲染组件初探
点击打开链接
如果您对虚幻的渲染图元汇编阶段了解不多,那么可以先看看上面那篇文章。
那么下面进入正题,我们以虚幻的CableComponent为例,来深入研究一下虚幻的渲染管线的图源汇编阶段。
下面分几个小节
(1)CableComponent的原理
(2)CableComponent在虚幻里的实现步骤代码实现
(3)通过CableComponent,总结虚幻的渲染管线的图元汇编阶段的各个细节
【1】CableComponent的原理
首先我们要知道cablecomponent的实现方法。下面先来看这张图
要实现绳子或者锁链,我们需要实时结算,更新顶点的位置,重新构建模型。说来轻巧,但是具体如何操作如何实现呢。
首先我们需要一堆粒子,此粒子非彼粒子(不要想得那么高端)。这里说的粒子可不是特效的粒子系统。这里的粒子其实就是一堆数据你可以理解为一堆数据位置标记。一个粒子里包含了粒子的新老位置信息喝是否可自由移动信息。我们利用各种公式去解算这些“隐形的”粒子,来重新拓扑整个模型。打开虚幻的源码,你可以看到定义粒子的结构体。
我们通过解算粒子的位置,来不断更新顶点缓冲区种顶点数据的位置,这样来达到使我们的绳索运动的目的。大概思路现在有了。
我们定义了一个结构体来描述粒子,粒子之间有作用力和反作用力(有公式来解算这些作用里,这里先把它放在一边),然后我们通过这些粒子(位置标记)来每帧更新顶点的位置,这样来让我们的绳子模型动起来。
有了上面的核心思路之后,我们再来捋一下我们要做的事情。
第一:我们要定义一些粒子(这个好办,其实就是一个FCableParticle的结构体数组)
第二:结算这些粒子,让它们有重力,有速度,感觉像是物理的。而且这些解算不能太费,要能在游戏里跑才行。
第三:利用第二步解算的粒子,重新构建整个模型。
从上面可以发现,第一步我们定义一个Cableparticle的数组就可以了,你能在虚幻的CableComponent.h里找到这个数组。
那么第二步和第三步如何操作呢。第二步我们需要一个比较经典的算法,韦尔莱算法。第三步我们需要了解如何在虚幻里更新上传顶点数据即可。
那么对于第二步来说,韦尔莱算法的公式推导可以点击这个传送门:
点击打开链接
我们使用这个公式莱计算位置的变化:
你可以在虚幻的源码种找到对应的函数
至于第三步模型的重新构建的话,主要需要注意几个点。第一是位置的构建,第二是UV的构建,第三是切线的构建,第四是indexbuffer的构建。对应虚幻源码种的void BuildCableMesh函数。那么它是如何构建模型的呢。
首先我们设置绳子的段数从而有了particle
接下来我们根据我们设置的cable的边数开始构建顶点和UV
先构建了一个面片,然后在把这个面片包裹在粒子周围。这样就很好理解怎么做UV映射了。
【2】CableComponent的实现步骤代码实现
下面我们就来一行一行代码得研究CableComponent
打开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; };
这里前置先声明了FPrimitiveSceneProxy这个类,这个类是场景代理,负责渲染线程和逻辑线程的交互,它会将顶点缓冲,索引缓冲数据提交。
然后定义了粒子元的数据,包含了三个数据,新,老位置数据和bFree数据,这个bFree数据控制这个粒子是否是自由的,能够参与位置运算的。如果它是false,那么它将被固定。我们看到一般情况下我们将cable拖到场景里,它的最前端和最后端是固定的,那是因为它的前后两端的那两个粒子的bfree属性是false的缘故。
接着往下翻就到了UCableComponent类的地方了。它继承自UMeshComponent。至于PrimitiveComponent meshComponent的关系,我在第一卷有详细介绍。这里就不赘述了。
//~ 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.
这里就是一些函数的重载,它们负责组件的注册和绘制等工作。virtual void OnRegister() override;这个函数负责组件的注册,会在组件构建的时候调用。virtual void TickComponent(。。。)这个函数会每帧都调用。
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;会创建场景代理。
再往下就是CableComponent自己的属性了,比如绳子的段数,边数。物理运算的时候迭代的次数,绳子的长度,绳子参与物理运算时受到的力等。
/** * 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;
这些参数在Detail面板能进行调节。可以自行去调节调节感受一下。这些数据里面有些数据会为后面的粒子数量的确定,模型构造时边的长度,顶点数量提供数据支持。
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。
完成初始化后,会执行Tick函数
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(); } }
这里还是很清晰的。先执行韦尔莱运算,然后对粒子进行约束,然后如果激活了物理,再计算物理碰撞。
SolveConstraints()会调用SolveDistanceConstraint。它的作用是把粒子约束在合理的范围内。这里可以把它理解为反作用力的效果计算,韦尔莱是作用力的计算部分。
/** 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); }); } }
可以看到,这里会将DynamicData发送到逻辑线程。找到这个Dynamic的定义
/** Dynamic data sent to render thread */ struct FCableDynamicData { /** Array of points */ TArray<FVector> CablePoints; };
一切就清晰啦。渲染线程会用逻辑线程发送过去的particles的position数据不停进行绘制
【3】
通过CableComponent,总结虚幻的渲染管线的图元汇编阶段的各个细节
那么最后来捋一下把
(1)渲染线程这边,场景代理会创建各种资源,如VertexBuffer,Indexbuffer,Material(关于shader这些这里不做讨论了,具体可以看【材质编辑器全解第一卷】Unity,UnrealEngine4等各大引擎材质编辑器原理详解),会收取逻辑线程发送过来的数据(一堆粒子的位置),利用这些位置信息重新更新顶点缓冲和索引缓冲,重新绘制网格。
(2)逻辑线程这边。OnRegister会初始化各个数据,TickComponent会每帧计算Particles的位置,最后把这个位置信息发送给渲染线程。
只要做到上述的这些,就可以完整掌握整个图元汇编的操作了。下面我们就把Unreal的这个CableComponent般到Unity里面去吧。
你也可以试试在unity里重现整个实现,以此来检测自己是否真正理解。下面是我unity的脚本代码
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); } }