使用噪声 -- Shader入门精要学习(14)

15 使用噪声

1 消融效果

**消融(dissolve)**效果常见于游戏中的角色死亡、地图烧毁等效果。在这些效果中,消融往往从不同的区域开始,并向看似随机的方向扩张,最后整个物体都将消失不见。在本节中,我们将在 Unity 中实现这种效果。

原理非常简单,概括来说就是噪声纹理+透明度测试。我们使用噪声纹理采样的结果和某个控制消融程度的阈值比较,如果小于阈值,就使用 clip 函数把它对应的像素裁剪掉,这些部分就对应了图中被“烧毁”的区域。而镂空区域边缘的烧焦效果则是将两种颜色混合,再用 pow 函数处理后,与原纹理颜色混合后的的结果。

Shader "MyShader/15-Noise/Dissolve"
{
    Properties
    {
        _BurnAmount ("Burn Amount", Range(0.0, 1.0)) = 0.0           // 控制消融程度,为时为正常效果,为1时会完全消融
        _LineWidth ("Burn Line Width", Range(0.0, 0.2)) = 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
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry"}
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
            Cull Off  // 关闭面片剔除
            
            CGPROGRAM
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

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

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

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

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);  
                o.uvBumpMap = TRANSFORM_TEX(v.texcoord, _BumpMap);
                o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);           // 计算纹理坐标

                TANGENT_SPACE_ROTATION;                                      // 获取rotation矩阵
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;  // 计算切线空间的光线方向(用于计算法线纹理)
                o.worldPos = UnityObjectToWorldDir(v.vertex);
                TRANSFER_SHADOW(o);                                          // 计算世界空间下的顶点位置和阴影纹理的采样坐标
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;                                  // 对噪声纹理采样
                clip(burn.r - _BurnAmount);                                                      // 与阈值进行比较并裁剪

                float3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpMap));  // 对法线纹理采样

                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);                 // t为1时说明此时位于消融的边界处,为0时说明像素为正常的模型颜色
                fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);                   // 混合两种火焰的颜色

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);                                   // 计算光照阴影

                fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));  // _BurnAmount为0时不显示消融效果
                return fixed4(finalColor, 1);
            }
            
            ENDCG
        }
    
        // 由于使用透明度测试的的物体阴影需要特别处理,如果仍需要使用普通的阴影Pass,那么被剔除的区域仍然会向其他物体投射阴影,造成”穿帮“。为了让物体的阴影也能配合透明度测试产生的效果,我们需要自定义一个投射阴影的Pass
        Pass
        {
            Tags { "LightMode" = "ShadowCaster" }  // 用于阴影投射的Pass的LightMode需要被设置为ShadowCaster
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster  // 指明它需要的编译指令
            #include "UnityCG.cginc"

            sampler2D _BurnMap;
            float4 _BurnMap_ST;
            float _BurnAmount;
            
            struct v2f
            {    
                V2F_SHADOW_CASTER;  // 定义阴影投射需要定义的变量
                float2 uvBurnMap : TEXCOORD01;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);  // 填充V2F_SHADOW_CASTER中声明的变量
                o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
                return o;
            }
            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;  // 计算噪声纹理的坐标
                clip(burn.r - _BurnAmount);                      // 使用噪声纹理的采样结果剔除片元
                SHADOW_CASTER_FRAGMENT(i);                       // 完成阴影投射
            }
            ENDCG
        }    
    }
    FallBack "Diffuse"
}

Unity 提供的这 3 个内置宏(在 UnityCG.cginc )文件中被定义,我们可以方便地自定义需要的阴影投射的 Pass,但由于这些宏需要使用一些特定的输入变量,因此我们需要保证为它们提供了这些变量。例如 TRANSFER_SHADOW_CASTER_NORMALOFFSET 会使用名称为 v 作为输入结构体,v 中需要包含顶点位置 v.vertex 和顶点法线 v.normal 的信息,我们可以直接使用内置的 appdate_base 结构体,它包含了这些必需的顶点变量。如果我们需要顶点动画,可以在顶点着色器中直接修改 v.vertex,再传递给 TRANSFER_SHADOW_CASTER_NORMALOFFSET 即可。

