Files
Fishing2/Assets/Scripts/ThirdParty/Rope/Rope.cs
2025-11-05 18:00:02 +08:00

1399 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 enum RopeInterpolation
{
None = 0,
Interpolate,
Extrapolate,
}
public partial class Rope : 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);
}
}
[Tooltip("绳子的半径。该值既用于构建视觉网格,也用于处理碰撞。")] [Range(0.0001f, 1.0f)]
public float radius = 0.05f;
[Tooltip("用于渲染绳子的材质。这可以是任何使用顶点位置和可选法线的材质。")]
public Material material;
[Tooltip("绳子使用的阴影投射模式")] public ShadowCastingMode shadowMode = ShadowCastingMode.On;
[Tooltip("用于初始放置绳子在世界中的生成点。目前,连续的生成点对被视为线性线段。")] [DisableInPlayMode]
public List<float3> spawnPoints = new List<float3>();
[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("高级(更改这些将需要调整基本特性)")]
[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,
energyLoss = 0.0025f,
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, 1.0f)]
public float collisionMargin;
public LayerMask ignoreLayers;
}
[Space] public CollisionSettings collisions = new CollisionSettings()
{
enabled = false,
influenceRigidbodies = true,
stride = 2,
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 float timeSinceFixedUpdate;
protected JobHandle simulationFrameHandle;
// State
protected NativeArray<float3> positions;
protected NativeArray<float3> prevPositions;
protected NativeArray<float3> interpolatedPositions;
protected NativeArray<float3> bitangents;
protected NativeArray<float> massMultipliers;
// Collision handling
protected NativeArray<int> collisionPlanesActive;
protected NativeArray<CollisionPlane> collisionPlanes;
protected Rigidbody[] collisionRigidbodies;
// Rigidbody connections
protected List<RigidbodyConnection> queuedRigidbodyConnections;
protected List<RigidbodyConnection> liveRigidbodyConnections;
protected NativeArray<ParticleTarget> particleTargets;
protected NativeArray<float3> particleTargetFeedbacks;
protected Measurements _measurements;
/// <summary>
/// 返回绳子的测量值。测量值在绳子首次初始化后保持不变。
/// </summary>
public Measurements measurements
{
get
{
if (!Initialize())
{
return new Measurements();
}
return _measurements;
}
}
public void OnValidate()
{
simulation.resolution = Mathf.Max(0.01f, simulation.resolution);
simulation.massPerMeter = Mathf.Max(0.01f, simulation.massPerMeter);
}
/// <summary>
/// 向绳子添加新的生成点。可以从编辑模式调用。
/// </summary>
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));
}
/// <summary>
/// 移除绳子的最后一个生成点。可以从编辑模式调用。
/// </summary>
public void PopSpawnPoint()
{
if (spawnPoints.Count <= 2)
{
return;
}
spawnPoints.RemoveAt(spawnPoints.Count - 1);
}
/// <summary>
/// 返回沿绳子曲线特定距离处的模拟粒子索引
/// </summary>
/// <param name="distance">沿绳子曲线的距离</param>
/// <returns>粒子索引</returns>
public int GetParticleIndexAt(float distance)
{
if (!Initialize() || _measurements.particleSpacing == 0.0f)
{
return 0;
}
return _measurements.GetParticleIndexAt(distance);
}
/// <summary>
/// 返回特定模拟粒子所在位置沿绳子曲线的标量距离。标量距离是0到1之间的值。
/// lengthMultiplier不考虑在内。要获得世界空间中沿绳子的距离请将标量距离乘以realCurveLength测量值。
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <returns>标量距离</returns>
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);
}
/// <summary>
/// 返回特定模拟粒子的当前位置
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <param name="respectInterpolation">返回的位置是否应遵循绳子的插值设置。</param>
/// <returns>世界空间中的当前位置</returns>
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];
}
}
/// <summary>
/// 设置特定模拟粒子的位置。由于粒子使用的积分方案,这也会更新速度。
/// 如果粒子的速度应保持不变也在调用SetPositionAt()之前设置速度到调用时的值。
/// 这样工作的原因是这种积分方案使得在遵守多个约束的同时更新位置变得容易。
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <param name="position">世界空间中的期望位置</param>
public void SetPositionAt(int particleIndex, float3 position)
{
if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length)
{
return;
}
CompletePreviousSimulationFrame();
positions[particleIndex] = position;
}
/// <summary>
/// 设置特定模拟粒子的位置。由于粒子使用的积分方案,这也会更新速度。
/// 如果粒子的速度应保持不变也在调用SetPositionAt()之前设置速度到调用时的值。
/// 这样工作的原因是这种积分方案使得在遵守多个约束的同时更新位置变得容易。
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <param name="position">世界空间中的期望位置</param>
/// <param name="maxImpulseStrength">可使用的最大允许冲量强度。如果为零,则不应用限制。</param>
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;
}
}
/// <summary>
/// 返回特定模拟粒子的当前速度
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <returns>世界空间中的速度</returns>
public float3 GetVelocityAt(int particleIndex)
{
if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length)
{
return float3.zero;
}
CompletePreviousSimulationFrame();
return (positions[particleIndex] - prevPositions[particleIndex]) / Time.fixedDeltaTime;
}
/// <summary>
/// 设置特定模拟粒子的速度
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <param name="velocity">世界空间中的期望速度</param>
public void SetVelocityAt(int particleIndex, float3 velocity)
{
if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length)
{
return;
}
CompletePreviousSimulationFrame();
prevPositions[particleIndex] = positions[particleIndex] - velocity * Time.fixedDeltaTime;
}
/// <summary>
/// 返回特定模拟粒子的质量倍增器。该值可用于增加或减少绳子部分的重量。
/// 值为0将使粒子不可移动。值为2将使粒子比其邻居重两倍。默认值为1。
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <returns>质量倍增器</returns>
public float GetMassMultiplierAt(int particleIndex)
{
if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length)
{
return 0.0f;
}
CompletePreviousSimulationFrame();
return massMultipliers[particleIndex];
}
/// <summary>
/// 设置特定模拟粒子的质量倍增器。该值可用于增加或减少绳子部分的重量。
/// 值为0将使粒子不可移动。值为2将使粒子比其邻居重两倍。默认值为1。
/// </summary>
/// <param name="particleIndex">模拟粒子的索引</param>
/// <param name="value">期望的质量倍增器</param>
public void SetMassMultiplierAt(int particleIndex, float value)
{
if (!Initialize() || particleIndex < 0 || particleIndex >= positions.Length)
{
return;
}
CompletePreviousSimulationFrame();
massMultipliers[particleIndex] = value;
}
/// <summary>
/// 查找距离特定点最近的模拟粒子
/// </summary>
/// <param name="point">世界空间中的点</param>
/// <param name="particleIndex">最近的模拟粒子的索引</param>
/// <param name="distance">世界空间中最近的模拟粒子沿绳子的距离</param>
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);
}
/// <summary>
/// 查找距离特定射线最近的模拟粒子
/// </summary>
/// <param name="ray">世界空间中的射线</param>
/// <param name="particleIndex">最近的模拟粒子的索引</param>
/// <param name="distance">世界空间中最近的模拟粒子沿绳子的距离</param>
/// <param name="distanceAlongRay">沿射线到距离模拟粒子最近的点的距离</param>
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);
}
/// <summary>
/// 为下一个模拟帧注册刚体连接。刚体连接是模拟粒子到传统刚体的双向耦合。
/// 确保从FixedUpdate()调用此方法。任何涉及刚体连接的模拟粒子将在模拟帧结束时将其质量倍增器重置为1。
/// </summary>
/// <param name="particleIndex">要连接的模拟粒子的索引</param>
/// <param name="rigidbody">要连接的刚体</param>
/// <param name="rigidbodyDamping">应用于刚体的阻尼量,范围[0, 1]</param>
/// <param name="pointOnBody">刚体上要连接的世界空间点</param>
/// <param name="stiffness">连接的刚度,范围[0, 1]</param>
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,
},
});
}
/// <summary>
/// 将绳子重置为其相对于当前变换的原始形状。在激活被停用和重新激活而不是销毁和实例化的池化游戏对象时很有用。
/// </summary>
public void ResetToSpawnCurve()
{
if (!Initialize())
{
return;
}
CompletePreviousSimulationFrame();
var localToWorld = (float4x4)transform.localToWorldMatrix;
spawnPoints.GetPointsAlongCurve(ref localToWorld, _measurements.particleSpacing, positions);
positions.CopyTo(prevPositions);
}
/// <summary>
/// 计算绳子的当前长度。与measurements.realCurveLength字段相比该值包括由于应力引起的绳子拉伸。
/// </summary>
public float GetCurrentLength()
{
if (!Initialize())
{
return 0.0f;
}
CompletePreviousSimulationFrame();
return positions.GetLengthOfCurve();
}
#region
public void OnEnable()
{
if (!initialized)
{
return;
}
CompletePreviousSimulationFrame();
}
public void Start()
{
Initialize();
}
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();
}
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();
}
#endregion
protected bool Initialize()
{
if (initialized)
{
return true;
}
if (!Application.isPlaying || spawnPoints.Count < 2)
{
// 不适用于编辑模式执行
return false;
}
lineRenderer = gameObject.GetComponent<LineRenderer>();
if (lineRenderer == null)
{
lineRenderer = gameObject.AddComponent<LineRenderer>();
}
lineRenderer.useWorldSpace = true;
lineRenderer.widthMultiplier = radius;
lineRenderer.material = material;
lineRenderer.shadowCastingMode = shadowMode;
lineRenderer.receiveShadows = false;
lineRenderer.alignment = LineAlignment.View;
// 状态
ComputeRealCurve(Allocator.Persistent, out _measurements, out positions);
prevPositions = new NativeArray<float3>(_measurements.particleCount, Allocator.Persistent);
positions.CopyTo(prevPositions);
if (interpolation != RopeInterpolation.None)
{
interpolatedPositions = new NativeArray<float3>(_measurements.particleCount, Allocator.Persistent);
positions.CopyTo(interpolatedPositions);
}
bitangents = new NativeArray<float3>(_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);
}
bitangents[bitangents.Length - 1] = bitangents[bitangents.Length - 2];
}
massMultipliers = new NativeArray<float>(_measurements.particleCount, Allocator.Persistent);
for (int i = 0; i < massMultipliers.Length; i++)
{
massMultipliers[i] = 1.0f;
}
// 碰撞处理
collisionPlanesActive = new NativeArray<int>(_measurements.particleCount, Allocator.Persistent);
collisionPlanes =
new NativeArray<CollisionPlane>(_measurements.particleCount * MaxCollisionPlanesPerParticle,
Allocator.Persistent);
collisionRigidbodies = new Rigidbody[collisionPlanes.Length];
// 刚体连接
queuedRigidbodyConnections = new List<RigidbodyConnection>();
liveRigidbodyConnections = new List<RigidbodyConnection>();
particleTargets = new NativeArray<ParticleTarget>(InitialParticleTargets, Allocator.Persistent);
particleTargetFeedbacks = new NativeArray<float3>(InitialParticleTargets, Allocator.Persistent);
initialized = true;
computingSimulationFrame = false;
return true;
}
/// <summary>
/// 计算真实曲线
/// </summary>
protected void ComputeRealCurve(Allocator allocator, out Measurements measurements,
out NativeArray<float3> 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<float3>(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,
};
}
protected Collider[] collisionQueryBuffer = new Collider[MaxCollisionPlanesPerParticle];
/// <summary>
/// 更新碰撞平面
/// </summary>
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();
}
/// <summary>
/// 准备刚体连接
/// </summary>
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<ParticleTarget>(newCapacity, Allocator.Persistent);
particleTargetFeedbacks.Dispose();
particleTargetFeedbacks = new NativeArray<float3>(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();
}
/// <summary>
/// 应用刚体反馈
/// </summary>
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();
}
/// <summary>
/// 安排下一个模拟帧
/// </summary>
protected void ScheduleNextSimulationFrame()
{
Profiler.BeginSample(nameof(ScheduleNextSimulationFrame));
computingSimulationFrame = true;
var gravity = (float3)Physics.gravity;
var simulate = new SimulateJob()
{
deltaTime = Time.fixedDeltaTime,
externalAcceleration = gravity,
energyKept = 1.0f - simulation.energyLoss,
positions = positions,
prevPositions = prevPositions,
massMultipliers = massMultipliers,
substeps = simulation.substeps,
solverIterations = simulation.solverIterations,
stiffness = simulation.stiffness,
desiredSpacing = _measurements.particleSpacing,
collisionsEnabled = collisions.enabled,
radius = radius + collisions.collisionMargin,
maxCollisionPlanesPerParticle = MaxCollisionPlanesPerParticle,
collisionPlanesActive = collisionPlanesActive,
collisionPlanes = collisionPlanes,
particleTargets = particleTargets,
particleTargetFeedbacks = particleTargetFeedbacks,
}.Schedule();
if (interpolation == RopeInterpolation.None)
{
simulationFrameHandle = new OutputVerticesJob()
{
positions = positions,
bitangents = bitangents,
}.Schedule(simulate);
}
else
{
// OutputVerticesJob必须在插值之后发生而插值发生在Update()中而不是FixedUpdate()
simulationFrameHandle = simulate;
}
JobHandle.ScheduleBatchedJobs();
Profiler.EndSample();
}
/// <summary>
/// 时间表插值
/// </summary>
protected void ScheduleInterpolation()
{
if (interpolation == RopeInterpolation.None)
{
return;
}
CompletePreviousSimulationFrame();
Profiler.BeginSample(nameof(ScheduleInterpolation));
computingSimulationFrame = true;
var invDt = 1.0f / Time.fixedDeltaTime;
JobHandle handle;
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,
}.Schedule(handle);
JobHandle.ScheduleBatchedJobs();
Profiler.EndSample();
}
/// <summary>
/// 完成先前的模拟帧
/// </summary>
protected void CompletePreviousSimulationFrame()
{
if (!computingSimulationFrame)
{
return;
}
Profiler.BeginSample(nameof(CompletePreviousSimulationFrame));
simulationFrameHandle.Complete();
computingSimulationFrame = false;
Profiler.EndSample();
}
protected void SubmitToRenderer()
{
if (material == null)
{
return;
}
Profiler.BeginSample(nameof(SubmitToRenderer));
// 使用简单线渲染
if (lineRenderer != null && simulation.enabled)
{
lineRenderer.positionCount = positions.Length;
for (int i = 0; i < positions.Length; i++)
{
lineRenderer.SetPosition(i, positions[i]);
}
}
Profiler.EndSample();
}
#region Jobs
[BurstCompile]
private struct SimulateJob : IJob
{
[ReadOnly] public float deltaTime;
[ReadOnly] public float3 externalAcceleration;
[ReadOnly] public float energyKept;
// 状态
public NativeArray<float3> positions;
public NativeArray<float3> prevPositions;
[ReadOnly] public NativeArray<float> massMultipliers;
// 形状
[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 int maxCollisionPlanesPerParticle;
[ReadOnly] public NativeArray<int> collisionPlanesActive;
public NativeArray<CollisionPlane> collisionPlanes;
// 刚体附件
[ReadOnly] public NativeArray<ParticleTarget> particleTargets;
public NativeArray<float3> 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 = 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(0, length);
}
}
}
/// <summary>
/// 位置插值计算,用于在渲染时平滑显示绳子的位置变化。
/// </summary>
[BurstCompile]
private struct InterpolatePositionsJob : IJob
{
[ReadOnly] public NativeArray<float3> positions;
[ReadOnly] public NativeArray<float3> prevPositions;
[ReadOnly] public float invDeltaTime;
[ReadOnly] public float timeSinceFixedUpdate;
[WriteOnly] public NativeArray<float3> 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);
}
}
}
/// <summary>
/// 进行位置外推计算,用于在渲染时预测和显示绳子的未来位置。
/// </summary>
[BurstCompile]
private struct ExtrapolatePositionsJob : IJob
{
[ReadOnly] public NativeArray<float3> positions;
[ReadOnly] public NativeArray<float3> prevPositions;
[ReadOnly] public float invDeltaTime;
[ReadOnly] public float timeSinceFixedUpdate;
[WriteOnly] public NativeArray<float3> 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;
}
}
}
/// <summary>
/// 计算平滑的双切线bitangent 对相邻顶点的切线进行平均处理,使绳子渲染更加平滑
/// </summary>
[BurstCompile]
private struct OutputVerticesJob : IJob
{
[ReadOnly] public NativeArray<float3> positions;
public NativeArray<float3> bitangents;
public void Execute()
{
var last = positions.Length - 1;
// 扩散双切线
var smoothedBitangents = new NativeArray<float3>(bitangents.Length, Allocator.Temp);
smoothedBitangents[0] = bitangents[0] + bitangents[1];
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];
// 重新标准化双切线
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));
}
bitangents[last] = bitangents[last - 1];
}
}
#endregion
}
}