Unity Shader学习记录(十一)

Unity Shader学习记录(十一)

  关于风格化渲染的东西内容非常丰富,除了以前提过的卡通风格之外,素描风格也是一种有趣的效果,它的原理是通过光照信息采样几张不同的,代表笔触的纹理贴图,并以采样结果作为效果渲染到画面上;这样一来就如同是真的实现了画笔效果那样,光照明亮的地方没什么笔画,阴影的地方笔画很密。
  此外还有一类重要的效果,那就是噪声(Noise),这一类效果泛用性很强,无论是腐蚀,灼烧,水纹乃至雾效都可以应用这种效果。
  噪声也经常和时间配合起来做出简单的动态效果。


素描风格渲染

  素描风格渲染本质上是根据光照信息在不同的笔触纹理贴图中采样,因此首先要准备相关的资源,示例中准备了六张不同笔触的素描笔画贴图。
  Shader部分的代码如下

Shader "Custom/HatchStyleShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _TileFactor ("Tile Factor", Float) = 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 { "RebderType"="Opaque" "Queue"="Geometry"}
        UsePass "Custom/OutlineShader/OUTLINE"
        Pass {
            Tags { "LightMode"="ForwardBase"}
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            #pragma multi_compile_fwdbase

            fixed4 _Color;
            float _TileFactor;
            sampler2D _Hatch0;
            sampler2D _Hatch1;
            sampler2D _Hatch2;
            sampler2D _Hatch3;
            sampler2D _Hatch4;
            sampler2D _Hatch5;

            struct a2v {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv :TEXCOORD0;
                fixed3 hatchWeights0 : TEXCOORD1;
                fixed3 hatchWeights1 : TEXCOORD2;
                float3 worldPos : TEXCOORD3;
                SHADOW_COORDS(4)
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord.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.0;
                // 根据漫反射情况决定笔触权重
                if(hatchFactor > 6.0) {
                    // Nothing
                } else if(hatchFactor > 5.0) {
                    o.hatchWeights0.x = hatchFactor - 5.0;
                } else if(hatchFactor > 4.0) {
                    o.hatchWeights0.x = hatchFactor - 4.0;
                    o.hatchWeights0.y = 1.0 - o.hatchWeights0.x;
                } else if(hatchFactor > 3.0) {
                    o.hatchWeights0.y = hatchFactor - 3.0;
                    o.hatchWeights0.z = 1.0 - o.hatchWeights0.y;
                } else if(hatchFactor > 2.0) {
                    o.hatchWeights0.z = hatchFactor - 2.0;
                    o.hatchWeights1.x = 1.0 - o.hatchWeights0.z;
                } else if(hatchFactor > 1,0) {
                    o.hatchWeights1.x = hatchFactor - 1.0;
                    o.hatchWeights1.y = 1.0 - o.hatchWeights1.x;
                } else {
                    o.hatchWeights1.y = hatchFactor;
                    o.hatchWeights1.z = 1.0 - 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.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

  需要注意的是描边的Pass直接使用了一个写好的Shader中的一部分,之前的文章里有相关的叙述,描边采用的是最简单的模型沿着法线外扩的方式。
  通过这个Shader渲染出来的模型在阴影部分就能看到笔画的痕迹,TileFactor设置越大则笔画越细,看起来也就越密集,实测表明设置为8的时候已经相当接近素描风格了。


消融效果:噪声的使用

  有些时候向规则的事物中添加“杂乱无章”的东西往往能造就一些奇妙的效果,而这种“杂乱无章”很多时候就是指的噪声(Noise)。这个概念最早来自物理学,指代一种发声体做无规则振动时发出的声音;后来被无线电通信借用过去,用于表达一种干扰信号。
  时至今日,很多地方都借用了“噪声”这个有些古老的概念,而其真正指代的东西却是五花八门的,但总结起来它们都会有个共同点,那就是“无规律的变化”。
  消融效果在游戏中并不少见,常用于表达角色死亡,物体损毁或消失等场景。在这些效果中,消融往往从各个区域开始,并以看似随机的方向扩张,最后整个物体消失不见。
  这样的效果实现起来方法多种多样,但最简单的一种无非就是“贴图+透明度测试”,使用噪声纹理采样的结果和消融阈值进行比较,如果超过阈值了则保留,达不到阈值则用clip函数裁减掉,而镂空区域的灼烧效果则是颜色混合的结果。
  要实现这个效果,所需的Shader代码如下

Shader "Custom/DissolveEffectShader" {
    Properties {
        _BurnAmount ("Burn Amount", Range(0.0, 1.0)) = 0.0
        _LineWidth ("Burn Line Width", Range(0.0, 2.0)) = 0.1
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}
        _BurnFirstColor ("Burn First Color", Color) = (1,0,0,1)
        _BurnSecondColor ("Burn Second Color", COlor) = (1,0,0,1)
        _BurnMap ("Burn Map", 2D) = "white" {}
    }
    SubShader {
        Pass {
            Tags { "LightMode"="ForwardBase"}
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase

            struct a2v {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uvMainTex : TEXCOORD0;
                float2 uvBumpTex : TEXCOORD1;
                float2 uvBurnTex : TEXCOORD2;
                float3 lightDir : TEXCOORD3;
                float3 worldPos : TEXCOORD4;
                SHADOW_COORDS(5)
            };

            sampler2D _MainTex;
            sampler2D _BumpMap;
            sampler2D _BurnMap;
            float4 _MainTex_ST;
            float4 _BumpMap_ST;
            float4 _BurnMap_ST;
            float _BurnAmount;
            float _LineWidth;
            fixed4 _BurnFirstColor;
            fixed4 _BurnSecondColor;

            v2f vert (a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uvBumpTex = TRANSFORM_TEX(v.texcoord, _BumpMap);
                o.uvBurnTex = TRANSFORM_TEX(v.texcoord, _BurnMap);
                TANGENT_SPACE_ROTATION;
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed3 burn = tex2D(_BurnMap, i.uvBurnTex).rgb;
                clip(burn.r - _BurnAmount);
                float3 tangentLightDir = normalize(i.lightDir);
                float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpTex));
                fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
                fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount);
                fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);
                burnColor = pow(burnColor, 5);
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));
                return fixed4(finalColor, 1);
            }
            ENDCG
        }
    }
}

  使用标准的切线空间下法线贴图来表现物体的表面,并在此基础上添加了一个噪声贴图层,使用两个颜色值来进行混合得到灼烧的色彩。注意到片元计算函数中的clip函数,那便是噪声贴图的应用,将采样结果与指定的阈值进行比较,小于阈值的片元直接裁剪掉。
  注意到混合颜色的时候使用了smoothstep函数,这个函数和lerp函数的作用相似,但它不是线性插值的,而是三次曲线的插值方式,这样能让色彩混合更平滑一些。
  新建材质并挂上这个Shader后调整BurnAmount的滑块就能看到效果了,如果想要做出动画效果,只需要通过脚本控制BurnAmount值或者就在Shader中使用时间变量来进行计算即可。
  值得一提的是,这种实现方法中,消融的效果完全取决于噪音纹理和颜色混合,因此实际操作中需要在美术人员的指导下进行配置。


