UNIT 04材质约 31 分钟 · 核心线
金属为什么在黑屋子里是黑的?
「机器人身上那些光线效果是不是有点奇怪?那些反光,你是用光栅还是光追的?会不会光追更好?还是说这是材质贴图导致的bug」C20。那天的答案是几样东西叠在一起,没有一样是贴图的 bug。这一课拆其中最大的一样:材质,和它周围的环境。一副近乎镜面的铬甲,为什么在暗场里只剩几道白。
同一个镜头的第 1000 帧。左:近乎镜面的铬甲,整颗头几乎是黑的,只剩几道白。右:v4,一次改了好几样:铬改成缎面金属(稍微磨砂的金属),加了几张只在反射里出现的大柔光卡,减少了小面的抖动,还换了渲染器。这一课在实验台上把前两样一样一样拆开。
04 · 1颜色不是物体自己的
冲照片的暗房只开一盏红色的安全灯。一张白相纸拿进去,看起来是什么颜色?红的。我们看见的从来不是「物体的颜色」,是从物体那里进到眼睛里的光。物体能决定的只是:照到它身上的光,它怎么还回去。
它有两种还法。一种是在最表面直接弹开,像镜子:入射角等于反射角(两个角都从法线量起),这叫镜面反射(specular reflection)。另一种是光钻进表层,在里面乱撞几下,再从四面八方散出来;乱撞的时候有些颜色被表层吃掉了,出来的是剩下的颜色,这叫漫反射(diffuse reflection)。3D 软件里的材质,就是一组告诉渲染器「光碰到这个表面会怎样」的参数;其中的基础色(base color),对非金属说的就是漫反射留下哪些颜色、吃掉哪些。
非金属(塑料、漆、木头、皮肤)两样都有:正对着看时,镜面反射只占大约 4%,而且几乎不挑颜色,照原样弹回去(越斜着看这一份越大);其余是漫反射。金属不一样:钻进去的那部分光立刻被吸收,出不来,所以没有漫反射,看到的全是表面弹开的那一份;而且金属弹回来的时候会挑颜色,金吃掉一部分蓝,剩下的偏暖,这就是金属的基础色。Blender 最常用的材质节点「原理化 BSDF」、网页和游戏常用的 3D 格式 glTF,都用同一个滑杆管这件事:金属度(metallic),0 是非金属,1 是金属,中间的值是两种按比例混合,比如 0.25 就是四分之三按非金属算、四分之一按金属算。
04 · 2金属不就是亮银色的吗?
一颗亮银色的铬球,放在哪都是亮银色吗?
一间黑屋子,只在左上方开一盏小灯。两颗球的基础色是同一种浅灰:左边是塑料(金属度 0),右边是镜子一样光滑的金属(金属度 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,右边的金属球会怎样?
- 还是全黑:粗糙度只管倒影清不清楚,不管亮不亮
- 小亮点摊开成一大团柔光,朝着灯的那一片亮起来
- 整颗球均匀地亮起来,和左边的塑料球一模一样
小亮点摊成了一大团光,看上去像一颗被灯照着的磨砂灰球。光没有变多:原来挤在一个点里的光,被摊到了一大片上,所以每一处都比原来那个点暗得多。按实验台用的这套实时公式,粗糙度从 0.08 拉到 0.6,金属反射出去的光的总量少了不到两成;少掉的那点是这套公式自己的缺口(它只算光在微小斜面上弹一次),真实的金属不会少。
- 来自材料
- 反射卡(第 4 节讲)的做法取自红魔 v4 的代码:几块大发光板,中间亮、往边上暗,只在反射里出现;v4 缎面金属的粗糙度(黑甲 0.28、银甲 0.23)。
- 本实验的设定
- 两张卡的大小、位置和亮度是本实验定的,和剖面图里画的一致;实验台里的卡也会照亮漫反射,红魔的卡特意设成只进反射。
- 它能证明
- 粗糙度把同样多的光摊开,不凭空加光;金属读不读得出,取决于它周围有没有东西可映。
- 它不能证明
- 这套实时公式越粗糙少算得越多(粗糙度 1 时只剩约三成),真实的金属不会;所以别拿它比较很粗糙时的亮度。
它和塑料球还是不一样:塑料球是整个朝灯的那一面都亮;金属球的光团聚在「小灯的倒影该在的地方」附近,离开那里就暗下去。猜「还是全黑」的,想的是粗糙度只管清晰度,可一个点被摊成一片,眼睛就读出了形状。
这就是 v4 改法的一半:铬改成了缎面金属,黑甲的粗糙度 0.28、银甲 0.23。灯带和灯的倒影,从刺眼的细线变成成片的渐变。
不看上面,用一句话说:粗糙度改变的是光的「多少」,还是光的「去向」?
04 · 4给金属一点东西去映
另一半改法更直接:金属映什么就是什么,那就给它东西映。拍产品的老办法:在镜头拍不到的地方立几块大柔光板,只为了让金属和玻璃上出现干净的大块高光。3D 里这种板叫反射卡,还能做到现实里做不到的事:Blender 可以把一个物体设成只在反射里出现,镜头看不见它,它也不照亮别的东西。红魔 v4 给机甲加了五张这样的卡:头顶一张 7 × 4 的,左右各一张,前面一张,后面一张红的。
回到黑屋子,粗糙度 0.08。在镜头拍不到的地方立两张大柔光卡:一张在头顶偏前,一张在镜头背后。右边的金属球会怎样?
- 看不出变化:镜头拍不到卡,卡就不在画面里
- 球上出现两大块干净的亮面,一眼就读出是金属
- 两颗球一起均匀地亮起来
卡不在画面里,却在金属球的倒影里。剖面图上,9 条视线里有 3 条拐出去撞到了卡:头顶那张落在球的上半部,镜头背后那张落在正对着你的那一小块。在上面的实验台换到「小灯 + 反射卡」
猜「看不出变化」的,想的是「画面里只有镜头看得见的东西」。可金属映的,恰恰是镜头看不见的那一圈:镜头背后、头顶、两边。塑料球只亮了一点,那是实验台的卡也照了漫反射;红魔的卡连这一点都没有。
04 · 5贴图:把参数画在表面上
到这里,一个材质就是几个数:基础色、金属度、粗糙度(还有透明、自发光等等)。可真东西不是处处一样:指纹处更亮,划痕处更毛,漆掉了的地方露出金属。办法是把这些数画成图:一张基础色图存颜色;一张粗糙度图、一张金属度图用灰度存一个 0 到 1 的数(黑是 0,白是 1);再加一张法线图(第 3 课)。每个像素存那一小块表面的参数,这些图就是贴图(texture)。
图是平的,表面是弯的,所以还要一张对照表,说清「表面上这一点,对着图上哪一点」:每个顶点在图上的坐标 (u, v),叫 UV(贴图坐标)。展 UV,就是把曲面剪开、摊平,像把橘子皮剥下来压平;摊得不好,表面上一小块对到图上一大块(或反过来),图案就被拉长或挤扁。不想展也行:从上下、左右、前后三个方向各投一次,每一处挑最正对着它的那个方向;斜面上两个方向差不多正,就两张各取一半混在一起,这叫三平面投影。红魔机甲的面板线和划痕粗糙度就是这么贴的。
04 · 6回到那副铬甲
现在可以回答那天的问题了。v4 之前的铬甲为什么是刺眼的黑白碎块?三样叠在一起:
- 材质和环境:近乎镜面的铬,映什么是什么;暗场里映到的几乎全是黑,只有几条灯带是白的(这一课)。
- 形状:面片故意随机抖过,相邻的小面各朝各的方向,把那几条灯带的倒影切成了碎块(第 3 课)。
- 渲染方式:当时用的 EEVEE 是光栅化,反射有两个来源:画面里已经有的东西,加一张很粗的周围环境图;画面外的东西只能从那张粗图里来,边上会缺、会拉丝(第 6 课)。
面板线的贴图只是在上面又叠了一层花纹。v4 的改法冲着前两样去:缎面金属、反射卡、减少抖动,同时把机甲换成了 Cycles;v5 再把黑背景换成机库,把盔甲换成细分曲面。
同一部片里还有一处同样的毛病:手机背面的金属贴纸,一进暗场就发黑 C28。那次换了一个思路修:贴纸的金属度降到 0.25,再给它一盏只照它的灯。
04 · 7 · 延伸皮肤为什么不像塑料,二次元为什么不按物理算
前面两种还光,一种在表面弹开,一种钻进表层乱撞几下就散出来。皮肤、蜡、玉、牛奶还有第三种:光钻得更深,在里面走上一段,再从旁边别的地方出来。这叫次表面散射(subsurface scattering,SSS)。它让明暗交界变得柔和,也让耳朵、鼻翼、指尖这些薄的地方逆光时透出一圈颜色。光在里面平均走多远,渲染器里是红、绿、蓝各一个数,叫散射半径。
一个人背对着夕阳站着,你从正面看他的耳朵。耳朵边上透出来的光是什么颜色?
- 白的:阳光本来就是白的
- 偏红
- 和脸上一样的肤色
偏红。光钻进耳朵以后,蓝光和绿光走不了多远就被吃掉了,红光走得最远,能从另一面透出来。猜「白的」的,把透过来的光当成了原样穿过去的光;可它在皮下走了一大段,路上被吃掉的颜色补不回来。
你看过 MotionLib 的写实版,说:「人物的建模太粗糙了,能更细腻更精致吗,或者做成二次元风格的」C42。查下来,毛病大多不在形状上:皮肤的散射半径只设了 0.5 毫米、强度 0.2,光刚钻进去就出来,和漫反射几乎没区别,皮肤像塑料;头发是一层贴了图的外壳,像头盔。精修时散射改成接近真实皮肤的尺度,发壳上长出两三万根顺着发流的发丝。散射也不是越大越好:开得太大,光在里面走得太远,五官的细节被抹平,脸就像蜡像。
另一条路是干脆不追写实。三渲二(toon shading)还是三维:同样的模型、骨骼、动作、灯光和相机,只在算颜色这一步不按物理来,改用画画的规矩。
三样东西:色阶,明暗不再连续过渡,压成两三级,亮面一个颜色、暗面一个颜色(VRM 角色常用的 MToon 材质里,暗面颜色叫 Shade Color,交界有多硬由 Shading Toony 控制);描边,常用「反向外壳」:把网格复制一层、沿法线往外推一点、翻转法线、只画背面,从正面看它只在轮廓上露出一圈,就是那道黑线;边缘光,在侧对视线的边上加一道亮边,把人从背景里勾出来。描边外壳是整整一层网格,每帧都要跟着骨骼一起变形,这是 CPU 摆场景的活之一:MotionLib 的镜头从 1 个角色加到 54 个,一帧从约 1 秒变成约 27 秒,当时的判断是多出来的主要在 CPU 摆场景这一步 C48。
看过两版以后你说:「后面写实版的不用渲染了,就渲染二次元那个就行了」C42。这个直觉有道理:风格化的画面不和真人比,写实版里皮肤像塑料、头发像头盔这类毛病,在二次元版里就不扎眼了。
带走一个问题
一颗镜面一样光滑的金色金属球,放进四面都是白墙的摄影棚里,看起来是金色的。把它搬到一片纯蓝的天空下(地面也刷成同样的蓝),它看起来会是什么颜色?这时候再说「它是金色的」,说的是它身上的哪个参数?
不看上面,写下这一课撞上的几堵墙,再展开对照
- 以为颜色是物体自己的 → 看见的是物体还回来的光:非金属的漫反射留下基础色,镜面反射几乎照原样;金属只有镜面反射,带上基础色的色调。
- 以为金属本来就亮 → 金属没有漫反射,映什么是什么,黑屋子里就是黑的;基础色调成白也救不回来。
- 以为粗糙度只管清不清楚 → 它把同样多的光摊开:一个刺眼的点变成一大团柔光,形状反而读得出来。
- 以为镜头拍不到的东西不影响画面 → 金属映的正是镜头拍不到的那一圈:反射卡、机库。
- 以为是贴图的 bug → 材质和环境、形状、渲染方式三样叠加,贴图只是最后一层花纹。