当前位置: 首页 > news >正文

Unity3D数学第五篇:几何计算与常用算法(实用算法篇)

Unity3D数学第一篇:向量与点、线、面(基础篇)

Unity3D数学第二篇:旋转与欧拉角、四元数(核心变换篇)

Unity3D数学第三篇:坐标系与变换矩阵(空间转换篇)

Unity3D数学第四篇:射线与碰撞检测(交互基础篇)

Unity3D数学第五篇:几何计算与常用算法(实用算法篇)

掌握了向量、旋转和坐标系这些基本概念,以及 Unity 的内置物理系统后,你已经可以构建出许多功能。然而,在更复杂的场景中,你可能需要深入到数学层面,自己实现一些几何计算实用算法。这些算法能够帮助你解决更精细的问题,例如判断一个点是否在三角形内部、计算最短距离、或者在特定几何约束下进行移动。

本篇将聚焦于这些通用的 3D 几何算法,它们是游戏开发中实现高级功能和优化性能的利器。我们将主要使用 C# 语言和 Unity 的 Vector3Quaternion 等结构体来演示,但这些数学原理本身是跨引擎通用的。


1. 向量的扩展应用:投影与反射

在前一篇中我们简要提到了向量投影和反射,它们在几何计算中扮演着至关重要的角色。

1.1 向量投影 (Vector Projection)

概念: 将一个向量 A 投影到另一个向量 B(或一个方向 N)上,得到在 B 方向上的分量。

  • 几何意义: 假设你有一束光垂直于向量 B 照射下来,向量 AB 上的阴影就是它的投影。

  • 计算公式:

    • 向量 A 在向量 B 上的投影长度 (标量):∣A∣cos(theta)=(AcdotB)/∣B∣

    • 向量 A 在向量 B 上的投影向量:((AcdotB)/∣B∣2)cdotB

    • 如果 B 是单位向量(即 ∣B∣=1),那么投影长度就是 AcdotB,投影向量就是 (AcdotB)cdotB。

  • Unity API:

    • Vector3.Project(vector, onNormal):将 vector 投影到 onNormal 方向上。onNormal 不一定是单位向量,函数内部会进行标准化。

    • Vector3.ProjectOnPlane(vector, planeNormal):将 vector 投影到由 planeNormal 定义的平面上。结果是 vector - Project(vector, planeNormal)

  • 应用场景:

    • 角色在斜坡上移动:

      • 当角色在斜坡上时,其重力 (Vector3.down) 并非垂直于地面。通过将重力向量投影到斜坡的法线向量上,我们可以得到垂直于斜坡表面的重力分量。

      • 将玩家的输入移动方向投影到斜坡平面上,可以确保角色沿着斜坡的表面移动,而不是穿透或浮空。

      • 示例代码:

        C#

        // 假设 moveInput 是玩家在水平面上的输入方向 (Vector3.forward, Vector3.right 等)
        // groundNormal 是当前地面检测到的法线
        Vector3 groundNormal = hit.normal; // 从 RaycastHit 获取的地面法线// 将玩家输入方向投影到地面法线定义的平面上
        // 这样无论地面多斜,玩家都沿着平面移动
        Vector3 projectedMoveDirection = Vector3.ProjectOnPlane(moveInput, groundNormal).normalized;// 应用移动
        transform.position += projectedMoveDirection * moveSpeed * Time.deltaTime;// 考虑重力沿着斜坡下滑 (可选,如果角色不是 RigidBody 控制)
        // 将重力投影到地面法线方向,得到垂直于地面的分量
        Vector3 perpendicularGravity = Vector3.Project(Physics.gravity, groundNormal);
        // 沿着斜坡下滑的分量 = 总重力 - 垂直于地面的分量
        Vector3 slideDownSlope = Physics.gravity - perpendicularGravity;
        // transform.position += slideDownSlope * Time.deltaTime;
    • 计算粒子在表面滑动: 当粒子碰到表面时,它的速度可以投影到表面法线定义的平面上,使其沿着表面滑动。

1.2 向量反射 (Vector Reflection)

