Unity3D数学第五篇:几何计算与常用算法(实用算法篇)
Unity3D数学第一篇:向量与点、线、面(基础篇)
Unity3D数学第二篇:旋转与欧拉角、四元数(核心变换篇)
Unity3D数学第三篇:坐标系与变换矩阵(空间转换篇)
Unity3D数学第四篇:射线与碰撞检测(交互基础篇)
Unity3D数学第五篇:几何计算与常用算法(实用算法篇)
掌握了向量、旋转和坐标系这些基本概念,以及 Unity 的内置物理系统后,你已经可以构建出许多功能。然而,在更复杂的场景中,你可能需要深入到数学层面,自己实现一些几何计算或实用算法。这些算法能够帮助你解决更精细的问题,例如判断一个点是否在三角形内部、计算最短距离、或者在特定几何约束下进行移动。
本篇将聚焦于这些通用的 3D 几何算法,它们是游戏开发中实现高级功能和优化性能的利器。我们将主要使用 C# 语言和 Unity 的 Vector3
、Quaternion
等结构体来演示,但这些数学原理本身是跨引擎通用的。
1. 向量的扩展应用:投影与反射
在前一篇中我们简要提到了向量投影和反射,它们在几何计算中扮演着至关重要的角色。
1.1 向量投影 (Vector Projection)
概念: 将一个向量 A
投影到另一个向量 B
(或一个方向 N
)上,得到在 B
方向上的分量。
-
几何意义: 假设你有一束光垂直于向量
B
照射下来,向量A
在B
上的阴影就是它的投影。 -
计算公式:
-
向量
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
和一条由两个端点 A
和 B
定义的线段,计算 P
到该线段的最短距离。
-
原理:
-
找到点
P
在线段所在直线上最近的点Q
。 -
判断点
Q
是否在线段AB
的范围内。-
如果在,那么
P
到线段AB
的距离就是P
到Q
的距离。 -
如果不在,那么
Q
在A
的外面,距离就是P
到A
的距离;或者Q
在B
的外面,距离就是P
到B
的距离。
-
-
-
计算步骤:
-
计算向量
AB = B - A
。 -
计算向量
AP = P - A
。 -
计算
AP
在AB
上的投影长度的比值t = Vector3.Dot(AP, AB) / AB.sqrMagnitude
。 -
钳制
t
到[0, 1]
范围:t = Mathf.Clamp01(t)
。 -
最近点
Q = A + t * AB
。 -
距离就是
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
就在三角形内部。这通常通过计算叉积来判断。-
计算三角形的法线
N = Cross(B-A, C-A)
。 -
对于每条边,例如
AB
,计算Cross(B-A, P-A)
。 -
如果
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.x
且rect1.max.x >= rect2.min.x
-
rect1.min.y <= rect2.max.y
且rect1.max.y >= rect2.min.y
-
如果两者都满足,则相交。
-
-
Unity API:
Rect
和Bounds
结构体。-
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
的范围是0
到1
。-
当
t=0
时,结果是start
。 -
当
t=1
时,结果是end
。 -
当
t=0.5
时,结果是start
和end
的中间点。
-
-
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 Spline 或 Hermite 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 面试问答
-
问:请解释向量投影(Vector Projection)和向量反射(Vector Reflection)的几何意义,并各举一个游戏中的应用场景。
-
考察点: 对向量操作几何含义的理解和实际应用。
-
解析:
-
投影: 衡量一个向量在另一个方向上的分量。
-
几何意义: 向量
A
在向量B
上的“阴影”。 -
应用: 角色在斜坡上移动时,将玩家的输入移动方向投影到斜坡平面上,确保角色沿着斜坡表面滑动,而不是穿透地面。
-
-
反射: 计算一个向量遇到表面后如何“反弹”出去。
-
几何意义: 光线击中镜面后反弹的路径。
-
应用: 弹球游戏中,球击中墙壁或挡板后,根据入射方向和表面法线计算球的反弹方向和速度。
-
-
-
-
问:在 3D 游戏中,你如何计算一个点到一条线段的最短距离?简述其原理和步骤。
-
考察点: 考查点到线段距离算法的掌握,以及对钳制操作的理解。
-
解析:
-
原理: 核心在于找到点
P
在线段所在直线上最近的点Q
,然后判断Q
是否在线段的范围内。 -
步骤:
-
定义线段
AB
的方向向量AB_vec = B - A
。 -
定义从线段起点
A
到点P
的向量AP_vec = P - A
。 -
计算
AP_vec
在AB_vec
上的投影长度的归一化参数t = Vector3.Dot(AP_vec, AB_vec) / AB_vec.sqrMagnitude
。 -
使用
Mathf.Clamp01(t)
将t
钳制在[0, 1]
之间,确保最近点在线段内部。 -
计算线段上离
P
最近的点Q = A + t * AB_vec
。 -
最终距离为
Vector3.Distance(P, Q)
。
-
-
-
-
问:什么是贝塞尔曲线?它在游戏开发中有什么用?你如何生成一个三次贝塞尔曲线上的点?
-
考察点: 对曲线生成算法的理解和应用。
-
解析:
-
概念: 贝塞尔曲线是一种由控制点定义的平滑曲线。曲线会经过起点和终点,但通常不经过中间的控制点,控制点通过“拉力”影响曲线的形状。
-
游戏应用: 角色或敌人沿着平滑路径移动的动画、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 几何计算的更深层次,涵盖了:
-
向量的投影与反射:理解了如何将向量分解或反弹,并在角色控制器、物理模拟中应用。
-
点与几何体的距离计算:学习了点到线段、点到平面的最短距离计算方法。
-
判断与相交测试:探讨了点在三角形内部的判断,以及包围盒相交检测。
-
插值与曲线:巩固了线性插值,并深入介绍了贝塞尔曲线等高级路径生成算法。
-
几何体的表示与操作:简要介绍了
Plane
和Bounds
等结构体的用法。 -
常用数学辅助算法:如
Mathf.Atan2()
和随机数生成,它们在实际开发中非常实用。
掌握这些通用的几何计算和算法,将极大地拓宽你在游戏开发中解决问题的思路。它们是实现自定义物理、高级 AI 行为、复杂动画系统以及各种独特游戏机制的基石。
在下一篇也是本系列的最后一篇《进阶话题与性能优化(高阶与思考篇)》中,我们将回顾并总结前面所学知识,讨论一些更高级的 3D 数学概念,以及在实际项目中如何对物理和数学计算进行性能优化。
现在,你对这些几何计算和常用算法是否已经有了更清晰的理解?在你的开发中,你有没有遇到过哪些需要自己手动实现几何计算的场景?
Unity3D数学第一篇:向量与点、线、面(基础篇)
Unity3D数学第二篇:旋转与欧拉角、四元数(核心变换篇)
Unity3D数学第三篇:坐标系与变换矩阵(空间转换篇)
Unity3D数学第四篇:射线与碰撞检测(交互基础篇)
Unity3D数学第五篇:几何计算与常用算法(实用算法篇)