Skip to content

数学基础

向量点乘能做什么?

向量点乘能做什么? 一句话理解:

点乘可以判断两个方向有多接近。

在游戏开发里,点乘特别常用,比如:

判断目标在前方还是后方
判断目标是否在视野角内
判断角色移动方向和朝向是否一致
计算光照强弱
计算一个速度在某个方向上的分量

vector-dot-product-csharp

点乘公式

点乘公式是:

A · B = |A| * |B| * cosθ

其中:

A 和 B 是两个向量
|A| 是 A 的长度
|B| 是 B 的长度
θ 是两个向量之间的夹角

如果两个向量都是单位向量,也就是长度都是 1

A · B = cosθ

这时点乘结果范围是:

-1 到 1

点乘结果怎么看?

如果两个向量都归一化了:

dot = 1

表示:

两个方向完全相同
dot = 0

表示:

两个方向互相垂直
dot = -1

表示:

两个方向完全相反

所以面试里可以这样记:

dot 越接近 1,方向越一致
dot 越接近 0,越接近垂直
dot 越接近 -1,方向越相反

用途 1:判断目标在前方还是后方

比如 NPC 想判断玩家是不是在自己前面。

思路是:

NPC 的 forward 和 NPC 到玩家的方向做点乘

如果:

dot > 0

说明玩家大致在 NPC 前方。

如果:

dot < 0

说明玩家在 NPC 后方。

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 Transform
public class DotFrontCheck : MonoBehaviour // 定义前后判断组件
{ // DotFrontCheck 类开始
    public Transform target; // 保存目标对象,比如玩家
    public bool IsTargetInFront() // 定义函数,判断目标是否在自己前方
    { // IsTargetInFront 函数开始
        Vector3 directionToTarget = target.position - transform.position; // 计算从自己指向目标的方向向量
        Vector3 normalizedDirection = directionToTarget.normalized; // 把方向向量归一化,方便点乘结果稳定在 -1 到 1
        float dot = Vector3.Dot(transform.forward, normalizedDirection); // 计算自己的前方向和目标方向的点乘
        return dot > 0f; // 如果 dot 大于 0,说明目标大致在前方
    } // IsTargetInFront 函数结束
} // DotFrontCheck 类结束

用途 2:判断目标是否在视野角内

如果 NPC 的视野是 90 度,意思是:

正前方左右各 45 度

用点乘判断会比 Vector3.Angle 更适合大量单位检测。

核心判断:

dot > cos(viewAngle / 2)

比如视野角是 90 度:

halfAngle = 45 度
cos(45°) ≈ 0.707

所以:

dot > 0.707

才在视野内。

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3、Mathf 和 Transform
public class DotViewCheck : MonoBehaviour // 定义视野角检测组件
{ // DotViewCheck 类开始
    public Transform target; // 保存目标对象
    public float viewAngle = 90f; // 定义视野角度
    public bool IsTargetInView() // 定义函数,判断目标是否在视野角内
    { // IsTargetInView 函数开始
        Vector3 directionToTarget = target.position - transform.position; // 计算从自己到目标的方向
        Vector3 normalizedDirection = directionToTarget.normalized; // 把目标方向归一化
        float dot = Vector3.Dot(transform.forward, normalizedDirection); // 计算前方向和目标方向的点乘
        float halfAngle = viewAngle * 0.5f; // 计算视野半角
        float cosThreshold = Mathf.Cos(halfAngle * Mathf.Deg2Rad); // 把半角转成弧度后计算 cos 阈值
        return dot >= cosThreshold; // 如果 dot 大于等于阈值,说明目标在视野角内
    } // IsTargetInView 函数结束
} // DotViewCheck 类结束

用途 3:计算移动方向是否和朝向一致

比如角色正在移动,你想判断:

角色是往前走,还是倒退,还是横向移动

可以用:

c
角色 forward · 移动方向
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 MonoBehaviour
public class MoveDirectionCheck : MonoBehaviour // 定义移动方向检测组件
{ // MoveDirectionCheck 类开始
    public string GetMoveType(Vector3 moveDirection) // 定义函数,根据移动方向判断移动类型
    { // GetMoveType 函数开始
        Vector3 normalizedMove = moveDirection.normalized; // 把移动方向归一化
        float dot = Vector3.Dot(transform.forward, normalizedMove); // 计算角色前方向和移动方向的点乘
        if (dot > 0.5f) // 如果点乘明显大于 0
        { // if 语句开始
            return "向前移动"; // 返回向前移动
        } // if 语句结束
        if (dot < -0.5f) // 如果点乘明显小于 0
        { // if 语句开始
            return "倒退移动"; // 返回倒退移动
        } // if 语句结束
        return "横向移动"; // 否则更接近横向移动
    } // GetMoveType 函数结束
} // MoveDirectionCheck 类结束

用途 4:计算投影,也就是某方向上的分量

比如你有一个速度:

velocity

你想知道它在角色前方向上有多少速度,就可以:

forwardSpeed = Vector3.Dot(velocity, transform.forward)

如果 transform.forward 是单位向量,那么点乘结果就是:

c
velocity 在 forward 方向上的分量
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 MonoBehaviour
public class VelocityProjection : MonoBehaviour // 定义速度投影组件
{ // VelocityProjection 类开始
    public float GetForwardSpeed(Vector3 velocity) // 定义函数,计算速度在角色前方向上的分量
    { // GetForwardSpeed 函数开始
        float forwardSpeed = Vector3.Dot(velocity, transform.forward); // 计算速度向量在前方向上的投影长度
        return forwardSpeed; // 返回前向速度,正数表示向前,负数表示向后
    } // GetForwardSpeed 函数结束
} // VelocityProjection 类结束

点乘和叉乘怎么区分?

点乘回答的是:

两个方向有多像?

结果是:

一个数字

叉乘回答的是:

从一个方向转到另一个方向,旋转轴或左右方向是什么?

结果是:

一个向量

简单记:

点乘看夹角大小
叉乘看旋转方向

面试高分回答

NOTE

向量点乘可以用来判断两个方向的夹角关系。公式是 A · B = |A||B|cosθ,如果两个向量都归一化,那么点乘结果就是 cosθ,范围在 [-1, 1]。结果接近 1 表示方向接近一致,接近 0 表示接近垂直,接近 -1 表示方向相反。在游戏开发里,点乘常用于判断目标是否在角色前方、是否在视野角内、移动方向和朝向是否一致、光照 Lambert 计算,以及求速度在某个方向上的投影。实际使用时通常先把方向向量归一化,这样 dot 的意义更稳定。

