碰撞检测之Ray-Cylinder检测

本文介绍如何求解射线与圆柱体的交点,包括坐标系转换、正交基构造等数学原理,以及具体的算法实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

正交基和标准正交基

首先来看定义

Let S = {v1v2, ... , vkbe a set of vectors in Rn, then S is called an orthogonal if 

        vi . vj=0

for all i not equal to j. An orthogonal set of vectors is called orthonormal if all vectors in S are unit vectors.


咳咳,翻译一下

对于 Rn中的一个向量集合 S={v1v2, ... , vk} , 如果存在

vi . vj = 0  (i != j)

那么它们就是一组正交基,如果它们都是单位向量,则它们还是标准正交基。


定理

1.给定一组正交基S = {v1, v2, ... , vk},那么他们是线性相关的。

2.对于 Rn中的一个正交基 S={v1v2, ... , vk},则 Rn中的向量 v 的以S为基的第 i 个坐标为

vj


求向量(5, 10) 在基 S = {(3/5, 4/5), (-4/5, 3/5)}中的坐标。

(5,10).(3/5,4/5) = 11 

(5,10).(-4/5, 3/5) = 2

[(5,10)]= (11,2)


给定一个空间向量,求出以这个向量为轴的正交基

假设给定的空间向量为

v = (a,b,c);

则与v垂直的向量必定满足

a*x + b*y + c*z = 0

这个向量可以是u = (0;-c;b) 或者 u = (-c,0,a);

已知两个向量,第三个向量利用向量叉乘就可以得出 

w = cross(u,v)

最后将u,v,w单位化,得到新的正交基。


不同正交基下的坐标变换


同一个向量,在不同的基下面的坐标是不同的。因此,可以用正交矩阵来代表坐标系(也可以看作基)从而写出在统一的参考系(全局坐标系)下同一个向量在不同基中的坐标。

 


已知一个基Q和向量v在它之中的坐标v’,以及另外一个基R,我们可以通过v=Qv’=Rv’’公式来计算v’’。

 

 

上面就是求v’’的公式,注意到右边需要计算基R的逆矩阵R^-1,因为基R是正交矩阵,而正交矩阵的一个重要性质就是逆等于转置。因此,我们可以把它写成

这个公式就是坐标转换公式。特别地,如果Q是和参考系相同的坐标系(3D编程中大多数情况下如此),比如世界坐标系,则Q是一个单位矩阵I,则我们可以把它写成

这个坐标转换公式可以解释为:对于世界坐标系中的向量v’,它在坐标系R中的坐标是v’’


我们还可以得到哟个有趣的结论:

假设原始的坐标系为x-(1,0,0), y-(0,1,0), z(0,0,1),这个坐标系经过一个旋转矩阵Matrix旋转之后,新的坐标系就是这个旋转矩阵的转置三个列向量。



圆柱体的射线求交

上面啰嗦到的两个地方这里都会用到。

首先还是定义圆柱体的类

 public class Cylinder : NGeometry
    {
        public Vector3 p0;
        public Vector3 p1;
        public float radius;

        public Cylinder(Vector3 _p0, Vector3 _p1, float _radius)
            : base(GeometryType.Cylinder)
        {
            p0 = _p0;
            p1 = _p1;
            radius = _radius;
        }

        public Cylinder() : base(GeometryType.Cylinder) { }

        public Vector3 ComputeDirection()
        {
            return p1 - p0;
        }
    }

看检测的代码,有点长

 public static bool Raycast(Ray ray, float distance, Cylinder cylinder, out RaycastHitInfo hitInfo)
        {
            hitInfo = new RaycastHitInfo();
            Vector3 cylinderDir = cylinder.ComputeDirection();
            Vector3 kW = cylinderDir;
            float fWLength = kW.magnitude;
            kW.Normalize();

            //two thin for check
            if (fWLength <= 1e-6f)
            {
                return false;
            }

            //generate orthonormal basis
            //cylinder along the z direction
            Vector3 kU = Vector3.zero;
            if (fWLength > 0.0f)
            {
                float fInvLength;
                if (Mathf.Abs(kW.x) >= Mathf.Abs(kW.y))
                {
                    fInvLength = 1.0f / Mathf.Sqrt(kW.x * kW.x + kW.z * kW.z);
                    kU.x = -kW.z * fInvLength;
                    kU.y = 0.0f;
                    kU.z = kW.x * fInvLength;
                }
                else
                {
                    // W.y or W.z is the largest magnitude component, swap them
                    fInvLength = 1.0f / Mathf.Sqrt(kW.y * kW.y + kW.z * kW.z);
                    kU.x = 0.0f;
                    kU.y = kW.z * fInvLength;
                    kU.z = -kW.y * fInvLength;
                }
            }
            Vector3 kV = Vector3.Cross(kW, kU);
            kV.Normalize();

            // compute intersection
            //Transform the ray to the cylinder's local coordinate
            //new Ray direction
            Vector3 kD = new Vector3(Vector3.Dot(kU, ray.direction), Vector3.Dot(kV, ray.direction), Vector3.Dot(kW, ray.direction));
            float fDLength = kD.magnitude;
            Debug.Log("fDLength: " + fDLength);
            kD.Normalize();

            float fInvDLength = 1.0f / fDLength;
            Vector3 kDiff = ray.origin - cylinder.p0;
            //new Ray origin
            Vector3 kP = new Vector3(Vector3.Dot(kU, kDiff), Vector3.Dot(kV, kDiff), Vector3.Dot(kW, kDiff));

            float fRadiusSqr = cylinder.radius * cylinder.radius;

            // Is the ray direction parallel to the cylinder direction? (or zero)
            if (Mathf.Abs(kD.z) >= 1.0f - Mathf.Epsilon || fDLength < Mathf.Epsilon)
            {
                float fAxisDir = Vector4.Dot(ray.direction, cylinderDir);

                float fDiscr = fRadiusSqr - kP.x * kP.x - kP.y * kP.y;
                // ray direction anti-parallel to the cylinder direction
                if (fAxisDir < 0 && fDiscr >= 0.0f)
                {
                    if (kP.z > fWLength)
                    {
                        hitInfo.distance = (kP.z - fWLength) * fInvDLength;
                    }
                    else if (kP.z < 0)
                    {
                        return false;
                    }
                    else if (kP.z > 0 && kP.z < fWLength)
                    {
                        hitInfo.distance = kP.z * fInvDLength;
                    }

                    if (hitInfo.distance > distance)
                        return false;
                    hitInfo.point = hitInfo.distance * ray.direction;
                    return true;
                }
                // ray direction parallel to the cylinder direction
                else if (fAxisDir > 0 && fDiscr >= 0.0f)
                {
                    if (kP.z > fWLength)
                    {
                        return false;
                    }
                    else if (kP.z < 0)
                    {
                        hitInfo.distance = -kP.z * fInvDLength;
                    }
                    else if (kP.z > 0 && kP.z < fWLength)
                    {
                        hitInfo.distance = (fWLength - kP.z) * fInvDLength;
                    }
                    if (hitInfo.distance > distance)
                        return false;
                    hitInfo.point = hitInfo.distance * ray.direction;
                    return true;
                }
                else
                {
                    //ray origin out of the circle
                    return false;
                }
            }

            // test intersection with infinite cylinder
            // set up quadratic Q(t) = a*t^2 + 2*b*t + c
            float fA = kD.x * kD.x + kD.y * kD.y;
            float fB = kP.x * kD.x + kP.y * kD.y;
            float fC = kP.x * kP.x + kP.y * kP.y - fRadiusSqr;
            float delta = fB * fB - fA * fC;
            // line does not intersect infinite cylinder
            if (delta < 0.0f)
            {
                return false;
            }

            // line intersects infinite cylinder in two points
            if (delta > 0.0f)
            {
                float fRoot = Mathf.Sqrt(delta);
                float fInv = 1.0f / fA;
                float fT = (-fB - fRoot) * fInv;
                float fTmp = kP.z + fT * kD.z;
                float dist0 = 0f, dist1 = 0f;

                float fT1 = (-fB + fRoot) * fInv;
                float fTmp1 = kP.z + fT * kD.z;

                //cast two point
                //fTmp <= fWLength to check intersect point between slab.
                if ((0.0f <= fTmp && fTmp <= fWLength) && (0.0f <= fTmp1 && fTmp1 <= fWLength))
                {
                    dist0 = fT * fInvDLength;
                    dist1 = fT1 * fInvDLength;
                    hitInfo.distance = Mathf.Min(dist0, dist1);
                    return true;
                }
                else if ((0.0f <= fTmp && fTmp <= fWLength))
                {
                    dist0 = fT * fInvDLength;
                    hitInfo.distance = dist0;
                    return true;
                }
                else if ((0.0f <= fTmp1 && fTmp1 <= fWLength))
                {
                    dist1 = fT1 * fInvDLength;
                    hitInfo.distance = dist1;
                    return true;
                }


                //If intersect the infinite cylinder but point not between slab, the ray may intersect cylinder's caps.
                //Test intersection with caps
                float deltaAngle = Vector4.Dot(ray.direction, cylinderDir);

                // Ray direction anti-parallel to the capsule direction
                if (deltaAngle < 0)
                {
                    if (kP.z > fWLength)
                    {
                        float deltaZ = kP.z - fWLength;
                        float angle = Vector3.Angle(ray.direction, -cylinderDir);
                        hitInfo.distance = (kP.z - fWLength) * fInvDLength / Mathf.Cos(angle * Mathf.Deg2Rad);
                    }
                    else if (kP.z < 0)
                    {
                        Debug.Log("No cap0");
                        return false;
                    }

                    if (hitInfo.distance > distance)
                        return false;
                    hitInfo.point = ray.origin + hitInfo.distance * ray.direction;
                    if (Vector3.Distance(hitInfo.point, cylinder.p1) > cylinder.radius)
                    {
                        return false;
                    }
                    return true;
                }
                // Direction parallel to the cylinder direction
                else if (deltaAngle > 0)
                {
                    if (kP.z > fWLength)
                    {
                        Debug.Log("No cap1");
                        return false;
                    }
                    else if (kP.z < 0)
                    {
                        float angle = Vector3.Angle(ray.direction, cylinderDir);
                        hitInfo.distance = -kP.z * fInvDLength / Mathf.Cos(angle * Mathf.Deg2Rad);
                    }
                    if (hitInfo.distance > distance)
                        return false;
                    hitInfo.point = ray.origin + hitInfo.distance * ray.direction;
                    if (Vector3.Distance(hitInfo.point, cylinder.p0) > cylinder.radius)
                    {
                        return false;
                    }
                    return true;
                }
            }
            // line is tangent to infinite cylinder
            else
            {
                float fT = -fB / fA;
                float fTmp = kP.z + fT * kD.z;
                if (0.0f <= fTmp && fTmp <= fWLength)
                {
                    hitInfo.distance = fT * fInvDLength;
                    return true;
                }
            }
            return false;
        }


简单梳理一下流程

1.将坐标系转换到了以圆柱体方向为轴的坐标系,用的就是给定一个空间向量,求出以这个向量为轴的正交基中提到的方法;

2.将Ray的origin和Direction都转到新的坐标系下,用的是上面的定理2。新的ray的origin为kP,direction为kD,射线上的点可以表示为kP+t*kD;

3.判断射线方向是否与Z轴方向平行,如果是,判断是否是圆柱体的上下面相交;

4.判断是否是圆柱相交,这里先假设圆柱是无限的,判断的方法是求解二次方程。这里详细说一下,二次函数的形式为

Q(t) = a*t^2 + 2*b*t + c

假设设想和圆柱相交,则必定存在射线上存在t,满足点kP+t*kD到Z轴的距离为圆柱的半径

(kP.x +t*kD.x)^2 +(kP.y +t*kD.y)^2 = R^2

先通过delta判断根的个数,最后求解就可以得到结果。


测试代码

using UnityEngine;
using System.Collections;
using NPhysX;

public class RayCylinderTester : MonoBehaviour {

    public GameObject cylinder;
    Cylinder _cylinder;
    Ray ray;
    float castDistance = 10f;
    // Use this for initialization
    void Start () {
        _cylinder = new Cylinder();
    }
	
	// Update is called once per frame
	void Update () {
        ray = new Ray(Vector3.zero, new Vector3(1, 1, 1));
        _cylinder.radius = 0.5f * cylinder.transform.localScale.x;
        _cylinder.p0 = cylinder.transform.position + cylinder.transform.rotation * Vector3.down * cylinder.transform.localScale.y;
        _cylinder.p1 = cylinder.transform.position + cylinder.transform.rotation * Vector3.up * cylinder.transform.localScale.y;

        Debug.DrawLine(_cylinder.p0, _cylinder.p1, Color.green);

        RaycastHitInfo hitinfo = new RaycastHitInfo();

        if (NRaycastTests.Raycast(ray, castDistance, _cylinder, out hitinfo))
        {
            Debug.DrawLine(ray.origin, ray.origin + ray.direction * hitinfo.distance, Color.red, 0, true);
        }
        else
        {
            Debug.DrawLine(ray.origin, ray.origin + ray.direction * castDistance, Color.blue, 0, true);
        }
    }
}




参考

Orthonormal Bases in Rn - http://ltcconline.net/greenl/courses/203/vectors/orthonormalbases.htm

PhysX 3.3  source code

Create orthonormal basis from a given vector - http://www.mathworks.com/matlabcentral/answers/72631-create-orthonormal-basis-from-a-given-vector

推导相机变换矩阵 - http://blog.csdn.net/popy007/article/details/5120158


using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerMorph : MonoBehaviour { public Transform upperHemisphere; // 上半球 public Transform lowerHemisphere; // 下半球 public List<CylinderData> cylinderData = new List<CylinderData>(); // 圆柱体数据 public float morphDuration = 0.5f; // 变形动画时长 public float sphereRadius = 0.5f; // 球半径 public float targetCylinderHeight = 0.5f; // 目标圆柱体高度 [System.Serializable] public class CylinderData { public Transform cylinder; // 圆柱体 public Color color; // 圆柱体颜色 public bool isActive; // 是否激活 [HideInInspector] public Vector3 originalScale; // 原始缩放值 [HideInInspector] public float modelHeight; // 模型原始高度 } [Header("视觉效果")] public ParticleSystem morphParticles; public AudioClip morphSound; private bool isSphere = true; // 当前状态 private bool isMorphing = false; // 是否正在变形 private AudioSource audioSource; private Vector3 originalUpperScale; // 上半球原始缩放 private Vector3 originalLowerScale; // 下半球原始缩放 private int activeCylinderCount = 0; // 当前激活的圆柱体数量 private float totalCylinderHeight; // 所有圆柱体堆叠后的总高度 [Header("变形推力")] [Range(0f, 500f)] public float pushForceMultiplier = 200f; // 大幅增加推力强度 //public float pushForceMultiplier = 10f; // 推力强度系数 public LayerMask pushableLayers; // 可以产生推力的层 [Header("推力效果")] public ParticleSystem pushParticles; // 新增粒子系统 // 在原有基础上添加 private Rigidbody playerRb; // 玩家的Rigidbody组件 void Start() { // 保存原始缩放值 originalUpperScale = upperHemisphere.localScale; originalLowerScale = lowerHemisphere.localScale; // 初始状态:球体 upperHemisphere.localPosition = Vector3.zero; lowerHemisphere.localPosition = Vector3.zero; audioSource = GetComponent<AudioSource>(); // 初始化圆柱体 InitializeCylinders(); // 添加获取Rigidbody playerRb = GetComponent<Rigidbody>(); } // 初始化圆柱体位置和状态 void InitializeCylinders() { for (int i = 0; i < cylinderData.Count; i++) { Transform cylinder = cylinderData[i].cylinder; // 保存原始缩放值 cylinderData[i].originalScale = cylinder.localScale; // 计算模型原始高度(使用Collider或Renderer) Renderer renderer = cylinder.GetComponent<Renderer>(); if (renderer != null) { cylinderData[i].modelHeight = renderer.bounds.size.y; } else { // 默认假设模型高度为2 cylinderData[i].modelHeight = 2f; Debug.LogWarning($"圆柱体 {i} 没有Renderer组件,使用默认高度2"); } cylinder.localPosition = Vector3.zero; // 设置初始缩放 - 压缩高度 Vector3 initialScale = cylinderData[i].originalScale; initialScale.y = CalculateScaleY(0.01f, i); // 压扁状态 cylinder.localScale = initialScale; // 设置材质颜色 if (renderer != null) { renderer.material.color = cylinderData[i].color; } // 初始状态全部禁用 cylinder.gameObject.SetActive(false); } } // 计算Y轴缩放值以达到目标高度 float CalculateScaleY(float targetHeight, int cylinderIndex) { // 缩放比例 = 目标高度 / 模型原始高度 float scaleRatio = targetHeight / cylinderData[cylinderIndex].modelHeight; // 应用原始缩放的Y分量 return cylinderData[cylinderIndex].originalScale.y * scaleRatio; } // 堆叠圆柱体(垂直排列在中心) void StackCylinders() { // 计算正确的总高度 totalCylinderHeight = activeCylinderCount * targetCylinderHeight; float baseHeight = 0f; // 计算整体偏移量,使堆叠居中 float centerOffset = -totalCylinderHeight / 2f; for (int i = 0; i < cylinderData.Count; i++) { if (!cylinderData[i].isActive) continue; // 计算圆柱体位置(在中心垂直堆叠) float yPosition = centerOffset + baseHeight + targetCylinderHeight / 2f; cylinderData[i].cylinder.localPosition = new Vector3(0f, yPosition, 0f); cylinderData[i].cylinder.localRotation = Quaternion.identity; // 设置圆柱体高度 - 目标高度为0.5f Vector3 scale = cylinderData[i].originalScale; scale.y = CalculateScaleY(targetCylinderHeight, i); cylinderData[i].cylinder.localScale = scale; // 更新基础高度 baseHeight += targetCylinderHeight; } // 更新半球位置(使其圆柱体堆叠对齐) UpdateHemispherePositions(); } // 更新半球位置 void UpdateHemispherePositions() { // 下半球位置(堆叠底部) lowerHemisphere.localPosition = new Vector3(0f, -totalCylinderHeight / 2f, 0f); // 上半球位置(堆叠顶部) upperHemisphere.localPosition = new Vector3(0f, totalCylinderHeight / 2f, 0f); } void Update() { if (Input.GetKeyDown(KeyCode.Space) && !isMorphing) { StartCoroutine(Morph()); } // 测试:增加圆柱- 改为使用PageUp键 if (Input.GetKeyDown(KeyCode.PageUp) && activeCylinderCount < cylinderData.Count) { AddCylinder(); } // 测试:减少圆柱- 改为使用PageDown键 if (Input.GetKeyDown(KeyCode.PageDown) && activeCylinderCount > 0) { RemoveCylinder(); } } // 添加新圆柱体 public void AddCylinder() { if (activeCylinderCount >= cylinderData.Count) return; cylinderData[activeCylinderCount].isActive = true; Transform newCylinder = cylinderData[activeCylinderCount].cylinder; int index = activeCylinderCount; // 始终设置新圆柱体的初始状态为压扁 newCylinder.localPosition = Vector3.zero; Vector3 scale = cylinderData[index].originalScale; scale.y = CalculateScaleY(0.01f, index); newCylinder.localScale = scale; activeCylinderCount++; // 即使球状态也计算正确的堆叠位置(但不应用) totalCylinderHeight = activeCylinderCount * targetCylinderHeight; // 显示新圆柱体 newCylinder.gameObject.SetActive(true); Debug.Log($"添加圆柱体,当前数量: {activeCylinderCount}, 总高度: {totalCylinderHeight}"); } // 移除圆柱体 public void RemoveCylinder() { if (activeCylinderCount <= 0) return; activeCylinderCount--; cylinderData[activeCylinderCount].isActive = false; cylinderData[activeCylinderCount].cylinder.gameObject.SetActive(false); // 更新总高度 totalCylinderHeight = activeCylinderCount * targetCylinderHeight; Debug.Log($"移除圆柱体,当前数量: {activeCylinderCount}, 总高度: {totalCylinderHeight}"); } IEnumerator Morph() { // 如果没有圆柱体且当前是球体状态,不需要变形 if (activeCylinderCount == 0 && isSphere) { Debug.Log("已经是球体状态且没有圆柱体,不需要变形"); yield break; } // 播放特效 if (morphParticles != null) morphParticles.Play(); if (morphSound != null && audioSource != null) audioSource.PlayOneShot(morphSound); isMorphing = true; float elapsed = 0f; // 确保总高度正确 totalCylinderHeight = activeCylinderCount * targetCylinderHeight; // 球体状态位置 Vector3 upperSpherePos = Vector3.zero; Vector3 lowerSpherePos = Vector3.zero; // 胶囊体状态位置 Vector3 upperCapsulePos = new Vector3(0f, totalCylinderHeight / 2f, 0f); Vector3 lowerCapsulePos = new Vector3(0f, -totalCylinderHeight / 2f, 0f); // 当前状态位置 - 总是使用实际位置 Vector3 upperStart = upperHemisphere.localPosition; Vector3 lowerStart = lowerHemisphere.localPosition; // 目标状态 Vector3 upperTarget, lowerTarget; bool morphingToCapsule = false; if (isSphere && activeCylinderCount > 0) // 只有当有圆柱体时才变形为胶囊 { // 球 -> 胶囊 upperTarget = upperCapsulePos; lowerTarget = lowerCapsulePos; morphingToCapsule = true; // 显示所有激活的圆柱体 for (int i = 0; i < cylinderData.Count; i++) { if (cylinderData[i].isActive) { cylinderData[i].cylinder.gameObject.SetActive(true); } } } else { // 胶囊 -> 球 或 没有圆柱体时保持球体 upperTarget = upperSpherePos; lowerTarget = lowerSpherePos; } // 存储圆柱体初始和目标状态 Vector3[] cylinderStartPositions = new Vector3[cylinderData.Count]; Vector3[] cylinderTargetPositions = new Vector3[cylinderData.Count]; Vector3[] cylinderStartScales = new Vector3[cylinderData.Count]; Vector3[] cylinderTargetScales = new Vector3[cylinderData.Count]; // 计算胶囊体堆叠位置(即使是球状态也需要) Dictionary<int, Vector3> capsulePositions = new Dictionary<int, Vector3>(); if (activeCylinderCount > 0) { float currentBaseHeight = 0f; float centerOffset = -totalCylinderHeight / 2f; for (int j = 0; j < cylinderData.Count; j++) { if (!cylinderData[j].isActive) continue; float yPos = centerOffset + currentBaseHeight + targetCylinderHeight / 2f; capsulePositions[j] = new Vector3(0f, yPos, 0f); currentBaseHeight += targetCylinderHeight; } } for (int i = 0; i < cylinderData.Count; i++) { if (!cylinderData[i].isActive) continue; cylinderStartPositions[i] = cylinderData[i].cylinder.localPosition; cylinderStartScales[i] = cylinderData[i].cylinder.localScale; if (isSphere && activeCylinderCount > 0) { // 球 -> 胶囊:使用预先计算的堆叠位置 cylinderTargetPositions[i] = capsulePositions[i]; // 计算目标缩放(高度为0.5) Vector3 targetScale = cylinderData[i].originalScale; targetScale.y = CalculateScaleY(targetCylinderHeight, i); cylinderTargetScales[i] = targetScale; } else { // 胶囊 -> 球:目标位置为中心点,高度压缩 cylinderTargetPositions[i] = Vector3.zero; // 计算目标缩放(压扁状态) Vector3 targetScale = cylinderData[i].originalScale; targetScale.y = CalculateScaleY(0.01f, i); cylinderTargetScales[i] = targetScale; } } // 动画过渡 while (elapsed < morphDuration) { float t = elapsed / morphDuration; t = Mathf.SmoothStep(0, 1, t); // 平滑过渡 // 保持半球原始缩放不变 upperHemisphere.localScale = originalUpperScale; lowerHemisphere.localScale = originalLowerScale; // 位置插值 Vector3 currentUpperPos = Vector3.Lerp(upperStart, upperTarget, t); Vector3 currentLowerPos = Vector3.Lerp(lowerStart, lowerTarget, t); upperHemisphere.localPosition = currentUpperPos; lowerHemisphere.localPosition = currentLowerPos; // 圆柱体位置和缩放插值 for (int i = 0; i < cylinderData.Count; i++) { if (!cylinderData[i].isActive) continue; cylinderData[i].cylinder.localPosition = Vector3.Lerp( cylinderStartPositions[i], cylinderTargetPositions[i], t ); cylinderData[i].cylinder.localScale = Vector3.Lerp( cylinderStartScales[i], cylinderTargetScales[i], t ); } // ==================== 新增:变形推力效果 ==================== if (morphingToCapsule && playerRb != null) { // 计算上半球的世界位置 Vector3 upperWorldPos = transform.TransformPoint(currentUpperPos); // 检测上半球是否接触物体 ApplyPushForce(upperWorldPos, upperHemisphere.up, t); // 计算下半球的世界位置 Vector3 lowerWorldPos = transform.TransformPoint(currentLowerPos); // 检测下半球是否接触物体 ApplyPushForce(lowerWorldPos, -lowerHemisphere.up, t); } // ========================================================= elapsed += Time.deltaTime; yield return null; } NewBehaviourScript movement = GetComponent<NewBehaviourScript>(); if (movement != null) { movement.SetMorphState(!isSphere); } // 最终状态 upperHemisphere.localPosition = upperTarget; lowerHemisphere.localPosition = lowerTarget; for (int i = 0; i < cylinderData.Count; i++) { if (!cylinderData[i].isActive) continue; cylinderData[i].cylinder.localPosition = cylinderTargetPositions[i]; cylinderData[i].cylinder.localScale = cylinderTargetScales[i]; } // 先更新状态:如果有圆柱体才切换状态 if (activeCylinderCount > 0) { isSphere = !isSphere; } // 正确隐藏圆柱体的逻辑: // 只有在球体状态时才隐藏圆柱体 if (isSphere || activeCylinderCount == 0) { for (int i = 0; i < cylinderData.Count; i++) { cylinderData[i].cylinder.gameObject.SetActive(false); } } isMorphing = false; // 更新碰撞体 UpdateColliders(); } // 应用推力效果 void ApplyPushForce(Vector3 position, Vector3 defaultDirection, float progress) { // 使用球形检测半球是否接触物体 Collider[] hitColliders = Physics.OverlapSphere(position, sphereRadius * 1f, pushableLayers);//0.8f5个有用且需要精准1f哪个方向都行 if (hitColliders.Length > 0) { Vector3 pushDirection = Vector3.zero; int validHits = 0; foreach (Collider col in hitColliders) { // 使用更可靠的接触点检测方法 Vector3 contactPoint = GetContactPoint(position, col); Vector3 direction = (position - contactPoint).normalized; // 确保方向有效 if (direction != Vector3.zero) { pushDirection += direction; validHits++; // 可视化接触点 Debug.DrawLine(position, contactPoint, Color.red, 0.5f); } } if (validHits > 0) { // 计算平均方向 pushDirection /= validHits; // 使用默认方向作为备选 if (pushDirection == Vector3.zero) { pushDirection = defaultDirection; } // 根据变形进度调整推力强度 float forceMultiplier = pushForceMultiplier * Mathf.Sin(progress * Mathf.PI); // 应用推力(使用Impulse模式确保明显效果) playerRb.AddForce(pushDirection * forceMultiplier, ForceMode.Impulse); // 可视化推力方向 Debug.DrawRay(position, pushDirection * forceMultiplier * 0.1f, Color.yellow, 1f);//0.1f和1f // 添加粒子效果 if (pushParticles != null) { pushParticles.transform.position = position; pushParticles.Play(); } } } } // 碰撞体切换功能 void UpdateColliders() { // 确保有碰撞体组件 SphereCollider sphereCollider = GetComponent<SphereCollider>(); CapsuleCollider capsuleCollider = GetComponent<CapsuleCollider>(); if (sphereCollider == null || capsuleCollider == null) return; // 如果没有圆柱体,强制使用球体碰撞体 if (activeCylinderCount == 0) { sphereCollider.enabled = true; capsuleCollider.enabled = false; // 设置球体碰撞体参数 sphereCollider.radius = sphereRadius; sphereCollider.center = Vector3.zero; return; } if (isSphere) { sphereCollider.enabled = true; capsuleCollider.enabled = false; // 设置球体碰撞体参数 sphereCollider.radius = sphereRadius; sphereCollider.center = Vector3.zero; } else { sphereCollider.enabled = false; capsuleCollider.enabled = true; // 计算胶囊体总高度(圆柱体高度) float totalHeight = totalCylinderHeight; // 设置胶囊碰撞体参数(中心位置为0) capsuleCollider.height = totalHeight; capsuleCollider.radius = sphereRadius; capsuleCollider.center = Vector3.zero; capsuleCollider.direction = 1; // Y轴方向 } } // 安全获取接触点的方法 // 安全获取接触点的方法 Vector3 GetContactPoint(Vector3 position, Collider collider) { // 尝试使用ClosestPoint(仅适用于支持的碰撞体) if (collider is BoxCollider || collider is SphereCollider || collider is CapsuleCollider || (collider is MeshCollider && ((MeshCollider)collider).convex)) { return collider.ClosestPoint(position); } // 对于不支持的碰撞体类型,使用射线检测 Ray ray = new Ray(position, collider.transform.position - position); RaycastHit hit; if (collider.Raycast(ray, out hit, sphereRadius * 2f)) { return hit.point; } // 回退方案:使用碰撞体边界上最近的点 return collider.ClosestPointOnBounds(position); } // 在编辑器中可视化位置 void OnDrawGizmosSelected() { if (!Application.isPlaying) return; Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position, sphereRadius); if (!isSphere && activeCylinderCount > 0) { // 绘制下半球 Gizmos.color = Color.blue; Gizmos.DrawWireSphere(transform.position + lowerHemisphere.localPosition, sphereRadius); // 绘制上半球 Gizmos.DrawWireSphere(transform.position + upperHemisphere.localPosition, sphereRadius); // 绘制圆柱体 Gizmos.color = Color.red; for (int i = 0; i < cylinderData.Count; i++) { if (!cylinderData[i].isActive) continue; Transform cylinder = cylinderData[i].cylinder; Vector3 center = transform.TransformPoint(cylinder.localPosition); Vector3 size = new Vector3( sphereRadius * 2, cylinder.localScale.y * cylinderData[i].modelHeight, sphereRadius * 2 ); Gizmos.DrawWireCube(center, size); } } } }我的移动代码是跟这个配合使用的
最新发布
07-09
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值