上一节我们得到了一个简单但是功能完备的曲面细分着色器的模板,而且细心的朋友可能已经注意到上一节我们创建的shader的名字是tutorial/chapter_2/water,这次我们将在上一节的曲面细分着色器的基础上创建一个有物理波浪的水面材质。
最终我们会得到如下图的一个水面材质:
后续的文章里我们会对该shader继续进行扩展,增加浪花和水下焦散效果
按照惯例,我们从零碎的知识点开始,最终提供一个完整的shader示例代码。
要得到有几何波动的水面,作为水面载体的mesh必须有足够的精度,为了得到这样的精度,
使用曲面细分是最简单的方式,如果硬件不支持曲面细分,也可以直接让3D建模师制作一个高模的平面,或者使用一些策略,让离玩家最近的水面保持高模而远处的水面使用低模。
我们对之前的曲面细分着色器的 constant hull shader 部分进行修改,使得细分因子根据当前patch离相机的距离动态调节:
float3 GetDistanceBasedTessFactor(float3 p0, float3 p1, float3 p2, float3 cameraPosWS, float tessMinDist, float tessMaxDist)
{
float3 edgePosition0 = 0.5 * (p1 + p2);
float3 edgePosition1 = 0.5 * (p0 + p2);
float3 edgePosition2 = 0.5 * (p0 + p1);
// In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,
// so the compiler will be able to optimize distance() to length().
float dist0 = distance(edgePosition0, cameraPosWS);
float dist1 = distance(edgePosition1, cameraPosWS);
float dist2 = distance(edgePosition2, cameraPosWS);
// The saturate will handle the produced NaN in case min == max
float fadeDist = tessMaxDist - tessMinDist;
float3 tessFactor;
tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);
tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);
tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);
return tessFactor;
}
PatchTess ConstantHS(InputPatch<VertexOut,3> patch,uint patchID:SV_PrimitiveID)
{
int factor=15;
PatchTess pt;
float3 tessFactor=GetDistanceBasedTessFactor(patch[0].PosWS,patch[1].PosWS,patch[2].PosWS,GetCameraPositionWS(),0,50);
pt.EdgeTess[0]=max(2,factor*tessFactor.x);
pt.EdgeTess[1]=max(2,factor*tessFactor.y);
pt.EdgeTess[2]=max(2,factor*tessFactor.z);
pt.InsideTess=max(2,factor*(tessFactor.x+tessFactor.y+tessFactor.z)/3);
return pt;
}
关于Gerstner wave的论文可以参考 《GPU Gems 1》 Chapter 1: Effective
Water Simulation from Physical Models
具体不多讲我们直接沾上Gerstner波的公式以及求偏导计算副切线,切线,法线的公式:
对上面的函数分别关于x和y求偏导就得了副切线和切线公式
而切线和副切线叉乘可以得到法线
公式中S(),C(),WA如下
有了BTN,就得到了切线空间下空间坐标系的一组标准正交基
上述公式是从标准的正弦波公式引申而来,因此里边的一些系数都对应的正弦波的一些物理参数:
参数Q作为一个常量,用来描述波峰的锋锐程度。其他参数物理意义和正弦波一致。
而对于角频率W的计算也做了一点修改:
W = sqrt(g*2*PI/L)
注:这里PI表示圆周率常量,sqrt 是hlsl中的求二次方根的函数
前面公式中用到了求和符号,目的是使用多个不同系数的 Gerstner波,利用多波形的干涉原理,生成视觉上没有明显重复的复杂波形。
上述公式转换为代码如下
float3 generatePosOffset(float3 p,float Q,float A,float W,float2 D,float Phi)
{
float x=Q*A*D.x*cos(W*dot(D,p.xz)+Phi*_Time.y);
float z=Q*A*D.y*cos(W*dot(D,p.xz)+Phi*_Time.y);
float y=A*sin(W*dot(D,p.xz)+Phi*_Time.y);
return float3(x,y,z);
}
float3 generateNormalOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-D.x*WA*C;
float z=-D.y*WA*C;
float y=_Q*WA*S;
return float3(x,y,z);
}
float3 generateBinormalOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-_Q*D.x*D.x*WA*S;
float z=-_Q*D.x*D.y*WA*S;
float y=D.x*WA*C;
return float3(x,y,z);
}
float3 generateTangentOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-_Q*D.x*D.y*WA*S;
float z=-_Q*D.y*D.y*WA*S;
float y=D.y*WA*C;
return float3(x,y,z);
}
上述代码的计算得到的是3D坐标和TBN的偏移量,要得到最终的3D坐标和TBN向量需要加上原始值
float3 posWS=float3(positionWS.x,0,positionWS.z)+offset;
float3 binormal=float3(1,0,0)+binormalOffset;
float3 tangent=float3(0,0,1)+tangentOffset;
float3 normal=float3(0,1,0)+normalOffset;
我们的原始水面是一个完全光滑的平面,因此法线为(0,1,0),对应的切线和副切线(也可以成为副法线)分别为(0,0,1)和(1,0,0)。如果使用的不是平面模型,可以使用模型的法线,切线作为基准。
有了基于物理的几何波作为水面的框架结构,我们接下来利用纹理波(实际上就是水面波纹的法线贴图)来得到更加精细的结果,到这之后和常规的水面材质实现是类似的。
half2 rotateUV(float2 uv,float angle)
{
float cosAngle=cos(angle);
float sinAngle=sin(angle);
float2x2 rotateM=float2x2(cosAngle,-sinAngle,sinAngle,cosAngle);
return mul(rotateM,uv);
}
float2 uv0=rotateUV(IN.uv0,_WaveMapAngle);
引入 _WaveMapAngle 参数,用来调整纹理波的采样方向
这里的采样方向和波的移动方向是完全不相干的内容,目的是让纹理波的波形移动方向(视觉移动方向)和几何波的方向一致,可以理解为我们在着色器里沿着水面旋转了这张法线贴图。
为了得到更加精细的结果,可以使用两张不同的水面法线纹理,分别代表不同尺寸的波形,这里只是描述原理,因此对同一张纹理使用不同的UV采样两次得到的是类似的结果。
float4 animUV=uv0.xyxy*float4(1,1,0.5,0.5)+float4(normalize(_Direction.xy),normalize(_Direction.zw))*_WaveSpeed*_TexWaveSpeed*_Time.y;
float4 n1=SAMPLE_TEXTURE2D(_BumpMap,sampler_BumpMap,animUV.xy);
float4 n2=SAMPLE_TEXTURE2D(_BumpMap,sampler_BumpMap,animUV.zy);
同样为了让纹理波的速度和几何波的速度匹配,我们单独引入了 _TexWaveSpeed 参数,控制纹理波的速度,通过调整可以让纹理波的速度接近几何波的速度。
默认URP不会把深度RT和颜色RT保存起来,需要在管线的配置里打开该功能:
在shader中引用这两张RT的代码如下:
TEXTURE2D(_CameraDepthTexture); // z buffer
SAMPLER(sampler_CameraDepthTexture);
TEXTURE2D(_CameraOpaqueTexture); // color buffer
SAMPLER(sampler_CameraOpaqueTexture);
在Builtin管线里,我们要得到颜色缓冲区的内容,通常是调用GrabPass把渲染水面之前的渲染结果捕获到一张额外的RT里。
而在Builtin管线里要获得深度缓冲区内容,需要对相机参数进行设置。 而在移动端设备上,不一定能靠这个办法拿到深度缓冲区内容,因此通常采用把深度值离线烘焙到水面模型的顶点色或者单独的一张纹理里的办法来得到深度值。
有了这些基本信息,就可以像Builtin管线下实现水体材质的常规做法来创建水面材质了。
完整代码如下:
Shader "tutorial/chapter_2/water"
{
Properties
{
_Q("Q",Range(0,1))=0.1
_WaveLength("波长",Range(0.01,30))=0.1
_WaveSpeed("波速度",Range(0.01,1))=0.1
_TexWaveSpeed("纹理波速度",Range(0.01,1))=0.1
_TexWaveAmplitude("纹理波振幅",Range(0,1))=0.5
_AmplitudeRatio("振幅和波长的比例",Range(0.00001,0.002))=0.1
_Direction("方向12",Vector)=(1,1,1,1) // 移动方向
_Direction2("方向34",Vector)=(1,1,1,1) // 移动方向
_Direction3("方向56",Vector)=(1,1,1,1) // 移动方向
_Direction4("方向78",Vector)=(1,1,1,1) // 移动方向
_ShallowColor("浅水区颜色",Color)=(1,1,1,1)
_DeepColor("深水区颜色",Color)=(1,1,1,1)
_DeepWaterDepth("深水区深度",float)=3
_RimPower("RimPower",Range(1,20))=5
_Reflection("Reflection",Range(0,1))=1
_Cube("Cube",Cube)="black"{}
_BumpMap("MicroDetail",2D)="bump"{}
_BumpMapLarge("Large Dtail",2D)="bump"{}
_Smoothness("Smoothness",Range(0.1,2))=1
_SpecularColor("SpecularColor",Color)=(1,1,1,1)
_WaveMapAngle("WaveMapAngle",Range(0,3.14))=0
}
SubShader
{
Pass
{
Tags{"RenderType"="Opaque" "Queue"="Transparent"}
// ZWrite Off
// Cull Off
HLSLPROGRAM
#pragma target 5.0
#pragma vertex vert
#pragma hull HS
#pragma domain DS
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
float _AmplitudeRatio;
float _Q;
float _WaveLength;
float _WaveSpeed;
float4 _Direction;
float4 _Direction2;
float4 _Direction3;
float4 _Direction4;
half4 _ShallowColor;
half4 _DeepColor;
half _WaveMapAngle;
float _DeepWaterDepth;
float _TexWaveAmplitude;
float _TexWaveSpeed;
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
TEXTURE2D(_CameraOpaqueTexture);
SAMPLER(sampler_CameraOpaqueTexture);
TEXTURECUBE(_Cube);
SAMPLER(sampler_Cube);
TEXTURE2D(_BumpMap);
float4 _BumpMap_ST;
SAMPLER(sampler_BumpMap);
float _RimPower;
float _Reflection;
half4 _SpecularColor;
half _Smoothness;
struct app_data
{
float4 positionOS:POSITION;
float2 uv0:TEXCOORD0;
};
struct VertexOut
{
float3 PosL:TEXCOORD0;
float3 PosWS:TEXCOORD1;
float2 uv0:TEXCOORD2;
};
VertexOut vert(app_data IN)
{
VertexOut o;
o.PosL=IN.positionOS.xyz;
o.PosWS=TransformObjectToWorld(IN.positionOS.xyz);
o.uv0=IN.uv0*_BumpMap_ST.xy+_BumpMap_ST.zw;
return o;
}
struct PatchTess
{
float EdgeTess[3]:SV_TessFactor;
float InsideTess:SV_InsideTessFactor;
};
real3 GetDistanceBasedTessFactor(real3 p0, real3 p1, real3 p2, real3 cameraPosWS, real tessMinDist, real tessMaxDist)
{
real3 edgePosition0 = 0.5 * (p1 + p2);
real3 edgePosition1 = 0.5 * (p0 + p2);
real3 edgePosition2 = 0.5 * (p0 + p1);
// In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,
// so the compiler will be able to optimize distance() to length().
real dist0 = distance(edgePosition0, cameraPosWS);
real dist1 = distance(edgePosition1, cameraPosWS);
real dist2 = distance(edgePosition2, cameraPosWS);
// The saturate will handle the produced NaN in case min == max
real fadeDist = tessMaxDist - tessMinDist;
real3 tessFactor;
tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);
tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);
tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);
return tessFactor;
}
PatchTess ConstantHS(InputPatch<VertexOut,3> patch,uint patchID:SV_PrimitiveID)
{
int factor=15;
PatchTess pt;
float3 tessFactor=GetDistanceBasedTessFactor(patch[0].PosWS,patch[1].PosWS,patch[2].PosWS,GetCameraPositionWS(),0,200);
pt.EdgeTess[0]=max(2,factor*tessFactor.x);
pt.EdgeTess[1]=max(2,factor*tessFactor.y);
pt.EdgeTess[2]=max(2,factor*tessFactor.z);
pt.InsideTess=max(2,factor*(tessFactor.x+tessFactor.y+tessFactor.z)/3);
return pt;
}
struct HullOut
{
float3 PosL:TEXCOORD0;
float2 uv0:TEXCOORD1;
};
[domain("tri")]
[partitioning("fractional_even")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
HullOut HS(InputPatch<VertexOut,3> p,uint i:SV_OutputControlPointID)
{
HullOut hout;
hout.PosL=p[i].PosL;
hout.uv0=p[i].uv0;
return hout;
}
struct DomainOut
{
float4 PosH:SV_POSITION;
float4 binormalWS:TEXCOORD0;
float4 tangentWS:TEXCOORD1;
float4 normalWS:TEXCOORD2;
float4 projection:TEXCOORD3;
float2 uv0:TEXCOORD4;
};
float3 generatePosOffset(float3 p,float Q,float A,float W,float2 D,float Phi)
{
float x=Q*A*D.x*cos(W*dot(D,p.xz)+Phi*_Time.y);
float z=Q*A*D.y*cos(W*dot(D,p.xz)+Phi*_Time.y);
float y=A*sin(W*dot(D,p.xz)+Phi*_Time.y);
return float3(x,y,z);
}
float3 generateNormalOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-D.x*WA*C;
float z=-D.y*WA*C;
float y=_Q*WA*S;
return float3(x,y,z);
}
float3 generateBinormalOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-_Q*D.x*D.x*WA*S;
float z=-_Q*D.x*D.y*WA*S;
float y=D.x*WA*C;
return float3(x,y,z);
}
float3 generateTangentOffset(float3 positionWS,float _Q,float _A,float _W,float2 D,float _Phi)
{
float S=sin(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float C=cos(_W*dot(D,positionWS.xz)+_Phi*_Time.y);
float WA=_W*_A;
float x=-_Q*D.x*D.y*WA*S;
float z=-_Q*D.y*D.y*WA*S;
float y=D.y*WA*C;
return float3(x,y,z);
}
[domain("tri")]
DomainOut DS(PatchTess patchTess,float3 baryCoords:SV_DomainLocation,const OutputPatch<HullOut,3> triangles)
{
DomainOut dout;
float3 p=triangles[0].PosL*baryCoords.x+triangles[1].PosL*baryCoords.y+triangles[2].PosL*baryCoords.z;
float2 uv=triangles[0].uv0*baryCoords.x+triangles[1].uv0*baryCoords.y+triangles[2].uv0*baryCoords.z;
dout.uv0=uv;
float3 positionWS= TransformObjectToWorld(p.xyz);
float2 D=normalize(_Direction.xy);
float2 D2=normalize(_Direction.zw);
float2 D3=normalize(_Direction2.xy);
float2 D4=normalize(_Direction2.zw);
float2 D5=normalize(_Direction3.xy);
float2 D6=normalize(_Direction3.zw);
float2 D7=normalize(_Direction4.xy);
float2 D8=normalize(_Direction4.zw);
const float g=9.8f; //重力常数
//float _W=sqrt(9.8f*2*PI/_WaveLength); //角频率
//float _Phi= _WaveSpeed*_W; //相位常量
//float _A=_AmplitudeRatio*_WaveLength; // 波长和振幅通常成正比
// 正弦波函数
// positionWS.y=_A*sin(dot(D,positionWS.xz)*_W+_Time.y*_Phi);
float3 offset=float3(0,0,0);
float3 normalOffset=float3(0,0,0);
float3 binormalOffset=float3(0,0,0);
float3 tangentOffset=float3(0,0,0);
for(uint i=0;i<16;i++)
{
float _QQ=saturate(_Q*(1+0.05*i));
float _W=sqrt(9.8f*2*PI/(_WaveLength*(1+0.05*i)));
float _Phi=_WaveSpeed*(1+0.05*i)*_W;
float _A=_AmplitudeRatio*_WaveLength*(1+0.05*i);
float2 realD=D;
switch(i)
{
case 0:
case 8:
break;
break;
case 1:
case 9:
realD=D2;
break;
case 2:
case 10:
realD=D3;
break;
case 3:
case 11:
realD=D4;
break;
case 4:
case 12:
realD=D5;
break;
case 5:
case 13:
realD=D6;
break;
case 6:
case 14:
realD=D7;
break;
case 7:
case 15:
realD=D8;
break;
}
offset+=generatePosOffset(positionWS,_QQ,_A,_W,realD,_Phi);
binormalOffset+=generateBinormalOffset(positionWS,_QQ,_A,_W,realD,_Phi);
tangentOffset+=generateTangentOffset(positionWS,_QQ,_A,_W,realD,_Phi);
normalOffset+=generateNormalOffset(positionWS,_QQ,_A,_W,realD,_Phi);
}
float3 posWS=float3(positionWS.x,0,positionWS.z)+offset;
dout.PosH=TransformWorldToHClip(posWS);
float3 binormal=float3(1,0,0)+binormalOffset;
float3 tangent=float3(0,0,1)+tangentOffset;
float3 normal=float3(0,1,0)+normalOffset;
dout.tangentWS=float4(tangent.x,binormal.x,normal.x,posWS.y);
dout.binormalWS=float4(tangent.y,binormal.y,normal.y,posWS.x);
dout.normalWS=float4(tangent.z,binormal.z,normal.z,posWS.z);
// 这里计算深度使用变形前的坐标
float4 positionCS= dout.PosH;// TransformWorldToHClip(positionWS);
float4 ndc = positionCS * 0.5f;
float4 positionNDC;
positionNDC.xy= float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
positionNDC.zw = positionCS.zw;
dout.projection=positionNDC;
return dout;
}
half3 UnpackNormalBlend(half4 n1,half4 n2,half scale){
n1.w*=n1.r;
n2.w*=n2.r;
half3 normal;
normal.xy=(n1.wy*2-1)+(n2.wy*2-1);
normal.xy*=scale;
normal.z=sqrt(1-saturate(dot(normal.xy,normal.xy)));
return normalize(normal);
}
half SpecularFactor(half gloss,half3 lightDir,half3 viewDir,half3 normalDir)
{
half reflectiveFactor=max(0.0,dot(-viewDir,reflect(lightDir,normalDir)));
half spec=pow(reflectiveFactor,gloss*128);
return spec;
}
half2 rotateUV(float2 uv,float angle)
{
float cosAngle=cos(angle);
float sinAngle=sin(angle);
float2x2 rotateM=float2x2(cosAngle,-sinAngle,sinAngle,cosAngle);
return mul(rotateM,uv);
}
half4 frag(DomainOut IN):SV_Target
{
float4 projection=IN.projection;
float sceneDepth=SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, projection.xy / projection.w).r;
float2 uv0=rotateUV(IN.uv0,_WaveMapAngle);
float4 animUV=uv0.xyxy*float4(1,1,0.5,0.5)+float4(normalize(_Direction.xy),normalize(_Direction.zw))*_WaveSpeed*_TexWaveSpeed*_Time.y;
float4 n1=SAMPLE_TEXTURE2D(_BumpMap,sampler_BumpMap,animUV.xy);
float4 n2=SAMPLE_TEXTURE2D(_BumpMap,sampler_BumpMap,animUV.zy);
float3 normalTS=UnpackNormalBlend(n1,n2,_TexWaveAmplitude);
float3 normalWS=(IN.normalWS.xyz);
float3 binormalWS=(IN.binormalWS.xyz);
float3 tangentWS=(IN.tangentWS.xyz);
normalWS=normalize(float3(dot(tangentWS,normalTS),dot(binormalWS,normalTS),dot(normalWS,normalTS)));
float sceneZ = LinearEyeDepth(sceneDepth, _ZBufferParams);
float thisZ = LinearEyeDepth(projection.z / projection.w, _ZBufferParams);
float far=_DeepWaterDepth;
float near=1;
float fadeShallowToDeep = saturate (((sceneZ - near) - thisZ)/(far-near));
float fadeZeroToShallow=saturate(((sceneZ-0)-thisZ)/(near-0));
float4 waterColor=lerp(_ShallowColor,_DeepColor,fadeShallowToDeep);
float alpha=lerp(0,waterColor.a,fadeZeroToShallow);
float3 posWS=float3(IN.binormalWS.w,IN.tangentWS.w,IN.normalWS.w);
float3 viewDirWS=normalize(GetCameraPositionWS()-posWS);
float3 reflectWS=reflect(-viewDirWS,normalize(float3(normalWS.x,normalWS.y,normalWS.z)));
half3 reflection=SAMPLE_TEXTURECUBE(_Cube,sampler_Cube,reflectWS).rgb;
half NdotV=saturate(1.0-dot(viewDirWS,normalWS.xyz));
half fresnel=pow(NdotV, _RimPower);
Light light=GetMainLight();
float3 L=normalize(light.direction);
half3 sceneColor=SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, projection.xy / projection.w+(lerp(0,0.08,alpha)*normalWS.xz)).rgb;
half3 cleanSceneColor=SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, projection.xy / projection.w).rgb;
float NdotL=saturate(dot(normalWS.xyz,L));
float3 diffuseColor=light.color*waterColor.rgb*(NdotL*0.5+0.5);
float3 specularColor=SpecularFactor(_Smoothness,L,viewDirWS,normalWS)*light.color*_SpecularColor;
//diffuseColor=lerp(sceneColor,diffuseColor,alpha);
diffuseColor=lerp(diffuseColor,reflection,fresnel*_Reflection);
diffuseColor+=specularColor;
half3 resultColor=lerp(sceneColor,diffuseColor,alpha);
return half4(resultColor,1);
}
ENDHLSL
}
}
}