SpecEnvoy
EN·中文

学习游戏工艺 › 动画与 3D

游戏工艺 · 第一课 · 零基础

为什么你的 3D 角色看着不对劲

一款真实游戏里的六个真实 bug,以及每一个教会我们的规则。 如果你这辈子没打开过任何一款 3D 软件 —— 就从这里开始,下面每个概念都配一张图, 或者一个你可以直接拖动的东西。

这些内容从哪来。我们做了一款叫 燧拳 的格斗游戏 —— 三个角色,用 Blender 建模,动画完全写在代码里。做的过程中,站长不断盯着屏幕说: 「脸糊成一团」「腿看着像 <>「整个人塌成一堆肢体」。 每一次,原因都是某个又小又具体的东西 —— 而且没有一次报错。

这里没有任何秘密,也几乎没有一样是我们发明的。技术全是公开的: Arc System Works 在 GDC 的演讲、 开源库 ozz-animation 里的足部 IK 原则、 1930 年代 Art Babbitt 的动画原理。属于我们的,只有「我们是怎么把它们做错的」这份清单。

先说清楚:一个 3D 角色到底是什么

三样彼此独立的东西。几乎所有「动画做得差」的问题,其实都是其中一样在替另一样背锅。

1

网格(mesh)

看得见的那层皮 —— 几千个三角形。它自己是一尊雕像,动不了。

2

骨架(skeleton)

一棵看不见的骨骼树。我们这套有 24 根:胯 → 脊 → 胸 → 颈 → 头,再加四肢。 网格上每个三角形都被「粘」在一根或几根骨骼上,还带一个权重表示粘得多紧。

3

姿势(pose)

此刻每根骨骼指向哪里。所谓动画,不过是一串姿势,外加它们发生在第几帧。

可以先做个思想实验。如果角色看着僵硬,先问:是姿势本身平淡, 还是骨架根本做不到你想要的动作?我们曾经花了整整两轮去写更好的姿势, 而那套骨架物理上就无法扭转。给一副表达不了的骨架加更多动画,全是白费力气。

拖动滑块 —— 这就是一条腿,仅此而已

两根骨头,两个角度。在计算机眼里一条腿就只有这些 —— 所以只要你不管它, 它非常乐意往反方向弯。见第二课。

第一课 —— 脸是画出来的,不是堆出来的

「脸糊成一团。」

我们最初的脸是用最顺手的办法做的:一个白球当眼白、一个深色球当瞳孔、一个小方块当眉毛, 统统贴到头上。听起来很合理,做出来很难看,而且是三个彼此独立的原因 —— 所以「把眼球做得更精致一点」这条路永远走不通。

  1. 每个球都有自己的影子。我们的角色是硬边分层打光(那种卡通质感)。 一个球贴在脸颊上,就会自带一条自己的明暗带,于是它读起来像 一个打了光的球摆在脸上,而不是一只眼睛。
  2. 每个球都有自己的描边。角色外围那圈黑边,做法是把模型再画一遍、 稍微放大、里外翻转。对十二个小球都来一次,整张脸就成了一堆带描边的疙瘩。
  3. 没有东西能比一个三角形更细。睫毛线、眼皮的折痕、一点高光 —— 全都是细的。 做成几何体就要真金白银的三角形,于是干脆就没做。缺了这些的脸,是一张面具。

正确的做法就是 Arc System Works 一直明说的那句:先把头的形状做好, 再把脸上去,作为一张贴图。我们在每个头的正面放了一小片曲面, 游戏加载时用普通的 2D 绘图指令把脸画在上面。

狐狸角色的脸由一堆独立几何球体拼成,读起来像一团疙瘩
之前 —— 眼睛、瞳孔、眉毛都是独立的实体。
同一个狐狸角色,脸改为画进贴图,能看到睫毛线与高光
之后 —— 同一颗头,脸是画上去的。三角形数量没变。
乌龟角色的脸部贴图平面图:两只琥珀色眼睛、睫毛线、眉毛与鼻梁折痕
这就是那张图,摊平的样子,之后会被裹到头部的曲面上。 这里的每一样 —— 睫毛线、高光、柔和的眉 —— 做成几何体都是不可能的。

