在模擬真實水面過程中,我們往往也會使用噪聲紋理。此時噪聲紋理通常會用做一個高度圖,以不斷修改水面的法線方向。爲了模擬水不斷流動的效果,我們會使用和時間相關的變量來對噪聲紋理進行採樣,當得到法線信息後,在進行正常的反射+折射計算,得到最後的水面波動效果。
本篇文章會使用一個噪聲紋理得到的法線貼圖,實現一個包含菲涅耳反射的水面效果,如下圖:
我們使用一張立方體紋理(cubemap)作爲環境紋理,模擬反射。爲了模擬折射效果,我們使用GrabPass來獲取當前屏幕的渲染紋理,並使用切線空間下的法線方向對像素的屏幕座標進行偏移,再使用該座標對渲染紋理進行屏幕採樣,從而模擬近似的折射效果,其中水波的法線紋理是由一張噪聲紋理生成而得,而且會隨着時間變化不斷平移,模擬波光粼粼的效果。除此之外,我們沒有使用一個定值來混合反射和折射顏色,而且使用之前提到的菲涅耳係數來動態決定混合係數,使用如下公式來計算菲涅耳係數:
fresnel=pow(1-max(0,v·n),4)
其中,v和n分別對應了視角方向和法線方向。它們之間的夾角越小,fresnel值越小,反射越弱,折射越強。菲涅耳係數還經常用於邊緣光照的計算中。
實現:
1.聲明屬性
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
_WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
_Distortion ("Distortion", Range(0, 100)) = 10
}
其中,_Color用於控制水面的顏色;_MainTex是水面波紋材質紋理,默認爲白色紋理;_WaveMap是一個由噪聲紋理生成的法線紋理;_Cubemap是用於模擬反射的立方體紋理;_Distortion則用於控制模擬折射時圖像的扭曲程度;_WaveXSpeed和_WaveYSpeed分別用於控制法線紋理在X和Y方向上的平移速度。
2.定義相應的渲染隊列,並使用GrabPass來獲取屏幕圖像:
SubShader {
// We must be transparent, so other objects are drawn before this one.
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
// This pass grabs the screen behind the object into a texture.
// We can access the result in the next pass as _RefractionTex
GrabPass { "_RefractionTex" }
首先在Subshader標籤將渲染隊列設置成Transparent,並把後面的RenderType設置爲Opaque。把Queue設置成Transparent可以確保該物體渲染時,其他所有不透明物體都已經被渲染到屏幕上了,否則就可能無法正確得到“透過水麪看到的圖像”。而設置RenderType則是爲了在使用着色器替換時,該物體可以在需要時被正確渲染。這通常發生在我們需要得到攝像機的深度和法線紋理時。隨後利用關鍵詞GrabPass定義了一個抓取屏幕圖像的Pass。字符串決定了抓取得到的屏幕圖像將會被存入哪個紋理中。
3.定義渲染水面所需 Pass,定義變量:
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;
注意我們還定義了_RefractionTex和_RefractionTex_TexelSize變量,這對應了在使用GrabPass時,指定的紋理名稱。_RefractionTex_TexelSize可以讓我們得到該紋理的紋素大小,例如一個大小爲256X512的紋理,它的紋素大小爲(1/256,1/512)。我們需要在對屏幕圖像的採樣座標進行偏移時使用該變量。
4.定義頂點着色器:
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 = mul(UNITY_MATRIX_MVP, 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).xyz;
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;
}
首先通過調用ComputeGrabScreenPos來得到對應被抓取屏幕圖像的採樣座標。接着計算了_MainTex和_BumpMap的採樣座標,並把它們分別存儲在一個float4類型變量的xy和zw分量中。由於我們需要在片元着色器中把法線方向從切線空間(由法線紋理採樣得到)變換到世界空間下,以便對CubeMap進行採樣,因此我們需要在這裏計算該頂點對應的從切線空間到世界空間的變換矩陣,並把該矩陣的每一行分別存儲在TtoW0、TtoW1和TtoW2的xyz分量中。這裏面使用的數學方法就行,得到切線空間下的三個座標軸(x、y、z軸分別對應了切線、副切線和法線的方向)在世界空間下的表示,再把它們依次按列組成一個變換矩陣即可。TwoW0等值的w分量同樣被利用起來,用於存儲世界空間下的頂點座標。
5.定義片元着色器:
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);
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);
}
首先通過TtoW0等變量的w分量得到世界座標,並用該值得到該片元對應的視角方向。除此以外,我們還使用內置的_Time.y變量和_WaveXSpeed、_WaveYSpeed屬性計算了法線紋理的當前偏移量,並利用該值對法線紋理進行兩次採樣(這是爲了模擬兩層交叉的水面波動的效果),對兩次結果相加並歸一化後得到切線空間下的法線方向。然後我們使用該值和_Distortion屬性以及_RefractionTex_TexelSize來對屏幕圖像的採樣座標進行偏移,模擬折射效果。_Distortion值越大,偏移量越大,水面背後的物體看起來變形程度越大。在這裏使用切線空間下的法線進行偏移,是因爲該空間下的法線可以反映頂點局部空間下的法線方向。需要注意的是,在計算偏移後的屏幕座標時,我們把偏移量額屏幕座標的z分量相乘,這是爲了模擬深度越大、折射程度越大的效果。如果不希望得到這個效果,可以直接把偏移值疊加到屏幕座標上。隨後對scrPos進行了透視除法,在使用該座標對抓取的屏幕圖像_RefractionTex進行採樣,得到模擬的折射顏色。
之後我們把法線方向從切線空間變換到了世界空間下(使用變換矩陣的每一行分別和法線方向點乘,構成新的法線方向),並據此得到視角方向相對法線方向的反射方向。隨後使用反射方向對Cubemap進行採樣,並把結果和主紋理顏色相乘後得到反射顏色。我們也對主紋理進行了紋理動畫,以模擬水波的效果。
爲了混合折射和反射顏色,我們隨後計算了菲涅耳係數,並據此來混合折射和反射顏色,作爲最終的輸出顏色。
本例中我們需要的是一張法線紋理,因此我們可以從該噪聲紋理的灰度值中生成需要的法線信息,這是通過在它的紋理面板中把紋理類型設置爲Normal map,並選中Create from grayscale來完成的。
最後,噪聲紋理是如果構建出來的?這些紋理可以被認爲是一種程序紋理,都是由計算機利用某些算法生成的。Perlin噪聲和Worley噪聲是兩種最常使用的噪聲類型,Perlin噪聲可以用於生成更自然的噪聲紋理,而Worley噪聲則通常用於模擬諸如石頭、水、紙張等多孔噪聲。Photoshop等圖像編輯軟件往往提供了類似的功能或插件,以幫助美術人員生成需要的噪聲紋理。