概念: 计算一个向量 A 在遇到一个表面(由其法线 N 定义)时,如何反射出去。

  • 几何意义: 想象一束光线击中镜面后反弹。

  • 计算公式: 反射向量 R=A−2(AcdotN)N

    • 其中 A 是入射向量,N 是表面法线(单位向量)。

    • 需要注意的是,这里的 A 通常是指向表面的向量(例如,物体的速度向量),因此其方向是与法线方向相反的。

  • Unity API: Vector3.Reflect(inDirection, inNormal)

  • 应用场景:

    • 弹球游戏: 球击中墙壁或挡板后反弹。

    • 激光反射: 模拟激光或光线在物体表面反射的轨迹。

    • 角色碰撞反弹: 角色撞到墙壁后轻轻弹开。

    • 示例代码:

      C#

      // 假设 currentVelocity 是物体的当前速度
      // hitNormal 是碰撞点的表面法线
      Vector3 currentVelocity = GetComponent<Rigidbody>().velocity;
      Vector3 hitNormal = collision.contacts[0].normal;// 计算反射速度
      // 注意:Vector3.Reflect 期望 inDirection 是指向表面的(例如,负的速度方向)
      Vector3 reflectedVelocity = Vector3.Reflect(currentVelocity, hitNormal);// 应用新的速度,并可能加上一些摩擦或能量损失
      GetComponent<Rigidbody>().velocity = reflectedVelocity * 0.8f; // 0.8f 模拟能量损失

2. 点与几何体的距离计算

计算点到各种几何体的距离是游戏开发中非常基础且重要的算法。

2.1 点到点距离

  • Unity API: Vector3.Distance(pointA, pointB)

  • 原理: 两个点之间向量的模。Vector3.Distance(A, B) 等同于 (A - B).magnitude

  • 性能优化: 如果只需要比较距离大小,使用 (A - B).sqrMagnitude 更高效。

2.2 点到线段距离

概念: 给定一个点 P 和一条由两个端点 AB 定义的线段,计算 P 到该线段的最短距离。

  • 原理:

    1. 找到点 P 在线段所在直线上最近的点 Q

    2. 判断点 Q 是否在线段 AB 的范围内。

      • 如果在,那么 P 到线段 AB 的距离就是 PQ 的距离。

      • 如果不在,那么 QA 的外面,距离就是 PA 的距离;或者 QB 的外面,距离就是 PB 的距离。

  • 计算步骤:

    1. 计算向量 AB = B - A

    2. 计算向量 AP = P - A

    3. 计算 APAB 上的投影长度的比值 t = Vector3.Dot(AP, AB) / AB.sqrMagnitude

    4. 钳制 t[0, 1] 范围:t = Mathf.Clamp01(t)

    5. 最近点 Q = A + t * AB

    6. 距离就是 Vector3.Distance(P, Q)

  • 应用场景:

    • 寻路: AI 角色判断自己距离路径线上最近的点有多远。

    • 技能范围: 判断一个单位是否在某个直线型技能的攻击范围内。

    • 绳索物理: 计算物体与绳索的距离,模拟拉力。

  • 示例代码:

    C#

    // Unity
    // 计算点 P 到线段 A-B 的最短距离
    public static float DistancePointToSegment(Vector3 P, Vector3 A, Vector3 B) {Vector3 AB = B - A;Vector3 AP = P - A;// 计算 AP 在 AB 上的投影长度的比例// 如果 t < 0,最近点在 A 的外部// 如果 t > 1,最近点在 B 的外部// 如果 0 <= t <= 1,最近点在线段 AB 内部float t = Vector3.Dot(AP, AB) / AB.sqrMagnitude; // AB.sqrMagnitude 避免了开方// 钳制 t 到 [0, 1] 范围,确保最近点在线段上t = Mathf.Clamp01(t);// 计算线段上离 P 最近的点 QVector3 Q = A + t * AB;// 返回 P 到 Q 的距离return Vector3.Distance(P, Q);
    }

2.3 点到平面距离

