如何做一個水面扭曲效果
如何做一個水面扭曲效果
本文參考教程,並加上自己的一些心得體會。所謂的扭曲效果,就是將給定的紋理貼圖進行取樣,並隨著時間進行流動,使得每個點的取樣路徑不完全一樣。為了做到這一點,首先我們需要一張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
}
}
}