向量叉乘能做什么?

向量叉乘能做什么? 一句话理解:

叉乘可以得到一个“垂直方向”,也可以判断一个目标在左边还是右边。

点乘的结果是一个数字:

float dot

叉乘的结果是一个向量:

Vector3 cross

vector-cross-product-csharp

叉乘公式

叉乘写作:

A × B

它的长度是:

c
|A × B| = |A| * |B| * sinθ

其中:

θ 是 A 和 B 的夹角

叉乘结果的方向:

垂直于 A 和 B 所在的平面

方向由:

右手定则

决定。

右手定则怎么理解?

你可以这样记:

右手四指从 A 的方向弯向 B 的方向
大拇指指向的方向就是 A × B 的方向

还有一个特别重要的性质:

A × B = -(B × A)

也就是说:

A 叉 B 和 B 叉 A,方向正好相反。

所以叉乘很适合判断:

旋转方向
左边还是右边
法线朝哪边

用途 1:判断目标在左边还是右边

在 Unity 里,角色通常在 XZ 平面移动,Y 是向上方向。

我们可以用:

c
Vector3.Cross(transform.forward, directionToTarget)

然后看:

c
cross.y

如果 cross.y 是正数,目标在一侧。 如果 cross.y 是负数,目标在另一侧。

注意:具体正负对应左还是右,和你传参顺序有关。

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 MonoBehaviour
public class CrossSideCheck : MonoBehaviour // 定义左右判断组件
{ // CrossSideCheck 类开始
    public Transform target; // 保存目标对象,比如玩家
    public string GetTargetSide() // 定义函数,判断目标在左边还是右边
    { // GetTargetSide 函数开始
        Vector3 directionToTarget = target.position - transform.position; // 计算从自己指向目标的方向
        Vector3 normalizedDirection = directionToTarget.normalized; // 把方向归一化,让方向判断更稳定
        Vector3 cross = Vector3.Cross(transform.forward, normalizedDirection); // 计算前方向和目标方向的叉乘
        if (cross.y > 0f) // 如果叉乘结果的 y 分量大于 0
        { // if 语句开始
            return "目标在左侧"; // 返回目标在左侧
        } // if 语句结束
        if (cross.y < 0f) // 如果叉乘结果的 y 分量小于 0
        { // if 语句开始
            return "目标在右侧"; // 返回目标在右侧
        } // if 语句结束
        return "目标在正前方或正后方"; // 如果 y 接近 0,说明目标大致在同一直线上
    } // GetTargetSide 函数结束
} // CrossSideCheck 类结束

用途 2:求三角形法线

三角形有三个点:

A
B
C

先算两条边:

AB = B - A
AC = C - A

然后:

normal = AB × AC

这个 normal 就是三角形面的法线方向。

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3
public static class NormalUtility // 定义法线工具类
{ // NormalUtility 类开始
    public static Vector3 GetTriangleNormal(Vector3 a, Vector3 b, Vector3 c) // 定义函数,根据三角形三个点计算法线
    { // GetTriangleNormal 函数开始
        Vector3 ab = b - a; // 计算从点 a 指向点 b 的边向量
        Vector3 ac = c - a; // 计算从点 a 指向点 c 的边向量
        Vector3 normal = Vector3.Cross(ab, ac); // 对两条边做叉乘,得到垂直于三角形面的向量
        Vector3 normalizedNormal = normal.normalized; // 把法线归一化,得到长度为 1 的方向
        return normalizedNormal; // 返回三角形单位法线
    } // GetTriangleNormal 函数结束
} // NormalUtility 类结束

用途 3:判断转向方向

比如角色要转向目标,你想知道:

应该向左转
还是向右转

可以用叉乘判断方向。

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 MonoBehaviour
public class TurnDirectionCheck : MonoBehaviour // 定义转向方向检测组件
{ // TurnDirectionCheck 类开始
    public Transform target; // 保存目标对象
    public float GetTurnSign() // 定义函数,返回转向方向符号
    { // GetTurnSign 函数开始
        Vector3 directionToTarget = target.position - transform.position; // 计算从自己到目标的方向
        directionToTarget.y = 0f; // 忽略高度差,只在水平面判断左右
        Vector3 normalizedDirection = directionToTarget.normalized; // 把目标方向归一化
        Vector3 cross = Vector3.Cross(transform.forward, normalizedDirection); // 计算前方向到目标方向的叉乘
        return Mathf.Sign(cross.y); // 返回 y 分量符号,用来表示左转或右转
    } // GetTurnSign 函数结束
} // TurnDirectionCheck 类结束

用途 4:计算面积

叉乘长度还有一个几何意义:

|A × B| 等于以 A、B 为边的平行四边形面积

所以三角形面积是:

c
|A × B| / 2
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3
public static class AreaUtility // 定义面积工具类
{ // AreaUtility 类开始
    public static float GetTriangleArea(Vector3 a, Vector3 b, Vector3 c) // 定义函数,计算三角形面积
    { // GetTriangleArea 函数开始
        Vector3 ab = b - a; // 计算三角形第一条边
        Vector3 ac = c - a; // 计算三角形第二条边
        Vector3 cross = Vector3.Cross(ab, ac); // 计算两条边的叉乘
        float parallelogramArea = cross.magnitude; // 叉乘长度等于平行四边形面积
        float triangleArea = parallelogramArea * 0.5f; // 三角形面积是平行四边形面积的一半
        return triangleArea; // 返回三角形面积
    } // GetTriangleArea 函数结束
} // AreaUtility 类结束

点乘和叉乘怎么区分?

点乘:

结果是一个数字
用来判断两个方向夹角有多大
常用于前后判断、视野角、投影

叉乘:

结果是一个向量
用来判断垂直方向、左右方向、法线方向
常用于左右判断、转向方向、求法线

简单记:

点乘看像不像
叉乘看往哪边

面试高分回答

IMPORTANT

向量叉乘的结果是一个向量,这个向量垂直于原来两个向量所在的平面,方向由右手定则决定,长度等于 |A||B|sinθ。叉乘常用于判断方向关系,比如在 Unity 的 XZ 平面里,可以用 Vector3.Cross(transform.forward, dirToTarget).y 判断目标在角色左边还是右边;也可以用三角形两条边做叉乘来计算面法线;叉乘长度还可以表示平行四边形面积,因此三角形面积可以用叉乘长度的一半计算。需要注意的是,叉乘不满足交换律,A × BB × A 的方向相反。

