563 lines
22 KiB
C#
563 lines
22 KiB
C#
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;
|
||
|
||
// -----------------------------
|
||
// 自动从 CapsuleCollider 计算
|
||
// -----------------------------
|
||
[Header("Auto from CapsuleColliders")]
|
||
[Tooltip("自动扫描自己与子物体的 CapsuleCollider,并计算尺寸/探针/采样尺度。")]
|
||
[SerializeField] bool _AutoFromCapsules = true;
|
||
|
||
[Tooltip("仅使用 enabled 的 CapsuleCollider。")]
|
||
[SerializeField] bool _OnlyEnabledCapsules = true;
|
||
|
||
[Tooltip("运行时也会每隔一定时间重建(应对浮漂缩放/更换碰撞体)。0 表示仅 Start/OnValidate 时重建。")]
|
||
[SerializeField] float _RuntimeRebuildInterval = 0f;
|
||
|
||
[Tooltip("给估算出来的直径乘一个系数,用于更稳的 Crest 查询与阻尼。通常 1.0~2.0。")]
|
||
[Range(0.5f, 3f)]
|
||
[SerializeField] float _WidthMultiplier = 1.2f;
|
||
|
||
[Tooltip("探针半径比例(相对估算半径)。越大越“撑开”,越抗翻滚,但也更容易被浪抬。建议 0.6~0.9")]
|
||
[Range(0.1f, 1.2f)]
|
||
[SerializeField] float _ProbeRadiusRatio = 0.75f;
|
||
|
||
[Tooltip("探针在底部之上的抬高(米)。用于避免探针刚好在最底点导致抖动。建议:半径的 5%~20%。")]
|
||
[SerializeField] float _ProbeLiftMeters = 0.0f;
|
||
|
||
[Tooltip("探针权重分配:四周总权重(其余给中心)。建议 0.6~0.9")]
|
||
[Range(0f, 1f)]
|
||
[SerializeField] float _RingWeight = 0.75f;
|
||
|
||
// -----------------------------
|
||
// 物理参数(吃水可控)
|
||
// -----------------------------
|
||
[Header("Buoyancy (controllable submergence)")]
|
||
[Tooltip("目标吃水比例:以“直径”为尺度。\n例:0.30 表示目标吃水深度约 = 直径*0.30。\n对小浮漂非常好调:想更浮(露更多)-> 降低;想更沉 -> 提高。")]
|
||
[Range(0.05f, 1.2f)]
|
||
[SerializeField] float _TargetSubmergenceRatio = 0.30f;
|
||
|
||
[Tooltip("浮力阻尼比(0~2 常用)。越大越不抖,但会显得黏。推荐 0.7~1.2")]
|
||
[Range(0f, 2f)]
|
||
[SerializeField] float _DampingRatio = 0.95f;
|
||
|
||
[Tooltip("最大总浮力(N)保护。若为 Infinity 则不限制。")]
|
||
[SerializeField] float _MaximumBuoyancyForce = 50f;
|
||
|
||
[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();
|
||
|
||
[Serializable]
|
||
sealed class DebugFields
|
||
{
|
||
[SerializeField] internal bool _DrawProbes = false;
|
||
[SerializeField] internal bool _DrawForces = false;
|
||
}
|
||
|
||
/// <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;
|
||
Vector3[] _QueryResultDisplacements;
|
||
Vector3[] _QueryResultVelocities;
|
||
Vector3[] _QueryResultNormal;
|
||
|
||
float _NextRebuildTime = 0f;
|
||
|
||
void Reset()
|
||
{
|
||
if (_RigidBody == null) TryGetComponent(out _RigidBody);
|
||
}
|
||
|
||
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);
|
||
|
||
if (_AutoFromCapsules)
|
||
{
|
||
TryRebuildFromCapsules(editor: false);
|
||
}
|
||
|
||
AllocateQueryArrays();
|
||
}
|
||
|
||
void FixedUpdate()
|
||
{
|
||
if (_water == null || _RigidBody == null) return;
|
||
|
||
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;
|
||
|
||
int probeCount = _Probes.Length;
|
||
int centerIndex = probeCount; // 最后一个点用于中心采样(表面速度/法线)
|
||
|
||
Vector3 comWorld = _RigidBody.worldCenterOfMass;
|
||
|
||
// Build query points
|
||
for (int i = 0; i < probeCount; i++)
|
||
{
|
||
_QueryPoints[i] = comWorld + transform.TransformVector(_Probes[i].localOffsetFromCOM);
|
||
}
|
||
_QueryPoints[centerIndex] = comWorld;
|
||
|
||
// Query waves
|
||
collisions.Query(
|
||
GetHashCode(),
|
||
_ObjectWidth,
|
||
_QueryPoints,
|
||
_QueryResultDisplacements,
|
||
_QueryResultNormal,
|
||
_QueryResultVelocities,
|
||
_Layer
|
||
);
|
||
|
||
// Surface velocity + flow
|
||
Vector3 surfaceVelocity = _QueryResultVelocities[centerIndex];
|
||
_SampleFlowHelper.Sample(transform.position, out var surfaceFlow, minimumLength: _ObjectWidth);
|
||
surfaceVelocity += new Vector3(surfaceFlow.x, 0f, surfaceFlow.y);
|
||
|
||
// 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++)
|
||
{
|
||
Vector3 p = _QueryPoints[i];
|
||
float waterHeight = _QueryResultDisplacements[i].y + _water.SeaLevel;
|
||
|
||
// depth: positive means submerged
|
||
float depth = (waterHeight - p.y) + _SubmergenceOffset;
|
||
if (depth <= 0f) continue;
|
||
|
||
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)
|
||
{
|
||
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)
|
||
{
|
||
Vector3 velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity;
|
||
Vector3 forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up;
|
||
|
||
_RigidBody.AddForceAtPosition(
|
||
_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.Force);
|
||
}
|
||
|
||
if (_Debug._DrawProbes)
|
||
{
|
||
for (int i = 0; i < probeCount; i++)
|
||
{
|
||
Debug.DrawLine(_QueryPoints[i], _QueryPoints[i] + Vector3.up * 0.05f, Color.yellow);
|
||
}
|
||
}
|
||
}
|
||
|
||
void AllocateQueryArrays()
|
||
{
|
||
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 作为基准,所以这里要把 basePoint(rb 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 local:center, 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));
|
||
}
|
||
}
|
||
} |