Unity-Shader读书笔记(13)
卡通风格的渲染
卡通风格的实现有很多的方法,其中之一就是使用基于色调的着色技术(tone-base
shading)。在实现中,我们往往使用漫反射系数对一张一维纹理进行采样,以控制漫反射的色调。卡通风格还需要进行描边处理,虽然之前我们已经做过描边了。但是这里是使用的是基于模型的描边方法。
渲染轮廓线
书中介绍了很多方法这些方法都在《Real Time
Rendering》中有提及,这里我就只说书中作者所用的方法。如果大家有兴趣可以去看这本书。
我们将使用两个Pass进行渲染。在第一个Pass中,我们会使用轮廓线颜色渲染整个背面的面片。并在视角空间下把模型顶点沿着法线方向向外扩展一段距离,以此让背部轮廓线可以被看见。但是对于一些内凹的模型就可能发生背面面片遮挡正面的情况。为了尽可能防止出现这样的情况,在扩张背面顶点前,我们先对顶点法线的z分量进行处理。我们让z分量等于一个定值,然后把法线归一化后再对顶点进行扩张。这样的好处在于扩张后的背面更加扁平化,从而降低了遮挡正面面片的可能性。
添加高光
卡通风格中的高光往往是模型上一块块分界明显的纯色区域。为了实现这种效果,我们就不能再使用之前学习的光照模型。书中给出的高光计算是先得到法线和半程向量(半程向量是Blinn-Phong模型的优化方法)进行点乘。然后我们把该值和阈值进行比较,如果小于该阈值则高光反射系数为0,否则返回1。
这种粗暴的方法会使得在高光区域的边界造成锯齿。这个问题的原因在于高光区域的边缘不是平滑渐变的,而是由0突变到1.想要对其进行抗锯齿处理,我们可以在边界处很小的一块区域进行平滑处理。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| Shader "Learn/ToonShader" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color Tint",Color) = (1,1,1,1) _Ramp ("Ramp Texture", 2D) = "white" {} _Outline ("Outline", Range(0,1)) = 0.1 _OutlineColor ("Outline Color",Color) = (0,0,0,1) _Specular ("Specular", Color) = (1,1,1,1) _SpecularScale ("Specular Scale", Range(0,0.1)) = 0.01 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 CGINCLUDE #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #pragma multi_compile_fwdbase
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 vertex : SV_POSITION; };
struct v2fFront { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; SHADOW_COORDS(3) };
sampler2D _MainTex; float4 _MainTex_ST; sampler2D _Ramp; float4 _Ramp_ST;
float4 _Color; float4 _OutlineColor; float4 _Specular;
float _Outline; float _SpecularScale;
v2f outLineVert(appdata v) { v2f o; float4 pos = float4(UnityObjectToViewPos(v.vertex),1); float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV,v.normal); normal.z = -0.5; pos += float4(normalize(normal),0) * _Outline; o.vertex = mul(UNITY_MATRIX_P,pos); return o; }
fixed4 outLineFrag(v2f i) : SV_TARGET0 { return fixed4(_OutlineColor.rgb,1); }
v2fFront vert(appdata v) { v2fFront o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
TRANSFER_SHADOW(o);
return o; }
fixed4 frag(v2fFront i) : SV_TARGET0 { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir); fixed4 col = tex2D(_MainTex,i.uv); fixed3 albedo = col.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 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp,float2(diff,diff)).rgb; fixed spec = dot(worldNormal,worldHalfDir); fixed w = fwidth(spec) * 2; fixed3 specular = _Specular.rgb * lerp(0,1,smoothstep(-w,w,spec + _SpecularScale - 1)) * step(0.0001,_SpecularScale); return fixed4(ambient + diffuse + specular,1); }
ENDCG
Pass { Name "Outline" Cull Front CGPROGRAM #pragma vertex outLineVert #pragma fragment outLineFrag ENDCG }
Pass { Cull Back Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } FallBack "Diffuse" }
|
_Ramp是用于控制漫反射色调的渐变纹理,_Outline用于控制轮廓线宽度,_OutlineColor控制轮廓线颜色,_Specular高光反射的颜色,_SpecularScale高光反射的阈值。
第一个Pass是用来偏移背面顶点并进行描线处理。对于片元函数来说只是简单输出线的颜色。我们使用Cull
Front指令来剔除正面。我们先将顶点数据从模型空间转换到视角空间(书中只是为了让描边在视角空间得到最好的效果)中。而顶点法线的转换比较困难,这是因为我们希望转换后的顶点法线能够保持原来垂直曲线的模样。具体为什么是使用这样的做法,之前的文章是有说明过的。为了防止遮挡的情况,我们将法线的z轴设定为-0.5这个定值,并做归一化处理(这里我就不能理解,这样法线不就不垂直了吗)。顶点沿着这个法线方向进行延伸,而这个延伸的值由我们来决定。最后我们要把修改后的顶点转到裁剪空间下。
第二个Pass是用来渲染正面并做出渲染出我们定义的光照模型。这个Pass大多是按照半兰伯特模型(如果你忘记了可以回到这篇文章看看Unity-Shader读书笔记(5))来的,只是多加了光照衰减和影子效果。我们先计算反射率albedo(我个人觉得还是叫漫反射系数比较好),这个是由纹理采集和我们设定的颜色来得到的最终结果。如之前所说,卡通风格在渲染高光的时候要进行特殊处理。我们先得到高光所需的点乘值。fwidth函数是用来进行抗锯齿处理的,它会得到邻域像素之间的近似(更多关于它的事情大家可以看以下的文章:fwidth
- Win32 apps | Microsoft Learn,Shader
常用函数总结
北海6516的博客-CSDN博客。接下来的函数你都可以从这两个文章中找到对应的说明)。接下来我们再通过特定的计算得到中间数w。然后使用lerp和smoothstep得到一个平滑的值。这里我们多加了一个step(0.0001,_SpecularScale)是因为如果_SpecularScale为0,就要完全消除高光反射。
PS:这里还有很多公式是没有详细说明,且我也找不到原理。比如fixed
w = fwidth(spec) * 2和spec + _SpecularScale -
1。前面我是知道做一个抗锯齿处理可是为什么乘2我就不知道了。或许在《Real
Time Rendering》中有说明,那只能之后有时间再看看了。
素描风格的渲染
这里书中介绍了几个概念“色调艺术映射”和“多级渐远纹理”。我并没有找到其他关于他们的资料。这里书中做法是用6张不同的纹理(多级渐远纹理),然后我们依照光照结果来决定像素点应该是去哪张纹理进行采样。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| Shader "Learn/Hatching" { Properties { _TileFactor ("Tile Factor", Float) = 1 _Color ("Color Tint", Color) = (1,1,1,1) _Outline ("Outline", Range(0, 1)) = 0.1
_Hatch0 ("Hatch 0", 2D) = "white" {} _Hatch1 ("Hatch 1", 2D) = "white" {} _Hatch2 ("Hatch 2", 2D) = "white" {} _Hatch3 ("Hatch 3", 2D) = "white" {} _Hatch4 ("Hatch 4", 2D) = "white" {} _Hatch5 ("Hatch 5", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "Queue" = "Geometry"} LOD 100
UsePass "Learn/ToonShader/Outline"
Pass { Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase
#include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float2 uv : TEXCOORD0; fixed3 hatchWeights0 : TEXCOORD1; fixed3 hatchWeights1 : TEXCOORD2; float3 worldPos : TEXCOORD3; float4 pos : SV_POSITION; SHADOW_COORDS(4) };
sampler2D _Hatch0; sampler2D _Hatch1; sampler2D _Hatch2; sampler2D _Hatch3; sampler2D _Hatch4; sampler2D _Hatch5; float4 _Hatch0_ST; float4 _Hatch1_ST; float4 _Hatch2_ST; float4 _Hatch3_ST; float4 _Hatch4_ST; float4 _Hatch5_ST;
float4 _Color; float _TileFactor; float _Outline;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.uv.xy * _TileFactor;
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex)); fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed diff = max(0, dot(worldLightDir,worldNormal));
o.hatchWeights0 = fixed3(0,0,0); o.hatchWeights1 = fixed3(0,0,0);
float hatchFactor = diff * 7;
if(hatchFactor > 6.0) {} else if(hatchFactor > 5.0) o.hatchWeights0.x = hatchFactor - 5; else if(hatchFactor > 4.0) { o.hatchWeights0.x = hatchFactor - 4; o.hatchWeights0.y = 1 - o.hatchWeights0.x; } else if(hatchFactor > 3.0) { o.hatchWeights0.y = hatchFactor - 3; o.hatchWeights0.z = 1 - o.hatchWeights0.y; } else if(hatchFactor > 2.0) { o.hatchWeights0.z = hatchFactor - 2; o.hatchWeights1.x = 1 - o.hatchWeights0.z; } else if(hatchFactor > 1.0) { o.hatchWeights1.x = hatchFactor - 1; o.hatchWeights1.y = 1 - o.hatchWeights1.x; } else { o.hatchWeights1.y = hatchFactor; o.hatchWeights1.z = 1 - o.hatchWeights1.y; }
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
TRANSFER_SHADOW(o); return o; }
fixed4 frag (v2f i) : SV_Target { fixed4 hatchTex0 = tex2D(_Hatch0,i.uv) * i.hatchWeights0.x; fixed4 hatchTex1 = tex2D(_Hatch1,i.uv) * i.hatchWeights0.y; fixed4 hatchTex2 = tex2D(_Hatch2,i.uv) * i.hatchWeights0.z; fixed4 hatchTex3 = tex2D(_Hatch3,i.uv) * i.hatchWeights1.x; fixed4 hatchTex4 = tex2D(_Hatch4,i.uv) * i.hatchWeights1.y; fixed4 hatchTex5 = tex2D(_Hatch5,i.uv) * i.hatchWeights1.z;
fixed4 whiteColor = fixed4(1,1,1,1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z - i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
return fixed4(hatchColor.rgb * _Color.rgb * atten, 1); } ENDCG } } }
|
_Color是用于控制模型颜色的数学。_TileFactor是纹理的平铺系数,_TileFactor越大,模型上的素描线条越密。_Hatch0到_Hatch5对应渲染时使用的6张素描纹理,它们的线条密度依次增大。
Shader中第一张Pass是复用之前的描边Pass。在第二个Pass的标签和相关的编译指令。由于我们需要顶点着色器中计算6张纹理的混合权重,我们使用hatchWeights0和hatchWeights1来存储这些混合权重。并且我们添加阴影所需要的设置。
对于顶点函数来说。我们首先对顶点进行基本的坐标变换。我们采样的uv坐标也不是像之前那样使用TRANSFORM_TEX而是直接使用_TileFactor
*
uv坐标。而在计算权重之前,我们首先要计算出这个顶点的漫反射系数值。然后我们将权重初始化为0,并将diff值缩放到[0,7]之间并用hatchFactor存储起来。我们将[0,7]的区间均匀划分为7个区间,通过判断hatchFactor所在的子区间来计算对应的纹理混合权重。
在片元函数中,我们使用之前得到的混合权重对每张纹理进行采样并和它们对应的权重值相乘得到每张纹理的采样颜色。我们还计算了纯白在渲染中的贡献度,这是通过从1中减去所有6张纹理的权重来得到的。这是因为素描中往往有留白的部分,因此我们希望在最后的渲染中光照最亮的部分是纯白色。最后我们混合各个颜色值并返回最终结果。
PS:素描Shader中很多公式只能看原来的论文才能懂了,只是我还没有找到原来的论文。所以我也是不明白为什么权重那样计算,后面计算纯白贡献又是那样计算的。