矩阵乘法在图形学中有什么用?

齐次坐标是什么? 齐次坐标就是:在普通坐标后面多加一个 w

普通 3D 坐标是:

(x, y, z)

齐次坐标是:

(x, y, z, w)

在图形学里最常见的是:

点:      (x, y, z, 1)
方向向量: (x, y, z, 0)

matrix-multiplication-graphics-csharp

为什么需要齐次坐标?

因为图形学里想用一个统一的矩阵来处理:

平移
旋转
缩放
投影

如果只用普通的 3D 向量 (x, y, z),旋转和缩放可以用矩阵表示,但平移不好统一放进普通 3x3 矩阵里。

加上 w 之后,就可以用 4x4 矩阵统一处理:

点变换
方向变换
模型矩阵
视图矩阵
投影矩阵

w = 1 表示点

点是空间中的一个位置。

比如:

(2, 3, 4, 1)

表示一个 3D 点:

x = 2
y = 3
z = 4

因为 w = 1,所以它会受到平移影响。

比如一个平移矩阵表示:

向右移动 10

那么点会从:

(2, 3, 4)

变成:

(12, 3, 4)

w = 0 表示方向

方向不是位置,它只表示“朝哪里”。

比如:

(0, 0, 1, 0)

表示朝前的方向。

因为 w = 0,所以它不会受到平移影响。

这很合理:

一个方向不能因为物体平移了,就改变方向。

比如角色从 (0,0,0) 移动到 (10,0,0),它的 forward 方向还是 forward,不应该变成另一个位置。

Unity C# 示例:点会受平移影响

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Matrix4x4、Vector3、Vector4 和 MonoBehaviour
public class HomogeneousPointExample : MonoBehaviour // 定义齐次坐标点示例组件
{ // HomogeneousPointExample 类开始
    private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
    { // Start 函数开始
        Matrix4x4 matrix = Matrix4x4.Translate(new Vector3(10f, 0f, 0f)); // 创建一个向右平移 10 的矩阵
        Vector4 point = new Vector4(2f, 3f, 4f, 1f); // 创建一个齐次坐标点,w 等于 1
        Vector4 result = matrix * point; // 用矩阵变换这个点,平移会生效
        Debug.Log(result); // 输出结果,x 会从 2 变成 12
    } // Start 函数结束
} // HomogeneousPointExample 类结束

Unity C# 示例:方向不会受平移影响

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Matrix4x4、Vector3、Vector4 和 MonoBehaviour
public class HomogeneousDirectionExample : MonoBehaviour // 定义齐次坐标方向示例组件
{ // HomogeneousDirectionExample 类开始
    private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
    { // Start 函数开始
        Matrix4x4 matrix = Matrix4x4.Translate(new Vector3(10f, 0f, 0f)); // 创建一个向右平移 10 的矩阵
        Vector4 direction = new Vector4(0f, 0f, 1f, 0f); // 创建一个齐次方向向量,w 等于 0
        Vector4 result = matrix * direction; // 用矩阵变换这个方向,平移不会生效
        Debug.Log(result); // 输出结果,方向仍然是 0,0,1,0
    } // Start 函数结束
} // HomogeneousDirectionExample 类结束

投影里的 w 有什么用?

透视投影之后,点会变成:

(x, y, z, w)

然后 GPU 会做一步:

透视除法

也就是:

c
x = x / w
y = y / w
z = z / w

这一步会产生近大远小的透视效果。

比如远处的点 w 更大,除以 w 后,屏幕上的位置会被压缩,看起来更靠近中心。

这就是为什么透视相机能表现:

近处大
远处小

Unity C# 示例:透视除法

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector4 和 MonoBehaviour
public class PerspectiveDivideExample : MonoBehaviour // 定义透视除法示例组件
{ // PerspectiveDivideExample 类开始
    private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
    { // Start 函数开始
        Vector4 clipPosition = new Vector4(4f, 2f, 8f, 2f); // 创建一个裁剪空间坐标,w 等于 2
        Vector3 ndcPosition = new Vector3(clipPosition.x / clipPosition.w, clipPosition.y / clipPosition.w, clipPosition.z / clipPosition.w); // 执行透视除法,得到 NDC 坐标
        Debug.Log(ndcPosition); // 输出结果,得到 2,1,4
    } // Start 函数结束
} // PerspectiveDivideExample 类结束

为什么方向要用 w = 0?

因为方向只代表:

朝向

不代表:

位置

所以方向应该受旋转影响,也应该受缩放影响,但不应该受平移影响。

比如:

角色 forward = (0, 0, 1)

角色移动到另一个地方后:

forward 还是 (0, 0, 1)

这就是 w = 0 的意义。

面试高分回答

NOTE

齐次坐标是在普通坐标后面增加一个 w 分量,把 (x, y, z) 扩展成 (x, y, z, w)。在图形学中,w = 1 通常表示点,点会受到平移影响;w = 0 通常表示方向向量,方向不会受到平移影响。齐次坐标的主要作用是让平移、旋转、缩放、投影都能统一用 4x4 矩阵乘法表示。模型顶点经过 ModelViewProjection 矩阵后会得到裁剪空间坐标 (x, y, z, w),随后通过透视除法 (x/w, y/w, z/w) 得到标准化设备坐标,从而实现透视效果。

齐次坐标是什么?

齐次坐标是什么? 齐次坐标就是在普通坐标后面多加一个 w

普通 3D 坐标:  (x, y, z)
齐次 3D 坐标:  (x, y, z, w)

在图形学里最常见的规则是:

点:      (x, y, z, 1)
方向:    (x, y, z, 0)

homogeneous-coordinates-csharp

为什么要多一个 w?

因为图形学希望用一个 4x4 矩阵统一处理:

平移
旋转
缩放
投影

如果只有 (x, y, z),平移不好统一放进矩阵乘法里。 加上 w 后,就可以写成:

变换后坐标 = 4x4矩阵 * (x, y, z, w)

w = 1:表示点

点有位置,所以会受平移影响。