水面波纹:运动的噪声

  噪声纹理的另一个作用就是用来模拟水波,由于水面很多时候都呈现出一种随机波动的状态,而要通过模型动画来展示这种波动的话,对模型的制作以及美术要求都相当高。
  很多时候,水面都是通过一块简单的平面来渲染的,表面的凹凸可以用法线贴图搞定,水波也可以用纹理来表现;但这并不足以表达“水”这个概念,因为水是透明的,是会流动的,它的表面会随着时间的变化而变化。
  渲染透明物体需要考虑折射和反射两个现象,反射可以通过视角向量和法线向量解决,但折射就必须使用GrabPass了,这个预定义的Pass会抓取当前已经渲染出来的画面并输入到一个虚拟的贴图采样器中,使其可以在下一个Pass中作为纹理贴图来使用。
  折射的原理就是通过GrabPass抓取到水面覆盖上去之前的场景,将其渲染为一张贴图纹理,在后续的Pass中使用这个临时贴图来采样水下部分的颜色。
  有了折射和反射,最后只需要混合这两个色彩即可,混合的方式除了定值混合之外,还可以使用菲涅尔公式来计算混合参数,得到动态混合的效果。
  有了以上的基础,下一个问题就是如何让水流动起来,Shader结合时间参数是一个靠谱的选择,但此时还有一个问题需要解决;Shader中自带的时间参数是线性的,通过它来制作有规律的变化很简单,比如正弦波之类的。但水波这种不规律的动作就需要一点额外的支持了,毕竟在Shader中应用随机数并不是个好主意,而且所需的计算也太过复杂。
  噪声贴图就可以用来模拟“无规律波动”这个概念,基本概念就是使用噪声贴图替代法线贴图,并且在计算过程中对顶点做出一定修改即可。
  Shader的代码如下

Shader "Custom/WaterWaveShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _WaveMap ("Wave Map", 2D) = "bump" {}
        _Cubemap ("Enviroment Cubemap", Cube) = "_Skybox" {}
        _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01
        _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
        _Distortion ("Distortion", Range(0, 100)) = 10
    }
    SubShader {
        Tags { "Queue"="Tansparent" "RenderType"="Opaque" }
        GrabPass { "_RefractionTex" }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _WaveMap;
            float4 _WaveMap_ST;
            samplerCUBE _Cubemap;
            fixed _WaveXSpeed;
            fixed _WaveYSpeed;
            float _Distortion;
            sampler2D _RefractionTex;
            float4 _RefractionTex_TexelSize;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 scrPos : TEXCOORD0;
                float4 uv : TEXCOORD1;
                float4 TtoW0 : TEXCOORD2;
                float4 TtoW1 : TEXCOORD3;
                float4 TtoW2 : TEXCOORD4;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.scrPos = ComputeGrabScreenPos(o.pos);
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap);
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);

                fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + speed)).rgb;
                fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - speed)).rgb;
                fixed3 bump = normalize(bump1 + bump2);

                float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
                fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
                fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed);
                fixed3 reflDir = reflect(-viewDir, bump);
                fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb * _Color.rgb;
                fixed3 fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);
                fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
                return fixed4(finalColor, 1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

  可以看到,在顶点计算函数中基本是照搬了世界空间下的法线贴图顶点处理,仅有的不同是使用了ComputeGrabScreenPos来得到GrabPass抓取的纹理图的坐标信息。
  而片元计算函数中就使用了噪声贴图来替代法线贴图,同时也用噪声信息修改了顶点的位置,这样一来水面的坐标和法线都会随着时间的变化而变化,也就是“流动”起来了。
  最后的菲涅尔公式计算混合参数,将折射和反射的颜色混合完成后,水面的渲染就结束了。

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/81288065