|
本文将对《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,1],均值为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中随着亮度变低,颜色的饱和度没有上升,暗部很阴沉,没有生气
(以后会针对这几点进行改进)
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|