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 三个坐标系的关系——一张图说清楚
这三个坐标系不是孤立的,它们通过变换矩阵串联在一起。下面这张图展示了它们之间的核心关系:
从这张图可以看得很清楚:物体坐标系通过「模型矩阵」变换到世界坐标系,世界坐标系再通过「视图矩阵」变换到相机坐标系。最后经过投影矩阵,才变成我们在屏幕上看到的画面。
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]}")
2.6 实际项目中的坐标系管理建议
我在几个大型仿真项目中总结了一些经验,分享给你:
- 统一约定写在项目文档第一页——哪个轴朝上、左手系还是右手系、角度单位用度还是弧度,这些必须白纸黑字写清楚。
- 写一个坐标系工具类——封装所有常见的变换操作,团队里所有人都用同一套代码,避免各自实现导致的不一致。
- 调试时可视化坐标系——在仿真场景中把三个坐标系的轴画出来(用红绿蓝三色表示XYZ),一眼就能看出问题。我见过太多因为坐标系搞反导致的bug,可视化是最快的排查手段。
- 注意浮点精度——多次变换累积后,矩阵可能会退化。定期对旋转矩阵做正交化,或者用四元数代替欧拉角,能避免很多诡异的问题。
好了,关于三个坐标系的定义就聊到这儿。记住一句话:坐标系是仿真的「语法」,语法错了,句子再漂亮也没用。下一节咱们会深入讲变换矩阵的具体计算方法,到时候这些概念都会用上。
可可精品资料库