如何做一个水面扭曲效果

本文参考教程,并加上自己的一些心得体会。所谓的扭曲效果,就是将给定的纹理贴图进行采样,并随着时间进行流动,使得每个点的采样路径不完全一样。为了做到这一点,首先我们需要一张flow map,用来保存每个点的采样方向,然后根据时间偏移:

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

运行查看,发现随着时间推移,画面越来越细碎,这时我们需要让表现有个周期性,就是画面能够在一个周期回到初始的状态。

如何做一个水面扭曲效果1.png
如何做一个水面扭曲效果2.png

我们可以使用frac函数来对时间进行约束,使得周期控制在1秒:

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

这样画面总会在1s后回到初始的状态,但是会带来新的问题,就是回到初始状态是一个跳变的过程,会让画面闪一下:

如何做一个水面扭曲效果3.gif

为了解决这个问题,我们可以加上淡入淡出的效果来缓解这个过渡:

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;
如何做一个水面扭曲效果4.gif

通过观察,我们发现这个淡入淡出是整个画面一起的,有些单调,可以让它们的步调稍稍不一致。因为我们的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;
如何做一个水面扭曲效果5.gif

接下来,我们希望把单次扰动的效果进行叠加,同时使用两次扰动,当然两次扰动的步调是不同的,在时间上存在一个相位差。这里我们将其设置为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;
}
如何做一个水面扭曲效果6.gif

但这样看上去周期重复的感觉很明显,为了淡化这种表现,我们可以再给uv加上偏移参数,让采样的uv需要过很久的时间才会重复:

uv = uv + float2(_UJump, _VJump) * (_Time.y - progress);
如何做一个水面扭曲效果7.gif

接下来,我们还可以给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,删去了没用的代码,得到最终的效果如下:

如何做一个水面扭曲效果8.gif

完整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
        }
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343