之前看到这个效果觉得很有意思,正好最近在学习水体渲染,就想自己实现一下。
做出来总感觉差了点意思,可能是参数或者法线贴图的问题?(疯狂甩锅x)
说起水体渲染,首先得让顶点波动起来,这里使用了《GPU Gems》里介绍的Gerstner波,使用了两个Gerstner波进行叠加
Gerstner波公式:
float3 ComputeWavePos(float3 vert)
{
float PI = 3.14159f;
float2 L = float2(max(_Lx, 0.0001), max(_Ly, 0.0001));
float2 w = float2(2 * PI / L.x, 2 * PI / L.y);
float2 phi = float2(_Sx, _Sy) * w;
float3 pos1 = float3(0, 0, 0);
float3 pos2 = float3(0, 0, 0);
pos1.x = _Q * _Ax * _D.x * cos(w.x * dot(float2(_D.x, _D.y), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.x = _Q * _Ay * _D.z * cos(w.y * dot(float2(_D.z, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
pos1.z = _Q * _Ax * _D.y * cos(w.x * dot(float2(_D.x, _D.y), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.z = _Q * _Ay * _D.w * cos(w.y * dot(float2(_D.z, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
pos1.y = _Ax * sin(w.x * dot(float2(_D.x, _D.z), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.y = _Ay * sin(w.y * dot(float2(_D.y, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
float3 pos = float3(vert.x + pos1.x + pos2.x, vert.y + pos1.y + pos2.y, vert.z + pos1.z + pos2.z);
return pos;
}
在顶点着色器里把顶点世界坐标代入公式,调整一下参数得到这样一坨抹布:
接下来要为这片水(抹布?)加上反射光,营造水面波光粼粼的感觉
首先采样法线贴图,因为在上一步仅仅是改变了顶点的位置,法线都还是原来的根根朝上。与其费时费力的转换法线还效果不佳,不如直接采样一张法线贴图。
在顶点着色器里求得切线空间到世界空间的转换矩阵,传给片元着色器
v2f vert(a2v v)
{
v2f o;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 wavePos = ComputeWavePos(worldPos);
worldPos = wavePos;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBitangent = cross(worldNormal, worldTangent) * v.tangent.w;
o.pos = mul(UNITY_MATRIX_VP, float4(wavePos, 1));
o.uv = TRANSFORM_TEX(v.texcoord, _Bump) * worldNormal.y;
o.Ttw0 = float4(worldTangent.x, worldBitangent.x, worldNormal.x, worldPos.x);
o.Ttw1 = float4(worldTangent.y, worldBitangent.y, worldNormal.y, worldPos.y);
o.Ttw2 = float4(worldTangent.z, worldBitangent.z, worldNormal.z, worldPos.z);
return o;
}
在片元着色器里使用时间和速度参数对采样坐标uv进行扰动,再对法线贴图进行采样反映射并转换到世界空间
float2 speed = half2(_BumpSpeed, _BumpSpeed) * _Time.y;
fixed3 bump1 = UnpackNormal(tex2D(_Bump, i.uv - speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_Bump, i.uv + speed)).rgb;
fixed3 bump = normalize(bump1 + bump2);
bump.xy *= _BumpSize;
bump.z = sqrt(1 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(bump, i.Ttw0.xyz), dot(bump, i.Ttw1.xyz), dot(bump, i.Ttw2.xyz)));
可是面片依然没有发生任何变化,那是因为此时还没有进行光照计算,凭借肉眼是无法观测法线的!
在片元着色器里使用视线关于刚刚获得的法线贴图里的法线得到的反射线,根据光路可逆原则,这个反射线就是入射光线。使用入射光线对Cubemap(这里使用的是摄像机生成的Cubemap)进行采样,得到反射颜色。把反射颜色输出,就可以看到面片表面变成波光粼粼了
float3 reflDir = reflect(-worldViewDir, bump);
half3 reflColor = texCUBE(_Cubemap, reflDir).rgb
到这一步,突然发现有个巨大的问题——图中的是一坨水,到现在我只得到了一片水……
使用这片水与Cube组合吧,因为水会动,边缘不能很好的吻合。
直接把材质赋给Cube吧,因为Unity里自带的Cube点很少,在顶点着色器里计算的波动效果很差。
最终我自己去maya做了一个顶面顶点多,其他面顶点少的Cube,这样虽然不光水面动,水体也动,但是因为波动比较小,水体的顶点也少,就忽略不计吧……
Cube:
赋给这个Cube刚才写的材质
Cube变成一大块镜子,我们只想在顶面进行Cubemap的采样,而不想在侧面也进行采样,侧面就保持原颜色加上折射就够了。
那么Cube本身的法线在这个时候就派上用场了,不是从法线贴图里得到的法线,而是Cube本身的法线。Cube侧面的法线的y分量都是0,而顶面的法线的y分量都是1,直接把reflColor乘上worldNormal的y分量即可
half3 reflColor = texCUBE(_Cubemap, reflDir).rgb * worldNormal.y;
侧面的颜色用_Color乘以 1-去worldNormal的y分量就可以了
half3 lateralColor = _Color * (1 - worldNormal.y);
接下来要做折射的模拟,光线经过两个密度不同的介质时会发生偏转,这就是光的折射。
使用GrabPass得到屏幕纹理,再使用ComputeGrabScreenPos函数计算得到的屏幕坐标对其进行采样,并用法线纹理的法线进行扰动,以模拟折射现象。
注意:因为顶面法线使用的是法线纹理储存的法线,而侧面使用的是原物体的法线,所以要计算两个折射颜色
GrabPass{ "_RefractionTex" }
float2 upOffset = bump.xy * _RefractionTex_TexelSize.xy * _RefractionScale;
float2 upScrPos = upOffset * i.scrPos.z + i.scrPos.xy;
fixed3 upRefrColor = tex2D(_RefractionTex, upScrPos / i.scrPos.w).rgb;
float2 lateralOffset = worldNormal.xy * _RefractionTex_TexelSize.xy * _RefractionScale;
float2 lateralScrPos = lateralOffset * i.scrPos.z + i.scrPos.xy;
fixed3 lateralRefrColor = tex2D(_RefractionTex, lateralScrPos / i.scrPos.w).rgb;
菲涅尔现象:视线垂直于表面时,反射较弱,而当视线非垂直表面时,视线与表面的夹角越小,反射越明显。
使用公式
计算反射光reflColor和折射光refrColor的混合参数fresnel
fixed fresnel = pow(1 - saturate(dot(worldViewDir, bump)), 5);
fixed3 upColor = reflColor * fresnel + refrColor * (1 - fresnel);
加上反射折射后,记得关掉水体的阴影投射和接收。最后使用worldNormal.y作为参数,混合侧面颜色和顶面颜色就大功告成。
fixed3 upColor = _UpColor * (reflColor * fresnel + upRefrColor * (1 - fresnel));
fixed3 lateralColor = lateralRefrColor * _LateralColor;
return fixed4(upColor * worldNormal.y + lateralColor * (1 - worldNormal.y), 1);
Shader源码
Shader "MyShaderTest/4_WaterEffect"
{
Properties
{
_Bump("Bump",2D) = "bump" {}
_BumpSize("Bump Size",float) = 1
_BumpSpeed("Bump Speed",Range(0,2)) = 1
_Cubemap("Cubemap",Cube) = "_Skybox" {}
_UpColor("Up Color",Color) = (1,1,1,1)
_LateralColor("Lateral Color",Color) = (1,1,1,1)
_RefractionScale("Refraction Scale",float) = 1
_Q("Q",float) = 1
_D("波12方向",vector) = (1,1,1,1)
_Ax("波1振幅",float) = 1
_Ay("波2振幅",float) = 1
_Lx("波1波长",float) = 1
_Ly("波2波长",float) = 1
_Sx("波1波速",float) = 1
_Sy("波2波速",float) = 1
}
SubShader
{
Tags{ "RenderType" = "Opaque" }
GrabPass{ "_RefractionTex" }
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 scrPos : TEXCOORD1;
float4 Ttw0 : TEXCOORD2;
float4 Ttw1 : TEXCOORD3;
float4 Ttw2 : TEXCOORD4;
};
sampler2D _Bump;
float4 _Bump_ST;
half _BumpSize;
half _BumpSpeed;
samplerCUBE _Cubemap;
fixed4 _UpColor;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
float _RefractionScale;
fixed4 _LateralColor;
float _Q;
float4 _D;
float _Ax;
float _Ay;
float _Lx;
float _Ly;
float _Sx;
float _Sy;
float3 ComputeWavePos(float3 vert)
{
float PI = 3.14159f;
float2 L = float2(max(_Lx, 0.0001), max(_Ly, 0.0001));
float2 w = float2(2 * PI / L.x, 2 * PI / L.y);
float2 phi = float2(_Sx, _Sy) * w;
float3 pos1 = float3(0, 0, 0);
float3 pos2 = float3(0, 0, 0);
pos1.x = _Q * _Ax * _D.x * cos(w.x * dot(float2(_D.x, _D.y), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.x = _Q * _Ay * _D.z * cos(w.y * dot(float2(_D.z, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
pos1.z = _Q * _Ax * _D.y * cos(w.x * dot(float2(_D.x, _D.y), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.z = _Q * _Ay * _D.w * cos(w.y * dot(float2(_D.z, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
pos1.y = _Ax * sin(w.x * dot(float2(_D.x, _D.z), float2(vert.x, vert.z)) + phi.x * _Time.y);
pos2.y = _Ay * sin(w.y * dot(float2(_D.y, _D.w), float2(vert.x, vert.z)) + phi.y * _Time.y);
float3 pos = float3(vert.x + pos1.x + pos2.x, vert.y + pos1.y + pos2.y, vert.z + pos1.z + pos2.z);
return pos;
}
v2f vert(a2v v)
{
v2f o;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 wavePos = ComputeWavePos(worldPos);
worldPos = wavePos;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBitangent = cross(worldNormal, worldTangent) * v.tangent.w;
o.pos = mul(UNITY_MATRIX_VP, float4(wavePos, 1));
o.uv = TRANSFORM_TEX(v.texcoord, _Bump) * worldNormal.y;
o.scrPos = ComputeGrabScreenPos(o.pos);
o.Ttw0 = float4(worldTangent.x, worldBitangent.x, worldNormal.x, worldPos.x);
o.Ttw1 = float4(worldTangent.y, worldBitangent.y, worldNormal.y, worldPos.y);
o.Ttw2 = float4(worldTangent.z, worldBitangent.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3 worldPos = float3(i.Ttw0.w,i.Ttw1.w,i.Ttw2.w);
float3 worldNormal = float3(i.Ttw0.z, i.Ttw1.z, i.Ttw2.z);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float2 speed = half2(_BumpSpeed, _BumpSpeed) * _Time.y;
fixed3 bump1 = UnpackNormal(tex2D(_Bump, i.uv - speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_Bump, i.uv + speed)).rgb;
fixed3 bump = normalize(bump1 + bump2);
bump.xy *= _BumpSize;
bump.z = sqrt(1 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(bump, i.Ttw0.xyz), dot(bump, i.Ttw1.xyz), dot(bump, i.Ttw2.xyz)));
float3 reflDir = reflect(-worldViewDir, bump);
half3 reflColor = texCUBE(_Cubemap, reflDir).rgb;
float2 upOffset = bump.xy * _RefractionTex_TexelSize.xy * _RefractionScale;
float2 upScrPos = upOffset * i.scrPos.z + i.scrPos.xy;
fixed3 upRefrColor = tex2D(_RefractionTex, upScrPos / i.scrPos.w).rgb;
float2 lateralOffset = worldNormal.xy * _RefractionTex_TexelSize.xy * _RefractionScale;
float2 lateralScrPos = lateralOffset * i.scrPos.z + i.scrPos.xy;
fixed3 lateralRefrColor = tex2D(_RefractionTex, lateralScrPos / i.scrPos.w).rgb;
fixed fresnel = pow(1 - saturate(dot(worldViewDir, bump)), 5);
fixed3 upColor = _UpColor * (reflColor * fresnel + upRefrColor * (1 - fresnel)) * worldNormal.y;
fixed3 lateralColor = lateralRefrColor * _LateralColor * (1 - worldNormal.y);
return fixed4(upColor + lateralColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}