Bootstrap

Unity-shader学习笔记(九)

Unity-shader学习笔记(九)

20 非真实感渲染

20.1卡通风格的渲染

实现卡通渲染的方法很多,其中之一是使用基于色调的着色技术。在实现中,会使用漫反射系数对一张一维纹理进行采样,以控制漫反射的色调。此外卡通风格通常还需要在物体边缘部分绘制轮廓,这不同于之前的屏幕后处理技术的对屏幕图像的描边,这是基于模型的描边方法,这种方法的实现更加的简单。

20.1.1渲染轮廓线

一共有五种类型用于绘制模型轮廓线:

①基于观察角度和表面法线的轮廓线绘制。

这种方法是使用视角方向和表面法线的点乘结果来得到轮廓线的信息。这种方法简单快速,可以在一个Pass中就得到渲染结果,但局限性很大,而且有些情况下模型渲染出来的描边效果不尽如人意。

②过程式几何轮廓线渲染。

这种方法是使用两个Pass渲染。第一个Pass渲染背面的面片,并使用某些技术让它的轮廓可见;第二个Pass再正常渲染正面的面片。这种方法的优点在快速有效,并且适用于绝大多数表面平滑的模型,但缺点也很明显,不适合立方体这种平整的模型。

③基于图像处理的轮廓线渲染。

前面所讲的边缘检测就是基于这种方法。优点在于可以适用于任何种类的模型。但局限性也很大,一些深度和法线变化很小的轮廓无法被检测出来,例如桌面上的纸张。

④基于轮廓边检测的轮廓线渲染

前三种渲染的最大问题是无法控制轮廓线的风格渲染。在一些需求下我们会被要求渲染出独特风格的轮廓线,例如水墨风格等。为此我们需要精确检测出轮廓线,然后对它们渲染。检测一条边是否是轮廓边的方法是:检测和这条边相邻的两个三角面片是否满足这个公式:
( n 0 ⋅ v > 0 ) ≠ ( n 1 ⋅ v > 0 ) (n_{0}\cdot v > 0) ≠(n_{1}\cdot v > 0) (n0v>0)=(n1v>0)
其中n0和n1分别表示两个相邻三家面片的法向,v是从视角到该边任意顶点的方向。这个公式的本质是在于检测两个相邻的三角片面是否一个朝正面、一个朝背面。这个检测可以在几何着色器(Geometry Shader)下完成。当然这种方法是有缺点的:实现上相对复杂,以及会出现动画连贯性问题,也就是由于是逐帧单独提取轮廓,就会在帧与帧之间出现跳跃性。

⑤第五种其实是前面四种的混合。

首先找到精确的轮廓线,把模型和轮廓边渲染到纹理中,再使用图像处理的方式识别出轮廓线,并在图像空间下进行风格化渲染。

先使用过程式几何轮廓线渲染的方法来对模型进行轮廓描边:使用两个Pass渲染模型——在第一个Pass中使用轮廓线颜色渲染整个背面的面片,并在视角空间下把模型顶点沿着法线方向向外扩张一段距离,以此来让背部轮廓线可见:

viewPos = viewPos + viewNormal * _Outline;

但是不能直接使用顶点法线进行扩展,因为对一些内凹的模型,就可能发生背面面片遮挡正面面片的情况。因此在扩张背面顶点之前,我们首先对顶点法线的z分量进行处理,使它们等于一个定值,然后把法线归一化后再对顶点进行扩张,这样做的好处在于扩展后的背面更加扁平化,从而降低了遮挡正面面片的可能性。

viewNoemal.z = -0.5;
viewNormal = normalize(viewNormal);
viewPos = viewPos + viewNormal * _Outline;
20.1.2 添加高光

为了在模型上实现卡通风格的一块块分界明显的纯色区域,我们就要实现Blinn-Phong模型并进行相应的改进,我们原本计算高光反射是:

float spec = pow(max(0, dot(normal, halfDir)), _Gloss);

在卡通渲染中我们需要将normal与halfDir的点乘结果与一个阈值进行比较,如果小于该阈值,则高光反射系数为0,否则返回1。

float spec = dot(worldNormal, worldHalfDir);
spec = step(threshold, spec);

我们使用step函数来实现和阈值比较的目的。step函数接受两个参数,一个参数是参考值,第二个参数是待比较的数值。如果第二个参数大于等于第一个参数,则返回1,否则返回0.

但这种粗暴的判断方法会在高光区域的边界造成锯齿。这是因为高光区域的边缘不是平滑渐变,而是由0突变到1。所以我们就需要在边界很小的一块区域内,进行平滑处理。

float spec = dot(worldNormal, worldHalfDir);
spec = lerp(0, 1, smoothstep(-w, w, spec - threshold));

smoothstep函数中w参数的意义是:当spec-threshold小于-w时,返回0,大于w时,返回1,否则在0~1之间进行插值。这样就可以在高光区域的边界处([-w,w]区间内)得到一个从0到1的平滑处理的spec值,从而实现抗锯齿的目的。w值可以由你自己定,但最好的是通过fwidth函数来得到邻域像素之间的近似导数值。

20.1.3 实现

①声明需要使用的各个属性

Properties{
    _Color ("Color Tint", Color) = (1,1,1,1)
    _MainText ("Main Tex", 2D) = "White" {}
    _Ramp ("Ramp Texture", 2D) = "White" {}
    _Outline ("Outline", Range
;