概念: 给定一个点 P 和一个平面(由平面上一点 planePoint 和其法线向量 planeNormal 定义),计算 P 到该平面的最短距离。

  • 原理:P 到平面的最短距离,就是向量 (P - planePoint) 在平面法线方向上的投影长度

  • 计算公式: 距离 = ∣(P−planePoint)cdotplaneNormal∣

    • 这里 planeNormal 必须是单位向量
  • Unity API: Plane 结构体提供了一些便捷方法。

    • new Plane(inNormal, inPoint)new Plane(point1, point2, point3)

    • plane.GetDistanceToPoint(point)

  • 应用场景:

    • 角色与地面高度: 判断角色距离地面有多高。

    • 裁切平面: 在自定义渲染中,判断物体是否在裁切平面一侧。

    • 水面检测: 物体是否在水面之上或之下。

  • 示例代码:

    C#

    // Unity
    // 计算点 P 到平面(由 planePoint 和 planeNormal 定义)的最短距离
    public static float DistancePointToPlane(Vector3 P, Vector3 planePoint, Vector3 planeNormal) {// 确保法线是单位向量planeNormal.Normalize();// 计算从平面上一点到 P 的向量Vector3 vectorFromPlaneToP = P - planePoint;// 点到平面的距离就是这个向量在平面法线方向上的投影长度// 使用绝对值,因为距离总是正数return Mathf.Abs(Vector3.Dot(vectorFromPlaneToP, planeNormal));
    }

3. 判断与相交测试

判断两个几何体是否相交,或者一个点是否在某个区域内,是游戏逻辑中非常常见的需求。

3.1 点在三角形内部判断 (Point-in-Triangle Test)

概念: 给定一个 3D 点 P 和一个由三个顶点 A, B, C 定义的三角形,判断 P 是否在该三角形的内部(假设 P 与三角形在同一平面上)。

  • 原理: 最常见的 3D 方法是同向法 (Same Side Test)重心坐标法 (Barycentric Coordinates)

    • 同向法: 如果点 P 与三角形的每个边(AB, BC, CA)的法线方向都相同,那么点 P 就在三角形内部。这通常通过计算叉积来判断。

      1. 计算三角形的法线 N = Cross(B-A, C-A)

      2. 对于每条边,例如 AB,计算 Cross(B-A, P-A)

      3. 如果 P 在三角形内部,那么 Cross(B-A, P-A)Cross(C-B, P-B)Cross(A-C, P-C) 这些向量与 N 的点积符号应该一致(都大于 0 或都小于 0)。

  • 应用场景:

    • 网格拾取: 当射线击中 MeshCollider 时,RaycastHit.triangleIndex 可以获取击中的三角形。如果你需要知道这个点击点在三角形的哪个具体位置,或进行更精细的碰撞检测。

    • 自定义碰撞检测: 判断一个 AI 单位是否在某个自定义的多边形区域内。

    • 贴图映射: 计算 3D 点在纹理上的 2D 坐标(与重心坐标结合)。

  • 示例代码(同向法,假设点 P 和三角形在同一平面):

    C#

    // Unity
    // 辅助函数:判断点P在向量A-B的哪一侧 (通过叉积的Z分量判断,适用于2D投影)
    // 对于3D,需要检查叉积与平面法线的点积符号
    private static bool SameSide(Vector3 p1, Vector3 p2, Vector3 a, Vector3 b) {Vector3 cp1 = Vector3.Cross(b - a, p1 - a);Vector3 cp2 = Vector3.Cross(b - a, p2 - a);return Vector3.Dot(cp1, cp2) >= 0; // 考虑浮点数误差,使用 >= 0
    }// 判断点 P 是否在三角形 A-B-C 内部 (假设 P 在三角形所在平面上)
    public static bool IsPointInTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) {// 计算三角形的法线,用于同向判断Vector3 normal = Vector3.Cross(b - a, c - a);// 使用同向法:点 P 必须与三角形的第三个顶点在每条边的同侧bool s1 = Vector3.Dot(Vector3.Cross(b - a, p - a), normal) >= 0;bool s2 = Vector3.Dot(Vector3.Cross(c - b, p - b), normal) >= 0;bool s3 = Vector3.Dot(Vector3.Cross(a - c, p - c), normal) >= 0;// 如果三个点积的符号一致,则点在三角形内部// (所有都 >= 0 或者所有都 <= 0)return (s1 == s2) && (s2 == s3);
    }

    注意: 这里的 IsPointInTriangle 假设 P 已经在三角形所在的平面上。如果 P 不在平面上,你需要先将 P 投影到该平面上再进行测试,或者使用更复杂的射线-三角形相交测试。

