眼睛往往是项目中腾挪空间较大的资产:它可以很简单,用一张贴图即可;它也可以非常复杂,美术大神会手动雕刻虹膜的每一条沟壑。作为通俗的“灵魂的窗口”,即便是风格化的卡通美术项目,眼睛的重要性也不容忽视。在关于眼睛的美术资产制作流程,可以参考这篇文档。
确立目标
与皮肤篇和头发篇一样,我们将基于 Cocos Creator 的 PBR 流程实现引擎中的眼球渲染效果。
我们的美术资源包括一张表现“眼白”(学名是巩膜)部分的颜色贴图,一张表现“眼眸”(学名是巩膜)部分的颜色贴图,一张法线贴图和一张MatCap贴图。其中,虹膜圆形的边缘用虹膜贴图的alpha通道表达。除此之外,我们还需要一些小技巧来表现眼珠在眼眶中的遮蔽关系,这将会在后文中详说。
奠定理论
眼睛的结构需要我们关注哪些点呢?我们仍然需要求教于参考图:
- 虹膜直径大约等于整个眼球的半径;
- 瞳孔的直径大约等于虹膜的半径;
- 眼球并不是正球体,在虹膜前方又突起的液泡结构;
首先我们需要了解的是:眼球不是一个正球形,在虹膜的正前方位置有一个圆形的突起。这是因为虹膜正前方有一个液泡的结构,而整个眼球又包裹在透明的巩膜里,所以眼球是一个整体流线型,在正前方有小突起的球体。这些细节,美术的同学会进行表现。
综合来说,虹膜将会是我们的核心,我们需要重点处理虹膜和巩膜、瞳孔以及它正前方的液泡的关系。
UV的处理和归一化
在头发篇中,我们已经聊到了UV数据和其他类型的数据一样,可以对它进行算数运算。我们熟悉的UV Tiling的功能就是通过用UV乘以一个常量实现的。对于虹膜贴图,我们也可以采用相同的处理:
vec2 offsetUV = v_uv * irisSize;
我们新建了一个浮点参数irisSize,并让他与UV数据直接相乘。结果和UV Tiling是一样的:虹膜贴图在UV上的比例缩小了(在irisSize取值大于1的情况下),并且在UV空出来的部分叠加上了同样的虹膜贴图。
当然,我们的眼珠只需要一个虹膜。我们既希望利用常量相乘的办法缩放UV,又不需要贴图的叠加,只需要在贴图的属性中将Wrap Mode设为clamp-to-edge即可。
叠加消除了,我们又遇到了新的问题:贴图似乎缩放到了左下方的角落里。我们需要对坐标系做归一化处理,让我们在缩放UV的同时,贴图可以保持在UDIM正中心。
vec2 offsetUV = (v_uv - 0.5) * irisSize + 0.5;
我们的虹膜大小和位置已经差不多了,下面我们需要将虹膜向后“推”进眼球里,以表现液泡和虹膜的前后关系。我们可以使用视差贴图的方法实现这个效果。
视差贴图
如上图所示,灰色平面代表物体的基本网格平面,在此基础上物体有突起的表面结构,用红色曲线表示。当我们以上图V向量的方向观察物体时,我们理应观察到红色曲线上的B点,当突起的表面结构不存在时,我们则会观察到基本网格上的A点。换言之:我们需要A点上的网格数据,去实现高度在B点的渲染效果。
我们知道,高度贴图(Height Map)表达的是物体切线空间的高度数据。也就是说,A点的切线空间高度数值(H(A))是可以通过贴图获得的。但是B点呢?我们通常会以A点的切线空间高度作为数值权重,以观察向量V的反方向(从片元指向摄像机)进行缩放,就可以大致得到B的位置坐标。这样的计算当然不能做到完全精准,但效果是我们可以接受的。
方法有了,我们需要做的第一步是获得从片元指向摄像机的向量,并将其转化到切线空间当中:
vec3 worldDirPosToCam = normalize(cc_cameraPos.xyz - v_position); vec3 tangentDirPosToCam = vec3(dot(worldDirPosToCam, v_tangent), dot(worldDirPosToCam, v_bitangent), dot(worldDirPosToCam, v_normal)); 我们可以利用得到的切线空间向量,对UV进行偏移,以偏移后的UV坐标读取切线空间的高度信息。这样我们就在A点得到了B点的高度输出: vec2 parallaxUV( vec3 V, vec2 uv, float iniHeight, float scalar ){ vec2 delta = V.xy / V.z * iniHeight * scalar; return uv - delta; }
上面的代码需要带入四个参数:V为我们刚求得的切线空间中的从片元指向摄像机的向量,uv为物体的原uv(即我们已经在皮肤篇和头发篇中使用过的“v_uv”),scalar为自定义的权重参数,iniHeight是片元的原高度数据,这个数据应该由一张贴图提供。在我们的着色器中,我们只需要用视差贴图做一些简单的像素偏移,因此没有准备专门的高度贴图,我们可以用颜色贴图的任意一个通道,或者直接使用一个常量0.5作为代替。
得到了视差贴图的函数,我们就可以把它用在虹膜上面了。
vec2 offsetUV = (v_uv - 0.5) * irisSize + 0.5; vec4 irisTex = texture(irisMap, offsetUV); vec2 irispUV = parallaxUV( tangentDirPosToCam, offsetUV, irisTex.r, parallaxScale ); vec3 irisColor = SRGBToLinear(texture(irisMap, irispUV).xyz);
我们可以用之前的缩放归一后的UV得到有视差效果的虹膜UV,用这套新UV赋予我们的虹膜贴图,得到的结果应该类似下图:
(gif传不了图,尴尬)
如图所示,随着权重数值的变化,我们的虹膜贴图应该能够沿着法线方向向前“推”或向后“缩”,同时我们也发现,我们目前的视差贴图只能达到一种近似的效果,随着权重数值增大,视差的效果也会越来越失真。因此我们在使用它时,需要注意将数值控制在比较低的范围内。
完成虹膜
虹膜的处理已经差不多了,下面我们需要处理一下瞳孔。
完成了虹膜的视差,我们如法炮制,对我们得到的视差UV做归一化处理。区别在于,这次我们将UV归一化,这相当于将所有的UV塌陷到归一化坐标的原点上。使用这个UV采样贴图,得到像素向坐标中心拉伸的效果。
接下来,就是制作一个遮罩将虹膜和瞳孔混合在一起了。
vec2 pupilpUV = normalize(irispUV - 0.5) + 0.5; float pupilIndex = (1.0 - length(v_uv - 0.5) * 2.0 * irisSize) * (0.8 * pupilSize); vec2 irisUV = mix(irispUV, pupilpUV, pupilIndex); vec3 irisColor = SRGBToLinear(texture(irisMap, irisUV).xyz) * irisColor.xyz;
通过自定义参数irisSize和pupilSize,我们可以分别控制虹膜和瞳孔的大小。我们也可以为虹膜贴图自定义一个偏转的颜色irisColor,快速制作出不同颜色的眼眸。
下面我们可以把虹膜贴到眼球上了。眼球的基本材质使用巩膜贴图,我们只需要把虹膜的部分叠加在上面即可。虹膜贴图的边缘部分是用alpha通道的渐变完成的,我们可以用指数运算控制渐变的曲线强度,从而控制虹膜边缘的硬度:
vec3 scleraTex = SRGBToLinear(texture(scleraMap, v_uv).xyz); float irisEdgeIndex = clamp(pow(irisTex.a, irisEdge), 0.0, 1.0); vec3 eyeBase = mix(scleraTex, irisColor, irisEdgeIndex) * irisColor.xyz;
目前眼球的固有颜色信息已经得到了。但是我们的眼球看上去和直接贴了一张颜色贴图没有什么区别。下面我们需要做的是:为眼球赋予“神”。
MatCap贴图
所谓有“神”的眼睛,可以简单概括为“有高光和/或有反光的眼睛”。如参考图所示,上面两张参考图中的眼睛显得更加生动和有活力,而下面两张则看上去非常死板,好似无机物。
然而,游戏中角色的眼睛并不是总能恰好反射环境中的光照,当环境有某些特定的需求或者从某些特定的角度观察时,眼睛很有可能没有足够的高光或反光。更何况,眼睛固然重要,但毕竟是一个较小的反射面,为此专门进行反射的光照计算似乎有点得不偿失。一个常见的折中办法是:把高光和反光作为贴图,永久地“贴”在眼睛表面。这样无论任何环境和角度,角色的眼睛里永远有星辰大海。
所谓MatCap贴图,顾名思义,是一张把整个材质(“Mat”-erial)的特性捕捉(“Cap”-ture)到像素内的贴图。MatCap贴图通常绘制的是一个球体,着色器会根据球体上的明暗面、高光和反射,为整个材质绘制明暗关系和高反光。美术的同学应该对MatCap并不陌生——ZBrush中用于渲染动辄上百万个多边形的材质正是使用MatCap着色器。因此MatCap有着效率极高,又足够能表现明暗关系和质感的优点。同时,MatCap的缺点也是显而易见的:无论从哪个角度观察,MatCap材质的明暗关系和高反光永远一成不变。
在我们的着色器中实现MatCap材质也非常简单:我们知道MatCap的特性是它永远正对观察方向,既然如此,得到一套永远正对摄像机的UV,用它来采样MatCap贴图即可。我们知道,法线数据表达的是物体表面片元正对的方向,因此把法线数据转换到视图空间,只取X和Y轴数据,就能得到我们想要的UV:
vec4 matCapUV = (cc_matView * vec4(v_normal, 0.0)) * 0.5 + 0.5; vec4 matCapUV = (cc_matView * vec4(v_normal, 0.0)) * 0.5 + 0.5;
确定了UV,剩下的工作就水到渠成了:
vec3 matCapColor = SRGBToLinear(texture(reflecMap, matCapUV.xy).xyz) * reflecAmt; vec3 eyeColor = eyeBase + matCapColor;
我们的着色器已经编写完成了,让我们来看看效果:
按理来说,我们该参考的图都参考了,该考虑的变量都考虑了,该做的工作都做了,但这白森森的眼神,还是直接营造出一种纸人既视感。尤其是从较远距离观察的时候,白的发亮的眼珠更是莫名惊悚。
这是因为:眼珠和身体的其他部位一样,应该相互产生遮蔽的关系。我们的眼珠是单独制作的,所以和眼皮没有暗部遮蔽,因此在整张脸上特别出挑。这也是角色渲染的一个常见问题:我们对人脸都太熟悉不过了,以至于人脸上如果出现异于常理的现象都会触发本能的警觉。而且当其他的部分越趋近于真实时,这种恐怖感越严重。
如果是一个静态的部位,这个问题非常好解决:烘培一张AO即可。但是对于角色来说,绝大多数的角色眼球是需要骨骼动画的,直接把AO烘培在眼球上显然不可取。我们需要做的是在眼球的模型前方再新建一个遮蔽的模型,给它赋予一个AO的透明贴图,单独作为AO保留在模型上。这个模型除了AO将不会起任何其他作用,因此只需要给予一个基本的Unlit材质,也不会消耗额外资源。这种做法,也是包括UE4在内的许多引擎选择的做法。
增加了AO之后,我们角色的眼神柔和了许多,眼球和眼眶的衔接也更自然了。
结语
我们对人物渲染的探索到这里就可以成功收官了。在我们试图解答一个个渲染问题的过程中,我们获得的不仅是皮肤、头发和眼睛,同时也包括了:
- 了解方形模糊和高斯模糊的原理,并实现高效率的模糊效果;
- 探索人类皮肤的Diffuse Profile并用代码重现现实观测的数据;
- 了解次表面散射和各向异性高光的逻辑和原理;
- 学习纵横行业30余年的Kajiya-Kay模型;
- 尝试视差贴图的渲染方法;
- 学习和使用MatCap贴图;
虽然我们只是在 Cocos Creator 现有的着色器上进行修改,但相信你也已经发现:在Cocos Creator着色器的基础上编写自己的着色器,不仅省去了大量GLSL基础工作,而且可以一步到位地获得PBR的基础渲染效果。在一个稳固的基础上,我们可以自由发挥,尝试各种各样的方法和模型, 实现丰富多样的渲染需求。
暂无关于此日志的评论。