c
using UnityEngine; // 引入 UnityEngine,用来使用 Matrix4x4、Vector4 和 MonoBehaviour
public class PointExample : MonoBehaviour // 定义点变换示例类
{ // 类开始
    private void Start() // Unity 启动时调用
    { // 函数开始
        Matrix4x4 matrix = Matrix4x4.Translate(new Vector3(10f, 0f, 0f)); // 创建一个向右平移 10 的矩阵
        Vector4 point = new Vector4(2f, 3f, 4f, 1f); // 创建一个点,w 等于 1
        Vector4 result = matrix * point; // 点乘矩阵后会受到平移影响
        Debug.Log(result); // 输出结果,x 会从 2 变成 12
    } // 函数结束
} // 类结束

w = 0:表示方向

方向没有位置,所以不应该受平移影响。

c
using UnityEngine; // 引入 UnityEngine,用来使用 Matrix4x4、Vector4 和 MonoBehaviour
public class DirectionExample : MonoBehaviour // 定义方向变换示例类
{ // 类开始
    private void Start() // Unity 启动时调用
    { // 函数开始
        Matrix4x4 matrix = Matrix4x4.Translate(new Vector3(10f, 0f, 0f)); // 创建一个向右平移 10 的矩阵
        Vector4 direction = new Vector4(0f, 0f, 1f, 0f); // 创建一个方向,w 等于 0
        Vector4 result = matrix * direction; // 方向乘矩阵后不会受到平移影响
        Debug.Log(result); // 输出结果,方向仍然是 0,0,1,0
    } // 函数结束
} // 类结束

投影里的 w 有什么用?

透视投影后,GPU 会得到:

(x, y, z, w)

然后做透视除法:

(x / w, y / w, z / w)

这个过程会产生:

近大远小

所以 w 不只是多出来的数字,它是透视投影的重要部分。

面试回答

NOTE

齐次坐标是在普通坐标后面增加一个 w 分量,把 (x, y, z) 扩展成 (x, y, z, w)。在图形学中,w = 1 通常表示点,点会受到平移影响;w = 0 通常表示方向向量,方向不会受到平移影响。齐次坐标的作用是让平移、旋转、缩放、投影都能统一用 4x4 矩阵乘法表示。投影后还会通过 (x/w, y/w, z/w) 做透视除法,从而实现近大远小的效果。

欧拉角为什么会万向节锁?

欧拉角为什么会万向节锁? 一句话理解:

欧拉角是按顺序绕三个轴旋转的,当某个中间轴转到特殊角度,比如 90 度时,另外两个旋转轴可能重合,导致少了一个旋转自由度。

这就叫:

c
万向节锁 Gimbal Lock

euler-gimbal-lock-csharp

欧拉角是什么?

欧拉角就是用三个角度表示旋转,比如 Unity Inspector 里常见的:

X = 30
Y = 60
Z = 10

它的意思不是“同时绕三个轴旋转”,而是:

按某个固定顺序,依次绕轴旋转。

比如可以理解成:

先绕 X 轴转
再绕 Y 轴转
再绕 Z 轴转

这个“顺序”非常重要,因为旋转不是普通加法:

先转 X 再转 Y

和:

先转 Y 再转 X

结果可能不一样。

万向节锁发生了什么?

正常情况下,我们希望三个旋转轴分别控制三个自由度:

Pitch:上下抬头低头
Yaw:左右转头
Roll:左右倾斜

也就是:

3 个角度控制 3 个方向变化

但当中间某个旋转轴转到接近 90° 时,两个旋转轴会重合。

结果就是:

两个角度控制的是同一个方向变化

原本应该有:

3 个自由度

现在变成:

2 个自由度

这就是“锁住”的意思。

为什么叫万向节锁?

可以把欧拉角想象成机械万向节:

外圈控制一个轴
中圈控制一个轴
内圈控制一个轴

正常情况下三个环方向不同,可以自由旋转。

但当中间环转到某个角度时:

外圈和内圈的旋转轴对齐了

这时你转两个不同的环,效果却变得很像,等于丢失了一个控制方向。

举个简单例子

假设一个相机使用欧拉角:

c
Yaw:左右转
Pitch:上下看
Roll:倾斜

当相机 Pitch 到接近 90°,也就是几乎垂直朝上时:

Yaw 和 Roll 可能变得很难区分

你想让它“左右转”,结果看起来像是在“滚转”。 你想让它“滚转”,结果又像是在改变另一个方向。

这不是 Unity 出 bug,而是欧拉角这种表示方式本身存在奇异点。

Unity 里为什么还会看到欧拉角?

Unity Inspector 显示的是:

c
Euler Angles

因为欧拉角对人类比较直观:

X 多少度
Y 多少度
Z 多少度

但是 Unity 内部旋转主要用的是:

Quaternion

也就是四元数。

原因就是:

四元数更适合做旋转计算和插值
不容易出现欧拉角万向节锁问题

容易出问题的写法

比如你每帧读欧拉角、改欧拉角、再写回去:

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Vector3
public class BadEulerRotate : MonoBehaviour // 定义一个不推荐的欧拉角旋转示例
{ // 类开始
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        Vector3 euler = transform.eulerAngles; // 读取当前欧拉角
        euler.x += 90f * Time.deltaTime; // 修改 X 轴角度
        euler.y += 90f * Time.deltaTime; // 修改 Y 轴角度
        transform.eulerAngles = euler; // 把欧拉角写回 Transform
    } // 函数结束
} // 类结束

这类写法在复杂旋转、连续旋转、接近特殊角度时,可能出现不稳定、跳变或者控制方向不符合预期。

更推荐:用 Quaternion 做旋转

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Quaternion 和 Vector3
public class GoodQuaternionRotate : MonoBehaviour // 定义一个推荐的四元数旋转示例
{ // 类开始
    public float yawSpeed = 90f; // 定义左右旋转速度
    public float pitchSpeed = 60f; // 定义上下旋转速度
    private float yaw; // 保存水平旋转角度
    private float pitch; // 保存俯仰旋转角度
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        yaw += Input.GetAxis("Horizontal") * yawSpeed * Time.deltaTime; // 根据水平输入累加 yaw
        pitch -= Input.GetAxis("Vertical") * pitchSpeed * Time.deltaTime; // 根据垂直输入累加 pitch
        pitch = Mathf.Clamp(pitch, -80f, 80f); // 限制 pitch,避免相机直接转到正上方或正下方
        Quaternion yawRotation = Quaternion.AngleAxis(yaw, Vector3.up); // 创建绕世界上方向旋转的四元数
        Quaternion pitchRotation = Quaternion.AngleAxis(pitch, Vector3.right); // 创建绕本地右方向旋转的四元数
        transform.rotation = yawRotation * pitchRotation; // 合并两个四元数,得到最终旋转
    } // 函数结束
} // 类结束