可以利用辅助脚本修改材质的 _BurnAmount 值,来得到消融动画。使用不同的噪声和纹理属性都会得到不同的消融效果。

2 水波效果

在模拟水面的过程中,我们往往也会使用噪声纹理。此时,噪声纹理通常会用作一个高度图,以不断修改水面的法线方向。为了根本模拟水不断流动的效果,我们会使用和时间相关的变量来对噪声纹理进行采样,当得到法线信息后,再进行正常的反射 + 折射计算,得到最后的水面波动效果。

本节中,我们将使用一个由噪声纹理得到的法线贴图,实现一个包含菲涅尔反射的水面效果。

我们曾在之前介绍过如何使用反射和折射来模拟一个透明玻璃的效果,本节使用的 Shader 和之前的基本相同。我们使用一张立方体纹理(Cubemap)作为环境纹理,模拟反射。为了模拟折射效果,我们使用 GrabPass 来获取当前屏幕的渲染纹理,并使用切线空间下的法线方向对像素的屏幕坐标进行偏移,再使用该坐标对渲染纹理进行屏幕采样,从而模拟近似的折射效果。

水波的法线纹理是由一张噪声纹理生成而得,而且随着时间变化不断平移,模拟波光粼粼的效果。除此之外,我们没有使用一个定值来混合反射和折射颜色,而是使用之前菲涅尔系数来动态决定混合系数。我们使用的公式计算菲涅尔系数:
f r e s n e l = p o w ( 1 − m a x ( 0 , v ⋅ n ) , 4 ) fresnel = pow(1 - max(0, v \cdot n), 4) fresnel=pow(1max(0,vn),4)
其中,v 和 n 分别对应了视角方向和法线方向。它们之间的夹角越小,fresnel 值越小,反射越弱,折射越强。菲涅尔系数还经常会用于边缘光照的计算中。

Shader "MyShader/15-Noise/WaterWave"
{
    Properties
    {
        _Color ("Main Color", Color) = (0, 0.15, 0.115, 1)              // 控制水面颜色
        _MainTex ("Base (RGB)", 2D) = "white" {}                        // 水面材质纹理
        _WaveMap ("Wave Map", 2D) = "bump" {}                           // 由噪声纹理生成的法线纹理
        _CubeMap ("Environment Cubemap", Cube) = "_SkyBox" { }          // 用于模拟反射的立方体纹理
        _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01  // 控制法线在X方向上的平移速度 
        _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01    // 控制法线在Y方向上的平移速度
        _Distortion ("Distortion", Range(0, 100)) = 10
    }
    SubShader
    {
        Tags 
        { 
            "Queue" = "Transparent"  // "Queue" = "Transparent": 确保物体渲染时,其他所有不透明物体都已经被渲染到屏幕上了
            "RenderType" = "Opaque"  // "RenderType" = "Opaque": 在使用着色器替换(Render Replacement)时,该物体可以在需要时被正确渲染
        }
        
        GrabPass { "_RefractionTex" }  // 抓取到的屏幕图像将被存在_RefractionTex纹理中
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _WaveMap;
            float4 _WaveMap_ST;
            samplerCUBE _CubeMap;
            fixed _WaveXSpeed;
            fixed _WaveYSpeed;
            fixed _Distortion;
            sampler2D _RefractionTex;        // 使用GrabPass时指定的纹理名称
            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 = UnityObjectToWorldDir(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;                              // 对屏幕的采样坐标进行偏移,模拟折射效果,_Distortion值越大,偏移量越大,水面背后的物体看起来的变形程度越大
                // 使用切线空间下的法线方向进行偏移,该空间下的法线可以反映顶点局部空间下的法线方向
                i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;                                                  // 与屏幕坐标的z相乘,模拟深度越大,偏移量越大的效果,最后将偏移值叠加到屏幕坐标上
                fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;                             // 使用透视除法,并对_RefractionTex进行采样,得到模拟的折射颜色

                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));  // 将法线方向从切线空间变换到世界空间下
                fixed3 reflDir = reflect(-viewDir, bump);                                                         // 得到视角方向相对于法线方向的反射方向
                fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed);                                               // 对主纹理进行纹理动画,模拟水波效果
                fixed3 reflCol = texCUBE(_CubeMap, reflDir).rgb * texColor.rgb * _Color.rgb;                      // 使用反射方向对CubeMap进行采样,并把结果和主纹理颜色相乘后得到反射颜色

                fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);                                         // 计算菲涅尔系数
                fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
                return fixed4(finalColor, 1);
            }
            
            ENDCG
        } 
    }
}