3.2 2D 包围盒相交 (Axis-Aligned Bounding Box - AABB)

概念: 两个轴对齐的矩形(2D 中的盒子)是否相交。

  • 原理: 两个 AABB 在 2D 空间中相交的条件是:它们在 X 轴上的投影重叠,并且它们在 Y 轴上的投影也重叠。

  • 计算方式:

    • rect1.min.x <= rect2.max.xrect1.max.x >= rect2.min.x

    • rect1.min.y <= rect2.max.yrect1.max.y >= rect2.min.y

    • 如果两者都满足,则相交。

  • Unity API: RectBounds 结构体。

    • Rect.Overlaps(otherRect)

    • Bounds.Intersects(otherBounds) (3D AABB)

  • 应用场景:

    • 2D 游戏碰撞检测: 简单的 2D 角色和障碍物碰撞。

    • UI 元素点击检测: 判断鼠标点击是否在某个 UI 按钮的矩形范围内。

    • 粗略剔除: 在更精确的 3D 碰撞检测前,进行初步的粗略剔除,如果 AABB 都不相交,则更精确的检测也无需进行。

3.3 圆/球体相交

  • 概念: 两个圆(2D)或两个球体(3D)是否相交。

  • 原理: 两个圆/球体相交的条件是:它们圆心/球心之间的距离小于或等于它们半径之和。

  • 计算方式:

    • 设圆心 C1, C2,半径 R1, R2

    • Distance(C1, C2) <= R1 + R2

    • 为了性能,通常比较距离的平方:DistanceSq(C1, C2) <= (R1 + R2)^2

  • Unity API: SphereCollider 之间的碰撞检测内部就是这种原理。


4. 插值与曲线

插值是在两个值之间平滑过渡的数学方法,在游戏动画、路径跟随等方面非常重要。

4.1 线性插值 (Linear Interpolation - Lerp)

  • 概念: 在两个点、向量、颜色或数值之间进行直线插值。

  • 公式: result = start + (end - start) * t,其中 t 的范围是 01

    • t=0 时,结果是 start

    • t=1 时,结果是 end

    • t=0.5 时,结果是 startend 的中间点。

  • Unity API: Vector3.Lerp(), Mathf.Lerp(), Color.Lerp(), Quaternion.Lerp()(注意 Quaternion.Lerp 只是线性插值,不如 Slerp 平滑)。

  • 应用场景:

    • 平滑移动: transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * speed);

    • 颜色渐变: UI 颜色淡入淡出。

    • 数值过渡: 血量、经验条的平滑变化。

4.2 球面线性插值 (Spherical Linear Interpolation - Slerp)

  • 概念: 专门用于四元数的平滑插值,保证最短路径和匀速旋转。

  • Unity API: Quaternion.Slerp(fromRotation, toRotation, t)

  • 应用场景: 前一篇已详述,是处理旋转动画的黄金标准。

4.3 曲线插值 (Spline Interpolation):贝塞尔曲线与样条曲线

当需要更复杂的平滑路径时,简单的线性插值就不够了。曲线插值算法应运而生。

  • 贝塞尔曲线 (Bézier Curves):

    • 控制点定义。最常见的是二次贝塞尔 (Quadratic Bézier) (1 个起点、1 个控制点、1 个终点) 和三次贝塞尔 (Cubic Bézier) (1 个起点、2 个控制点、1 个终点)。

    • 曲线始终经过起点和终点,但不一定经过控制点。控制点决定了曲线的形状(“拉力”)。

    • 公式(三次贝塞尔为例): P(t)=(1−t)3P_0+3(1−t)2tP_1+3(1−t)t2P_2+t3P_3

      • P_0,P_1,P_2,P_3 是控制点。

      • t 在 [0, 1] 之间。

    • Unity 应用: Unity 没有直接的 Bezier 类,但你可以自己实现上述公式,或者使用一些第三方库。

    • 应用场景:

      • 动画路径: 角色沿着平滑的曲线路径移动。

      • UI 动画: 按钮、面板的复杂运动轨迹。

      • 弹道模拟: 投掷物体的曲线运动。

  • 样条曲线 (Spline Curves):

    • 例如 Catmull-Rom SplineHermite Spline

    • 通常会经过所有或大部分定义的点,从而创建一条平滑的路径。

    • Catmull-Rom Spline 优点: 它能生成一条通过所有控制点的平滑曲线,且每个曲线段只受相邻几个点的影响,易于控制。

    • 应用场景:

      • 相机路径: 相机沿着预设的路径平滑移动。

      • AI 巡逻路径: AI 角色沿着复杂的路径巡逻。

      • 过山车模拟: 轨道生成。

  • 实现示例(三次贝塞尔):

    C#

    // Unity
    // 计算三次贝塞尔曲线上的一个点
    public static Vector3 GetCubicBezierPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {t = Mathf.Clamp01(t);float u = 1 - t;float tt = t * t;float uu = u * u;float uuu = uu * u;float ttt = tt * t;Vector3 p = uuu * p0; // (1-t)^3 * P0p += 3 * uu * t * p1; // 3 * (1-t)^2 * t * P1p += 3 * u * tt * p2; // 3 * (1-t) * t^2 * P2p += ttt * p3;        // t^3 * P3return p;
    }

    要在 Unity 中绘制贝塞尔曲线,可以在 OnDrawGizmos() 中循环 t 值,调用 GetCubicBezierPoint 并用 Gizmos.DrawLine 连接这些点。