为什么四元数能避免?

四元数不是用“三个按顺序的轴角”来表达旋转。

它用一种更稳定的数学形式表示 3D 旋转。 所以它不会因为某个中间轴转到 90°,就让两个轴重合。

简单说:

欧拉角:三次顺序旋转,容易出现轴重合
四元数:整体表示一个旋转,不依赖三个万向节轴

面试高分回答

NOTE

欧拉角用三个角度按固定顺序表示旋转,比如 Pitch、Yaw、Roll。它的问题是旋转轴会随着前面的旋转而改变,当中间某个旋转达到特殊角度,比如接近 90° 时,两个旋转轴可能重合,导致原本三个自由度只剩两个自由度,这就是万向节锁。表现上就是某些方向无法独立控制,旋转可能突然变得不符合预期。Unity Inspector 会显示欧拉角是因为它直观,但内部更适合用四元数表示旋转。实际开发中应尽量用 Quaternion 做旋转计算和插值,避免频繁读取、累加、写回 transform.eulerAngles

四元数有什么优势?

四元数有什么优势?

一句话:欧拉角适合人看,四元数适合程序算。 Unity 里的 Quaternion 就是用来表示 3D 旋转的,它最大的价值是:稳定、平滑、适合组合旋转,并且能避免万向节锁。

quaternion-advantages-csharp

核心优势

1. 避免万向节锁

欧拉角是按 X、Y、Z 三个轴依次旋转。 当某两个旋转轴重合时,就会丢失一个自由度,这就是万向节锁

四元数不是“绕 X 再绕 Y 再绕 Z”,它更像是:

绕某一个空间中的旋转轴,一次性旋转某个角度。

所以它不会因为三个轴的顺序问题导致轴重合。

2. 旋转插值更平滑

角色慢慢转向敌人、相机平滑跟随、导弹慢慢朝目标转,这些都很适合用:

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Quaternion、Vector3 和 Time
public class QuaternionLookAtExample : MonoBehaviour // 定义一个使用四元数平滑转向目标的示例组件
{ // 类开始
    public Transform target; // 保存目标对象
    public float rotateSpeed = 5f; // 定义旋转速度
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        if (target == null) // 如果目标为空
        { // if 语句开始
            return; // 没有目标就直接返回
        } // if 语句结束
        Vector3 direction = target.position - transform.position; // 计算从自己指向目标的方向
        if (direction.sqrMagnitude <= 0.0001f) // 如果方向长度几乎为 0
        { // if 语句开始
            return; // 目标和自己几乎重合,无法计算朝向
        } // if 语句结束
        Quaternion targetRotation = Quaternion.LookRotation(direction.normalized); // 根据目标方向创建目标旋转
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); // 使用 Slerp 平滑插值到目标旋转
    } // 函数结束
} // 类结束

Slerp 是球面插值,比普通线性插值更适合旋转,看起来会更自然。

3. 组合旋转方便

四元数可以通过乘法组合旋转:

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Quaternion 和 Vector3
public class QuaternionCombineExample : MonoBehaviour // 定义四元数组合旋转示例组件
{ // 类开始
    private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
    { // 函数开始
        Quaternion yaw = Quaternion.AngleAxis(45f, Vector3.up); // 创建绕 Y 轴旋转 45 度的四元数
        Quaternion pitch = Quaternion.AngleAxis(30f, Vector3.right); // 创建绕 X 轴旋转 30 度的四元数
        Quaternion finalRotation = yaw * pitch; // 组合旋转,注意乘法顺序会影响结果
        transform.rotation = finalRotation; // 把组合后的旋转赋给当前物体
    } // 函数结束
} // 类结束

注意:四元数乘法顺序很重要yaw * pitchpitch * yaw 结果可能不一样。

4. 比旋转矩阵更紧凑

旋转矩阵通常需要 9 个数表示 3D 旋转。 四元数只需要 4 个数:x, y, z, w

所以它更省空间,也更适合做运行时旋转计算。

常见坑

不要直接改:

c
transform.rotation.x

因为 x、y、z、w 不是欧拉角,它们不是“X 轴多少度、Y 轴多少度、Z 轴多少度”。

正确做法通常是用:

c
Quaternion.Euler(...)
Quaternion.LookRotation(...)
Quaternion.AngleAxis(...)
Quaternion.Slerp(...)

面试高分回答

TIP

四元数是一种表示三维旋转的方式。相比欧拉角,它不依赖按顺序绕三个轴旋转,所以可以避免万向节锁;相比旋转矩阵,它只需要 4 个数,更紧凑。四元数非常适合做旋转组合和旋转插值,比如 Unity 中常用 Quaternion.LookRotationQuaternion.SlerpQuaternion.AngleAxis。欧拉角更适合人看和配置,四元数更适合程序内部计算。

法线为什么要用逆转置矩阵变换?

法线为什么要用逆转置矩阵变换?

一句话:因为法线的职责是保持和表面垂直,而普通模型矩阵在“非等比缩放”后会破坏这种垂直关系。

normal-inverse-transpose-csharp

先理解法线是什么

法线 Normal 是一个方向,表示“表面朝向哪里”。

比如一个地面平面:

地面方向:横着
法线方向:竖着向上

它最重要的性质是:

法线必须垂直于表面

用数学说就是:

切线 T · 法线 N = 0

点乘等于 0,说明两个方向互相垂直。

为什么普通矩阵变换会出问题?

模型矩阵 M 可以包含:

c
平移 Translation
旋转 Rotation
缩放 Scale
错切 Shear

对于顶点位置来说,用模型矩阵直接乘是对的:

P' = M × P

对于切线来说,一般也可以跟着表面变换:

c
T' = M × T

但是法线不能简单写成:

c
N' = M × N

因为如果模型发生了非等比缩放,例如:

X 方向放大 2 倍
Y 方向不变
Z 方向不变

表面会被拉伸,切线方向会变,但法线如果也直接被拉伸,就可能不再和表面垂直。

用一个很小的例子看懂

原来有:

切线 T = (1, 1)
法线 N = (1, -1)

它们点乘:

c
T · N = 1 × 1 + 1 × (-1) = 0