3 再谈全局雾效

13.3 中讲到如何使用深度纹理来实现一种基于屏幕后处理的全局雾效。我们由深度纹理重建每个像素在世界空间下的位置,再使用一个基于高度的公式来计算雾效的混合系数,最后使用该系数来混合雾的颜色和原屏幕颜色。13.3 节的实现效果是基于高度的均匀雾效,即在同一个高度上,雾的浓度是相同的。然而一些时候我们希望可以模拟一种不均匀的雾效,同时让雾不断飘动,使雾看起来更飘渺。而这就可以通过一张噪声纹理来实现。

本节的实现非常简单,我们只是添加了噪声相关的参数和属性,并在 Shader 的片元着色器中对高度的计算添加了噪声的影响。

Shader "MyShader/15-Noise/FogWithNoise"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _FogDensity ("Fog Density", Float) = 1.0
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        _FogStart ("Fog Start", Float) = 0.0
        _FogEnd ("Fog End", Float) = 0.0
        _NoiseTex ("Noise Texture", 2D) = "white" {}
        _FogXSpeed ("Fog Horizontal Speed", Float) = 0.1
        _FogYSpeed ("Fog Vertical Speed", Float) = 0.1
        _NoiseAmount ("Noise Amount", Float) = 1
    }
    SubShader
    {
        CGINCLUDE
        #include "UnityCG.cginc"
        
        float4x4 _FrustumCornersRay;
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _CameraDepthTexture;
        half _FogDensity;
        fixed4 _FogColor;
        float _FogStart;
        float _FogEnd;
        sampler2D _NoiseTex;
        half _FogXSpeed;
        half _FogYSpeed;
        half _NoiseAmount;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0;
            half2 uv_depth : TEXCOORD1;
            float4 interpolatedRay : TEXCOORD2;
        };

        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;

            #if UNITY_UV_STARTS_AT_TOP // 处理平台差异
            if(_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y;
            #endif

            // 利用纹理坐标判断该点对应了四个角中的哪个角(个人认为这里世界坐标的获取不够准确)
            // 这里是将整个屏幕看作一个贴图,texcoord里面记录的是每个点对应的屏幕纹理
            int index = 0;
            if(v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
            {
                index = 0;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y < 0.5)
            {
                index = 1;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
            {
                index = 2;
            }
            else
            {
                index = 3;
            }

            #if UNITY_UV_STARTS_AT_TOP  // 处理平台差异
            if(_MainTex_TexelSize.y < 0)
                index = 3 - index;
            #endif
            o.interpolatedRay = _FrustumCornersRay[index];

            return o;
        }

        fixed4 frag(v2f i) : SV_Target
        {
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));  // 得到视角空间下的线性深度值
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;               // 计算世界空间下的位置
            float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);                                    // 计算当前雾效的偏移量
            float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;                      // 对噪声纹理进行采样,得到噪声值
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);                          // 计算像素高度对应的雾效系数
            fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));                              // 将雾效系数与设置的雾效强度、噪声进行混合,并把结果截取在[0, 1]范围内
            fixed4 finalColor = tex2D(_MainTex, i.uv);                                                  // 采样屏幕坐标,获取原颜色
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);                           // 将雾的颜色与实际颜色进行混合,得到最终颜色
            return finalColor;
        }
            
        ENDCG
        
        Pass 
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }   
    }
    FallBack Off
}
  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值