5. 几何体的表示与操作

在 3D 数学中,除了点、向量,我们还需要处理更复杂的几何体。

5.1 平面 (Plane)

  • 概念: 由一个法线向量和一个平面上的点定义。所有垂直于法线并经过该点的点都构成这个平面。

  • Unity API: Plane 结构体。

    • new Plane(Vector3 inNormal, Vector3 inPoint)

    • new Plane(Vector3 a, Vector3 b, Vector3 c) (通过三个点定义平面)

    • plane.GetDistanceToPoint(point):点到平面的距离。

    • plane.GetSide(point):判断点在平面哪一侧(通过点积判断符号)。

    • plane.Raycast(ray, out enter):射线与平面相交测试。

  • 应用场景:

    • 投影: 将点或向量投影到平面上。

    • 裁剪: 图形学中的视锥体裁剪,判断多边形是否在裁剪面内部。

    • 水面: 模拟水面。

    • 自定义碰撞: 检测点或物体是否与某个隐形平面相交。

5.2 线段 (Line Segment)

  • 概念: 具有明确起点和终点的有限长度直线。

  • 表示: 通常由两个 Vector3 (起点和终点) 表示。

  • 应用:

    • 上一节的点到线段距离。

    • 线段与线段相交(2D 常用)。

    • 线段与平面相交。

5.3 包围盒 (Bounding Box)

  • 概念: 用于包围 3D 几何体的简单形状,通常用于粗略的碰撞检测和剔除优化。

    • AABB (Axis-Aligned Bounding Box): 轴对齐包围盒。它的边总是平行于世界坐标轴,因此旋转时盒子会变大。

    • OBB (Oriented Bounding Box): 有向包围盒。它可以任意方向旋转,更紧密地包围物体,但计算更复杂。

  • Unity API: Bounds 结构体(通常指 AABB)。

    • new Bounds(center, size)

    • bounds.Contains(point):判断点是否在包围盒内。

    • bounds.Intersects(otherBounds):判断两个 AABB 是否相交。

    • bounds.Expand(amount):扩大包围盒。

    • bounds.Encapsulate(point/bounds):使包围盒包含某个点或另一个包围盒。

  • 应用场景:

    • 粗略碰撞检测: 在进行精确的网格碰撞检测之前,先判断两个物体的 AABB 是否相交。如果不相交,就无需进行更昂贵的计算。

    • 视锥体剔除 (Frustum Culling): 判断物体的包围盒是否在摄像机的视锥体内,从而决定是否渲染。

    • 空间划分结构: 在八叉树 (Octree) 或 BVH (Bounding Volume Hierarchy) 等数据结构中,用包围盒来组织空间中的物体。


6. 常用数学辅助算法

