一转眼已经有一个多月没更新了额,时间过得真快。今天终于决定要更新Part 2了。
----那我们就继续吧。
上篇已经讲完了漫反射、高光和光照贴图,本篇将会介绍角色背光时边缘光与基于阈值图的面部阴影的实现方法。
我们知道,将光照方向与物体表面法线方向点乘,可以获得光照方向越接近表面法线方向,计算值越大的结果。我们可以使用这个结果来配合smoothstep等函数来区分物体的亮暗面。
----那么请想象一下,如果我们将视线方向与物体表面法线方向点乘,可以获得什么结果呢?
是不是相当于:我们的视线就是光源,直视哪个片元,哪个片元的点乘结果就越大。
(我们说的视线方向,指的都是指向眼睛的向量方向。)
现在我们将这个计算的结果先使用Saturate函数控制在0-1之间。然后用1减去这个值,将它反过来。
我们就可以得到如下结果:视线越垂直于片元表面,那里的值就越接近0;视线与片元表面的法线方向越偏离,甚至相反,那里的值就越接近1。
将这个结果直接输出,我们来看一下是什么效果。
可以看到,角色的边缘光效果已经有了“雏形”。
以上效果使用的原理便是著名的“Fresnel菲涅尔”。
但是目前来看,这个效果还是有些过于“浑浊”。为了更加接近“卡通绘画”的效果,我们需要让边缘光更加生硬,过度不要过于柔和。
使用的手段十分简单,和之前进行二值化的操作一模一样:使用smoothstep。
我们将以上计算的结果作为smoothstep的第三个参数,然后声明两个暴露在外的float类型参数作为smoothstep的前两个控制参数,将黑白区域进行平滑二值化。
以下是经过smoothstep以及调参之后的输出结果:
OK,这就是我们想要的效果。
下面要对这个效果进行一个简单的处理,让角色只有在背光时,才会出现边缘光效果。
这个处理的手段十分简单,我们直接创建一个向量,让它指向角色身后,然后让光源方向点乘它。这样就可以获得:光源越直射角色身后,点乘结果越大的效果。
要创建这个背身向量,我使用了UnityObjectToWorldDir函数,它可以接受一个模型空间的向量,将它转换到世界空间下,因为我的光源方向是在世界空间下计算的。我们在进行向量运算时,一定要确保它们在同一个空间。
float3 Front = normalize(UnityObjectToWorldDir(float3(0, 1, 0)));
//获得世界空间下的角色面朝方向的向量,(0,1,0)是角色模型空间的面朝方向的方向向量。"-Front"就是背身向量
接下来,我们将这个值使用Saturate函数限制到0-1之间,然后直接乘以之前计算的边缘光的值。
这样一来,当光源方向不从角色身后照射时,就不会有任何边缘光效果了。并且,当光源从角色侧面慢慢转向角色身后时,边缘光会越来越亮,这个效果非常好。
但是,以上操作会让边缘光的过度重新变得有些“浑浊”,所以我们再使用一次smoothstep,将边缘光重新二值化。最后再将计算结果乘以一个HDR颜色,背光边缘光效果就大功告成了。
以下是这一项的具体shader代码:
float rimDot = 1 - saturate(dot(viewDir, normal)); //基础Fresnel
rimDot = smoothstep(_minRim , _maxRim , rimDot); //第一次平滑二值化
float rimIntensity = rimDot * saturate(dot(-Front.xz,N_lightDir.xz));
//边缘光数值乘以背身向量与光源方向的点乘,后面之所以只使用.xz平面的计算结果,是不希望光源的高低对边缘光产生影响
rimIntensity = smoothstep(_RimAmount - 0.3, _RimAmount + 0.1, rimIntensity);//第二次平滑二值化
half4 rim = rimIntensity * _RimColor; //结果乘以HDR颜色,颜色是暴露在外面的参数,可以随时调整
我将边缘光的颜色调成了橙黄色,这样就可以获得背光时产生黄昏边缘光的效果了。
每想到阈值图这件事,我都抑制不住自己内心的赞美。想到这玩意儿的人实在是太聪明了。
卡通风格的面部渲染其实是很难做好的一件事,首先面部是玩家经常关注的地方,所以它的渲染一定不能马虎,如果直接使用和其他位置相同的shader来渲染面部的阴影,这个阴影一定是很难看的。
因为面部的结构其实是有些复杂的,如果使用法线来计算阴影,不仅计算量很大,而且一定会是写实的阴影风格。一直很常用的面部渲染方案是使用法线修正技术,使面部的法线不跟随面部结构走,而是类似球面法线的那种感觉,这样算出来的阴影更贴近卡通风格。但是这种方案十分繁琐,要修改法线且不论,更关键的是它无法获得所见即所得的高度可控面部阴影,美术人员制作起来其实是十分困难的。
很多卡渲风格的游戏干脆直接放弃了面部阴影,让面部不产生阴影。
阈值图的出现不仅解决了上述的所有问题,还减少了shader的计算量,因为它根本不需要面部法线。
先来看看这个神奇的玩意儿长什么样吧:
这就是面部阈值图了。它是基于面部的UV进行制作的,具体的制作方法目前还是游戏公司的机密。某乎上有很多大佬对阈值图的制作方法进行了猜测和尝试,想了解可以去某乎搜索相关信息。
那么它的作用是什么呢?
我们先将它丢进PS里去看看。
使用吸管工具来看看它的颜色值。
你会发现它是一个灰度图,从右向左灰度值有规律降低。但似乎还是无法明白它的使用方法。
接下来,给它加一个阈值图层,我们再看看它的样子。
下面就是加了阈值图层后,它的样子(阈值为255):
阈值为128时:
没错,阈值图会根据阈值参数而改变黑白区域大小,而黑色区域的形状,正好就是我们需要的面部阴影形状。
阈值参数到底是如何改变这张图的样子的呢?
其实,阈值就相当于shader中的step函数,它对阈值图进行了无过度的二值化。当某一处的灰度值小于阈值时,就输出0;大于阈值时,就输出1。
举个例子,阈值图左边除了三角区域,其他位置的灰度值都小于128,所以当阈值为128时,这张图的左边除了三角区域就输出了0,也就是黑色了。
这就是阈值图为什么能够使面部阴影高度可控的原因。
我们现在有了阈值图,缺的就是阈值了。下面,我们去unity来使用一下这张阈值图吧。
先来讨论一下,阈值在引擎中该如何得到。
我们刚刚在PS中,是通过手调来改变阈值的,但是在游戏中,我们就需要实时算出这个阈值。
那么使用什么量来算呢?
首先肯定需要光照方向,然后让它点乘一个向量。这个向量该是什么呢?
我们反向思考:
这是阈值为最大的255时,阈值图呈现的样子。当面部阴影是这样的时候,应该是光源正好从侧面照射面部的时候。也就是说,当光源正好从侧面照射面部时,光照方向与某个向量的值点乘值为0。
所以这个向量就是角色的侧身方向向量。
并且左,右两个方向向量都需要用到。
这时我们还会发现一个问题,我们在PS中将阈值从255调整到1的过程中,这张图的黑色部分始终是从右脸向左脸移动的。也就是说,它应该是用在光源方向从角色右侧向角色正面移动的过程中的。
也就是说,这张图还少了一半,即光源位于角色左侧时的阈值图。
好在,一张貌美的脸一定是左右对称的,所以我们只需要将这张图反过来采样,就可以获得光源位于角色左侧时的阈值图了。
下面直接给代码和注释:
half4 ilmTex = tex2D(_IlmTex, i.uv); //采样阈值图
half4 r_ilmTex = tex2D(_IlmTex, float2(1 - i.uv.x, i.uv.y)); //使用反向uv的手段来反向采样阈值图
float3 Left = normalize(UnityObjectToWorldDir(float3(0, 0, -1))); //世界空间角色正左侧方向向量
float3 Right = -Left; //世界空间角色正右侧方向向量。
float ctrl = step(0, dot(Front.xz, N_lightDir.xz));
//当光源从角色背后照射时,面部应该为全阴影。所以要有一个控制量。
float faceShadow= ctrl * min(step(dot(Left.xz, N_lightDir.xz), r_ilmTex.r), step(dot(Right.xz, N_lightDir.xz), ilmTex.r));
//右侧光源方向就用右侧的阈值图,左侧光源方向就用反向采样的左侧阈值图,使用min函数取两个阈值计算结果的最小值进行融合。
//最后乘以ctrl,使得背光时面部表现为全阴影
half4 diffuse = lerp(_AmbientColor, _LightColor0, faceShadow); //lerp函数为最后的面部颜色赋值
//_AmbientColor是暴露在外的参数,可以随时调整面部阴影颜色。
以上。
面部阈值图的使用原理已经进行了比较详细的介绍,具体有一些细节不太好叙述,比如为什么要使用min函数进行左侧和右侧阈值图的融合。但是只要自己有心去思考,想理解是非常简单的。
如果有什么问题,欢迎在评论区给我留言,只要看到我都会回的。
接下来的第三篇,我会做一些总结,并叙述一些当前阶段尚未解决的问题,还会补充一些细节的知识点。