2、坐标系定义:世界坐标系、物体坐标系、相机坐标系

做仿真这么多年,我越来越觉得坐标系这东西,就像我们说话时的「主语」——你搞不清主语是谁,整句话就乱了。坐标系定义不清楚,后面所有的变换计算都是空中楼阁。

今天咱们就把这三个最核心的坐标系掰扯清楚:世界坐标系、物体坐标系、相机坐标系。说白了,它们就是描述「东西在哪儿、朝哪儿看」的三套语言。

2.1 世界坐标系——整个场景的「绝对参考系」

世界坐标系,你可以把它想象成整个仿真世界的「大地基准」。不管你的物体怎么动、相机怎么转,世界坐标系是唯一不变的。

关键特征:

  • 原点固定,通常定义在场景中心或某个方便的位置
  • 三个轴(X, Y, Z)方向固定,一般遵循右手定则
  • 所有其他坐标系最终都要转换到世界坐标系下统一处理

我个人习惯把世界坐标系的原点放在场景的地面中心,Y轴朝上。为什么?因为这样重力方向就是 -Y,处理物理仿真时特别直观。我在做自动驾驶仿真项目时,就吃过一次亏——有人把Z轴朝上,结果跟传感器数据对不上,排查了半天才发现是坐标系约定不一致。

重要原则: 世界坐标系一旦定义好,整个团队必须统一遵守。换项目可以换约定,但一个项目内绝不能混用。

2.2 物体坐标系——每个物体自己的「小天地」

物体坐标系,也叫局部坐标系或模型坐标系。每个物体都有自己的坐标系,原点通常在物体的中心或某个关键点(比如车辆的质心)。

你想想看,一个机器人手臂有6个关节,每个关节都有自己的局部坐标系。如果都用世界坐标系来描述,那计算量会爆炸,而且代码根本没法维护。

物体坐标系的作用:

  • 方便定义物体自身的几何形状和属性
  • 简化旋转、缩放等局部变换
  • 通过父子层级关系,实现复杂运动链

举个例子,一个汽车模型在物体坐标系下,车头指向 +X,左侧指向 +Y,车顶指向 +Z。当这辆车在世界坐标系中移动时,我们只需要维护一个「物体坐标系→世界坐标系」的变换矩阵,所有局部点都能快速算出全局位置。

我的经验: 定义物体坐标系时,尽量让坐标轴与物体的主要对称轴对齐。这样后续做碰撞检测、包围盒计算都会省很多事。

2.3 相机坐标系——从「谁在看」的角度描述世界

相机坐标系,说白了就是以相机为原点建立的一套坐标系。它的Z轴通常指向相机的观察方向(也就是镜头朝向),X轴指向相机的右侧,Y轴指向相机的上方。

为什么要单独搞一个相机坐标系?因为渲染管线最终需要知道「从相机视角看,物体在什么位置」。世界坐标系下的坐标,必须经过「视图变换」转到相机坐标系下,才能进行后续的投影和裁剪。

相机坐标系的定义要点:

  • 原点:相机光心(镜头中心)
  • Z轴:观察方向(注意,OpenGL中Z轴指向屏幕外,即 -Z 方向)
  • X轴:相机右侧
  • Y轴:相机上方

嗯,这里要注意一个坑:不同的图形API对相机坐标系的约定不一样。OpenGL用的是右手系,Z轴指向屏幕外;DirectX用的是左手系,Z轴指向屏幕内。我曾经在项目里把这两个搞混了,结果渲染出来的场景整个是镜像的,找了三天bug才发现是坐标系符号反了。

避坑指南: 如果你同时使用多个图形库或仿真引擎,务必在代码入口处统一坐标系约定。建议写一个坐标系转换工具函数,把所有输入都转成内部统一格式。

2.4 三个坐标系的关系——一张图说清楚

这三个坐标系不是孤立的,它们通过变换矩阵串联在一起。下面这张图展示了它们之间的核心关系:

世界坐标系 World Coordinate 原点固定,全局唯一 所有坐标的最终归宿 物体坐标系 Object Coordinate 每个物体独立拥有 定义局部几何与运动 相机坐标系 Camera Coordinate 以相机为原点 决定「看到什么」 模型矩阵 (局部→全局) 视图矩阵 (全局→相机视角) 变换流程 物体坐标系 → 模型矩阵 → 世界坐标系 → 视图矩阵 → 相机坐标系 → 投影矩阵 → 屏幕 最终渲染管线:物体坐标 → 世界坐标 → 相机坐标 → 裁剪空间 → 屏幕像素 注:模型矩阵 = 平移 × 旋转 × 缩放(顺序不可颠倒)