6.1 坐标轴的旋转:Mathf.Atan2()

  • 概念: Mathf.Atan2(y, x) 函数返回点 (x, y) 与原点之间的连线相对于正 X 轴的夹角(以弧度表示)。它的结果范围是 (-PI, PI]

  • 优点: 相比 Mathf.Atan()Atan2 可以正确处理所有四个象限的角,并且不会出现除零问题。

  • 应用场景:

    • 2D 角色看向鼠标: 计算角色朝向鼠标的旋转角度。

    • 获取向量的方向角度: 将一个 2D 向量转换为角度。

  • 示例代码:

    C#

    // Unity
    // 让 2D 角色看向鼠标位置 (假设 Y 轴向上,X 轴向右)
    Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    Vector3 directionToMouse = mouseWorldPos - transform.position;// 计算朝向鼠标的 Y 轴旋转角度 (2D 平面旋转)
    float angle = Mathf.Atan2(directionToMouse.y, directionToMouse.x) * Mathf.Rad2Deg;// 应用旋转 (例如,一个只在 2D 平面旋转的物体)
    // transform.rotation = Quaternion.Euler(0, 0, angle - 90); // 减 90 度是因为默认 transform.up 是 Y 轴
    // 或者直接使用 LookRotation (推荐,更通用):
    // targetRotation = Quaternion.LookRotation(directionToMouse);
    // transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * speed);

6.2 随机数生成与应用

虽然不是严格的几何计算,但在游戏开发中,随机性经常与几何位置、方向结合。

  • Unity API: Random.Range(min, max)

  • 应用场景:

    • 粒子生成: 在一个球体或立方体区域内随机生成粒子位置。

    • AI 随机游走: AI 随机选择一个方向进行短暂移动。

    • 掉落物品: 在击败敌人后,在一定范围内随机掉落物品。

  • 示例代码:

    C#

    // Unity
    // 在以当前物体为中心,半径为 5 的球体内随机生成一个点
    Vector3 randomPointInSphere = transform.position + Random.insideUnitSphere * 5f;
    // Random.insideUnitSphere 返回一个半径为 1 的单位球体内的随机点// 在以当前物体为中心,边长为 (10, 10, 10) 的立方体内的随机点
    Vector3 randomPointInCube = transform.position + new Vector3(Random.Range(-5f, 5f),Random.Range(-5f, 5f),Random.Range(-5f, 5f)
    );

6.3 数学常数与函数

  • Mathf.PI / Mathf.Deg2Rad / Mathf.Rad2Deg 弧度与角度转换,以及圆周率。

  • Mathf.Clamp() / Mathf.Clamp01() 将数值限制在给定范围内。

  • Mathf.LerpUnclamped() 不钳制 t 的线性插值,允许外推。

  • Mathf.Approximately() 比较两个浮点数是否近似相等(考虑到浮点数精度问题,避免直接 == 比较)。


7. 实用算法组合应用:高级路径跟随与约束

将上述几何计算组合起来,我们可以实现更复杂的逻辑。

7.1 沿着曲线的精确路径跟随

通过结合贝塞尔曲线LookRotation,可以实现物体沿着复杂曲线路径平滑移动和转向。

C#

// Unity
public Vector3 p0, p1, p2, p3; // 三次贝塞尔曲线的四个控制点
public float totalPathTime = 5f; // 走完整个路径所需时间
private float currentTime = 0f;void Update() {currentTime += Time.deltaTime;float t = currentTime / totalPathTime;if (t > 1f) {// 路径走完,可以循环或者停止currentTime = 0f; // 循环路径return;}// 1. 获取当前时刻在曲线上的位置Vector3 currentPosition = GetCubicBezierPoint(p0, p1, p2, p3, t);// 2. 获取下一个时刻在曲线上的位置,用于计算前进方向float nextT = Mathf.Min(t + 0.01f, 1f); // 取一个非常小的步进,避免 t 溢出Vector3 nextPosition = GetCubicBezierPoint(p0, p1, p2, p3, nextT);// 3. 计算前进方向Vector3 forwardDirection = (nextPosition - currentPosition).normalized;// 4. 应用位置和旋转transform.position = currentPosition;if (forwardDirection.magnitude > 0.001f) { // 避免方向向量为零导致 LookRotation 错误transform.rotation = Quaternion.LookRotation(forwardDirection);}
}// 三次贝塞尔曲线点计算函数(如前面所示)
public static Vector3 GetCubicBezierPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { /* ... */ }// 在 Scene 视图中绘制曲线,方便调试
void OnDrawGizmos() {Gizmos.color = Color.cyan;if (p0 != Vector3.zero && p1 != Vector3.zero && p2 != Vector3.zero && p3 != Vector3.zero) {for (float i = 0; i < 1f; i += 0.05f) {Gizmos.DrawLine(GetCubicBezierPoint(p0, p1, p2, p3, i), GetCubicBezierPoint(p0, p1, p2, p3, i + 0.05f));}}
}

7.2 限制物体在平面上移动

结合点到平面距离和向量投影,可以实现物体被约束在特定平面上的移动。

C#

// Unity
public Vector3 planeNormal = Vector3.up; // 平面的法线
public Vector3 planePoint = Vector3.zero; // 平面上的一个点 (例如世界原点)void Update() {// 假设玩家输入控制了 transform.position// 获取当前位置在平面上的投影Vector3 projectedPosition = transform.position - Vector3.Project(transform.position - planePoint, planeNormal);// 将物体位置设置为投影点,从而约束在平面上transform.position = projectedPosition;
}

