一帧画面是怎么来的

UNIT 04材质约 31 分钟 · 核心线

金属为什么在黑屋子里是黑的?

「机器人身上那些光线效果是不是有点奇怪?那些反光,你是用光栅还是光追的?会不会光追更好?还是说这是材质贴图导致的bug」C20。那天的答案是几样东西叠在一起,没有一样是贴图的 bug。这一课拆其中最大的一样:材质,和它周围的环境。一副近乎镜面的铬甲,为什么在暗场里只剩几道白。

v4 之前的机甲头部特写:近乎镜面的铬甲在黑背景里几乎全黑,只有几道刺眼的白色碎块和红色的边。
v4 之前 · 第 1000 帧
v4 的机甲头部特写:缎面金属的头盔从亮到暗有连续的渐变,一眼能读出是金属。
v4 · 第 1000 帧

同一个镜头的第 1000 帧。左:近乎镜面的铬甲,整颗头几乎是黑的,只剩几道白。右:v4,一次改了好几样:铬改成缎面金属(稍微磨砂的金属),加了几张只在反射里出现的大柔光卡,减少了小面的抖动,还换了渲染器。这一课在实验台上把前两样一样一样拆开。

04 · 1颜色不是物体自己的

冲照片的暗房只开一盏红色的安全灯。一张白相纸拿进去,看起来是什么颜色?红的。我们看见的从来不是「物体的颜色」,是从物体那里进到眼睛里的光。物体能决定的只是:照到它身上的光,它怎么还回去。

它有两种还法。一种是在最表面直接弹开,像镜子:入射角等于反射角(两个角都从法线量起),这叫镜面反射(specular reflection)。另一种是光钻进表层,在里面乱撞几下,再从四面八方散出来;乱撞的时候有些颜色被表层吃掉了,出来的是剩下的颜色,这叫漫反射(diffuse reflection)。3D 软件里的材质,就是一组告诉渲染器「光碰到这个表面会怎样」的参数;其中的基础色(base color),对非金属说的就是漫反射留下哪些颜色、吃掉哪些。

非金属(塑料、漆、木头) 照进来的光 镜面反射:约 4% 漫反射:带上基础色, 往四面八方散开 钻进表层、乱撞几下再出来 金属 照进来的光 镜面反射:几乎全部, 带上基础色的色调 光进不去:没有漫反射
两种还光的方式。非金属两样都有;金属只有镜面反射。示意图,箭头长短不按比例。

非金属(塑料、漆、木头、皮肤)两样都有:正对着看时,镜面反射只占大约 4%,而且几乎不挑颜色,照原样弹回去(越斜着看这一份越大);其余是漫反射。金属不一样:钻进去的那部分光立刻被吸收,出不来,所以没有漫反射,看到的全是表面弹开的那一份;而且金属弹回来的时候会挑颜色,金吃掉一部分蓝,剩下的偏暖,这就是金属的基础色。Blender 最常用的材质节点「原理化 BSDF」、网页和游戏常用的 3D 格式 glTF,都用同一个滑杆管这件事:金属度(metallic),0 是非金属,1 是金属,中间的值是两种按比例混合,比如 0.25 就是四分之三按非金属算、四分之一按金属算。

04 · 2金属不就是亮银色的吗?

一颗亮银色的铬球,放在哪都是亮银色吗?

一间黑屋子,只在左上方开一盏小灯。两颗球的基础色是同一种浅灰:左边是塑料(金属度 0),右边是镜子一样光滑的金属(金属度 1)。哪颗看起来更亮?

  1. 金属球:金属本来就是亮银色的
  2. 差不多一样亮:基础色一样
  3. 塑料球;金属球几乎全黑,只有一个小亮点

塑料球被小灯照亮了半边;金属球几乎和背景一样黑,只剩一个小白点(虚线圈标的是它的位置)。

实验台 S-1黑屋子里的两颗球
来自材料
基础色、金属度、粗糙度是 Blender 原理化 BSDF 和 glTF 共用的参数;非金属正对着看约 4% 的镜面反射(折射率 1.5)。
本实验的设定
屋子、灯的位置和亮度是本实验定的。画面由浏览器实时算(three.js 的物理材质,和 Blender 同一类公式);周围的屋子先拍成一张一圈的环境图,再按粗糙度模糊,金属去这张图里取颜色,不是路径追踪。环境图里只有屋子,所以两颗球互相映不到对方。
它能证明
同一个基础色,非金属会被灯照亮,金属只映出它周围有什么。
它不能证明
Cycles 或 EEVEE 渲出来的具体亮度;右边的剖面图只切了竖直一刀,是示意,不逐像素对应三维画面。