说明它们是垂直的。

现在模型做非等比缩放:

c
M = Scale(2, 1)

切线被变换成:

c
T' = (2, 1)

如果错误地用普通矩阵变换法线:

c
wrongN = (2, -1)

再点乘:

c
T' · wrongN = 2 × 2 + 1 × (-1) = 3

结果不是 0,说明它们已经不垂直了。

这就会导致光照错误: 表面明明应该朝某个方向,却被当成另一个方向来打光。

逆转置矩阵怎么来的

我们希望变换后仍然满足:

c
T' · N' = 0

切线变换后是:

c
T' = M × T

所以要求:

c
(M × T) · N' = 0

为了让这个关系成立,法线不能用 M,而要用:

c
N' = (M⁻¹)ᵀ × N

也就是:

法线矩阵 = 模型矩阵的逆矩阵,再转置

中文叫:

逆转置矩阵

为什么是“逆”

因为模型矩阵把表面拉歪了。 法线要反过来修正这种拉伸影响,所以需要用 M⁻¹

为什么还要“转置”

因为点乘公式里,矩阵从切线那边“搬到”法线那边时,需要变成转置形式。

你不用死背推导,面试里可以这样记:

切线跟着表面变。
法线要保证继续垂直于切线。
为了保持点乘为 0,法线必须用模型矩阵的逆转置。

C# 示例:手动用逆转置矩阵变换法线

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Matrix4x4、Vector3 和 Mathf
public static class NormalTransformUtility // 定义一个法线变换工具类
{ // 工具类开始
    public static Vector3 TransformNormal(Matrix4x4 modelMatrix, Vector3 normal) // 定义一个使用逆转置矩阵变换法线的方法
    { // 方法开始
        Matrix4x4 inverseModelMatrix = modelMatrix.inverse; // 先求模型矩阵的逆矩阵
        Matrix4x4 normalMatrix = inverseModelMatrix.transpose; // 再对逆矩阵做转置,得到法线矩阵
        Vector3 transformedNormal = normalMatrix.MultiplyVector(normal); // 使用法线矩阵变换法线方向
        Vector3 normalizedNormal = transformedNormal.normalized; // 把变换后的法线重新归一化成单位向量
        return normalizedNormal; // 返回最终正确的法线方向
    } // 方法结束
} // 工具类结束

为什么最后要 normalized?

因为矩阵变换可能改变法线长度。 但光照计算通常只需要方向,不需要长度。

所以变换后一般要:

Normalize

否则光照强弱也可能被法线长度影响。

什么时候普通矩阵也没问题?

如果模型只有:

旋转
平移
等比缩放

普通变换通常不会破坏法线方向。

但如果有:

非等比缩放
错切变换

就必须用逆转置矩阵。

Unity 里什么时候需要关心?

大多数情况下,Unity 的标准 Shader 或 SRP Shader 已经帮你处理了法线变换。

你需要特别关心的场景是:

自己写 Shader
自己做 Mesh 变形
自己在 CPU 侧计算法线
自己做程序化建模
自己写自定义光照

面试高分回答

IMPORTANT

法线不能像顶点位置一样直接用模型矩阵变换,因为法线的本质约束是要和表面切线保持垂直。模型矩阵如果包含非等比缩放或错切,直接变换法线会破坏这种垂直关系,导致光照错误。为了让变换后的切线 T' 和变换后的法线 N' 仍然满足 T' · N' = 0,法线应该使用模型矩阵的逆转置矩阵,即 N' = (M⁻¹)ᵀ × N,并且变换后要重新归一化。

插值 LerpSlerp 区别是什么?

Lerp 和 Slerp 的区别

一句话:Lerp 是沿直线插值,Slerp 是沿球面弧线插值。 所以你可以这样记:位置、颜色、数值常用 Lerp;旋转、朝向常用 Slerp。

lerp-vs-slerp-csharp

先理解插值是什么

插值就是:给你一个起点 A,一个终点 B,再给你一个进度 t,算出中间值。

t = 0    结果是 A
t = 0.5  结果在 A 和 B 中间
t = 1    结果是 B

Lerp 是什么

Lerp 全称是 Linear Interpolation,中文叫线性插值

公式可以理解成:

结果 = A + (B - A) * t

它的特点是:沿直线从 A 走到 B。

适合这些场景:

血条从 100 平滑变到 50
UI 透明度从 0 变到 1
颜色从红色渐变到蓝色
物体位置从 A 点移动到 B 点
数值从小变大

Lerp 示例:位置平滑移动

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Vector3 和 Time
public class LerpMoveExample : MonoBehaviour // 定义一个使用 Lerp 移动物体的示例组件
{ // 类开始
    public Transform startPoint; // 保存起点 Transform
    public Transform endPoint; // 保存终点 Transform
    public float duration = 2f; // 保存从起点移动到终点需要的总时间
    private float timer; // 保存当前已经移动了多久
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        timer += Time.deltaTime; // 每帧累加经过的时间
        float t = timer / duration; // 把时间转换成 0 到 1 的插值进度
        t = Mathf.Clamp01(t); // 把 t 限制在 0 到 1 之间,避免超过终点
        transform.position = Vector3.Lerp(startPoint.position, endPoint.position, t); // 使用 Lerp 沿直线从起点移动到终点
    } // 函数结束
} // 类结束

Slerp 是什么

Slerp 全称是 Spherical Linear Interpolation,中文叫球面线性插值

它不是沿直线走,而是:沿球面上的圆弧从一个方向转到另一个方向。

它特别适合这些场景:

角色平滑转向敌人
相机平滑转向目标
飞行物慢慢朝目标方向转
两个 Quaternion 旋转之间平滑过渡

Slerp 示例:角色平滑看向目标

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Quaternion、Vector3 和 Time
public class SlerpRotateExample : MonoBehaviour // 定义一个使用 Slerp 平滑旋转的示例组件
{ // 类开始
    public Transform target; // 保存目标对象
    public float rotateSpeed = 3f; // 保存旋转速度
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        if (target == null) // 判断目标对象是否为空
        { // if 语句开始
            return; // 如果没有目标,就直接结束本帧逻辑
        } // if 语句结束
        Vector3 direction = target.position - transform.position; // 计算从自己指向目标的方向
        if (direction.sqrMagnitude <= 0.0001f) // 判断方向长度是否几乎为 0
        { // if 语句开始
            return; // 如果自己和目标几乎重合,就无法计算有效朝向
        } // if 语句结束
        Quaternion targetRotation = Quaternion.LookRotation(direction.normalized); // 根据目标方向生成目标旋转
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); // 使用 Slerp 沿球面弧线平滑转向目标
    } // 函数结束
} // 类结束