规则。属于表面上的痕迹的细节,放进贴图; 改变剪影的细节 —— 口鼻、耳朵、角 —— 放进网格。动手做之前先问自己,你要做的是哪一种。

第二课 —— 膝盖只朝一个方向弯

「腿看着还是有点像 <>。」

这条报告妙就妙在,描述本身就是诊断。<> 指向相反 —— 前腿的膝盖朝前弯,后腿的膝盖朝弯。

为什么?姿势把每根骨骼存成一个方向。有人把左腿镜像出右腿,而镜像意味着翻转左右 —— 但他连前后也一起翻了。多翻了一个轴。结果大腿朝后、小腿朝前,也就是一个反着弯的膝盖。 这个错误存在于 66 个不同的姿势里。

两条腿踩在完全相同的位置,只有膝盖不同

脚和胯都被锁死了。翻转膝盖完全不改变角色在哪里 —— 却决定了他看不看得像个人。

真正有意思的是我们在哪里修的。不是去改那 66 个姿势。 手工改 66 个数字不叫修复,那叫给自己制造 66 次再犯的机会 —— 而且对下个月别人写的第 67 个姿势毫无帮助。「膝盖朝前弯」是关于腿的事实, 不是风格选择,所以它属于驱动骨架的那段代码。现在每个姿势都自动获得它, 包括那些还不存在的姿势。

规则。当大量手写的数据都违反同一条规则时, 去修那条规则,而不是那些数据。

还有我们修它时踩的坑。第一版用了一个「这个角色朝哪边」的缓存值, 而它只在角色创建时被赋值过一次,之后从没更新。于是对面朝左的那个角色, 它把膝盖修到了错误的方向。我们能发现,只因为测试同时检查了两个朝向。 只测一个方向的话,方向类的 bug 每次都能蒙混过关。

第三课 —— 脚为什么会打滑

一个行走循环就是一小段循环播放的姿势。最直觉的播法是按时间推进:每一帧往前走一点。 这么做角色就会飘 —— 因为这个循环根本不知道身体实际移动了多快。你把他放慢,腿还在原速蹬。

改成按移动距离推进 —— 走了多远,循环就推进多少 —— 脚就踩住了。 其它什么都不用改。这是一行的差别,也是我们对「走路」做过的最大改进。

同一个行走循环,两种时钟

上面:按时间推进 —— 触地点在往后滑。下面:按距离推进 —— 触地点钉在原处。 把速度拖慢,看上面那个开始打滑。

规则。用循环真正代表的那个量去驱动它。 走路代表的是「走过的地面」,那就用走过的地面去推进它。

第四课 —— 不能所有关节同时到位

我们的拳很僵,当时以为是关键帧不够。并不是。问题在于所有关节都在同一瞬间到位, 而活的东西没有一样是这样动的。

迪士尼动画师 Art Babbitt 把它叫作关节的依次断开:任何动作里,胯先走, 胸晚一拍跟上,然后是肩、肘、手。收回来的时候头最后动,因为头重。 我们的实现是一次减法 —— 每根骨骼读的是同一段动画,只是往回退几帧。

同一段动画,一边加延迟一边不加

两边的关键帧完全一样。把延迟拖到 0,手臂就变成雨刮器。

但命中的那一刻不能延迟。我们第一版给所有东西加了固定延迟, 结果一记快拳里,伤害已经结算了、拳头还在起手 —— 你按下按键,什么都没看见。 格斗游戏里接触的那一帧才是把「打中了」卖给玩家的东西,所以现在延迟会在命中瞬间归零, 到收招阶段再回来。风格不能吃掉玩家真正在读的那个东西。

第五课 —— 那一堆肢体

「被连打几下之后,整个人塌成一堆乱七八糟的肢体。」

这个我们试了三次才对,而每一个错误答案都比正确答案更值钱。

第一次 —— 那一跤摔了两遍

倒地的身体是用一套简单物理算的。它把每根骨骼都瞄准了世界里的真实方向 —— 身体已经躺平了 —— 然后告诉角色「你还要前倾 81 度」。 代码老老实实又倾了一次,把肋骨折回到了胯上面。

