HuldaGnodim 发表于 2023-2-22 10:34

《Unity Shader入门精要》中的卡通渲染

本文将对《Unity Shader入门精要》中的卡通渲染进行讲解,并复习一遍环境光、漫反射、高光,文中代码均来自本书。笔者刚入门shader,如有错误,请多包涵。
大致思路


[*]有两个pass,一个渲染背面(Cull Front),作为描边


2.另一个渲染正面(Cull Back),得到最终效果


第一个pass的顶点着色器

v2f vert (a2v v) {
                                v2f o;
                                float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
                                float3 normal = mul(UNITY_MATRIX_IT_MV, v.normal);
                                normal.z = -0.5;
                                pos = pos + float4(normal, 0) * _Outline;
                                o.pos = mul(UNITY_MATRIX_P, pos);
                                return o;
                        }在第一个pass的顶点着色器中,先将模型空间下法线和顶点转换到观察空间,用顶点坐标+=法线方向的向量,对顶点延法线方向进行偏移,这样物体就“变大了”,以至于第二个pass渲染完前面的面还没有完全遮住它,还能看到边缘,这就是描边的原理。
至于为什么要先把坐标转换到观察空间,这完全是因为第5行:normal.z = -0.5; 如果没有这一步,直接转换到裁剪空间后再加上法线方向的向量就行,效果完全相同,但normal.z=-0.5在不同的空间中的意义不同。
做个实验来说明这行代码在不同空间下执行的区别,假如normal.z=100
实验情况1:在观察空间下,描边会向前100,跑到模型的前面,如下图


并且描边会随着视角的改变而改变位置,这是因为观察空间下,深度线(z方向)如下图所示,改变z后,描边会随着这些线的方向移动


实验情况二:在世界坐标下,z=100时,描边会沿着世界坐标的z偏移100,转动视角时描边不动,因为世界坐标不会随着视角的改变而改变
结论:回到实际的参数,z=-0.5可以让描边往后面躲一躲,防止后面的描边穿到前面来,观察空间下,无论视角如何变化,描边总是不会穿到前面来,而世界空间下,描边在有些视角仍然会穿到前面
第一个pass的片元着色器

直接输出描边颜色,没啥好说的
第二个pass的顶点着色器

v2f vert (a2v v) {
                                v2f o;
                                o.pos = UnityObjectToClipPos(v.vertex);
                               
                                o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
                                o.worldNormal= UnityObjectToWorldNormal(v.normal);
                                o.worldPos =UnityObjectToWorldDir(v.vertex);
                                TRANSFER_SHADOW(o);
                               
                                return o;
                        }在这里先把法线和顶点坐标转换到世界空间并传递到片元着色器中,避免在片元着色器中进行坐标变换
第二个pass的片元着色器

float4 frag(v2f i) : SV_Target {
                                fixed3 worldNormal = normalize(i.worldNormal);
                                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                               
                                fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
                                //环境光
                                fixed4 c = tex2D (_MainTex, i.uv);
                                fixed3 albedo = c.rgb * _Color.rgb;       
                                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                                //漫反射
                                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                                fixed diff =dot(worldNormal, worldLightDir);
                                diff = (diff * 0.5 + 0.5) * atten;
                                fixed3 sampledColor=tex2D(_Ramp, float2(diff, diff)).rgb;
                                fixed luminance = 0.2125 * sampledColor.r + 0.7154 * sampledColor.g + 0.0721 * sampledColor.b;
                                fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
                          
                                fixed3 diffuse = _LightColor0.rgb * albedo * sampledColor;
                                //高光
                                fixed spec = dot(worldNormal, worldHalfDir);
                                fixed w = fwidth(spec) ;
                                fixed3 specular = _Specular.rgb * smoothstep(-w, w, spec + _SpecularScale-1) ;
                               
                                return fixed4(ambient +specular+diffuse , 1.0);
                        }环境光
albedo是漫反射系数,我理解为物体原本的颜色(纹理采样得到的颜色*可以在外部调节的颜色),在计算环境光(ambient)和漫反射(diffuse)时都要用到。ambient由环境光颜色*albedo得到。
漫反射(与普通渲染不同的地方)
diffuse采用的是半兰伯特模型,与兰伯特模型相比,颜色值被映射到,均值为0.5,这样背光面不是全黑的,而是有亮度变化的,下面的是公式, m_{diffuse} 是这里的albedo;n和l是世界空间下的法线和光线方向,法线和光线夹角越小,相乘值越大,就越亮,很合理。不过这里还要乘以光照衰减atten,用于制造阴影,不然就只考虑到了光照和法线的方向,没有考虑到物体的遮挡,背面就会偏亮。
更重要的不同在于代码中用diff(漫反射系数)采样纹理_Ramp(下图),漫反射系数diff(也可以理解为亮度)到了某个值会突变,这就是卡通渲染的特点



来自书内截图



_Ramp

高光(与普通渲染不同的地方)
高光的实现也和普通的不同,我们需要的是圆形高光,有着干净利落的边缘,所以我们的高光是从0突变到1的,
高光系数spec可以理解为视线与反射光线方向的接近程度。下图中,v越接近光源的反射光方向,那h就和n夹角越小,如果正好是在反射方向上,那么夹角就是0,反射最强烈。


fwidth在这里的作用是得到一个很小的值,这里是spec的邻域像素之间的近似导数值,我理解为高光系数spec的变化速度。
smoothstep:第三个变量小于-w 时,返回0,大于w 时,返回1,否则在0到1之间进行插值。这是为了在高光的边缘抗锯齿。为什么它能抗锯齿呢?由于区间(-w,w)很小,因此大部分是返回0或1,只有在边缘处为会在0到1之间插值。第三个参数中,spec最大是1,最小是零点几,减去1后区间变成(-0.x,0),再加上用于调节高光区域大小的系数_SpecularScale(0到0.1),当它为0时,第三参数就会被映射到(-0.x,0),无高光,当它为1时,第三参数就会被映射到(-0.x,0.1),高光会很大。
最后把这三个颜色加起来输出就行了。
修改不同的texture还能有不同的效果:





我觉得这种的阴影处理很好看

书中的渲染效果(下面第一张图)和某网课中渲染效果(下面第二张)的差距:

[*]图1在面向高光的边缘处没有白色,只有白色描边,图2中除了白色描边,边缘处还有白色边缘(因为另一个黑色描边的球也有白色的边缘)
[*]图1中的高光太小,形状太接近圆形
[*]图1中随着亮度变低,颜色的饱和度没有上升,暗部很阴沉,没有生气
(以后会针对这几点进行改进)



xiangtingsl 发表于 2023-2-22 10:43

求问下某网课是哪门课[蹲]

Baste 发表于 2023-2-22 10:43

aboutCG上搜栗子,他有个卡通材质教学
页: [1]
查看完整版本: 《Unity Shader入门精要》中的卡通渲染