旁边的剖面图说了为什么。它倒着追:从镜头出发的每一条视线,碰到金属球都按镜面反射拐一个弯(光路可以倒着走,倒着追和光正着走是同一条路)。9 条视线拐出去,撞到的全是黑墙。金属球上每一点的颜色,就是那条视线拐出去以后撞到的东西的颜色,再乘上基础色:撞到白墙,金色的球那一点就是金色;撞到黑墙,什么基础色都是黑。小灯只是一个点,要恰好拐到它才亮,剖面图里的 9 条一条都没碰上;画面上那个小亮点,就是球上恰好拐到它的那一小片。塑料球不一样:光钻进表层再往四面八方散,不管小灯在哪、你站在哪,总有一份散到你眼里。

猜「金属本来就亮」的,把画面上看到的亮当成了金属自己的。在实验台上把 01 切到「亮天空」:同一颗金属球立刻变成亮银色,上半截映天,下半截映地面。球一点没变,只是屋子换了。

你后来又说过一句:「现在背景是全黑的 能不能做的更好看」C24。全黑的背景没有纵深:镜头一动,远近的东西本该相对移动(视差),黑背景什么也不动;而对金属来说,它连映的东西都没有。v5 把黑背景换成了机库:地砖、光环、列柱上的灯带。第 3 课那张 v5 的头盔,身上那些亮面就是机库映上去的。

04 · 3粗糙度:镜子被磨花了

真实的表面放大了看,是无数个朝向不一的小斜面。这些小斜面越乱,同一个方向进来的光被弹向的方向就越散:镜子里清清楚楚的一盏灯,在磨砂金属上变成一团柔光。「乱的程度」就是粗糙度(roughness),0 是镜子,1 是完全磨砂。

还是那间黑屋子。把两颗球的粗糙度从 0.08 拉到 0.6,右边的金属球会怎样?

  1. 还是全黑:粗糙度只管倒影清不清楚,不管亮不亮
  2. 小亮点摊开成一大团柔光,朝着灯的那一片亮起来
  3. 整颗球均匀地亮起来,和左边的塑料球一模一样

小亮点摊成了一大团光,看上去像一颗被灯照着的磨砂灰球。光没有变多:原来挤在一个点里的光,被摊到了一大片上,所以每一处都比原来那个点暗得多。按实验台用的这套实时公式,粗糙度从 0.08 拉到 0.6,金属反射出去的光的总量少了不到两成;少掉的那点是这套公式自己的缺口(它只算光在微小斜面上弹一次),真实的金属不会少。

实验台 S-2粗糙度和反射卡
来自材料
反射卡(第 4 节讲)的做法取自红魔 v4 的代码:几块大发光板,中间亮、往边上暗,只在反射里出现;v4 缎面金属的粗糙度(黑甲 0.28、银甲 0.23)。
本实验的设定
两张卡的大小、位置和亮度是本实验定的,和剖面图里画的一致;实验台里的卡也会照亮漫反射,红魔的卡特意设成只进反射。
它能证明
粗糙度把同样多的光摊开,不凭空加光;金属读不读得出,取决于它周围有没有东西可映。
它不能证明
这套实时公式越粗糙少算得越多(粗糙度 1 时只剩约三成),真实的金属不会;所以别拿它比较很粗糙时的亮度。

它和塑料球还是不一样:塑料球是整个朝灯的那一面都亮;金属球的光团聚在「小灯的倒影该在的地方」附近,离开那里就暗下去。猜「还是全黑」的,想的是粗糙度只管清晰度,可一个点被摊成一片,眼睛就读出了形状。

这就是 v4 改法的一半:铬改成了缎面金属,黑甲的粗糙度 0.28、银甲 0.23。灯带和灯的倒影,从刺眼的细线变成成片的渐变。

不看上面,用一句话说:粗糙度改变的是光的「多少」,还是光的「去向」?

04 · 4给金属一点东西去映

