第 2 章 坐标:给虚拟世界一个”地址系统”
上一章结尾我让你想:你闭着眼也知道”桌子在左边”,靠的是参照物。这一章我们就把这件事坐实——而且会坐实到一个你可能没料到的程度:计算机描述”一个东西在哪”,居然需要好几套坐标。
别急,咱们从最朴素的开始。
2.1 为什么”位置”必须先有”参照”
你寄快递,地址得写”XX 市 XX 路 XX 号”。为什么不能只写”往前走三百米”?因为快递员不知道你面朝哪、站在哪。
“往前走三百米”这类描述,叫相对位置——它依赖于”谁在描述、面朝哪”。而”XX 路 XX 号”叫绝对位置——它不依赖任何人,是全城共用的一个固定网格。
游戏里也一样。要描述一个物体在哪,你必须先回答:相对于什么?
- 相对于”世界”的中心?→ 这是世界坐标(World Space)。
- 相对于”它自己老爸(父物体)”?→ 这是局部坐标(Local Space)。
- 相对于”摄像机(你的眼睛)”?→ 这是视图 / 屏幕坐标(View / Screen Space)。
你看,坐标不是”一个东西”,而是一组”以谁为原点”的选择。 这个点想通了,后面一大半”为什么这么麻烦”的疑问就散了。
2.2 从一维到三维:我们怎么描述一个点
先别管 3D,从 1D 想。
一条笔直的马路,我在路上画个刻度。你站在第 5 格,记作 x = 5。这就是一维坐标:一个数字,定位置。
加一条垂直的路,成十字。现在位置要两个数:(x=5, y=3)——横向第 5,纵向第 3。这是二维,平面够用了。
再升起一座立交桥,高度也要标。于是 (x=5, y=3, z=2):横向 5、纵向 3、离地 2。这就是三维坐标,游戏世界的基本货币。
小插曲:为什么是 x / y / z 这三个字母?历史习惯。横 x、竖 y、深 z,约定俗成。Unity 里默认是 y 轴朝上(和数学里 z 朝上不同,建模软件 Maya 是 y 朝上,Blender 也是;但早期 OpenGL 数学习惯 z 朝上)。这个”朝上的是哪个轴”的小差异,坑过无数新手——你导入一个模型发现它”躺平”了,多半是坐标轴约定没对齐。
所以”坐标”说白了就是:在一套网格里,用几个数字标定一个点的位置。 一维 1 个数,二维 2 个,三维 3 个。
2.2.5 坐标的细节:负数、小数、单位和”连续 vs 离散”
在往前走之前,先把”坐标到底是什么数”这点坐实,不然后面你调参数时会有说不清的别扭。
坐标可以是负数。 原点只是你定的一个”零”,它的两边都有数。你在原点左边 5 米,就是 x = -5;右边是 +5。负号不代表”错误”,只代表”在原点另一侧”。这点和你的世界坐标原点放哪有关——很多项目把原点放在地图正中心,于是整个场景一半是正、一半是负。
坐标是连续的实数,不是离散的格子。 你可以精确站在 (3.5, 2.25, -1.1) 这个点,不必须是整格。这一点要和后面第 3 章的”像素”对照着记:世界坐标是连续的实数空间,像素是离散的整数格子。游戏世界里你爱站哪站哪;但当你真正被”画”到屏幕上时,那个连续位置会被四舍五入/采样到某一个具体的像素格子里——连续 → 离散,这正是全教程反复出现的核心哲学(声音采样也是连续→离散,见第 9 章)。
坐标是有单位的。 在 Unity 里,默认 1 个单位 = 1 米。所以 position = (10, 0, 0) 不是”第 10 格”,是”离原点 10 米”。这点极重要,因为物理相关的东西都按这个单位算:重力 9.8 是”每秒每秒掉 9.8 米”,角色移动速度 5 是”每秒走 5 米”,摄像机远裁剪面 1000 是”1000 米外不画”。一旦你把一个 1 单位当作 1 厘米去建模,整个物理系统(碰撞、重力、速度)全得跟着改比例,这是新手最容易踩的隐性坑——模型导入进来大得像座山,多半是导出时单位设成了厘米、Unity 当成了米。
回头看 2.1:负数、小数点、单位,全都是”参照系 + 刻度规则”的产物。换个原点,负号就翻面;换个刻度,数值就缩放。这再次印证那句话——坐标的本质是参照系的选择。
2.3 三套坐标:同一个物体,三个”地址”
现在上硬货。一个物体在游戏里,同时拥有好几套坐标值,而且数值可以完全不一样。这反直觉,但必须懂。
假设场景里有一辆坦克,停在世界坐标 (10, 0, 0) 的位置。坦克上焊着一门炮管,炮管相对于坦克自己的中心,在局部坐标里是 (0, 1, 2)(往前 2、往上 1)。
那么炮管在世界里到底在哪?
你心算一下:坦克在世界 (10,0,0),炮管相对坦克 (0,1,2),所以炮管世界位置 = 坦克位置 + 相对偏移 = (10, 1, 2)。
看,同一根炮管:
- 在坦克的坐标系里,它是
(0,1,2)(局部)。 - 在世界的坐标系里,它是
(10,1,2)(世界)。
两套数字,描述的是同一个点。区别只在于”站在谁脚下当原点”。
为什么要这么搞?因为局部坐标让”组装”变简单。设计师在坦克模型里放炮管,只关心”炮管相对坦克中心在哪”,不用管坦克后来被摆到世界哪个角落。坦克在世界里挪来挪去,炮管自动跟着走——因为引擎每次渲染都做一道”局部 → 世界”的换算。
这就像你公司的工位:HR 档案里你的座位是”3 楼 B 区 12 号”(世界坐标),而你对同事说”我就在你斜对面”(相对坐标)。两种说法都对,用处不同。
2.4 第三套:屏幕坐标(视图坐标)
还有更狠的一层。你盯着屏幕看,屏幕上每个像素也有个位置,比如”屏幕左起第 800 像素、上起第 450 像素”。这是屏幕坐标。
关键问题:同一个坦克,在世界里是 (10,1,2),在你的视网膜(摄像机)里,却对应屏幕上的某个像素点。世界坐标和屏幕坐标之间,隔着一个叫**投影(Projection)**的东西——下一章摄像机会细讲,这里你先记住:从世界到屏幕,要经过”摄像机”这道转换。
于是你发现,光描述”坦克在哪”就涉及三套坐标:
- 局部(相对父物体)
- 世界(相对场景原点)
- 视图 / 屏幕(相对摄像机、再压扁到屏幕)
而引擎内部,每帧都在它们之间来回换算。你调一个物体的 transform.position,改的是世界坐标;你在 Hierarchy 里拖子物体,看的是局部坐标;最终它画到屏幕哪,由视图坐标决定。
2.5 “变换”:为什么同一个物体在不同眼里位置不同
前面那道”局部 + 偏移 = 世界”的加法,其实是**变换(Transform)**最简单的一种。变换就是”把点从一套坐标,搬到另一套坐标”的数学操作。
最常见的三种变换:
- 平移(Translation):整体挪个位置。加法。
- 旋转(Rotation):转个朝向。
- 缩放(Scale):放大缩小。
你看到 Unity 里每个物体都有 Position / Rotation / Scale 三个属性,本质上就是这三件事。它们合起来,定义”这个物体相对于它父级,处在什么位置、什么朝向、多大”。
这里我埋个伏笔,不展开,因为你刚入门:平移、旋转、缩放,在数学上可以统一写成一个**矩阵(Matrix)**的乘法。这就是为什么你以后会看到
transform.matrix、projectionMatrix这种词。你现在只要知道:变换 = 搬坐标,而矩阵是搬坐标的”搬运工公式”。等讲到摄像机投影(第 7 章)和 shader(如果以后深入),你会回来感谢现在埋下的这个钩子。
2.5.5 变换的数学直觉:为什么旋转/缩放是”乘法”而不是”加法”
2.5 我说”平移是加法、旋转缩放是乘法”,这里值得停下来真正想通,因为它决定了你以后看矩阵、看 shader 时会不会懵。
平移为什么是加法?因为”整体挪位置”就是给每个点都加上同一个偏移量:新位置 = 旧位置 + 偏移。简单、线性、好懂。
但旋转没法用加法表示,我给你一个反直觉的铁证:
取一个平面上的点
(1, 0)(x 轴上离原点 1 米)。现在让它绕原点转 90°,它去哪了?答案是(0, 1)(跑到 y 轴上)。
你试试看,能不能找到某个”加数”让 (1,0) 变成 (0,1)?加 (a, b) 的话,(1+a, 0+b) = (0, 1) → a=-1, b=1。可一旦你给所有点都加 (-1, 1),点 (0,0) 会变成 (-1,1)——原点都被挪走了,这根本不是”绕原点旋转”,这是”整体平移”。结论:旋转不是平移,加法的游戏规则装不下旋转。
于是数学家用乘法(矩阵)来装旋转。一个绕原点转 θ 的旋转,写出来是:
新 x = x·cosθ - y·sinθ
新 y = x·sinθ + y·cosθ
你看,输出 (新x, 新y) 是输入的 (x, y) 经过一组”交叉相乘再加减”得到的——这正是 2.5 埋的伏笔里说的”矩阵”。矩阵就是一张规则表,把输入向量的每个分量,按规则重新组合成输出。 平移、旋转、缩放,全都能写成这种”规则表”,于是引擎可以把它们叠成一个总矩阵,一次算完(第 27 章你会亲手写这些矩阵)。
为什么你”不用算但必须懂”?因为 Unity 里你只拖 Rotation 的角度滑块,引擎在背后把角度变成旋转矩阵,再和 Position、Scale 拼成一个”变换矩阵”,一次性把模型所有顶点从局部搬到世界。哪天你发现”模型转的方向和我预期相反""缩放后光照怪怪的”,根因都在这套矩阵规则里——懂了它,你调的是直觉,不是盲拧。
2.6 一个生活化总结
把坐标系统想成**“同一栋楼,三种门牌”**:
- 局部坐标 = 你在家说”拖鞋在门右边”(以家门为原点)。
- 世界坐标 = 快递单写”幸福小区 3 栋 502”(以全城为原点)。
- 屏幕坐标 = 你手机拍了张照,拖鞋在”照片右下角”(以相机取景框为原点)。
三套门牌指向同一个拖鞋,但用处天差地别。游戏引擎的全部空间逻辑,就是在这三套门牌之间精确搬家的手艺。
2.7 现在回头看你的疑问
你最初问的是”坐标等等,重要的是理解”。我希望这一章留给你的不是”x y z 三个数”这种废话,而是:
- 坐标的本质是参照系的选择(先问”相对谁”)。
- 游戏用多套坐标,是为了让”组装 / 摆放 / 观看”各干各的、互不拖累。
- 你调
Position,是在世界坐标里摆;物体自身带Rotation/Scale,是在局部里定义自己。 - “变换”是把点从一套坐标搬到另一套——这是后面所有高级概念(摄像机、骨骼动画、特效)的公共地基。
2.7.5 坐标系的”手性”:左手系 vs 右手系(提前讲清,省得你懵)
2.9 会提到”Unity 用左手坐标系”,但这个词不解释清楚,你读的时候会糊。这里提前拆。
光有”x/y/z 三个轴”,还不够确定整套坐标怎么转。你还得约定:三个轴之间是什么朝向关系——也就是”手性(handedness)”。
- 右手系:伸右手,食指指 x、中指指 y、大拇指指 z,三轴满足这个相对朝向。数学课本、OpenGL 习惯用右手系。
- 左手系:伸左手,食指 x、中指 y、大拇指 z。DirectX、Unity 用左手系。
区别看起来只是”把右手换左手”,后果却很实在:同一个旋转方向,在两种坐标系里”正方向”是反的。比如”绕 y 轴转 90°“,右手系里可能是顺时针,左手系里就变成逆时针——你写一个旋转公式,换套坐标系,结果直接镜像。
一个好记的判断法(握拳法):拇指朝某个轴的正方向,四指弯曲的方向,就是绕这个轴旋转的正方向。 你分别用左右手比一下绕 y 轴的旋转,会立刻发现两手套出的”正转方向”相反——这就是手性差异的体感来源。
为什么这坑人?因为 Blender(右手、z-up)导出的模型,到 Unity(左手、y-up)里,不仅”哪个轴朝上”变了,连”旋转正方向”都可能反。结果不是躺平,就是镜像(左手变右手、文字反着贴)。所以导入设置里常有”Convert Left-handed / 翻转法线 / 旋转根节点”这些勾选——根子都在坐标系约定(参照系)没对齐,正是 2.9 那个翻车的深层原因。你越早把”手性”装进脑子里,以后调模型越省事。
2.8 一个完整推演:从局部一路算到屏幕
把前面几节串成一次真实计算。还是那辆坦克:
- 炮管局部坐标 = (0, 1, 2)(相对坦克中心)。
- 坦克世界坐标 = (10, 0, 0),且坦克没旋转、没缩放。
- 炮管世界坐标 = 坦克的变换(平移)作用于炮管局部 = (10,0,0) + (0,1,2) = (10, 1, 2)。这步叫”局部 → 世界”。
接下来摄像机登场。假设摄像机在世界 (0, 0, 5),面朝 +z 看:
- 引擎先用”视图变换”:把世界坐标减去摄像机位置,得到相对摄像机的坐标。炮管相对摄像机 = (10,1,2) - (0,0,5) = (10, 1, -3)。注意 z 变负——炮管在摄像机”背后”(z 更小),透视里这意味着它不会被画出来。
- 再用”投影变换”:把 (x, y, z) 按透视公式压成屏幕上的 (屏幕x, 屏幕y)。近大远小就发生在这一步。
同一个炮管,经历局部 → 世界 → 视图 → 屏幕四道变换才落到你眼里。每道都是”换一套参照系”。Unity 里的 transform.localToWorldMatrix、Camera.WorldToScreenPoint 这类 API,本质就是帮你做这几道搬坐标的算术。
回头看 2.5 埋的伏笔:平移是加法,旋转/缩放/投影是乘法,全都能写成矩阵。四道变换连乘成一个矩阵链,一次算完。你不用手写,但理解”在搬坐标”,调 API 就不会懵。
2.9 一个真实翻车:为什么模型导入后”躺平”
你以后从 Blender / Maya 导入模型到 Unity,十有八九遇到:模型明明站着,进来却平躺。根源就在坐标约定:
- Unity 是 y 轴朝上(y-up),用左手坐标系。
- Blender 默认 y-up,但 3ds Max 等常是 z-up;且不同工具左手/右手坐标系不同。
同一个”朝上”的向量,两边解释成不同轴,模型就转了 90°。解决不是改模型,而是在导入设置里勾选”Convert Left-handed”或旋转根节点欧拉角。这整个坑,根源就是”坐标轴的约定(参照系)没对齐”——第 2 章教的东西,第一天就能救你一次。
2.10 Unity 实操:Transform 面板与”三级嵌套”的真实换算
理论和菜单对上号,才算真的会。打开 Unity,点选任意物体,Inspector 里第一个组件就是 Transform,里面有 Position / Rotation / Scale 三个字段。这里有个极易混的点:
Inspector 里显示的 Position,是”局部坐标”(相对父级);而脚本里
transform.position拿到的是”世界坐标”。
也就是说,同一个物体,面板写着 (0,0,0)(相对父级在原点),但 transform.position 可能返回 (10,0,0)(因为父级在世界 (10,0,0))。别以为面板和代码打架了,它们说的是两套参照系——这正是第 2 章的核心。想看世界坐标,在 Inspector 里点 Transform 组件右上角那个小菜单(齿轮)→ “Debug”,就能看到 Position 旁多出世界坐标;或直接用代码 transform.position。
真实三级嵌套案例(你以后做塔防/载具必遇):
坦克 Tank (世界坐标 (10, 0, 0),无旋转)
└ 炮塔 Turret (相对坦克局部 (0, 2, 0),可绕 y 轴旋转)
└ 炮管 Barrel(相对炮塔局部 (0, 0, 3))
炮管的世界坐标怎么算?忽略旋转时,就是沿父子链把局部偏移累加:
世界 = 坦克(10,0,0) + 炮塔(0,2,0) + 炮管(0,0,3) = (10, 2, 3)。
如果炮塔绕 y 转了 30°,那炮管的局部偏移 (0,0,3) 会先被这个旋转矩阵”转一下”再累加——引擎每帧递归地做这条”局部→父级→…→世界”的链,自动帮你算。你只管给每个部件设它”相对自己老爸在哪”,整体怎么摆、怎么转,引擎兜底。这正是 2.3 说的”局部坐标让组装变简单”在工程里的真身。
一个隐蔽的坑:缩放是继承的。 父级 Scale = (2,2,2),所有子物体(炮塔、炮管)都被放大 2 倍,因为缩放沿父子链向下传递。更危险的是非均匀缩放 (2, 1, 1):它会把子物体沿 x 轴拉成椭圆,连带把碰撞体、法线方向一起拉伸变形,结果光照算错、物理穿模。所以日常保持 Scale = (1,1,1),只有你要刻意做”压扁/拉长”特效时才动它。
排查口诀:物体位置”明明不对劲”,先查它的父级有没有被移动/旋转/缩放。无数”我明明设了 (0,0,0) 为什么还飘在那”的 bug,根因是——它在一个被挪过的父级底下,面板上的 (0,0,0) 只是相对父级的零,不是世界的零。
2.11 坐标与内存:顶点在显存里就是一串浮点数
最后把”坐标”落回你最熟的东西——数组。你作为程序员,看到”一组数描述一个点”,应该立刻想到:这不就是 float[3] 吗?没错。
一个 3D 模型(网格)在文件/显存里,本质就是两大坨数组:
- 顶点数组(vertex buffer):每三个
float描述一个顶点的局部坐标(x, y, z)。一个三角形 = 3 个顶点 = 9 个float;一万个三角形就是三万个float排成一长条。 - 索引数组(index buffer):告诉引擎”第几个、第几个、第几个顶点凑成一个三角形”。
坐标系统一之后,所有这些点的局部坐标就规规矩矩排成一个连续的 float[],GPU 按索引批量读取、批量变换。这就是为什么”坐标”从来不是抽象概念——它直接决定显存里那串数字怎么排、GPU 怎么算。
还有个和 2.3 呼应的点:模型文件里存的是局部坐标(相对模型自己的原点),世界坐标是渲染时临时算出来的,通常每帧重算、不长期存。所以”局部坐标让组装简单”不仅是设计优雅,更是省内存、省 IO 的实打实选择——你只存一份局部坐标,靠变换矩阵随时算出任意位置的世界坐标。你调 transform.position 改的是那套”矩阵”,不是去改顶点数组里每一个数。
记牢这条:坐标是概念,浮点数组是它在计算机里的真身,变换矩阵是两者之间的搬运工。 等第 27 章你亲手写矩阵时,会彻底验证这句话。
【思考题】
- 为什么游戏里”局部坐标”很有用?举一个”如果只有世界坐标会有多麻烦”的例子。
- 同一个炮管,局部
(0,1,2)、坦克世界(10,0,0),炮管世界位置是多少?如果坦克移到(20,0,0),炮管世界位置又是多少?局部坐标变没变? - 屏幕坐标和世界坐标之间,隔着什么转换?这个转换依赖”谁”的位置和朝向?
- 你在生活里,有没有”同一件事,用不同参照系说,数值不同”的情况?(比如导航、舞步、团队站位)
【参考答案】
- 局部坐标让”部件相对整体”的摆放与整体在世界里的移动解耦。若只有世界坐标:设计师每摆一个螺丝都得先算好坦克最终停哪、再算螺丝绝对位置;坦克一挪,所有螺丝的世界坐标全要重算。有了局部坐标,螺丝只记”相对坦克中心在哪”,坦克整体移动时引擎自动换算,设计师零负担。
- 炮管世界 = 坦克世界 + 相对偏移 = (10,0,0)+(0,1,2) = (10,1,2)。坦克移到 (20,0,0) 后,炮管世界 = (20,1,2)。局部坐标没变,还是 (0,1,2)——这正是局部坐标的价值:部件自己的”相对描述”稳定,跟着父级走。
- 隔着投影(Projection)转换,具体是”世界坐标 → 视图坐标 → 屏幕坐标”。这个转换依赖摄像机的位置和朝向(以及它的投影参数,如视野角、近远裁剪面,第 7 章讲)。同一物体,摄像机挪个位置,屏幕坐标立刻变。
- 开放题。例子:舞蹈里”向左一步”以自己身体为参照;导航 App 里”前方 200 米右转”以车头为参照;足球战术板里”你站左后卫位”以球场为参照。本质都是”参照系不同,同一位置的描述数值不同”。