From 6ccd2e16bdb3665acf3b7e1deec3f7c2081b200a Mon Sep 17 00:00:00 2001 From: BobSong <605277374@qq.com> Date: Tue, 4 Nov 2025 23:41:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Rope/Examples/00_Main/Main.unity | 5 +- Assets/Scripts/ThirdParty/Rope/Rope2.cs | 1805 +++++++++++++++++ Assets/Scripts/ThirdParty/Rope/Rope2.cs.meta | 3 + 3 files changed, 1809 insertions(+), 4 deletions(-) create mode 100644 Assets/Scripts/ThirdParty/Rope/Rope2.cs create mode 100644 Assets/Scripts/ThirdParty/Rope/Rope2.cs.meta diff --git a/Assets/Scripts/ThirdParty/Rope/Examples/00_Main/Main.unity b/Assets/Scripts/ThirdParty/Rope/Examples/00_Main/Main.unity index 27f932670..565ba3ce1 100644 --- a/Assets/Scripts/ThirdParty/Rope/Examples/00_Main/Main.unity +++ b/Assets/Scripts/ThirdParty/Rope/Examples/00_Main/Main.unity @@ -3783,7 +3783,7 @@ MonoBehaviour: material: {fileID: 2100000, guid: 189732c736fdb544f98524f96c34aff6, type: 2} shadowMode: 1 rendering: - useSimpleLineRenderer: 0 + useSimpleLineRenderer: 1 simpleLineWidth: 0.02 spawnPoints: - x: 0 @@ -3792,9 +3792,6 @@ MonoBehaviour: - x: -2.4452248 y: 0.06797419 z: 0.039803684 - - x: -4.45739 - y: 0.10728941 - z: 0.036379516 interpolation: 0 simulation: enabled: 1 diff --git a/Assets/Scripts/ThirdParty/Rope/Rope2.cs b/Assets/Scripts/ThirdParty/Rope/Rope2.cs new file mode 100644 index 000000000..f7f3b6526 --- /dev/null +++ b/Assets/Scripts/ThirdParty/Rope/Rope2.cs @@ -0,0 +1,1805 @@ +using System.Collections.Generic; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; + +namespace NBF +{ + + public class Rope2 : MonoBehaviour + { + protected const int MaxCollisionPlanesPerParticle = 3; + protected const int InitialParticleTargets = 3; + protected const int MaxRigidbodyConnections = 24; + + public struct Measurements + { + public float spawnCurveLength; + public float realCurveLength; + public int segmentCount; + public int particleCount; + public float particleSpacing; + + public int GetParticleIndexAt(float distance) + { + return math.clamp((int)(distance / particleSpacing + 0.5f), 0, particleCount - 1); + } + } + + public struct OnSplitParams + { + public int minParticleIndex; + public int maxParticleIndex; + public Measurements preSplitMeasurements; + } + + public struct EditorColors + { + public Color ropeSegments; + public Color simulationParticle; + public Color collisionParticle; + public Color spawnPointHandle; + } + + public static readonly EditorColors Colors = new EditorColors() + { + ropeSegments = Color.black, + simulationParticle = new Color(0.2f, 0.8f, 0.2f, 0.5f), + collisionParticle = new Color(1.0f, 0.92f, 0.016f, 0.5f), + spawnPointHandle = new Color(0.1f, 0.5f, 0.8f), + }; + + [Tooltip("绳子的半径。该值既用于构建视觉网格,也用于处理碰撞。")] [Range(0.001f, 1.0f)] + public float radius = 0.05f; + + [Tooltip("绳子的视觉网格中每个段使用的顶点数。更多的顶点会使绳子看起来更圆,但会增加视觉网格的总体顶点和三角形数量。该值不会影响绳子的模拟。")] + [DisableInPlayMode] + [Range(3, 32)] + public int radialVertices = 6; + + [Tooltip("绳子是否为圆形环。如果启用,绳子的最后一个生成点将连接到第一个生成点。")] [DisableInPlayMode] + public bool isLoop = false; + + [Tooltip("用于渲染绳子的材质。这可以是任何使用顶点位置和可选法线的材质。")] + public Material material; + + [Tooltip("绳子使用的阴影投射模式")] public ShadowCastingMode shadowMode = ShadowCastingMode.On; + + [System.Serializable] + public struct RenderingSettings + { + [Tooltip("使用简单线渲染而非完整网格渲染。适用于细绳如鱼线,性能更好。")] + public bool useSimpleLineRenderer; + + [Tooltip("简单线渲染的宽度。")] + public float simpleLineWidth; + } + + [Space] public RenderingSettings rendering = new RenderingSettings() + { + useSimpleLineRenderer = false, + simpleLineWidth = 0.02f + }; + + // public CustomMeshSettings customMesh = new CustomMeshSettings() + // { + // mesh = null, + // rotation = 90.0f, + // scale = Vector3.one, + // stretch = false, + // }; + + [Tooltip("用于初始放置绳子在世界中的生成点。目前,连续的生成点对被视为线性线段。")] [DisableInPlayMode] + public List spawnPoints = new List(); + + [Tooltip("在FixedUpdate()调用之间使用的插值模式。只有在固定更新率较低时才有意义。有关更多信息,请参见Rigidbody.interpolation的文档。")] [DisableInPlayMode] + public RopeInterpolation interpolation = RopeInterpolation.None; + + [System.Serializable] + public struct SimulationSettings + { + [Tooltip("独立于绳子渲染打开或关闭模拟。用例可能是程序化地禁用距离相机太远或不可见的绳子。")] + public bool enabled; + + [Header("基本特性")] [Tooltip("每米的模拟粒子数。更高的分辨率会使绳子看起来更平滑,但需要更多计算。")] [DisableInPlayMode] + public float resolution; + + [Tooltip("绳子每米的质量。该值用于通过RopeRigidbodyConnection组件与刚体交互。")] [Delayed] + public float massPerMeter; + + [Tooltip("绳子刚度的度量。请注意,实际刚度在很大程度上取决于求解器迭代次数和使用的物理时间步长,如果更改一个值,可能需要重新调整其他值。这个特定值不影响性能。")] [Range(0.01f, 1.0f)] + public float stiffness; + + [Tooltip("每次固定更新时从模拟中移除的能量百分比。用于模拟空气阻力。不影响性能。")] [Range(0.0f, 1.0f)] + public float energyLoss; + + [Header("修改器")] [Tooltip("一个动态缩短或延长绳子的乘法因子。例如,这可以用于创建可伸缩的抓钩。")] [Range(0.0f, 2.0f)] + public float lengthMultiplier; + + [Tooltip("应用于绳子的重力百分比。较低的重力倍增器可能有助于拉直否则会下垂的绳子,但应被视为'hack',因为绳子会表现得像在太空中一样。")] [Range(0.0f, 1.0f)] + public float gravityMultiplier; + + [Tooltip("是否使用此组件的自定义重力值或全局物理重力")] public bool useCustomGravity; + + [Tooltip("不使用全局重力时,此特定绳子使用的重力")] public float3 customGravity; + + [Header("高级(更改这些将需要调整基本特性)")] + [Range(1, 10)] + [Tooltip("每个固定更新应分割成的子步数。高子步数会产生更硬的模拟,因为重力引起的微小偏转可以及早被抵消。例外情况是如果绳子在两个刚体之间固定,则项目的固定更新率决定刚度。")] + public int substeps; + + [Tooltip("为此绳子运行的求解器迭代次数。高分辨率绳子需要更多迭代才能变硬。更多迭代需要更多计算。")] [Range(1, 32)] + public int solverIterations; + } + + [Space] public SimulationSettings simulation = new SimulationSettings() + { + enabled = true, + resolution = 10.0f, + massPerMeter = 0.2f, + stiffness = 1.0f, + lengthMultiplier = 1.0f, + energyLoss = 0.0025f, + gravityMultiplier = 1.0f, + useCustomGravity = false, + customGravity = Physics.gravity, + substeps = 4, + solverIterations = 2, + }; + + + [System.Serializable] + public struct CollisionSettings + { + [Tooltip("启用绳子的碰撞处理,使其对通过RopeConnection组件连接的碰撞器以外的碰撞器做出反应。对主线程性能要求很高。")] + public bool enabled; + + [Tooltip("绳子碰撞时是否应影响刚体。")] public bool influenceRigidbodies; + + [Tooltip( + "检查并响应每个第n个模拟粒子的碰撞。值为1将使每个模拟粒子都对碰撞做出反应,值为2将使每隔一个粒子对碰撞做出反应,以此类推。由于每个粒子执行一次球体重叠测试,低值非常消耗性能。当绳子被选中时,碰撞粒子由黄色球体可视化。")] + [Range(1, 20)] + public int stride; + + [Tooltip("绳子的动态摩擦系数。用于在绳子被拖拽时减慢绳子的速度。")] [Range(0.0f, 20.0f)] + public float friction; + + [Tooltip("防止小半径绳子容易穿过几何体的额外距离(添加到绳子半径之上)")] [Range(0.0f, 1.0f)] + public float collisionMargin; + + public LayerMask ignoreLayers; + } + + [Space] public CollisionSettings collisions = new CollisionSettings() + { + enabled = false, + influenceRigidbodies = true, + stride = 2, + friction = 0.1f, + collisionMargin = 0.025f, + ignoreLayers = 0, + }; + + protected struct CollisionPlane + { + public float3 point; + public float3 normal; + public float3 velocityChange; + + public float3 feedback; + } + + protected struct ParticleTarget + { + public int particleIndex; + public float3 position; + public float stiffness; + } + + protected struct RigidbodyConnection + { + public Rigidbody rigidbody; + public float rigidbodyDamping; + public ParticleTarget target; + } + + // 添加LineRenderer组件引用 + protected LineRenderer lineRenderer; + protected bool initialized; + protected bool computingSimulationFrame; + protected bool simulationDisabledPrevFrame; + protected bool wasSplit; + protected float timeSinceFixedUpdate; + protected JobHandle simulationFrameHandle; + + // State + protected NativeArray positions; + protected NativeArray prevPositions; + protected NativeArray interpolatedPositions; + protected NativeArray bitangents; + protected NativeArray massMultipliers; + + // Collision handling + protected NativeArray collisionPlanesActive; + protected NativeArray collisionPlanes; + protected Rigidbody[] collisionRigidbodies; + + // Rigidbody connections + protected List queuedRigidbodyConnections; + protected List liveRigidbodyConnections; + protected NativeArray particleTargets; + protected NativeArray particleTargetFeedbacks; + + // Rendering + protected NativeArray vertices; + protected NativeArray normals; + protected NativeArray cosLookup; + protected NativeArray sinLookup; + + protected Mesh mesh; + + protected Measurements _measurements; + + /// + /// 返回绳子的测量值。测量值在绳子首次初始化后保持不变。 + /// + public Measurements measurements + { + get + { + if (!Initialize()) + { + return new Measurements(); + } + + return _measurements; + } + } + + /// + /// 视觉网格的当前世界空间边界 + /// + public Bounds currentBounds + { + get + { + if (!Initialize()) + { + return new Bounds(); + } + + return mesh.bounds; + } + } + + public void OnValidate() + { + simulation.resolution = Mathf.Max(0.01f, simulation.resolution); + simulation.massPerMeter = Mathf.Max(0.01f, simulation.massPerMeter); + rendering.simpleLineWidth = Mathf.Max(0.001f, rendering.simpleLineWidth); + } + + /// + /// 向绳子添加新的生成点。可以从编辑模式调用。 + /// + public void PushSpawnPoint() + { + if (spawnPoints.Count == 0) + { + spawnPoints.Add(Vector3.right); + return; + } + + var prev = spawnPoints.Count >= 2 ? spawnPoints[spawnPoints.Count - 2] : float3.zero; + var current = spawnPoints[spawnPoints.Count - 1]; + spawnPoints.Add(current + math.normalizesafe(current - prev)); + } + + /// + /// 移除绳子的最后一个生成点。可以从编辑模式调用。 + /// + public void PopSpawnPoint() + { + if (spawnPoints.Count <= 2) + { + return; + } + + spawnPoints.RemoveAt(spawnPoints.Count - 1); + } + + /// + /// 返回沿绳子曲线特定距离处的模拟粒子索引 + /// + /// 沿绳子曲线的距离 + /// 粒子索引 + public int GetParticleIndexAt(float distance) + { + if (!Initialize() || _measurements.particleSpacing == 0.0f) + { + return 0; + } + + return _measurements.GetParticleIndexAt(distance); + } + + /// + /// 返回特定模拟粒子所在位置沿绳子曲线的标量距离。标量距离是0到1之间的值。 + /// lengthMultiplier不考虑在内。要获得世界空间中沿绳子的距离,请将标量距离乘以realCurveLength测量值。 + /// + /// 模拟粒子的索引 + /// 标量距离 + public float GetScalarDistanceAt(int particleIndex) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return 0.0f; + } + + return math.clamp((float)particleIndex / (measurements.particleCount - 1), 0.0f, 1.0f); + } + + /// + /// 返回特定模拟粒子的当前位置 + /// + /// 模拟粒子的索引 + /// 返回的位置是否应遵循绳子的插值设置。 + /// 世界空间中的当前位置 + public float3 GetPositionAt(int particleIndex, bool respectInterpolation = false) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return float3.zero; + } + + CompletePreviousSimulationFrame(); + if (respectInterpolation && interpolation != RopeInterpolation.None) + { + return interpolatedPositions[particleIndex]; + } + else + { + return positions[particleIndex]; + } + } + + /// + /// 设置特定模拟粒子的位置。由于粒子使用的积分方案,这也会更新速度。 + /// 如果粒子的速度应保持不变,也在调用SetPositionAt()之前设置速度到调用时的值。 + /// 这样工作的原因是这种积分方案使得在遵守多个约束的同时更新位置变得容易。 + /// + /// 模拟粒子的索引 + /// 世界空间中的期望位置 + /// 可使用的最大允许冲量强度。如果为零,则不应用限制。 + public void SetPositionAt(int particleIndex, float3 position) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return; + } + + CompletePreviousSimulationFrame(); + + positions[particleIndex] = position; + } + + /// + /// 设置特定模拟粒子的位置。由于粒子使用的积分方案,这也会更新速度。 + /// 如果粒子的速度应保持不变,也在调用SetPositionAt()之前设置速度到调用时的值。 + /// 这样工作的原因是这种积分方案使得在遵守多个约束的同时更新位置变得容易。 + /// + /// 模拟粒子的索引 + /// 世界空间中的期望位置 + /// 可使用的最大允许冲量强度。如果为零,则不应用限制。 + public void SetPositionAt(int particleIndex, float3 position, float maxImpulseStrength) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length || maxImpulseStrength < 0.0f) + { + return; + } + + CompletePreviousSimulationFrame(); + + if (maxImpulseStrength == 0.0f) + { + positions[particleIndex] = position; + } + else + { + var particleMass = massMultipliers[particleIndex] * simulation.massPerMeter * + _measurements.realCurveLength / _measurements.particleCount; + if (particleMass <= 0.0f) + { + return; + } + + var delta = position - positions[particleIndex]; + var integratedImpulse = delta * particleMass; + var integratedImpulseLength = math.length(integratedImpulse); + + var maxIntegratedImpulse = maxImpulseStrength * Time.fixedDeltaTime; + if (integratedImpulseLength > maxIntegratedImpulse) + { + integratedImpulse *= maxIntegratedImpulse / integratedImpulseLength; + } + + positions[particleIndex] += integratedImpulse / particleMass; + } + } + + /// + /// 返回特定模拟粒子的当前速度 + /// + /// 模拟粒子的索引 + /// 世界空间中的速度 + public float3 GetVelocityAt(int particleIndex) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return float3.zero; + } + + CompletePreviousSimulationFrame(); + return (positions[particleIndex] - prevPositions[particleIndex]) / Time.fixedDeltaTime; + } + + /// + /// 设置特定模拟粒子的速度 + /// + /// 模拟粒子的索引 + /// 世界空间中的期望速度 + public void SetVelocityAt(int particleIndex, float3 velocity) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return; + } + + CompletePreviousSimulationFrame(); + prevPositions[particleIndex] = positions[particleIndex] - velocity * Time.fixedDeltaTime; + } + + /// + /// 返回特定模拟粒子的质量倍增器。该值可用于增加或减少绳子部分的重量。 + /// 值为0将使粒子不可移动。值为2将使粒子比其邻居重两倍。默认值为1。 + /// + /// 模拟粒子的索引 + /// 质量倍增器 + public float GetMassMultiplierAt(int particleIndex) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return 0.0f; + } + + CompletePreviousSimulationFrame(); + return massMultipliers[particleIndex]; + } + + /// + /// 设置特定模拟粒子的质量倍增器。该值可用于增加或减少绳子部分的重量。 + /// 值为0将使粒子不可移动。值为2将使粒子比其邻居重两倍。默认值为1。 + /// + /// 模拟粒子的索引 + /// 期望的质量倍增器 + public void SetMassMultiplierAt(int particleIndex, float value) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length) + { + return; + } + + CompletePreviousSimulationFrame(); + massMultipliers[particleIndex] = value; + } + + /// + /// 查找距离特定点最近的模拟粒子 + /// + /// 世界空间中的点 + /// 最近的模拟粒子的索引 + /// 世界空间中最近的模拟粒子沿绳子的距离 + public void GetClosestParticle(float3 point, out int particleIndex, out float distance) + { + if (!Initialize()) + { + particleIndex = -1; + distance = 0.0f; + return; + } + + CompletePreviousSimulationFrame(); + positions.GetClosestPoint(point, out particleIndex, out distance); + } + + /// + /// 查找距离特定射线最近的模拟粒子 + /// + /// 世界空间中的射线 + /// 最近的模拟粒子的索引 + /// 世界空间中最近的模拟粒子沿绳子的距离 + /// 沿射线到距离模拟粒子最近的点的距离 + public void GetClosestParticle(Ray ray, out int particleIndex, out float distance, out float distanceAlongRay) + { + if (!Initialize()) + { + particleIndex = -1; + distance = 0.0f; + distanceAlongRay = 0.0f; + return; + } + + CompletePreviousSimulationFrame(); + positions.GetClosestPoint(ray, out particleIndex, out distance, out distanceAlongRay); + } + + /// + /// 为下一个模拟帧注册刚体连接。刚体连接是模拟粒子到传统刚体的双向耦合。 + /// 确保从FixedUpdate()调用此方法。任何涉及刚体连接的模拟粒子将在模拟帧结束时将其质量倍增器重置为1。 + /// + /// 要连接的模拟粒子的索引 + /// 要连接的刚体 + /// 应用于刚体的阻尼量,范围[0, 1] + /// 刚体上要连接的世界空间点 + /// 连接的刚度,范围[0, 1] + public void RegisterRigidbodyConnection(int particleIndex, Rigidbody rigidbody, float rigidbodyDamping, + float3 pointOnBody, float stiffness) + { + if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length || !enabled || + !simulation.enabled) + { + return; + } + + queuedRigidbodyConnections.Add(new RigidbodyConnection() + { + rigidbody = rigidbody, + rigidbodyDamping = rigidbodyDamping, + target = new ParticleTarget() + { + particleIndex = particleIndex, + position = pointOnBody, + stiffness = stiffness, + }, + }); + } + + /// + /// 将绳子重置为其相对于当前变换的原始形状。在激活被停用和重新激活而不是销毁和实例化的池化游戏对象时很有用。 + /// + public void ResetToSpawnCurve() + { + if (!Initialize()) + { + return; + } + + CompletePreviousSimulationFrame(); + + var localToWorld = (float4x4)transform.localToWorldMatrix; + + spawnPoints.GetPointsAlongCurve(ref localToWorld, _measurements.particleSpacing, positions); + positions.CopyTo(prevPositions); + } + + /// + /// 计算绳子的当前长度。与measurements.realCurveLength字段相比,该值包括由于应力引起的绳子拉伸。 + /// + public float GetCurrentLength() + { + if (!Initialize()) + { + return 0.0f; + } + + CompletePreviousSimulationFrame(); + + return positions.GetLengthOfCurve(isLoop); + } + + protected Rope2 InstantiateSplitRope(int minIdx, int maxIdx, string identifier) + { + var count = maxIdx - minIdx + 1; + if (minIdx < 0 || maxIdx > positions.Length - 1 || count < 2) + { + return null; + } + + // 创建两个大致放置在新绳子位置的生成点(这将创建漂亮的双切线) + var targetLength = _measurements.realCurveLength * ((float)count / _measurements.particleCount); + var point0 = positions[minIdx]; + var point1 = positions[maxIdx]; + var delta = point1 - point0; + var simplifiedLength = math.length(delta); + point1 += math.normalizesafe(delta) * (targetLength - simplifiedLength); + + var rope = Instantiate(gameObject, Vector3.zero, Quaternion.identity).GetComponent(); + rope.name = identifier; + rope.isLoop = false; + rope.spawnPoints = new List() + { + point0, + point1, + }; + + if (rope.Initialize()) + { + // 现在更新模拟粒子以完全匹配原始绳子 + for (int i = 0; i < rope.positions.Length; i++) + { + var sourceIdx = minIdx + i; + if (sourceIdx >= positions.Length) + { + break; + } + + rope.positions[i] = positions[sourceIdx]; + rope.prevPositions[i] = prevPositions[sourceIdx]; + } + + // 使新绳子与旧绳子大小相同 + rope._measurements.realCurveLength = rope.GetCurrentLength(); + rope._measurements.particleSpacing = _measurements.particleSpacing; + + var param = new OnSplitParams() + { + minParticleIndex = minIdx, + maxParticleIndex = maxIdx, + preSplitMeasurements = _measurements, + }; + + rope.SendMessage("OnRopeSplit", param, SendMessageOptions.DontRequireReceiver); + } + + return rope; + } + + /// + /// 在特定模拟粒子处分割绳子并返回新实例化游戏对象的绳子组件。确保 + /// 提供的数组正好有2个槽位。将向每个新创建的绳子发送Unity消息'OnRopeSplit(Rope.OnSplitParams)'。 + /// + /// 分割点的模拟粒子索引 + /// 如果不为null,一个正好有2个元素的数组,其中将返回新的绳子游戏对象 + public void SplitAt(int particleIndex, Rope2[] outNewRopes = null) + { + if (!Initialize() || (outNewRopes != null && outNewRopes.Length != 2) || wasSplit) + { + return; + } + + wasSplit = true; + + var fst = InstantiateSplitRope(0, particleIndex, name + "_split0"); + var snd = InstantiateSplitRope(particleIndex + 1, positions.Length - 1, name + "_split1"); + + Destroy(gameObject); + + if (outNewRopes != null) + { + outNewRopes[0] = fst; + outNewRopes[1] = snd; + } + } + + protected void ComputeRealCurve(Allocator allocator, out Measurements measurements, + out NativeArray points) + { + var localToWorld = (float4x4)transform.localToWorldMatrix; + + var spawnCurveLength = spawnPoints.GetLengthOfCurve(ref localToWorld); + var segmentCount = math.max(1, (int)(spawnCurveLength * simulation.resolution)); + var particleCount = segmentCount + 1; + var particleSpacing = spawnCurveLength / segmentCount; + + points = new NativeArray(particleCount, allocator); + spawnPoints.GetPointsAlongCurve(ref localToWorld, particleSpacing, points); + var realCurveLength = points.GetLengthOfCurve(ref localToWorld); + + measurements = new Measurements() + { + spawnCurveLength = spawnCurveLength, + realCurveLength = realCurveLength, + segmentCount = segmentCount, + particleCount = particleCount, + particleSpacing = particleSpacing, + }; + } + + public void OnEnable() + { + if (!initialized) + { + return; + } + + CompletePreviousSimulationFrame(); + } + + public void Start() + { + Initialize(); + } + + public void OnDisable() + { + if (!initialized) + { + return; + } + + CompletePreviousSimulationFrame(); + + simulationDisabledPrevFrame = true; + } + + public void OnDestroy() + { + if (!initialized) + { + return; + } + + CompletePreviousSimulationFrame(); + + // 状态 + positions.Dispose(); + prevPositions.Dispose(); + if (interpolatedPositions.IsCreated) + { + interpolatedPositions.Dispose(); + } + + bitangents.Dispose(); + massMultipliers.Dispose(); + + // 碰撞处理 + collisionPlanesActive.Dispose(); + collisionPlanes.Dispose(); + collisionRigidbodies = null; + + // 刚体连接 + particleTargets.Dispose(); + particleTargetFeedbacks.Dispose(); + + // 渲染 + vertices.Dispose(); + normals.Dispose(); + cosLookup.Dispose(); + sinLookup.Dispose(); + + Destroy(mesh); + } + + protected bool Initialize() + { + if (initialized) + { + return true; + } + + if (!Application.isPlaying || spawnPoints.Count < 2) + { + // 不适用于编辑模式执行 + return false; + } + + if (rendering.useSimpleLineRenderer) + { + lineRenderer = gameObject.GetComponent(); + if (lineRenderer == null) + { + lineRenderer = gameObject.AddComponent(); + } + + lineRenderer.useWorldSpace = true; + lineRenderer.loop = isLoop; + lineRenderer.widthMultiplier = rendering.simpleLineWidth; + lineRenderer.material = material; + lineRenderer.shadowCastingMode = shadowMode; + lineRenderer.receiveShadows = false; + lineRenderer.alignment = LineAlignment.View; + } + + // 状态 + ComputeRealCurve(Allocator.Persistent, out _measurements, out positions); + + prevPositions = new NativeArray(_measurements.particleCount, Allocator.Persistent); + positions.CopyTo(prevPositions); + if (interpolation != RopeInterpolation.None) + { + interpolatedPositions = new NativeArray(_measurements.particleCount, Allocator.Persistent); + positions.CopyTo(interpolatedPositions); + } + + bitangents = new NativeArray(_measurements.particleCount, Allocator.Persistent); + { + var up = new float3(0.0f, 1.0f, 0.0f); + + for (int i = 0; i < bitangents.Length; i++) + { + var tangent = positions[(i + 1) % bitangents.Length] - positions[i]; + + var bitangent = math.normalizesafe(math.cross(up, tangent)); + if (math.all(bitangent == float3.zero)) + { + bitangent = math.normalizesafe(math.cross(up + new float3(0.0f, 0.0f, -1.0f), tangent)); + } + + bitangents[i] = bitangent; + + up = math.cross(tangent, bitangent); + } + + if (!isLoop) + { + bitangents[bitangents.Length - 1] = bitangents[bitangents.Length - 2]; + } + } + + massMultipliers = new NativeArray(_measurements.particleCount, Allocator.Persistent); + for (int i = 0; i < massMultipliers.Length; i++) + { + massMultipliers[i] = 1.0f; + } + + // 碰撞处理 + collisionPlanesActive = new NativeArray(_measurements.particleCount, Allocator.Persistent); + collisionPlanes = + new NativeArray(_measurements.particleCount * MaxCollisionPlanesPerParticle, + Allocator.Persistent); + collisionRigidbodies = new Rigidbody[collisionPlanes.Length]; + + // 刚体连接 + queuedRigidbodyConnections = new List(); + liveRigidbodyConnections = new List(); + particleTargets = new NativeArray(InitialParticleTargets, Allocator.Persistent); + particleTargetFeedbacks = new NativeArray(InitialParticleTargets, Allocator.Persistent); + + // 渲染 + vertices = new NativeArray(_measurements.particleCount * radialVertices, Allocator.Persistent); + normals = new NativeArray(vertices.Length, Allocator.Persistent); + cosLookup = new NativeArray(radialVertices, Allocator.Persistent); + sinLookup = new NativeArray(radialVertices, Allocator.Persistent); + + for (int i = 0; i < radialVertices; i++) + { + var angle = ((float)i / (radialVertices - 1)) * Mathf.PI * 2.0f; + cosLookup[i] = Mathf.Cos(angle); + sinLookup[i] = Mathf.Sin(angle); + } + + // 注意,网格创建后三角形和uv保持不变 + var triangleParticleMax = isLoop ? _measurements.particleCount : _measurements.particleCount - 1; + var radialTriangleCount = triangleParticleMax * (radialVertices - 1) * 2 * 3; + var capTriangleCount = isLoop ? 0 : 2 * (radialVertices - 3) * 3; + var triangleCount = radialTriangleCount + capTriangleCount; + var triangles = new int[triangleCount]; + + int idx = 0; + for (int i = 0; i < triangleParticleMax; i++) + { + int vertexOffset0 = i * radialVertices; + int vertexOffset1 = ((i + 1) % _measurements.particleCount) * radialVertices; + + for (int j = 0; j < radialVertices - 1; j++) + { + int v0 = vertexOffset0 + j + 0; + int v1 = vertexOffset0 + j + 1; + int v2 = vertexOffset1 + j + 0; + int v3 = vertexOffset1 + j + 1; + + triangles[idx++] = v0; + triangles[idx++] = v1; + triangles[idx++] = v2; + triangles[idx++] = v2; + triangles[idx++] = v1; + triangles[idx++] = v3; + } + } + + if (!isLoop) + { + for (int i = 1; i < radialVertices - 2; i++) + { + triangles[idx++] = 0; + triangles[idx++] = i + 1; + triangles[idx++] = i; + } + + int vertexOffset = triangleParticleMax * radialVertices; + for (int i = 1; i < radialVertices - 2; i++) + { + triangles[idx++] = vertexOffset; + triangles[idx++] = vertexOffset + i; + triangles[idx++] = vertexOffset + i + 1; + } + } + + var uvs = new Vector2[vertices.Length]; + for (int i = 0; i < _measurements.particleCount; i++) + { + var uv = new Vector2 + { + x = ((float)i / (_measurements.particleCount - 1)) * _measurements.realCurveLength, + }; + + for (int j = 0; j < radialVertices; j++) + { + uv.y = (float)j / (radialVertices - 1); + uvs[i * radialVertices + j] = uv; + } + } + + mesh = new Mesh + { + name = gameObject.name + "_rope" + }; + mesh.MarkDynamic(); + mesh.SetVertices(vertices); + mesh.SetNormals(normals); + mesh.uv = uvs; + mesh.triangles = triangles; + + initialized = true; + computingSimulationFrame = false; + return true; + } + + protected Collider[] collisionQueryBuffer = new Collider[MaxCollisionPlanesPerParticle]; + + public void UpdateCollisionPlanes() + { + if (!collisions.enabled) + { + return; + } + + Profiler.BeginSample(nameof(UpdateCollisionPlanes)); + + var deltaTime = Time.fixedDeltaTime; + var layerMask = ~collisions.ignoreLayers; + var safeRadius = radius + collisions.collisionMargin; + var safeRadiusSq = safeRadius * safeRadius; + var extendedRadius = safeRadius * 1.5f; + + for (int i = 0; i < collisionPlanesActive.Length; i++) + { + if (i % collisions.stride != 0) + { + collisionPlanesActive[i] = 0; + for (int j = 0; j < MaxCollisionPlanesPerParticle; j++) + { + collisionRigidbodies[i * MaxCollisionPlanesPerParticle + j] = null; + } + + continue; + } + + var planeCount = 0; + + // 使用下一帧的投影位置 + var pos = positions[i]; + var prevPos = prevPositions[i]; + var vel = pos - prevPos; + prevPos = pos; + pos += vel; + + // 检查重叠 + var hitCount = + Physics.OverlapSphereNonAlloc(pos, extendedRadius, collisionQueryBuffer, + layerMask); // 使用稍大的球体来捕获更多碰撞 + for (int j = 0; j < hitCount && planeCount < MaxCollisionPlanesPerParticle; j++) + { + var collider = collisionQueryBuffer[j]; + var meshCollider = collider as MeshCollider; + + if (collider is BoxCollider || + collider is SphereCollider || + collider is CapsuleCollider || + (meshCollider != null && meshCollider.convex)) + { + var closestPoint = (float3)Physics.ClosestPoint(pos, collider, collider.transform.position, + collider.transform.rotation); + + var normal = math.normalizesafe(pos - closestPoint); + if (math.all(normal == float3.zero)) + { + continue; + } + + collisionPlanes[i * MaxCollisionPlanesPerParticle + planeCount] = new CollisionPlane() + { + point = closestPoint, + normal = normal, + velocityChange = collider.attachedRigidbody != null + ? (float3)collider.attachedRigidbody.GetPointVelocity(closestPoint) * deltaTime + : float3.zero, + }; + collisionRigidbodies[i * MaxCollisionPlanesPerParticle + planeCount] = + collider.attachedRigidbody; + planeCount++; + } + } + + // 检查快速移动 + if (planeCount < MaxCollisionPlanesPerParticle) + { + var movementSq = math.lengthsq(vel); + if (movementSq > safeRadiusSq) + { + if (Physics.Linecast(prevPos, pos, out RaycastHit hit, layerMask)) + { + collisionPlanes[i * MaxCollisionPlanesPerParticle + planeCount] = new CollisionPlane() + { + point = hit.point, + normal = hit.normal, + velocityChange = hit.rigidbody != null + ? (float3)hit.rigidbody.GetPointVelocity(hit.point) * deltaTime + : float3.zero, + }; + collisionRigidbodies[i * MaxCollisionPlanesPerParticle + planeCount] = hit.rigidbody; + planeCount++; + } + } + } + + collisionPlanesActive[i] = planeCount; + } + + Profiler.EndSample(); + } + + protected void PrepareRigidbodyConnections() + { + Profiler.BeginSample(nameof(PrepareRigidbodyConnections)); + + liveRigidbodyConnections.AddRange(queuedRigidbodyConnections); + queuedRigidbodyConnections.Clear(); + + if (liveRigidbodyConnections.Count > particleTargets.Length) + { + if (liveRigidbodyConnections.Count > MaxRigidbodyConnections) + { + Debug.LogWarning( + $"本帧遇到过多的活动刚体连接({liveRigidbodyConnections.Count})。" + + $"限制执行到最大值({MaxRigidbodyConnections})以避免性能下降..."); + } + else + { + var newCapacity = liveRigidbodyConnections.Count * 2; + + particleTargets.Dispose(); + particleTargets = new NativeArray(newCapacity, Allocator.Persistent); + particleTargetFeedbacks.Dispose(); + particleTargetFeedbacks = new NativeArray(newCapacity, Allocator.Persistent); + } + } + + for (int i = 0; i < particleTargets.Length; i++) + { + if (i < liveRigidbodyConnections.Count) + { + var c = liveRigidbodyConnections[i]; + + if (!c.rigidbody) + { + c.target.stiffness = 0.0f; + } + + particleTargets[i] = c.target; + + // 如果刚体是运动学的,则使粒子不可移动 + if (c.rigidbody && c.rigidbody.isKinematic) + { + massMultipliers[c.target.particleIndex] = 0.0f; + } + } + else + { + particleTargets[i] = new ParticleTarget() + { + particleIndex = -1, + }; + } + } + + Profiler.EndSample(); + } + + protected void ApplyRigidbodyFeedback() + { + Profiler.BeginSample(nameof(ApplyRigidbodyFeedback)); + + var particleMass = simulation.massPerMeter * _measurements.realCurveLength / _measurements.particleCount; + var invDtAndSim = 1.0f / (Time.fixedDeltaTime * simulation.substeps * simulation.solverIterations); + + // 碰撞 + if (collisions.enabled && collisions.influenceRigidbodies) + { + for (int i = 0; i < collisionPlanesActive.Length; i++) + { + if (i % collisions.stride != 0) + { + continue; + } + + int activePlaneCount = collisionPlanesActive[i]; + for (int j = 0; j < activePlaneCount; j++) + { + Rigidbody rb = collisionRigidbodies[i * MaxCollisionPlanesPerParticle + j]; + if (rb != null && !rb.isKinematic) + { + CollisionPlane plane = collisionPlanes[i * MaxCollisionPlanesPerParticle + j]; + float3 impulse = plane.feedback * (particleMass * invDtAndSim); + rb.ApplyImpulseNow(plane.point, impulse); + } + } + } + } + + // 连接 + if (liveRigidbodyConnections.Count > 0) + { + var iterationCount = math.min(liveRigidbodyConnections.Count, particleTargetFeedbacks.Length); + + for (int i = 0; i < iterationCount; i++) + { + var c = liveRigidbodyConnections[i]; + + // 应用冲量 + if (c.rigidbody) + { + float3 impulse = particleTargetFeedbacks[i] * (particleMass * invDtAndSim); + c.rigidbody.ApplyImpulseNow(c.target.position, impulse); + + if (c.rigidbodyDamping > 0.0f) + { + float3 normal = math.normalizesafe(impulse); + c.rigidbody.SetPointVelocityNow(c.target.position, normal, 0.0f, c.rigidbodyDamping); + } + } + + // 重置粒子质量倍增器(如果身体是运动学的可能已更改) + massMultipliers[c.target.particleIndex] = 1.0f; + } + + liveRigidbodyConnections.Clear(); + } + + Profiler.EndSample(); + } + + protected void ScheduleNextSimulationFrame() + { + Profiler.BeginSample(nameof(ScheduleNextSimulationFrame)); + + computingSimulationFrame = true; + + var gravity = simulation.useCustomGravity ? simulation.customGravity : (float3)Physics.gravity; + + var simulate = new SimulateJob() + { + deltaTime = Time.fixedDeltaTime, + externalAcceleration = gravity * simulation.gravityMultiplier, + energyKept = 1.0f - simulation.energyLoss, + + positions = positions, + prevPositions = prevPositions, + massMultipliers = massMultipliers, + + isLoop = isLoop, + substeps = simulation.substeps, + solverIterations = simulation.solverIterations, + stiffness = simulation.stiffness, + desiredSpacing = _measurements.particleSpacing * simulation.lengthMultiplier, + + collisionsEnabled = collisions.enabled, + radius = radius + collisions.collisionMargin, + friction = collisions.friction, + maxCollisionPlanesPerParticle = MaxCollisionPlanesPerParticle, + collisionPlanesActive = collisionPlanesActive, + collisionPlanes = collisionPlanes, + + particleTargets = particleTargets, + particleTargetFeedbacks = particleTargetFeedbacks, + }.Schedule(); + + if (interpolation == RopeInterpolation.None) + { + simulationFrameHandle = new OutputVerticesJob() + { + positions = positions, + bitangents = bitangents, + isLoop = isLoop, + radialVertices = radialVertices, + radius = radius, + cosLookup = cosLookup, + sinLookup = sinLookup, + vertices = vertices, + normals = normals, + }.Schedule(simulate); + } + else + { + // OutputVerticesJob必须在插值之后发生,而插值发生在Update()中,而不是FixedUpdate() + simulationFrameHandle = simulate; + } + + JobHandle.ScheduleBatchedJobs(); + + Profiler.EndSample(); + } + + protected void ScheduleInterpolation() + { + if (interpolation == RopeInterpolation.None) + { + return; + } + + CompletePreviousSimulationFrame(); + + Profiler.BeginSample(nameof(ScheduleInterpolation)); + + computingSimulationFrame = true; + + var invDt = 1.0f / Time.fixedDeltaTime; + + var handle = new JobHandle(); + + if (interpolation == RopeInterpolation.Interpolate) + { + handle = new InterpolatePositionsJob() + { + positions = positions, + prevPositions = prevPositions, + + invDeltaTime = invDt, + timeSinceFixedUpdate = timeSinceFixedUpdate, + + interpolatedPositions = interpolatedPositions, + }.Schedule(); + } + else + { + handle = new ExtrapolatePositionsJob() + { + positions = positions, + prevPositions = prevPositions, + + invDeltaTime = invDt, + timeSinceFixedUpdate = timeSinceFixedUpdate, + + interpolatedPositions = interpolatedPositions, + }.Schedule(); + } + + simulationFrameHandle = new OutputVerticesJob() + { + positions = interpolatedPositions, + bitangents = bitangents, + isLoop = isLoop, + radialVertices = radialVertices, + radius = radius, + cosLookup = cosLookup, + sinLookup = sinLookup, + vertices = vertices, + normals = normals, + }.Schedule(handle); + + JobHandle.ScheduleBatchedJobs(); + + Profiler.EndSample(); + } + + protected void CompletePreviousSimulationFrame() + { + if (!computingSimulationFrame) + { + return; + } + + Profiler.BeginSample(nameof(CompletePreviousSimulationFrame)); + + simulationFrameHandle.Complete(); + computingSimulationFrame = false; + + Profiler.EndSample(); + } + + protected static void FillMeshFrames(ref NativeArray positions, ref NativeArray bitangents, + Matrix4x4[] meshFrames, float spacing, bool isLoop, float rotationOffset, Vector3 scaleMultiplier, + bool stretch) + { + var scale = scaleMultiplier * 0.5f * spacing; + if (stretch) + { + scale.z = scaleMultiplier.z * 0.5f; + } + + var currentRotation = 0.0f; + for (var i = 0; i < positions.Length; i++) // 在这里使用positions数组,希望编译器能优化掉边界检查... + { + var tangent = Vector3.zero; + if (isLoop) + { + tangent = positions[(i + 1) % positions.Length] - positions[i]; + } + else + { + tangent = i < positions.Length - 1 + ? positions[i + 1] - positions[i] + : positions[i] - positions[i - 1]; + } + + var frameScale = stretch + ? new Vector3(scale.x, scale.y, scale.z * tangent.magnitude) + : scale; + tangent.Normalize(); + + var frameRotation = Quaternion.LookRotation(tangent, bitangents[i]) * + Quaternion.Euler(0.0f, 0.0f, currentRotation); + currentRotation += rotationOffset; + + customMeshFrames[i] = Matrix4x4.TRS(positions[i], frameRotation, frameScale); + } + } + + protected static Matrix4x4[] customMeshFrames; + + protected void SubmitToRenderer() + { + if (material == null) + { + return; + } + + Profiler.BeginSample(nameof(SubmitToRenderer)); + if (rendering.useSimpleLineRenderer) + { + // 使用简单线渲染 + if (lineRenderer != null && simulation.enabled) + { + lineRenderer.positionCount = positions.Length; + for (int i = 0; i < positions.Length; i++) + { + lineRenderer.SetPosition(i, positions[i]); + } + } + } + else + { + // 默认绳子圆柱体 + if (simulation.enabled) + { + mesh.SetVertices(vertices); + mesh.SetNormals(normals); + mesh.RecalculateBounds(); + } + + Graphics.DrawMesh(mesh, Matrix4x4.identity, material, gameObject.layer, null, 0, null, shadowMode); + } + + Profiler.EndSample(); + } + + public void FixedUpdate() + { + timeSinceFixedUpdate = 0.0f; + + if (!initialized) + { + return; + } + + if (!simulation.enabled) + { + simulationDisabledPrevFrame = true; + return; + } + + CompletePreviousSimulationFrame(); // 固定更新可能在每个渲染帧中运行多次 + + if (simulationDisabledPrevFrame) + { + queuedRigidbodyConnections.Clear(); + liveRigidbodyConnections.Clear(); + } + + simulationDisabledPrevFrame = false; + + transform.position = positions[0]; + + ApplyRigidbodyFeedback(); // 来自前一帧 + + UpdateCollisionPlanes(); + + PrepareRigidbodyConnections(); + + ScheduleNextSimulationFrame(); + } + + public void LateUpdate() + { + timeSinceFixedUpdate += Time.deltaTime; + + if (!initialized) + { + return; + } + + if (interpolation != RopeInterpolation.None) + { + ScheduleInterpolation(); + } + + CompletePreviousSimulationFrame(); + + SubmitToRenderer(); + } + +#if UNITY_EDITOR + public void OnDrawGizmos() + { + if (Application.isPlaying || spawnPoints.Count < 2 || !enabled) + { + return; + } + + ComputeRealCurve(Allocator.Temp, out Measurements measurements, out NativeArray points); + + Gizmos.color = Colors.ropeSegments; + for (int i = 0; i < points.Length - 1; i++) + { + Gizmos.DrawLine(points[i], points[i + 1]); + } + + if (isLoop && points.Length > 1) + { + Gizmos.DrawLine(points[points.Length - 1], points[0]); + } + + if (UnityEditor.Selection.Contains(gameObject)) + { + for (int i = 0; i < points.Length; i++) + { + if (collisions.enabled && i % collisions.stride == 0) + { + Gizmos.color = Colors.collisionParticle; + } + else + { + Gizmos.color = Colors.simulationParticle; + } + + Gizmos.DrawSphere(points[i], radius); + } + } + + points.Dispose(); + } + + public void OnDrawGizmosSelected() + { + if (!initialized) + { + return; + } + + var bounds = currentBounds; + Gizmos.color = Color.gray; + Gizmos.DrawWireCube(bounds.center, bounds.size); + } +#endif + + [BurstCompile] + private struct SimulateJob : IJob + { + [ReadOnly] public float deltaTime; + [ReadOnly] public float3 externalAcceleration; + [ReadOnly] public float energyKept; + + // 状态 + public NativeArray positions; + public NativeArray prevPositions; + [ReadOnly] public NativeArray massMultipliers; + + // 形状 + [ReadOnly] public bool isLoop; + [ReadOnly] public int substeps; + [ReadOnly] public int solverIterations; + [ReadOnly] public float stiffness; + [ReadOnly] public float desiredSpacing; + + // 碰撞处理 + [ReadOnly] public bool collisionsEnabled; + [ReadOnly] public float radius; + [ReadOnly] public float friction; + [ReadOnly] public int maxCollisionPlanesPerParticle; + [ReadOnly] public NativeArray collisionPlanesActive; + public NativeArray collisionPlanes; + + // 刚体附件 + [ReadOnly] public NativeArray particleTargets; + public NativeArray particleTargetFeedbacks; + + public void Execute() + { + // 准备模拟 + for (int i = 0; i < particleTargetFeedbacks.Length; i++) + { + particleTargetFeedbacks[i] = float3.zero; + } + + // 模拟 + float dt = deltaTime / substeps; + float invDt = 1.0f / dt; + bool forwardSolve = true; + + for (int substep = 0; substep < substeps; substep++) + { + for (int i = 0; i < positions.Length; i++) + { + if (massMultipliers[i] == 0.0f) + { + prevPositions[i] = positions[i]; + continue; + } + + var pos = positions[i]; + var prevPos = prevPositions[i]; + + var vel = (pos - prevPos) * invDt; + vel += externalAcceleration * dt; + vel *= energyKept; + + prevPositions[i] = pos; + positions[i] += vel * dt; + } + + for (int iter = 0; iter < solverIterations; iter++) + { + int loopCount = isLoop ? positions.Length : positions.Length - 1; + + // 应用杆约束 + if (forwardSolve) // 交替向前和向后求解以平衡误差 + { + for (int i = 0; i < loopCount; i++) + { + ApplyStickConstraint(i, (i + 1) % positions.Length); + } + } + else + { + for (int i = loopCount - 1; i >= 0; i--) + { + ApplyStickConstraint(i, (i + 1) % positions.Length); + } + } + + forwardSolve = !forwardSolve; + + // 应用碰撞约束 + if (collisionsEnabled) + { + for (int i = 0; i < positions.Length; i++) + { + for (int j = 0; j < collisionPlanesActive[i]; j++) + { + int planeIndex = i * maxCollisionPlanesPerParticle + j; + CollisionPlane plane = collisionPlanes[planeIndex]; + ApplyCollisionConstraint(i, ref plane); + collisionPlanes[planeIndex] = plane; + } + } + } + + // 应用刚体连接 + for (int i = 0; i < particleTargets.Length; i++) + { + var target = particleTargets[i]; + if (target.particleIndex == -1) + { + continue; + } + + var delta = (target.position - positions[target.particleIndex]) * target.stiffness; + positions[target.particleIndex] += delta; + particleTargetFeedbacks[i] -= delta * massMultipliers[target.particleIndex]; + } + } + } + } + + private void ApplyStickConstraint(int idx0, int idx1) + { + var delta = positions[idx0] - positions[idx1]; + var dist = math.length(delta); + if (dist > 0.0f) + { + delta /= dist; + } + else + { + delta = 0.0f; + } + + var correction = (dist - desiredSpacing) * stiffness; + + var w0 = massMultipliers[idx0]; + if (w0 > 0.0f) + { + w0 = 1.0f / w0; + } + + var w1 = massMultipliers[idx1]; + if (w1 > 0.0f) + { + w1 = 1.0f / w1; + } + + var invSumW = w0 + w1; + if (invSumW > 0.0f) + { + invSumW = 1.0f / invSumW; + } + + positions[idx0] -= delta * (correction * w0 * invSumW); + positions[idx1] += delta * (correction * w1 * invSumW); + } + + private void ApplyCollisionConstraint(int idx, ref CollisionPlane plane) + { + float dist = math.dot(positions[idx] - plane.point, plane.normal); + if (dist <= radius) + { + float depth = radius - dist; + float3 correction = plane.normal * depth; + positions[idx] += correction; + plane.feedback -= correction * massMultipliers[idx]; + + // 摩擦 + var delta = (positions[idx] - prevPositions[idx]) - plane.velocityChange; + var length = math.lengthsq(delta); + if (length > 0.0f) + { + length = math.sqrt(length); + delta /= length; + } + + prevPositions[idx] += delta * math.min(depth * friction, length); + } + } + } + + [BurstCompile] + private struct InterpolatePositionsJob : IJob + { + [ReadOnly] public NativeArray positions; + [ReadOnly] public NativeArray prevPositions; + + [ReadOnly] public float invDeltaTime; + [ReadOnly] public float timeSinceFixedUpdate; + + [WriteOnly] public NativeArray interpolatedPositions; + + public void Execute() + { + var scalar = timeSinceFixedUpdate * invDeltaTime; + + for (int i = 0; i < interpolatedPositions.Length; i++) + { + interpolatedPositions[i] = math.lerp(prevPositions[i], positions[i], scalar); + } + } + } + + [BurstCompile] + private struct ExtrapolatePositionsJob : IJob + { + [ReadOnly] public NativeArray positions; + [ReadOnly] public NativeArray prevPositions; + + [ReadOnly] public float invDeltaTime; + [ReadOnly] public float timeSinceFixedUpdate; + + [WriteOnly] public NativeArray interpolatedPositions; + + public void Execute() + { + for (int i = 0; i < interpolatedPositions.Length; i++) + { + var vel = (positions[i] - prevPositions[i]) * invDeltaTime; + + interpolatedPositions[i] = positions[i] + vel * timeSinceFixedUpdate; + } + } + } + + [BurstCompile] + private struct OutputVerticesJob : IJob + { + [ReadOnly] public NativeArray positions; + + public NativeArray bitangents; + + [ReadOnly] public bool isLoop; + [ReadOnly] public int radialVertices; + [ReadOnly] public float radius; + [ReadOnly] public NativeArray cosLookup; + [ReadOnly] public NativeArray sinLookup; + + [WriteOnly] public NativeArray vertices; + [WriteOnly] public NativeArray normals; + + public void Execute() + { + var last = positions.Length - 1; + + // 扩散双切线 + var smoothedBitangents = new NativeArray(bitangents.Length, Allocator.Temp); + + smoothedBitangents[0] = bitangents[0] + bitangents[1]; + if (isLoop) + { + smoothedBitangents[0] += bitangents[last]; + } + + for (int i = 1; i < bitangents.Length - 1; i++) + { + smoothedBitangents[i] = bitangents[i - 1] + bitangents[i] + bitangents[i + 1]; + } + + smoothedBitangents[last] = bitangents[last - 1] + bitangents[last]; + if (isLoop) + { + smoothedBitangents[last] += bitangents[0]; + } + + // 重新标准化双切线 + for (int i = 0; i < bitangents.Length; i++) + { + var tangent = positions[(i + 1) % positions.Length] - positions[i]; + var normal = math.cross(tangent, smoothedBitangents[i]); + + bitangents[i] = math.normalizesafe(math.cross(normal, tangent)); + } + + if (!isLoop) + { + bitangents[last] = bitangents[last - 1]; + } + + // 设置顶点 + for (int i = 0; i < positions.Length; i++) + { + var tangent = float3.zero; + if (isLoop) + { + tangent = positions[(i + 1) % positions.Length] - positions[i]; + } + else + { + tangent = i < last + ? positions[i + 1] - positions[i] + : positions[i] - positions[i - 1]; + } + + var bitangent = bitangents[i]; + var normal = math.normalizesafe(math.cross(tangent, bitangent)); + + for (int j = 0; j < radialVertices; j++) + { + float3 extent = bitangent * cosLookup[j] + normal * sinLookup[j]; + vertices[i * radialVertices + j] = positions[i] + extent * radius; + normals[i * radialVertices + j] = extent; + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ThirdParty/Rope/Rope2.cs.meta b/Assets/Scripts/ThirdParty/Rope/Rope2.cs.meta new file mode 100644 index 000000000..a9c349ea1 --- /dev/null +++ b/Assets/Scripts/ThirdParty/Rope/Rope2.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 291103b05c314ad39c8a118ccc37871b +timeCreated: 1762270828 \ No newline at end of file