这对于制作平面上的车辆、2.5D 视角游戏中的角色移动等非常有用。


8. 常见面试题与深入思考

8.1 面试问答

  1. 问:请解释向量投影(Vector Projection)和向量反射(Vector Reflection)的几何意义,并各举一个游戏中的应用场景。

    • 考察点: 对向量操作几何含义的理解和实际应用。

    • 解析:

      • 投影: 衡量一个向量在另一个方向上的分量。

        • 几何意义: 向量 A 在向量 B 上的“阴影”。

        • 应用: 角色在斜坡上移动时,将玩家的输入移动方向投影到斜坡平面上,确保角色沿着斜坡表面滑动,而不是穿透地面。

      • 反射: 计算一个向量遇到表面后如何“反弹”出去。

        • 几何意义: 光线击中镜面后反弹的路径。

        • 应用: 弹球游戏中,球击中墙壁或挡板后,根据入射方向和表面法线计算球的反弹方向和速度。

  2. 问:在 3D 游戏中,你如何计算一个点到一条线段的最短距离?简述其原理和步骤。

    • 考察点: 考查点到线段距离算法的掌握,以及对钳制操作的理解。

    • 解析:

      • 原理: 核心在于找到点 P 在线段所在直线上最近的点 Q,然后判断 Q 是否在线段的范围内。

      • 步骤:

        1. 定义线段 AB 的方向向量 AB_vec = B - A

        2. 定义从线段起点 A 到点 P 的向量 AP_vec = P - A

        3. 计算 AP_vecAB_vec 上的投影长度的归一化参数 t = Vector3.Dot(AP_vec, AB_vec) / AB_vec.sqrMagnitude

        4. 使用 Mathf.Clamp01(t)t 钳制在 [0, 1] 之间,确保最近点在线段内部。

        5. 计算线段上离 P 最近的点 Q = A + t * AB_vec

        6. 最终距离为 Vector3.Distance(P, Q)

  3. 问:什么是贝塞尔曲线?它在游戏开发中有什么用?你如何生成一个三次贝塞尔曲线上的点?

    • 考察点: 对曲线生成算法的理解和应用。

    • 解析:

      • 概念: 贝塞尔曲线是一种由控制点定义的平滑曲线。曲线会经过起点和终点,但通常不经过中间的控制点,控制点通过“拉力”影响曲线的形状。

      • 游戏应用: 角色或敌人沿着平滑路径移动的动画、UI 元素的复杂动画轨迹、投掷物体的弹道模拟、工具中创建曲线路径。

      • 生成三次贝塞尔曲线上的点: 给定四个控制点 P_0,P_1,P_2,P_3 和一个参数 tin[0,1],曲线上的点 P(t) 的公式是:

        P(t)=(1−t)3P_0+3(1−t)2tP_1+3(1−t)t2P_2+t3P_3

        通过在 [0, 1] 范围内迭代 t 值,可以计算出曲线上连续的点,从而绘制或跟随这条曲线。