从这张图可以看得很清楚:物体坐标系通过「模型矩阵」变换到世界坐标系,世界坐标系再通过「视图矩阵」变换到相机坐标系。最后经过投影矩阵,才变成我们在屏幕上看到的画面。

2.5 代码示例:三个坐标系的定义与转换

下面我用Python和NumPy演示一下这三个坐标系的定义和基本转换。这段代码我在多个仿真项目中都用过,算是比较通用的模板。

import numpy as np

# ========== 1. 定义世界坐标系 ==========
# 世界坐标系原点 (0,0,0),轴方向固定
world_origin = np.array([0.0, 0.0, 0.0])
world_x = np.array([1.0, 0.0, 0.0])
world_y = np.array([0.0, 1.0, 0.0])
world_z = np.array([0.0, 0.0, 1.0])

# ========== 2. 定义物体坐标系 ==========
# 假设物体在世界坐标系中的位置为 (5, 2, 0),绕Y轴旋转30度
obj_pos = np.array([5.0, 2.0, 0.0])
theta = np.radians(30)

# 构建模型矩阵(物体→世界)
cos_t, sin_t = np.cos(theta), np.sin(theta)
rotation_y = np.array([
    [cos_t, 0, sin_t, 0],
    [0, 1, 0, 0],
    [-sin_t, 0, cos_t, 0],
    [0, 0, 0, 1]
])
translation = np.array([
    [1, 0, 0, obj_pos[0]],
    [0, 1, 0, obj_pos[1]],
    [0, 0, 1, obj_pos[2]],
    [0, 0, 0, 1]
])
model_matrix = translation @ rotation_y  # 注意顺序:先旋转再平移

# 物体局部点 (1, 0, 0) 在世界坐标系中的位置
local_point = np.array([1.0, 0.0, 0.0, 1.0])
world_point = model_matrix @ local_point
print(f"物体局部点 (1,0,0) 在世界坐标中的位置: {world_point[:3]}")

# ========== 3. 定义相机坐标系 ==========
# 相机位置在 (10, 5, 10),看向原点
cam_pos = np.array([10.0, 5.0, 10.0])
target = np.array([0.0, 0.0, 0.0])
up = np.array([0.0, 1.0, 0.0])

# 构建视图矩阵(世界→相机)
def look_at(eye, center, up):
    f = (center - eye) / np.linalg.norm(center - eye)
    s = np.cross(f, up)
    s = s / np.linalg.norm(s)
    u = np.cross(s, f)
    
    view = np.eye(4)
    view[0, :3] = s
    view[1, :3] = u
    view[2, :3] = -f
    view[0, 3] = -np.dot(s, eye)
    view[1, 3] = -np.dot(u, eye)
    view[2, 3] = np.dot(f, eye)
    return view

view_matrix = look_at(cam_pos, target, up)

# 将世界坐标点转换到相机坐标系
camera_point = view_matrix @ world_point
print(f"世界点 {world_point[:3]} 在相机坐标系中的位置: {camera_point[:3]}")
小技巧: 写变换矩阵时,我习惯用4x4齐次坐标。虽然多了一维,但平移和旋转可以统一处理,代码更干净。另外,矩阵乘法的顺序千万别搞反——先缩放、再旋转、最后平移,这是铁律。

2.6 实际项目中的坐标系管理建议

我在几个大型仿真项目中总结了一些经验,分享给你:

  1. 统一约定写在项目文档第一页——哪个轴朝上、左手系还是右手系、角度单位用度还是弧度,这些必须白纸黑字写清楚。
  2. 写一个坐标系工具类——封装所有常见的变换操作,团队里所有人都用同一套代码,避免各自实现导致的不一致。
  3. 调试时可视化坐标系——在仿真场景中把三个坐标系的轴画出来(用红绿蓝三色表示XYZ),一眼就能看出问题。我见过太多因为坐标系搞反导致的bug,可视化是最快的排查手段。
  4. 注意浮点精度——多次变换累积后,矩阵可能会退化。定期对旋转矩阵做正交化,或者用四元数代替欧拉角,能避免很多诡异的问题。
血的教训: 我曾经在一个多传感器融合项目中,激光雷达和摄像头用了不同的坐标系约定(一个Z轴朝上,一个Y轴朝上)。标定数据看起来没问题,但融合结果总是飘的。最后发现是坐标系转换时少乘了一个旋转矩阵。从那以后,我要求所有传感器数据进入系统前,必须先过一道坐标系统一检查。

好了,关于三个坐标系的定义就聊到这儿。记住一句话:坐标系是仿真的「语法」,语法错了,句子再漂亮也没用。下一节咱们会深入讲变换矩阵的具体计算方法,到时候这些概念都会用上。


可可精品资料库