新增动态水物理插件

This commit is contained in:
Bob.Song
2026-02-27 17:44:21 +08:00
parent a6e061d9ce
commit 60744d113d
2218 changed files with 698551 additions and 189 deletions

View File

@@ -1,68 +1,126 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine;
using WaveHarmonic.Crest;
namespace NBF
{
/// <summary>
/// 小型浮漂/小物体:基于 Crest 的浮力(自动从多个 CapsuleCollider 计算尺寸与探针)
/// - 自动收集自身与子物体上的 CapsuleCollider可多个
/// - 计算整体长轴、长度、最大直径、底部高度
/// - 自动设置 Crest 查询尺度 _ObjectWidth与直径同量级
/// - 自动生成 5 个探针:底部四周 + 底部中心(更稳)
/// - 浮力:弹簧(k) + 阻尼(c)ForceMode.Force质量参与
/// </summary>
public sealed class BobberFloating : MonoBehaviour
{
[Header("Crest")]
public WaterRenderer _water;
[SerializeField] Rigidbody _RigidBody;
[Tooltip("要瞄准哪一层水的碰撞层。")] [SerializeField]
CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves;
[Tooltip("要瞄准哪一层水的碰撞层。")]
[SerializeField] CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves;
[Header("浮力")]
[Header("力强度")]
[Tooltip("对于探测器而言,大致为 100 比 1 的质量与力的比例,以使质心保持在表面附近。对于“对齐法线”,默认值适用于具有默认刚体的默认球体。")]
[SerializeField]
float _BuoyancyForceStrength = 10f;
// -----------------------------
// 自动从 CapsuleCollider 计算
// -----------------------------
[Header("Auto from CapsuleColliders")]
[Tooltip("自动扫描自己与子物体的 CapsuleCollider并计算尺寸/探针/采样尺度。")]
[SerializeField] bool _AutoFromCapsules = true;
[Header("扭矩强度")] [Tooltip("使船体方向与水的法线方向一致时所施加扭矩的大小。")] [SerializeField]
float _BuoyancyTorqueStrength = 8f;
[Tooltip("仅使用 enabled 的 CapsuleCollider。")]
[SerializeField] bool _OnlyEnabledCapsules = true;
[Header("最大力矩")] [Tooltip("将浮力值固定在此数值上。\n\n适用于处理完全浸没的物体。")] [SerializeField]
float _MaximumBuoyancyForce = 100f;
[Tooltip("运行时也会每隔一定时间重建(应对浮漂缩放/更换碰撞体。0 表示仅 Start/OnValidate 时重建。")]
[SerializeField] float _RuntimeRebuildInterval = 0f;
[Header("高度偏移")] [Tooltip("从变换中心到船体底部的高度偏移(如果存在)。\n\n默认值适用于默认球体。该值无需精确测量从中心到底部的距离。")] [SerializeField]
float _CenterToBottomOffset = -1f;
[Tooltip("给估算出来的直径乘一个系数,用于更稳的 Crest 查询与阻尼。通常 1.0~2.0。")]
[Range(0.5f, 3f)]
[SerializeField] float _WidthMultiplier = 1.2f;
[Tooltip("顺着波浪 “冲浪” 的近似流体动力学效果。")] [Range(0, 1)] [SerializeField]
float _AccelerateDownhill;
[Tooltip("探针半径比例(相对估算半径)。越大越“撑开”,越抗翻滚,但也更容易被浪抬。建议 0.6~0.9")]
[Range(0.1f, 1.2f)]
[SerializeField] float _ProbeRadiusRatio = 0.75f;
[Tooltip("探针在底部之上的抬高(米)。用于避免探针刚好在最底点导致抖动。建议:半径的 5%~20%。")]
[SerializeField] float _ProbeLiftMeters = 0.0f;
[Header("拖拽")] [Tooltip("在水中时使用拖拽功能。\n将此属性添加到刚体所声明的拖拽力上。")] [SerializeField]
Vector3 _Drag = new(2f, 3f, 1f);
[Tooltip("探针权重分配:四周总权重(其余给中心)。建议 0.6~0.9")]
[Range(0f, 1f)]
[SerializeField] float _RingWeight = 0.75f;
[Tooltip("在水中会产生旋转阻力。\n\n将此阻力添加到刚体上已声明的旋转阻力值之上。")] [SerializeField]
float _AngularDrag = 0.2f;
// -----------------------------
// 物理参数(吃水可控)
// -----------------------------
[Header("Buoyancy (controllable submergence)")]
[Tooltip("目标吃水比例:以“直径”为尺度。\n例0.30 表示目标吃水深度约 = 直径*0.30。\n对小浮漂非常好调想更浮露更多-> 降低;想更沉 -> 提高。")]
[Range(0.05f, 1.2f)]
[SerializeField] float _TargetSubmergenceRatio = 0.30f;
[Tooltip("施加拉力的位置的垂直偏移量。")] [SerializeField]
float _ForceHeightOffset;
[Tooltip("浮力阻尼比0~2 常用)。越大越不抖,但会显得黏。推荐 0.7~1.2")]
[Range(0f, 2f)]
[SerializeField] float _DampingRatio = 0.95f;
[Tooltip("最大总浮力N保护。若为 Infinity 则不限制。")]
[SerializeField] float _MaximumBuoyancyForce = 50f;
[Header("波响应")] [Tooltip("用于物理计算的物体宽度。\n\n此值越大波响应的滤波效果和平滑程度就越高。如果无法对较大波长进行滤波则应增加 LOD 级别。")] [SerializeField]
float _ObjectWidth = 3f;
[Tooltip("吃水偏移(米)。用于校准 pivot/碰撞体中心误差。\n>0 更容易浮起(等效更浅),<0 更沉。")]
[SerializeField] float _SubmergenceOffset = 0f;
[Header("Water drag (relative to water surface velocity/flow)")]
[Tooltip("相对水的线性阻力N per m/s。小浮漂建议从 (0.2, 0.6, 0.2) 起。")]
[SerializeField] Vector3 _Drag = new(0.2f, 0.6f, 0.2f);
[Tooltip("角阻力N*m per rad/s。小浮漂建议 0.01~0.05。")]
[SerializeField] float _AngularDrag = 0.03f;
[Header("Optional downhill acceleration")]
[Range(0, 1)]
[SerializeField] float _AccelerateDownhill = 0f;
[Header("Force application height offset")]
[SerializeField] float _ForceHeightOffset = 0f;
// -----------------------------
// 自动生成的“几何结果”
// -----------------------------
[Header("Computed (read-only)")]
[SerializeField] float _ComputedLength = 0.1f;
[SerializeField] float _ComputedDiameter = 0.04f;
[SerializeField] Vector3 _ComputedAxisLocal = Vector3.up; // 刚体局部坐标中的长轴(单位向量)
[SerializeField] float _ComputedBottomLocalY = -0.02f; // 刚体局部坐标中“整体最低点”的 y沿长轴方向投影不是y这里是按 rb local Y仅用于Debug显示
[Header("Wave response / query scale")]
[Tooltip("用于 Crest 查询的物体宽度(米)。会自动从 Capsule 直径推算。")]
[SerializeField] float _ObjectWidth = 0.04f;
// -----------------------------
// Debug
[Space(10)] [SerializeField] DebugFields _Debug = new();
// -----------------------------
[Space(10)]
[SerializeField] DebugFields _Debug = new();
[System.Serializable]
[Serializable]
sealed class DebugFields
{
[SerializeField] internal bool _DrawQueries = false;
[SerializeField] internal bool _DrawProbes = false;
[SerializeField] internal bool _DrawForces = false;
}
internal const string k_FixedUpdateMarker = "Crest.FloatingObject.FixedUpdate";
static Unity.Profiling.ProfilerMarker s_FixedUpdateMarker = new(k_FixedUpdateMarker);
/// <summary>
/// 这个物体的任何部分是否浸泡在水中?
/// </summary>
/// <summary>是否有任意探针入水</summary>
public bool InWater { get; private set; }
// 探针(刚体局部空间,相对 worldCenterOfMass
struct Probe
{
public Vector3 localOffsetFromCOM;
public float weight;
}
Probe[] _Probes = Array.Empty<Probe>();
readonly SampleFlowHelper _SampleFlowHelper = new();
Vector3[] _QueryPoints;
@@ -70,150 +128,436 @@ namespace NBF
Vector3[] _QueryResultVelocities;
Vector3[] _QueryResultNormal;
internal FloatingObjectProbe[] _Probe = new FloatingObjectProbe[] { new() { _Weight = 1f } };
float _NextRebuildTime = 0f;
void Reset()
{
if (_RigidBody == null) TryGetComponent(out _RigidBody);
}
private void Start()
void OnValidate()
{
_ObjectWidth = Mathf.Max(0.001f, _ObjectWidth);
_RuntimeRebuildInterval = Mathf.Max(0f, _RuntimeRebuildInterval);
_ProbeLiftMeters = Mathf.Max(0f, _ProbeLiftMeters);
if (!Application.isPlaying)
{
if (_AutoFromCapsules)
{
TryRebuildFromCapsules(editor: true);
}
AllocateQueryArrays();
}
}
void Start()
{
if (_RigidBody == null) TryGetComponent(out _RigidBody);
var points = _Probe;
// Advanced 还需要为中心增设一个位置。
var length = points.Length;
_QueryPoints = new Vector3[length];
_QueryResultDisplacements = new Vector3[length];
_QueryResultVelocities = new Vector3[length];
_QueryResultNormal = new Vector3[length];
if (_AutoFromCapsules)
{
TryRebuildFromCapsules(editor: false);
}
AllocateQueryArrays();
}
private void FixedUpdate()
void FixedUpdate()
{
s_FixedUpdateMarker.Begin(this);
if (_water == null || _RigidBody == null) return;
var points = _Probe;
if (_AutoFromCapsules && _RuntimeRebuildInterval > 0f && Time.time >= _NextRebuildTime)
{
_NextRebuildTime = Time.time + _RuntimeRebuildInterval;
TryRebuildFromCapsules(editor: false);
AllocateQueryArrays();
}
// 查询
if (_Probes == null || _Probes.Length == 0) return;
// Crest provider
var collisions = _water.AnimatedWavesLod.Provider;
// 更新查询点。
for (var i = 0; i < points.Length; i++)
int probeCount = _Probes.Length;
int centerIndex = probeCount; // 最后一个点用于中心采样(表面速度/法线)
Vector3 comWorld = _RigidBody.worldCenterOfMass;
// Build query points
for (int i = 0; i < probeCount; i++)
{
var point = points[i];
_QueryPoints[i] =
transform.TransformPoint(point._Position + new Vector3(0, _RigidBody.centerOfMass.y, 0));
_QueryPoints[i] = comWorld + transform.TransformVector(_Probes[i].localOffsetFromCOM);
}
_QueryPoints[centerIndex] = comWorld;
_QueryPoints[^1] = transform.position + new Vector3(0, _RigidBody.centerOfMass.y, 0);
// Query waves
collisions.Query(
GetHashCode(),
_ObjectWidth,
_QueryPoints,
_QueryResultDisplacements,
_QueryResultNormal,
_QueryResultVelocities,
_Layer
);
collisions.Query(GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements,
_QueryResultNormal, _QueryResultVelocities, _Layer);
//我们可以将表面速度过滤为最近两帧中的较小值。
//存在一种极端情况:当波长被开启 / 关闭时,会产生单帧速度尖峰——
//因为此时水面确实会发生极快的运动。
var surfaceVelocity = _QueryResultVelocities[^1];
// Surface velocity + flow
Vector3 surfaceVelocity = _QueryResultVelocities[centerIndex];
_SampleFlowHelper.Sample(transform.position, out var surfaceFlow, minimumLength: _ObjectWidth);
surfaceVelocity += new Vector3(surfaceFlow.x, 0, surfaceFlow.y);
surfaceVelocity += new Vector3(surfaceFlow.x, 0f, surfaceFlow.y);
if (_Debug._DrawQueries)
// Compute buoyancy coefficients
float g = Mathf.Abs(Physics.gravity.y);
// 目标吃水深度(米):按直径比例
float targetSubmergence = Mathf.Max(0.0005f, _ComputedDiameter * _TargetSubmergenceRatio);
// 总弹簧系数k ≈ m*g / targetSubmergence
float kTotal = (_RigidBody.mass * g) / targetSubmergence;
// 总阻尼c = 2*sqrt(k*m)*dampingRatio
float cTotal = 2f * Mathf.Sqrt(Mathf.Max(0.0001f, kTotal * _RigidBody.mass)) * _DampingRatio;
InWater = false;
float totalBuoyMag = 0f;
// Apply distributed buoyancy
for (int i = 0; i < probeCount; i++)
{
Debug.DrawLine(transform.position + 5f * Vector3.up,
transform.position + 5f * Vector3.up + surfaceVelocity, new(1, 1, 1, 0.6f));
}
Vector3 p = _QueryPoints[i];
float waterHeight = _QueryResultDisplacements[i].y + _water.SeaLevel;
{
var height = _QueryResultDisplacements[0].y + _water.SeaLevel;
var bottomDepth = height - transform.position.y - _CenterToBottomOffset;
var normal = _QueryResultNormal[0];
// depth: positive means submerged
float depth = (waterHeight - p.y) + _SubmergenceOffset;
if (depth <= 0f) continue;
if (_Debug._DrawQueries)
InWater = true;
float w = Mathf.Max(0.0001f, _Probes[i].weight);
// spring
float FiSpring = (kTotal * w) * depth;
// damping (vertical relative velocity)
Vector3 pointVel = _RigidBody.GetPointVelocity(p);
float vRel = Vector3.Dot(pointVel - surfaceVelocity, Vector3.up);
float FiDamp = -(cTotal * w) * vRel;
float Fi = FiSpring + FiDamp;
if (Fi < 0f) Fi = 0f;
totalBuoyMag += Fi;
Vector3 force = Fi * Vector3.up;
_RigidBody.AddForceAtPosition(force, p, ForceMode.Force);
if (_Debug._DrawForces)
{
var surfPos = transform.position;
surfPos.y = height;
DebugUtility.DrawCross(Debug.DrawLine, surfPos, normal, 1f, Color.red);
}
InWater = bottomDepth > 0f;
if (!InWater)
{
s_FixedUpdateMarker.End();
return;
}
var buoyancy = _BuoyancyForceStrength * bottomDepth * bottomDepth * bottomDepth *
-Physics.gravity.normalized;
if (_MaximumBuoyancyForce < Mathf.Infinity)
{
buoyancy = Vector3.ClampMagnitude(buoyancy, _MaximumBuoyancyForce);
}
_RigidBody.AddForce(buoyancy, ForceMode.Acceleration);
// 在水面上滑行的近似流体动力学
if (_AccelerateDownhill > 0f)
{
_RigidBody.AddForce(_AccelerateDownhill * -Physics.gravity.y * new Vector3(normal.x, 0f, normal.z),
ForceMode.Acceleration);
}
// 朝向
// 与水面垂直。默认使用一个垂直方向,但也可以使用单独的垂直方向。
// 根据船的长度与宽度的比例。这会根据船只的不同而产生不同的旋转效果。
// dimensions.
{
var normalLatitudinal = normal;
if (_Debug._DrawQueries)
Debug.DrawLine(transform.position, transform.position + 5f * normalLatitudinal, Color.green);
var torqueWidth = Vector3.Cross(transform.up, normalLatitudinal);
_RigidBody.AddTorque(torqueWidth * _BuoyancyTorqueStrength, ForceMode.Acceleration);
_RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity);
Debug.DrawLine(p, p + force * 0.02f, Color.cyan);
}
}
// 相对于水进行拖拽操作
if (!InWater) return;
// Clamp buoyancy (simple safety)
if (_MaximumBuoyancyForce < Mathf.Infinity && totalBuoyMag > _MaximumBuoyancyForce)
{
float excess = totalBuoyMag - _MaximumBuoyancyForce;
_RigidBody.AddForce(-excess * Vector3.up, ForceMode.Force);
}
// Optional downhill acceleration (usually 0 for bobber)
if (_AccelerateDownhill > 0f)
{
Vector3 normal = _QueryResultNormal[centerIndex];
_RigidBody.AddForce(_AccelerateDownhill * g * new Vector3(normal.x, 0f, normal.z), ForceMode.Force);
}
// Angular drag
if (_AngularDrag > 0f)
{
_RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity, ForceMode.Force);
}
// Linear drag relative to water
if (_Drag != Vector3.zero)
{
var velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity;
Vector3 velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity;
Vector3 forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up;
var forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up;
_RigidBody.AddForceAtPosition(
_Drag.x * Vector3.Dot(transform.right, -velocityRelativeToWater) * transform.right, forcePosition,
ForceMode.Acceleration);
_RigidBody.AddForceAtPosition(_Drag.y * Vector3.Dot(Vector3.up, -velocityRelativeToWater) * Vector3.up,
forcePosition, ForceMode.Acceleration);
_Drag.x * Vector3.Dot(transform.right, -velocityRelativeToWater) * transform.right,
forcePosition,
ForceMode.Force);
_RigidBody.AddForceAtPosition(
_Drag.y * Vector3.Dot(Vector3.up, -velocityRelativeToWater) * Vector3.up,
forcePosition,
ForceMode.Force);
_RigidBody.AddForceAtPosition(
_Drag.z * Vector3.Dot(transform.forward, -velocityRelativeToWater) * transform.forward,
forcePosition, ForceMode.Acceleration);
forcePosition,
ForceMode.Force);
}
s_FixedUpdateMarker.End();
}
}
static class DebugUtility
{
public delegate void DrawLine(Vector3 position, Vector3 up, Color color, float duration);
public static void DrawCross(DrawLine draw, Vector3 position, float r, Color color, float duration = 0f)
{
draw(position - Vector3.up * r, position + Vector3.up * r, color, duration);
draw(position - Vector3.right * r, position + Vector3.right * r, color, duration);
draw(position - Vector3.forward * r, position + Vector3.forward * r, color, duration);
if (_Debug._DrawProbes)
{
for (int i = 0; i < probeCount; i++)
{
Debug.DrawLine(_QueryPoints[i], _QueryPoints[i] + Vector3.up * 0.05f, Color.yellow);
}
}
}
public static void DrawCross(DrawLine draw, Vector3 position, Vector3 up, float r, Color color,
float duration = 0f)
void AllocateQueryArrays()
{
up.Normalize();
var right = Vector3.Normalize(Vector3.Cross(up, Vector3.forward));
var forward = Vector3.Cross(up, right);
draw(position - up * r, position + up * r, color, duration);
draw(position - right * r, position + right * r, color, duration);
draw(position - forward * r, position + forward * r, color, duration);
int probeCount = _Probes?.Length ?? 0;
int n = probeCount + 1; // +1 center sample
if (n <= 1) n = 2;
if (_QueryPoints != null && _QueryPoints.Length == n) return;
_QueryPoints = new Vector3[n];
_QueryResultDisplacements = new Vector3[n];
_QueryResultVelocities = new Vector3[n];
_QueryResultNormal = new Vector3[n];
}
/// <summary>
/// 公开按钮:手动重建(你也可以在 Inspector 右键脚本 -> Reset然后 Play
/// </summary>
[ContextMenu("Rebuild From CapsuleColliders")]
public void RebuildFromCapsules()
{
TryRebuildFromCapsules(editor: Application.isEditor && !Application.isPlaying);
AllocateQueryArrays();
}
bool TryRebuildFromCapsules(bool editor)
{
var capsules = GetComponentsInChildren<CapsuleCollider>(includeInactive: true);
if (capsules == null || capsules.Length == 0)
{
// 没胶囊就不改
return false;
}
// 收集点(在刚体局部空间)
List<Vector3> ptsLocal = new List<Vector3>(capsules.Length * 8);
// 同时估算“长轴”:取所有胶囊在 rb local 的端点云,计算 AABB 最大跨度轴
// 这比“看某个 Capsule.direction”更鲁棒因为你说可能多个、分布复杂
foreach (var cap in capsules)
{
if (cap == null) continue;
if (_OnlyEnabledCapsules && !cap.enabled) continue;
AddCapsuleSamplePointsInRbLocal(cap, ptsLocal);
}
if (ptsLocal.Count < 2) return false;
// AABB in rb local
Vector3 min = ptsLocal[0];
Vector3 max = ptsLocal[0];
for (int i = 1; i < ptsLocal.Count; i++)
{
min = Vector3.Min(min, ptsLocal[i]);
max = Vector3.Max(max, ptsLocal[i]);
}
Vector3 size = max - min;
// 估算长轴:取跨度最大的轴
// 注意:这是 rb local 坐标的轴x/y/z足够用于“尺寸”和“探针布局”
int axis = 0;
float spanX = size.x;
float spanY = size.y;
float spanZ = size.z;
if (spanY >= spanX && spanY >= spanZ) axis = 1;
else if (spanZ >= spanX && spanZ >= spanY) axis = 2;
else axis = 0;
Vector3 axisLocal = axis == 0 ? Vector3.right : axis == 1 ? Vector3.up : Vector3.forward;
// length along that axis
float length = axis == 0 ? spanX : axis == 1 ? spanY : spanZ;
// diameter取另外两轴的最大跨度更保守
float dia;
if (axis == 0) dia = Mathf.Max(spanY, spanZ);
else if (axis == 1) dia = Mathf.Max(spanX, spanZ);
else dia = Mathf.Max(spanX, spanY);
dia = Mathf.Max(0.001f, dia);
_ComputedLength = length;
_ComputedDiameter = dia;
_ComputedAxisLocal = axisLocal;
// Crest 查询尺度:直径同量级 * multiplier且给下限
_ObjectWidth = Mathf.Max(0.002f, _ComputedDiameter * _WidthMultiplier);
// 估算“底部位置”(用于探针 y
// 我们按“长轴”方向找最小投影的点作为底部
float minProj = float.PositiveInfinity;
Vector3 bottom = ptsLocal[0];
for (int i = 0; i < ptsLocal.Count; i++)
{
float proj = Vector3.Dot(ptsLocal[i], axisLocal);
if (proj < minProj)
{
minProj = proj;
bottom = ptsLocal[i];
}
}
// 这个值只是给你在 Inspector 看一眼,不参与计算
_ComputedBottomLocalY = bottom.y;
// 生成探针:以“底部附近”一圈 + 中心
BuildDefaultProbes(axisLocal, bottom, _ComputedDiameter);
if (!editor)
{
// 运行时也确保 query arrays 充足
AllocateQueryArrays();
}
return true;
}
void BuildDefaultProbes(Vector3 axisLocal, Vector3 bottomLocal, float diameter)
{
// 以刚体局部空间为准,探针 offset 是“相对 COM 的局部偏移”
// 我们把探针放在 “底部投影点附近”,并沿“横向平面”撑开
float radius = diameter * 0.5f;
float ringR = radius * _ProbeRadiusRatio;
// 底部沿长轴方向略抬高,避免探针刚好在最底点导致 jitter
// 这里用“长轴”方向抬高,而不是简单的 localY
float lift = _ProbeLiftMeters;
if (lift <= 0f)
{
// 默认:半径的 10%
lift = radius * 0.10f;
}
Vector3 basePoint = bottomLocal + axisLocal * lift;
// 需要在“横向平面”找两条正交方向(在 rb local 坐标中)
// 任意取一个与 axisLocal 不平行的向量,构造正交基
Vector3 t1 = Vector3.Cross(axisLocal, Vector3.up);
if (t1.sqrMagnitude < 1e-6f) t1 = Vector3.Cross(axisLocal, Vector3.right);
t1.Normalize();
Vector3 t2 = Vector3.Cross(axisLocal, t1).normalized;
// 权重分配
float ringTotal = Mathf.Clamp01(_RingWeight);
float centerW = 1f - ringTotal;
float eachRingW = ringTotal / 4f;
// 探针点rb local最后转成 “相对 COM 的 local offset”
// 注意:我们使用 worldCenterOfMass 作为基准,所以这里要把 basePointrb local转换成 offset-from-COM
Vector3 comLocal = transform.InverseTransformPoint(_RigidBody.worldCenterOfMass);
var probes = new Probe[5];
// 四周
probes[0] = new Probe { localOffsetFromCOM = (basePoint + t1 * ringR) - comLocal, weight = eachRingW };
probes[1] = new Probe { localOffsetFromCOM = (basePoint - t1 * ringR) - comLocal, weight = eachRingW };
probes[2] = new Probe { localOffsetFromCOM = (basePoint + t2 * ringR) - comLocal, weight = eachRingW };
probes[3] = new Probe { localOffsetFromCOM = (basePoint - t2 * ringR) - comLocal, weight = eachRingW };
// 底部中心
probes[4] = new Probe { localOffsetFromCOM = basePoint - comLocal, weight = Mathf.Max(0.0001f, centerW) };
_Probes = probes;
}
void AddCapsuleSamplePointsInRbLocal(CapsuleCollider cap, List<Vector3> outPtsRbLocal)
{
// 计算胶囊端点(世界) + 若干“半径点”,再转换到 rb local
// 胶囊在 cap.transform localcenter, direction(0=x 1=y 2=z), height, radius
Transform ct = cap.transform;
Vector3 centerW = ct.TransformPoint(cap.center);
// direction axis in world
Vector3 axisW =
cap.direction == 0 ? ct.right :
cap.direction == 1 ? ct.up :
ct.forward;
axisW.Normalize();
// 处理缩放radius 取垂直于轴的最大缩放分量(保守)
Vector3 s = ct.lossyScale;
float sx = Mathf.Abs(s.x);
float sy = Mathf.Abs(s.y);
float sz = Mathf.Abs(s.z);
float radiusScale;
float heightScale;
if (cap.direction == 0)
{
heightScale = sx;
radiusScale = Mathf.Max(sy, sz);
}
else if (cap.direction == 1)
{
heightScale = sy;
radiusScale = Mathf.Max(sx, sz);
}
else
{
heightScale = sz;
radiusScale = Mathf.Max(sx, sy);
}
float r = Mathf.Max(0.0005f, cap.radius * radiusScale);
float h = Mathf.Max(0.001f, cap.height * heightScale);
// 圆柱部分半长(端点到端点的距离为 h-2r
float half = Mathf.Max(0f, (h * 0.5f) - r);
Vector3 p0W = centerW + axisW * half;
Vector3 p1W = centerW - axisW * half;
// 在世界空间构造两个与 axisW 正交的方向
Vector3 n1 = Vector3.Cross(axisW, Vector3.up);
if (n1.sqrMagnitude < 1e-6f) n1 = Vector3.Cross(axisW, Vector3.right);
n1.Normalize();
Vector3 n2 = Vector3.Cross(axisW, n1).normalized;
// 采样点:两个端点 + 端点的四周半径点 + 中心四周半径点(足够稳定估算整体 AABB
AddWorldPoint(p0W, outPtsRbLocal);
AddWorldPoint(p1W, outPtsRbLocal);
AddWorldPoint(p0W + n1 * r, outPtsRbLocal);
AddWorldPoint(p0W - n1 * r, outPtsRbLocal);
AddWorldPoint(p0W + n2 * r, outPtsRbLocal);
AddWorldPoint(p0W - n2 * r, outPtsRbLocal);
AddWorldPoint(p1W + n1 * r, outPtsRbLocal);
AddWorldPoint(p1W - n1 * r, outPtsRbLocal);
AddWorldPoint(p1W + n2 * r, outPtsRbLocal);
AddWorldPoint(p1W - n2 * r, outPtsRbLocal);
AddWorldPoint(centerW + n1 * r, outPtsRbLocal);
AddWorldPoint(centerW - n1 * r, outPtsRbLocal);
AddWorldPoint(centerW + n2 * r, outPtsRbLocal);
AddWorldPoint(centerW - n2 * r, outPtsRbLocal);
}
void AddWorldPoint(Vector3 world, List<Vector3> outPtsRbLocal)
{
// rb local即本脚本 transform 的 local
// 注意:这里假设脚本挂在 Rigidbody 所在物体上(通常是这样)
outPtsRbLocal.Add(transform.InverseTransformPoint(world));
}
}
}