8.2 深入思考:数学算法的性能与精度

  • 浮点数精度: 在进行复杂的几何计算时,尤其是在比较浮点数相等性 (==) 时,要特别注意浮点数精度问题。通常应该使用一个很小的epsilon 值 (Epsilon),例如 Mathf.Approximately(a, b)Mathf.Abs(a - b) < Epsilon 来进行比较。这对于判断点是否在平面上、两条线是否平行等场景非常重要。

  • 开方运算与平方距离: 在只需要比较距离大小(而非具体距离值)的场景中,始终优先使用平方距离(例如 sqrMagnitude)来避免昂贵的开方运算,这是一种重要的性能优化手段。

  • 缓存与预计算: 对于一些复杂的几何结构(如网格),如果某些计算结果是静态的或不经常变化的(例如三角形法线),可以考虑在加载时预计算并缓存起来,避免在运行时重复计算。


总结与展望

本篇教程带你进入了 3D 几何计算的更深层次,涵盖了:

  • 向量的投影与反射:理解了如何将向量分解或反弹,并在角色控制器、物理模拟中应用。

  • 点与几何体的距离计算:学习了点到线段、点到平面的最短距离计算方法。

  • 判断与相交测试:探讨了点在三角形内部的判断,以及包围盒相交检测。

  • 插值与曲线:巩固了线性插值,并深入介绍了贝塞尔曲线等高级路径生成算法。

  • 几何体的表示与操作:简要介绍了 PlaneBounds 等结构体的用法。

  • 常用数学辅助算法:如 Mathf.Atan2() 和随机数生成,它们在实际开发中非常实用。

掌握这些通用的几何计算和算法,将极大地拓宽你在游戏开发中解决问题的思路。它们是实现自定义物理、高级 AI 行为、复杂动画系统以及各种独特游戏机制的基石。

在下一篇也是本系列的最后一篇《进阶话题与性能优化(高阶与思考篇)》中,我们将回顾并总结前面所学知识,讨论一些更高级的 3D 数学概念,以及在实际项目中如何对物理和数学计算进行性能优化。

现在,你对这些几何计算和常用算法是否已经有了更清晰的理解?在你的开发中,你有没有遇到过哪些需要自己手动实现几何计算的场景?

Unity3D数学第一篇:向量与点、线、面(基础篇)

Unity3D数学第二篇:旋转与欧拉角、四元数(核心变换篇)

Unity3D数学第三篇:坐标系与变换矩阵(空间转换篇)

Unity3D数学第四篇:射线与碰撞检测(交互基础篇)

Unity3D数学第五篇:几何计算与常用算法(实用算法篇)

http://www.lryc.cn/news/606723.html

相关文章:

  • 【BFS】P7555 [USACO21OPEN] Maze Tac Toe S|普及+
  • 【C#学习Day16笔记】XML文件、 事件Event 、Json数据
  • JavaWeb--Student2025项目:条件查询、批量删除、新增、修改
  • 2025新征程杯全国54校园足球锦标赛在北京世园公园隆重开幕
  • Java 开发中的 DTO 模式:从理论到实践的完整指南
  • Java 大视界 -- Java 大数据机器学习模型在电商产品定价策略优化与市场竞争力提升中的应用(375)
  • 下次接好运~
  • 【苍穹外卖项目】Day05
  • PyTorch基础——张量计算
  • IO流-文件实例
  • 力扣-最大单词长度乘积
  • 鸿蒙智能居家养老系统构思(续二)—— 适老化烹饪中心详细构思
  • C#实现左侧折叠导航菜单
  • 思途JSP学习 0801
  • 退出python的base环境
  • 【常见分布及其特征(8)】连续型随机变量-正态分布*
  • JAVA结合AI
  • 高速公路桥梁安全监测系统解决方案
  • k8s云原生rook-ceph pvc快照与恢复(下)
  • 如何保护 Redis 实例的安全?
  • C++对象访问有访问权限是不是在ide里有效
  • 解决MySQL不能编译存储过程的问题
  • Rust → WebAssembly 的性能剖析全指南
  • (一)React +Ts(vite创建项目)
  • Activity之间互相发送数据
  • django的数据库原生操作sql
  • 注解退散!纯XML打造MyBatis持久层的终极形态
  • 第11届蓝桥杯Python青少组_国赛_高级组_2020年10月真题
  • 人员定位卡人脸智能充电发卡机
  • 赛博算命之八字测算事业运势的Java实现(四柱、五行、十神、流年、格局详细测算)