1. 程式人生 > 實用技巧 >如何做一個水面扭曲效果

如何做一個水面扭曲效果

如何做一個水面扭曲效果

本文參考教程,並加上自己的一些心得體會。所謂的扭曲效果,就是將給定的紋理貼圖進行取樣,並隨著時間進行流動,使得每個點的取樣路徑不完全一樣。為了做到這一點,首先我們需要一張flow map,用來儲存每個點的取樣方向,然後根據時間偏移:

float2 flowVector = tex2D(_FlowMap, i.uv);
float2 uv = i.uv + flowVector * _Time.y;
fixed4 col = tex2D(_MainTex, uv);
return col;

執行檢視,發現隨著時間推移,畫面越來越細碎,這時我們需要讓表現有個週期性,就是畫面能夠在一個週期回到初始的狀態。

我們可以使用frac函式來對時間進行約束,使得週期控制在1秒:

float2 uv = i.uv + flowVector * frac(_Time.y);

這樣畫面總會在1s後回到初始的狀態,但是會帶來新的問題,就是回到初始狀態是一個跳變的過程,會讓畫面閃一下:

為了解決這個問題,我們可以加上淡入淡出的效果來緩解這個過渡:

float progress = frac(_Time.y);
float2 uv = i.uv + flowVector * progress;
float w = 1 - 2 * abs(progress - 0.5);
fixed4 col = tex2D(_MainTex, uv) * w;

通過觀察,我們發現這個淡入淡出是整個畫面一起的,有些單調,可以讓它們的步調稍稍不一致。因為我們的flow map只用了rg兩個通道,這裡可以再使用a通道來代表每個取樣點的步調偏移(其實就是a通道放了一張noise map):

float4 flowSample = tex2D(_FlowMap, i.uv);
float2 flowVector = flowSample.rg * 2 - 1;
float flowNoise = flowSample.a;
float progress = frac(_Time.y + flowNoise);
float2 uv = i.uv + flowVector * progress;
float w = 1 - 2 * abs(progress - 0.5);
fixed4 col = tex2D(_MainTex, uv) * w;

接下來,我們希望把單次擾動的效果進行疊加,同時使用兩次擾動,當然兩次擾動的步調是不同的,在時間上存在一個相位差。這裡我們將其設定為0.5,使得上面w的權重之和為1:

float3 flowUVW(float2 uv, float offset)
{
    float4 flowSample = tex2D(_FlowMap, uv);
    float2 flowVector = flowSample.rg * 2 - 1;
    float flowNoise = flowSample.a;
    float progress = frac(_Time.y + flowNoise + offset);
    uv = uv + flowVector * progress;
    float w = 1 - 2 * abs(progress - 0.5);

    return float3(uv, w);
}

fixed4 frag (v2f i) : SV_Target
{
    float3 uvwa = flowUVW(i.uv, 0);
    float3 uvwb = flowUVW(i.uv, 0.5);
    fixed4 cola = tex2D(_MainTex, uvwa.xy) * uvwa.z;
    fixed4 colb = tex2D(_MainTex, uvwb.xy) * uvwb.z;
    return cola + colb;
}

但這樣看上去週期重複的感覺很明顯,為了淡化這種表現,我們可以再給uv加上偏移引數,讓取樣的uv需要過很久的時間才會重複:

uv = uv + float2(_UJump, _VJump) * (_Time.y - progress);

接下來,我們還可以給flow map加上tiling,加上引數控制uv隨時間偏移的速度,控制從flow map中取樣的方向向量強弱程度:

float progress = frac(_Time.y * _Speed + flowNoise + offset);
uv += flowVector * progress;
uv += float2(_UJump, _VJump) * (_Time.y - progress);
uv *= _Tiling;

另外,我們可以控制初始取樣的偏移,使得當w達到峰值時,即取樣權重最大時,對應的uv偏移到一個可以控制的位置:

uv += flowVector * (progress + _FlowOffset);

最後,我們為水面加上法線資訊,這裡使用了derivative map來計算水面的法線和高度資訊。derivative map的ag通道儲存了高度在兩個切線方向上的導數,b通道儲存了原始的高度。在此基礎上還可以繼續調製水面的高度,讓其與flow map的方向向量強弱掛鉤(流動越強,波浪越大,高度越大)。我們用flow map的b通道來儲存這一資訊,算出水面的高度:

float flowSpeed = flowSample.b * _FlowStrength;
float finalHeightScale = flowSpeed * _HeightScaleModulated + _HeightScale;

然後,我們的derivative map儲存了高度在兩個切線方向上的導數,在切線空間,高度對應的實際上是N法線這條軸,兩個切線方向向量分別為(1, 0, x),(0, 1, y),那麼叉乘即可得到法線為(-x, -y, 1)。

float3 flowNormal(float4 flowSample)
{
    float3 normal = flowSample.agb;
    normal.xy = -(normal.xy * 2 - 1);
    normal.z = 1;
    return normal;
}

float3 normala = flowNormal(tex2D(_DerivHeightMap, uvwa.xy)) * uvwa.z * finalHeightScale;
float3 normalb = flowNormal(tex2D(_DerivHeightMap, uvwb.xy)) * uvwb.z * finalHeightScale;
float3 normal = normalize(normala + normalb);