另一半改法更直接:金属映什么就是什么,那就给它东西映。拍产品的老办法:在镜头拍不到的地方立几块大柔光板,只为了让金属和玻璃上出现干净的大块高光。3D 里这种板叫反射卡,还能做到现实里做不到的事:Blender 可以把一个物体设成只在反射里出现,镜头看不见它,它也不照亮别的东西。红魔 v4 给机甲加了五张这样的卡:头顶一张 7 × 4 的,左右各一张,前面一张,后面一张红的。

回到黑屋子,粗糙度 0.08。在镜头拍不到的地方立两张大柔光卡:一张在头顶偏前,一张在镜头背后。右边的金属球会怎样?

  1. 看不出变化:镜头拍不到卡,卡就不在画面里
  2. 球上出现两大块干净的亮面,一眼就读出是金属
  3. 两颗球一起均匀地亮起来

卡不在画面里,却在金属球的倒影里。剖面图上,9 条视线里有 3 条拐出去撞到了卡:头顶那张落在球的上半部,镜头背后那张落在正对着你的那一小块。在上面的实验台换到「小灯 + 反射卡」

猜「看不出变化」的,想的是「画面里只有镜头看得见的东西」。可金属映的,恰恰是镜头看不见的那一圈:镜头背后、头顶、两边。塑料球只亮了一点,那是实验台的卡也照了漫反射;红魔的卡连这一点都没有。

04 · 5贴图:把参数画在表面上

到这里,一个材质就是几个数:基础色、金属度、粗糙度(还有透明、自发光等等)。可真东西不是处处一样:指纹处更亮,划痕处更毛,漆掉了的地方露出金属。办法是把这些数画成图:一张基础色图存颜色;一张粗糙度图、一张金属度图用灰度存一个 0 到 1 的数(黑是 0,白是 1);再加一张法线图(第 3 课)。每个像素存那一小块表面的参数,这些图就是贴图(texture)。

图是平的,表面是弯的,所以还要一张对照表,说清「表面上这一点,对着图上哪一点」:每个顶点在图上的坐标 (u, v),叫 UV(贴图坐标)。展 UV,就是把曲面剪开、摊平,像把橘子皮剥下来压平;摊得不好,表面上一小块对到图上一大块(或反过来),图案就被拉长或挤扁。不想展也行:从上下、左右、前后三个方向各投一次,每一处挑最正对着它的那个方向;斜面上两个方向差不多正,就两张各取一半混在一起,这叫三平面投影。红魔机甲的面板线和划痕粗糙度就是这么贴的。

贴图 (0.7, 0.3) u:0 → 1 v 表面上同一个点
UV 是一张对照表:表面上每个顶点对着图上的哪一点。示意图。

04 · 6回到那副铬甲

现在可以回答那天的问题了。v4 之前的铬甲为什么是刺眼的黑白碎块?三样叠在一起:

  1. 材质和环境:近乎镜面的铬,映什么是什么;暗场里映到的几乎全是黑,只有几条灯带是白的(这一课)。
  2. 形状:面片故意随机抖过,相邻的小面各朝各的方向,把那几条灯带的倒影切成了碎块(第 3 课)。
  3. 渲染方式:当时用的 EEVEE 是光栅化,反射有两个来源:画面里已经有的东西,加一张很粗的周围环境图;画面外的东西只能从那张粗图里来,边上会缺、会拉丝(第 6 课)。

面板线的贴图只是在上面又叠了一层花纹。v4 的改法冲着前两样去:缎面金属、反射卡、减少抖动,同时把机甲换成了 Cycles;v5 再把黑背景换成机库,把盔甲换成细分曲面。

同一部片里还有一处同样的毛病:手机背面的金属贴纸,一进暗场就发黑 C28。那次换了一个思路修:贴纸的金属度降到 0.25,再给它一盏只照它的灯。

04 · 7 · 延伸皮肤为什么不像塑料,二次元为什么不按物理算

前面两种还光,一种在表面弹开,一种钻进表层乱撞几下就散出来。皮肤、蜡、玉、牛奶还有第三种:光钻得更深,在里面走上一段,再从旁边别的地方出来。这叫次表面散射(subsurface scattering,SSS)。它让明暗交界变得柔和,也让耳朵、鼻翼、指尖这些薄的地方逆光时透出一圈颜色。光在里面平均走多远,渲染器里是红、绿、蓝各一个数,叫散射半径。