核心区别

Lerp: 从 A 到 B 走直线,适合位置、颜色、数值。

Slerp: 从 A 到 B 沿圆弧 / 球面变化,适合方向和旋转。

Lerp 计算更简单,性能更便宜。 Slerp 计算更复杂,但旋转效果更自然。

为什么旋转更推荐 Slerp

旋转不是普通直线运动。

如果你把一个方向转到另一个方向,本质上是在一个球面上改变朝向。 用 Slerp 会更像真实的“转过去”,角度变化更自然。

尤其是两个旋转差距比较大时,SlerpLerp 更稳定、更符合直觉。

常见坑

第一个坑:每帧都这样写:

c
transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime); // 这会变成越来越慢的缓动,而不是固定时间到达

这不是“2 秒到达目标”,而是每帧都从当前位置往目标靠一点,效果会越来越慢。

第二个坑:t 通常应该是 01。 如果 t = 0.3,表示走了 30%。 如果 t = 1,表示到达终点。

第三个坑:不要直接对欧拉角乱用 Lerp。 旋转一般优先使用 Quaternion.Slerp

面试高分回答

IMPORTANT

Lerp 是线性插值,会沿直线从起点过渡到终点,适合位置、颜色、数值等线性变化。Slerp 是球面线性插值,会沿球面圆弧从一个方向或旋转过渡到另一个方向,适合 Quaternion 旋转、角色转向、相机转向等场景。简单说,直线变化用 Lerp,旋转方向用 Slerp

贝塞尔曲线怎么理解?

贝塞尔曲线怎么理解?

一句话:贝塞尔曲线就是一条被控制点“拉弯”的平滑曲线。 它一定会经过起点和终点,但不一定经过中间控制点,中间控制点更像“手柄”,负责控制曲线往哪里弯、弯得多厉害。

bezier-curve-csharp

最简单的理解

假设有三个点:

P0:起点
P1:控制点
P2:终点

曲线从 P0 出发,到 P2 结束,但会被 P1 往某个方向“吸过去”。 所以 P1 不一定在曲线上,它主要负责影响曲线形状。

t 是什么

t 是进度,通常范围是 0 ~ 1

t = 0    曲线上的点在起点
t = 0.5  曲线上的点在中间附近
t = 1    曲线上的点在终点

也就是说:我们不断让 t 从 0 增加到 1,就能采样出整条曲线。

二次贝塞尔怎么来的

二次贝塞尔有 3 个点:P0、P1、P2

它可以理解成“不断 Lerp”:

c
A = Lerp(P0, P1, t)
B = Lerp(P1, P2, t)
最终点 = Lerp(A, B, t)

这就是著名的 De Casteljau 思想:先在线段上找点,再在新点之间继续找点。

公式是:

c
B(t) = (1 - t)^2 * P0 + 2 * (1 - t) * t * P1 + t^2 * P2

三次贝塞尔是什么

三次贝塞尔有 4 个点:P0、P1、P2、P3

P0:起点
P1:控制起点方向
P2:控制终点方向
P3:终点

三次贝塞尔更灵活,游戏里更常见,比如:

相机轨迹
飞行物弧线
UI 动画路径
技能弹道
角色跳跃曲线
路径预览线

C# 示例:计算二次和三次贝塞尔点

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 Mathf
public static class BezierUtility // 定义一个贝塞尔曲线工具类
{ // 工具类开始
    public static Vector3 Quadratic(Vector3 p0, Vector3 p1, Vector3 p2, float t) // 定义二次贝塞尔曲线采样方法
    { // 方法开始
        t = Mathf.Clamp01(t); // 把 t 限制在 0 到 1 之间,避免超出曲线范围
        float u = 1f - t; // 计算 1 - t,方便套用贝塞尔公式
        Vector3 point = u * u * p0 + 2f * u * t * p1 + t * t * p2; // 根据二次贝塞尔公式计算曲线上的点
        return point; // 返回曲线在当前 t 位置上的点
    } // 方法结束
    public static Vector3 Cubic(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) // 定义三次贝塞尔曲线采样方法
    { // 方法开始
        t = Mathf.Clamp01(t); // 把 t 限制在 0 到 1 之间,保证采样点在曲线范围内
        float u = 1f - t; // 计算 1 - t,方便后面套用三次贝塞尔公式
        Vector3 point = u * u * u * p0 + 3f * u * u * t * p1 + 3f * u * t * t * p2 + t * t * t * p3; // 根据三次贝塞尔公式计算曲线上的点
        return point; // 返回曲线在当前 t 位置上的点
    } // 方法结束
} // 工具类结束

Unity 示例:用 LineRenderer 画一条三次贝塞尔曲线

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Transform、LineRenderer 和 Vector3
[RequireComponent(typeof(LineRenderer))] // 要求当前物体必须带有 LineRenderer 组件
public class BezierLineDrawer : MonoBehaviour // 定义一个绘制贝塞尔曲线的组件
{ // 类开始
    public Transform p0; // 保存曲线起点
    public Transform p1; // 保存第一个控制点
    public Transform p2; // 保存第二个控制点
    public Transform p3; // 保存曲线终点
    public int segmentCount = 30; // 保存曲线分段数量,数量越大曲线越平滑
    private LineRenderer lineRenderer; // 保存 LineRenderer 组件引用
    private void Awake() // Unity 在脚本实例加载时调用 Awake
    { // 函数开始
        lineRenderer = GetComponent<LineRenderer>(); // 获取当前物体上的 LineRenderer 组件
    } // 函数结束
    private void Update() // Unity 每帧调用 Update
    { // 函数开始
        if (p0 == null || p1 == null || p2 == null || p3 == null) // 判断四个点是否有任意一个为空
        { // if 语句开始
            return; // 如果控制点不完整,就不绘制曲线
        } // if 语句结束
        lineRenderer.positionCount = segmentCount + 1; // 设置 LineRenderer 需要显示的点数量
        for (int i = 0; i <= segmentCount; i++) // 从第 0 段遍历到最后一段
        { // for 循环开始
            float t = (float)i / segmentCount; // 把当前段编号转换成 0 到 1 的曲线进度
            Vector3 point = BezierUtility.Cubic(p0.position, p1.position, p2.position, p3.position, t); // 计算当前 t 对应的贝塞尔曲线点
            lineRenderer.SetPosition(i, point); // 把计算出的点设置到 LineRenderer 上
        } // for 循环结束
    } // 函数结束
} // 类结束