原教程用的surface shader,這裡將其轉成vert/frag shader,刪去了沒用的程式碼,得到最終的效果如下:

完整shader程式碼如下:

Shader "Custom/DistortionFlowShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        [NoScaleOffset] _FlowMap ("Flow (RG, A noise)", 2D) = "black" {}
        [NoScaleOffset] _DerivHeightMap ("Deriv (AG) Height (B)", 2D) = "black" {}
        _Color ("Color", Color) = (1,1,1,1)
        _UJump ("U jump per phase", Range(-0.25, 0.25)) = 0.25
		_VJump ("V jump per phase", Range(-0.25, 0.25)) = 0.25
        _Tiling ("Tiling", Float) = 1
        _Speed ("Speed", Float) = 1
        _FlowStrength ("Flow Strength", Float) = 1
        _FlowOffset ("Flow Offset", Float) = 0
        _HeightScale ("Height Scale, Constant", Float) = 0.25
		_HeightScaleModulated ("Height Scale, Modulated", Float) = 0.75
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "UnityPBSLighting.cginc"
            #include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float3 viewDir : TEXCOORD1;
                half3 sh : TEXCOORD2; // SH
                float4 worldPos : TEXCOORD3;
                UNITY_SHADOW_COORDS(4)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _FlowMap;
            sampler2D _DerivHeightMap;
            fixed4 _Color;
            float _UJump;
            float _VJump;
            float _Tiling;
            float _Speed;
            float _FlowStrength;
            float _FlowOffset;
            float _HeightScale;
            float _HeightScaleModulated;
            half _Glossiness;
		    half _Metallic;

            float3 flowUVW(float4 flowSample, float2 uv, float offset)
            {
                float2 flowVector = (flowSample.rg * 2 - 1) * _FlowStrength;
                float flowNoise = flowSample.a;
                float flowVar = _Time.y * _Speed + flowNoise + offset;
                float progress = frac(flowVar);
                uv += flowVector * (progress + _FlowOffset);
                uv += float2(_UJump, _VJump) * (flowVar - progress);
                uv *= _Tiling;
                float w = 1 - 2 * abs(progress - 0.5);

                return float3(uv, w);
            }

            float3 flowNormal(float4 flowSample)
            {
                float3 normal = flowSample.agb;
                normal.xy = -(normal.xy * 2 - 1);
                normal.z = 1;
                return normal;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = v.normal;
                o.tangent = v.tangent;
                o.viewDir = WorldSpaceViewDir(v.vertex);
                o.worldPos = v.vertex;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float4 flowSample = tex2D(_FlowMap, i.uv);
                float flowSpeed = flowSample.b * _FlowStrength;
                float finalHeightScale = flowSpeed * _HeightScaleModulated + _HeightScale;
                float3 uvwa = flowUVW(flowSample, i.uv, 0);
                float3 uvwb = flowUVW(flowSample, i.uv, 0.5);
                fixed4 cola = tex2D(_MainTex, uvwa.xy) * uvwa.z;
                fixed4 colb = tex2D(_MainTex, uvwb.xy) * uvwb.z;
                float3 normala = flowNormal(tex2D(_DerivHeightMap, uvwa.xy)) * uvwa.z * finalHeightScale;
                float3 normalb = flowNormal(tex2D(_DerivHeightMap, uvwb.xy)) * uvwb.z * finalHeightScale;
                float3 tangentNormal = normalize(normala + normalb);
                fixed4 texColor = (cola + colb) * _Color;

                float3 normal = normalize(i.normal);
                float4 tangent = normalize(i.tangent);
                float3 binormal = cross(normal, tangent) * tangent.w;
                float3x3 tangentToLocal = {
                    tangent.x,  binormal.x, normal.x,
                    tangent.y,  binormal.y, normal.y,
                    tangent.z,  binormal.z, normal.z
                };

                float3 worldNormal = normalize(UnityObjectToWorldNormal(mul(tangentToLocal, tangentNormal)));

                fixed4 c = 0;
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 worldViewDir = normalize(i.viewDir);
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)

                UnityGI gi;
                UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
                gi.indirect.diffuse = 0;
                gi.indirect.specular = 0;
                gi.light.color = _LightColor0.rgb;
                gi.light.dir = lightDir;

                UnityGIInput giInput;
                UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
                giInput.light = gi.light;
                giInput.worldPos = i.worldPos;
                giInput.worldViewDir = worldViewDir;
                giInput.atten = atten;
                giInput.probeHDR[0] = unity_SpecCube0_HDR;
                giInput.probeHDR[1] = unity_SpecCube1_HDR;
                #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION)
                    giInput.boxMin[0] = unity_SpecCube0_BoxMin;
                #endif

                SurfaceOutputStandard o;
                o.Albedo = texColor.rgb;
                o.Metallic = _Metallic;
			    o.Smoothness = _Glossiness;
                o.Emission = 0.0;
                o.Alpha = texColor.a;
                o.Occlusion = 1.0;
                o.Normal = worldNormal;

                LightingStandard_GI(o, giInput, gi);
                c += LightingStandard (o, worldViewDir, gi);
                UNITY_OPAQUE_ALPHA(c.a);
                return c;
            }
            ENDCG
        }
    }
}