规则。如果某样东西已经描述了最终结果, 就不要再叠加一次「到达这个结果的过程」。

第二次 —— 没有人知道膝盖是什么

那套物理是用距离规则把身体串起来的:小腿离大腿必须是这么远。 可是距离规则里没有任何东西能阻止一个关节整个折平。在一具静止的身体上量到: 脖子 171° —— 头折下去埋进了胸口 —— 一个膝盖 168°, 小腿反穿过大腿。

解法是个经典做法:要限制一个关节的角度,就去限制它的祖父孩子之间的距离。 约束住「胯到脚」,就等于约束住了膝盖。

第三次 —— 根本不是物理的问题

站长又发来一张截图,这次是另一个角色。这一回的原因,是那段「防止脚陷进地面」的代码。 它会把整个身体往上抬所需要的高度 —— 但它测量身体位置时, 读到的是一份已经包含了上一帧抬升量的旧数据。

这就构成了一个反馈回路,而反馈回路是有脾气的:

同一个修正,三种强度

低于 0.5 会稳定下来 —— 但只停在它需要的一半上,所以脚一直差着一点。 正好等于 1.0 时永远弹来弹去。再高就发散了。

我们代码里那个强度是随帧时间变化的,而只要游戏掉到每秒 20 帧以下,它就正好等于 1.0。 在 60 帧下一切正常。在慢一点的手机上,整个身体在离地 1.56 米0.14 米之间每一帧跳一次, 脚却被钉在原地 —— 于是腿被拉长一米再缩回去,无限循环。那就是那一堆。

被击倒的角色渲染成一团缠在一起、悬在地面之上的肢体
之前 —— 那「一堆」。不是姿势差:是身体在被瞬移。
同样两个角色正常站在地面上
之后 —— 只改了一行,关于在哪里取那次测量。

规则。任何「先测量、再修改」的代码,必须测当前的值。 另外,帧率是测试的一部分 —— 这个 bug 在 60 帧下稳如泰山, 在 20 帧以下永远停不下来。

第六课 —— 怎么知道你真的修好了

这一页上的每个 bug,都是人盯着屏幕发现的,没有一个是测试发现的。所以我们写了测试。 然后测试开始骗我们,而这件事教给我们的,比那些 bug 还多。

相对量是瞎的

我们检查「头到胯的距离不能塌陷」—— 这是个好检查,也确实抓到过折起来的身体。 但当整个角色悬在离地 0.86 米时,这个值稳稳地是 0.54 米。 要问一个数字是相对什么量出来的。

一个样本不算测试

我们测了一次倒地,通过了。然后我们故意把代码改坏,看测试会不会发现 —— 它没有。 测五次倒地就抓到了。故意弄坏自己的代码,确认测试会变红。

先说清楚「坏」长什么样

动手修之前,先把坏的那版量一遍并把数字写下来。 「折起来是 0.41,正常是 0.82,及格线 0.70」,这是测试;「看着好多了」不是。

还有一个反复咬到我们的:截止时间做标准的测试, 会放过那些擦边通过的东西。我们要求某个交接「必须在倒地时间之内完成」, 而一个坏掉的版本在结束前一帧刚好挤进去 —— 技术上正确,屏幕上根本看不见。 标准必须是预算:截止时间,减去这件事真正需要的时长。

整堂课,九句话

  1. 表面上的痕迹进贴图,剪影进网格。
  2. 先把头的形状做好,再把脸画上去。
  3. 膝盖朝前弯。这类事实写进代码,不要写进数据。
  4. 行走循环按移动距离推进,不要按时间。
  5. 活物不会所有部位同时到位 —— 但永远不要延迟命中的那一刻。
  6. 如果姿势已经描述了结果,就不要再叠加那段过程。
  7. 距离规则不知道关节是什么。去约束「祖父到孩子」。
  8. 测当前的值,不是上一帧的。并且要在糟糕的帧率下测。
  9. 在动手修之前,先写下「坏」长什么样。

这里的一切都跑在 燧拳 里 —— 从标题画面打开招式集,可以逐帧看每一段动画。 那个工具建好之后一小时内就找出了四个失灵的按键和一个建模 bug,这也是最后一课: 先把「让你能看见」的那个东西做出来。