皮下 照进来的白光 绿、蓝:没走多远 红:走得最远,从几处之外出来 散射半径 = 平均走多远 红、绿、蓝各一个数,皮肤的红色最大
光在皮下走一段再出来。示意图,路径是画的,长短只表示红光走得最远。

一个人背对着夕阳站着,你从正面看他的耳朵。耳朵边上透出来的光是什么颜色?

  1. 白的:阳光本来就是白的
  2. 偏红
  3. 和脸上一样的肤色

偏红。光钻进耳朵以后,蓝光和绿光走不了多远就被吃掉了,红光走得最远,能从另一面透出来。猜「白的」的,把透过来的光当成了原样穿过去的光;可它在皮下走了一大段,路上被吃掉的颜色补不回来。

你看过 MotionLib 的写实版,说:「人物的建模太粗糙了,能更细腻更精致吗,或者做成二次元风格的」C42。查下来,毛病大多不在形状上:皮肤的散射半径只设了 0.5 毫米、强度 0.2,光刚钻进去就出来,和漫反射几乎没区别,皮肤像塑料;头发是一层贴了图的外壳,像头盔。精修时散射改成接近真实皮肤的尺度,发壳上长出两三万根顺着发流的发丝。散射也不是越大越好:开得太大,光在里面走得太远,五官的细节被抹平,脸就像蜡像。

另一条路是干脆不追写实。三渲二(toon shading)还是三维:同样的模型、骨骼、动作、灯光和相机,只在算颜色这一步不按物理来,改用画画的规矩。

按物理算 从亮到暗连续过渡 色阶 压成两级,中间一条硬边 +描边+边缘光 轮廓一圈黑,背光一侧一道亮
同一个球、同一盏灯的三种算法。示意图,代码画的。

三样东西:色阶,明暗不再连续过渡,压成两三级,亮面一个颜色、暗面一个颜色(VRM 角色常用的 MToon 材质里,暗面颜色叫 Shade Color,交界有多硬由 Shading Toony 控制);描边,常用「反向外壳」:把网格复制一层、沿法线往外推一点、翻转法线、只画背面,从正面看它只在轮廓上露出一圈,就是那道黑线;边缘光,在侧对视线的边上加一道亮边,把人从背景里勾出来。描边外壳是整整一层网格,每帧都要跟着骨骼一起变形,这是 CPU 摆场景的活之一:MotionLib 的镜头从 1 个角色加到 54 个,一帧从约 1 秒变成约 27 秒,当时的判断是多出来的主要在 CPU 摆场景这一步 C48。

三渲二短片里屋顶慢舞的一对:头发和衣服的明暗只有两三级,轮廓外有一圈细线,脸和发梢的侧边被夕阳勾出亮边。
三渲二《一生》屋顶慢舞的一帧(局部)。看头发和衣服:明暗只有两三级,轮廓上一圈细线,脸和发梢的侧边是夕阳的边缘光。角色是 VRoid 官方样例模型,MToon 着色、EEVEE 渲;背景板是 AI 生成的。

看过两版以后你说:「后面写实版的不用渲染了,就渲染二次元那个就行了」C42。这个直觉有道理:风格化的画面不和真人比,写实版里皮肤像塑料、头发像头盔这类毛病,在二次元版里就不扎眼了。

带走一个问题

一颗镜面一样光滑的金色金属球,放进四面都是白墙的摄影棚里,看起来是金色的。把它搬到一片纯蓝的天空下(地面也刷成同样的蓝),它看起来会是什么颜色?这时候再说「它是金色的」,说的是它身上的哪个参数?

不看上面,写下这一课撞上的几堵墙,再展开对照
  1. 以为颜色是物体自己的 → 看见的是物体还回来的光:非金属的漫反射留下基础色,镜面反射几乎照原样;金属只有镜面反射,带上基础色的色调。
  2. 以为金属本来就亮 → 金属没有漫反射,映什么是什么,黑屋子里就是黑的;基础色调成白也救不回来。
  3. 以为粗糙度只管清不清楚 → 它把同样多的光摊开:一个刺眼的点变成一大团柔光,形状反而读得出来。
  4. 以为镜头拍不到的东西不影响画面 → 金属映的正是镜头拍不到的那一圈:反射卡、机库。
  5. 以为是贴图的 bug → 材质和环境、形状、渲染方式三样叠加,贴图只是最后一层花纹。