常见误区

贝塞尔曲线不是“必须经过所有点”。 它通常只经过起点和终点,中间控制点只是影响弯曲方向。

控制点离曲线越远,曲线被拉得越明显。 控制点越多,曲线越灵活,但也越难控制。

面试高分回答

NOTE

贝塞尔曲线是一种通过控制点定义的平滑曲线。它通过参数 t01 逐步采样曲线上的点。二次贝塞尔由 3 个点控制,三次贝塞尔由 4 个点控制。中间控制点不一定在曲线上,它们主要决定曲线的切线方向和弯曲程度。在游戏开发中,贝塞尔曲线常用于相机轨迹、飞行物弹道、UI 动画路径、技能范围提示和路径预览。

包围盒 AABB 和 OBB 区别是什么?

AABB 和 OBB 区别是什么?

一句话:AABB 不旋转,永远对齐坐标轴;OBB 会旋转,能更贴合物体。 它们都是“包围盒”,作用是用一个简单盒子包住复杂物体,先做快速碰撞判断。

aabb-vs-obb-csharp

AABB 是什么

AABB 全称是 Axis-Aligned Bounding Box,中文叫轴对齐包围盒

它的特点是:

盒子的边永远和世界坐标轴平行

也就是说,不管物体怎么旋转,AABB 自己都不跟着旋转。

它通常用两个点表示:

min:盒子的最小坐标
max:盒子的最大坐标

比如 2D 里:

min = 左下角
max = 右上角

3D 里:

min = 最小 x、最小 y、最小 z
max = 最大 x、最大 y、最大 z

AABB 的优点

AABB 最大的优点是:快,非常快。

判断两个 AABB 是否相交,只要看它们在每个轴上的范围有没有重叠。

比如 2D 中:

X 轴范围重叠
Y 轴范围重叠

两个都重叠,盒子才相交。

AABB 的缺点

如果物体旋转了,AABB 仍然不能旋转。 所以它可能会变得很大,包住很多空白区域。

这会导致:

明明两个物体没有真的碰到
但它们的 AABB 可能已经相交

这种情况叫误判,也叫 false positive。

OBB 是什么

OBB 全称是 Oriented Bounding Box,中文叫有向包围盒

它的特点是:

盒子可以跟着物体方向旋转

所以如果一个长方体斜着放,OBB 也可以斜着包住它。

OBB 通常用这些信息表示:

center:中心点
halfSize:半尺寸
axisX:盒子的本地 X 方向
axisY:盒子的本地 Y 方向
axisZ:盒子的本地 Z 方向

OBB 的优点

OBB 比 AABB 更贴合旋转物体。 所以它的误判更少。

比如:

斜着的车辆
旋转的武器
倾斜的箱子
长条形障碍物

这些物体用 OBB 会比 AABB 更精确。

OBB 的缺点

OBB 的问题是:计算更复杂。

AABB 只需要比较 minmax。 OBB 因为有旋转,不能简单比较世界坐标范围。

OBB 和 OBB 的碰撞检测,常用:

SAT:Separating Axis Theorem,分离轴定理

零基础先记住:OBB 更准,但更贵。

C# 示例:AABB 碰撞检测

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3
public struct AABB // 定义一个 AABB 结构体
{ // 结构体开始
    public Vector3 min; // 保存包围盒在世界空间中的最小坐标
    public Vector3 max; // 保存包围盒在世界空间中的最大坐标
    public bool Intersects(AABB other) // 定义判断当前 AABB 是否和另一个 AABB 相交的方法
    { // 方法开始
        bool overlapX = min.x <= other.max.x && max.x >= other.min.x; // 判断两个盒子在 X 轴上的范围是否重叠
        bool overlapY = min.y <= other.max.y && max.y >= other.min.y; // 判断两个盒子在 Y 轴上的范围是否重叠
        bool overlapZ = min.z <= other.max.z && max.z >= other.min.z; // 判断两个盒子在 Z 轴上的范围是否重叠
        return overlapX && overlapY && overlapZ; // 三个轴都重叠时,两个 AABB 才相交
    } // 方法结束
} // 结构体结束

C# 示例:OBB 的基本数据结构

c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3 和 Quaternion
public struct OBB // 定义一个 OBB 结构体
{ // 结构体开始
    public Vector3 center; // 保存 OBB 的中心点
    public Vector3 halfSize; // 保存 OBB 在本地三个轴上的半尺寸
    public Quaternion rotation; // 保存 OBB 的旋转方向
    public Vector3 AxisX => rotation * Vector3.right; // 计算 OBB 自己的本地 X 轴在世界空间中的方向
    public Vector3 AxisY => rotation * Vector3.up; // 计算 OBB 自己的本地 Y 轴在世界空间中的方向
    public Vector3 AxisZ => rotation * Vector3.forward; // 计算 OBB 自己的本地 Z 轴在世界空间中的方向
} // 结构体结束

游戏里通常怎么用

真实项目里常见流程是:

第一步:用 AABB 做粗略筛选
第二步:筛掉明显不可能碰撞的物体
第三步:剩下少量物体再用 OBB、Collider、Mesh 或 SAT 精确检测

这就是碰撞检测里的:

c
Broad Phase:粗检测
Narrow Phase:精检测

怎么选

大量物体、追求速度、只做粗略判断:用 AABB。 旋转物体、需要更贴合、更少误判:用 OBB。 游戏引擎里通常不是二选一,而是:AABB 先筛,OBB 或更精细检测再确认。

面试高分回答

IMPORTANT

AABB 是轴对齐包围盒,它的边始终和世界坐标轴平行,通常用 minmax 表示,优点是相交判断非常快,适合做 Broad Phase 粗检测;缺点是物体旋转后可能包得很松,误判较多。OBB 是有向包围盒,它可以跟随物体旋转,更贴合物体,误判更少,但碰撞判断更复杂,通常要考虑盒子的局部坐标轴,OBB 之间常用分离轴定理检测。实际游戏中常见做法是先用 AABB 快速筛选,再对候选物体做 OBB 或 Collider 精确检测。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0