最近一直在阅读Real-Time Rendering 4th,看到烦闷之时拿起了GPU Gems翻了翻,虽然这本书如今看来已经有些年头了,但书中的案例都是非常经典的,而且很多案例如今看来也很不错。
GPU Gems第一部分是对自然效果的模拟,其中第一章介绍了如何用物理模型进行高效的水模拟。
这一章大体分为两个部分,前半部分介绍了如何用简单正弦波的加和以及其改进版本Gerstner波对顶点进行变换,并计算其法线。后半部分介绍了如何生成和使用纹理波。另外,书中还介绍了一部分运行时的处理方法。
在之前的学习中,我了解到,噪声纹理和快速傅里叶变换(FFT)也能达到水模拟的效果。
今天这篇文章是对前半部分的实现,也就是基于顶点变换的水模拟。使用的平台依旧是Unity以及其中的surface shader。
一个好的基于顶点变换的水波模拟,对于网格细分有一定的要求。理论而言,细分程度的越大的网格意味着对正弦函数有更大的采样率,当网格细分程度低于正弦波的Nyquist频率的二倍时,会导致不正确的波形的产生。事实上,当网格细分程度不够高时,虽然能够在网格层面满足波形的需要,但是在计算表面法线并生成表面光照时,在波峰处容易产生比较明显的锯齿和不正确的阴影。如图:
对于Unity而言,新建的Plane具有有限个细分网格。想要产生更多的小三角形,就必须进行曲面细分(Tesselation),这部分代码我们可以在Unity官方技术文档中找到这部分代码:Surface Shaders with DX11 / OpenGL Core Tessellation
从优化角度讲,我们只需要在水波波峰出产生较多的细分三角形,其余位置并不需要,但是Unity Shader提供的接口并不多,但是Unlit Shader应该可以实现自定义曲面细分。这里我使用了Edge length based tessellation,代码如下:
Shader "Tessellation Sample" {
Properties {
_EdgeLength ("Edge length", Range(2,50)) = 15
_MainTex ("Base (RGB)", 2D) = "white" {}
_DispTex ("Disp Texture", 2D) = "gray" {}
_NormalMap ("Normalmap", 2D) = "bump" {}
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
_Color ("Color", color) = (1,1,1,0)
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
#pragma target 4.6
#include "Tessellation.cginc"
struct appdata {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
float _EdgeLength;
float4 tessEdge (appdata v0, appdata v1, appdata v2)
{
return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
sampler2D _DispTex;
float _Displacement;
void disp (inout appdata v)
{
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
v.vertex.xyz += v.normal * d;
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
sampler2D _NormalMap;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Specular = 0.2;
o.Gloss = 1.0;
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
ENDCG
}
FallBack "Diffuse"
}
顶点位置计算:
W i ( x , y , t ) = 2 A i × ( sin ( D ⃗ i ⋅ ( x , y ) × ω i + t × ϕ i ) + 1 2 ) k W_i(x,y,t)=2A_i\times(\frac{\sin (\vec D_i\cdot(x,y)\times\omega_i+t\times \phi_i)+1}{2})^{k} Wi(x,y,t)=2Ai×(2sin(Di⋅(x,y)×ωi+t×ϕi)+1)k
顶点法线计算:
N ( x , y ) = ( − δ δ x ( H ( x , y , t ) ) , − δ δ y ( H ( x , y , t ) ) , 1 ) N(x,y)=(-\frac{\delta}{\delta x}(H(x,y,t)),-\frac{\delta}{\delta y}(H(x,y,t)),1) N(x,y)=(−δxδ(H(x,y,t)),−δyδ(H(x,y,t)),1)
在模拟海面时,方向向量为常量,效果如下:
相关着色器代码如下:
Shader "GPU GEMS/Chapter 1/Directional Wave" {
Properties {
_EdgeLength ("Edge length", Range(2,50)) = 15
_MainTex ("Base (RGB)", 2D) = "white" {}
_Color ("Color", color) = (1,1,1,0)
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
_Wave1_A ("Wave 1 A",Range(0,2))=1
_Wave2_A ("Wave 2 A",Range(0,2))=1.5
_Wave3_A ("Wave 3 A",Range(0,2))=2
_Direction ("Direction",Vector)=(1.0,2.0,3.0,4.0)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:Wave tessellate:tessEdge nolightmap
#pragma target 4.6
#include "Tessellation.cginc"
struct appdata {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
float _EdgeLength;
fixed _Wave1_A;
fixed _Wave2_A;
fixed _Wave3_A;
fixed4 _Direction;
fixed sinWavePart1(float x,float y){
return sin(dot(fixed2(_Direction.x,_Direction.y),fixed2(x,y))*1+_Time.y*1)*0.5+0.5;
}
fixed sinWavePart2(float x,float y){
return sin(dot(fixed2(_Direction.x,_Direction.z),fixed2(x,y))*2+_Time.y*2)*0.5+0.5;
}
fixed sinWavePart3(float x,float y){
return sin(dot(fixed2(_Direction.y,_Direction.z),fixed2(x,y))*3+_Time.y*3)*0.5+0.5;
}
float4 tessEdge (appdata v0, appdata v1, appdata v2)
{
return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
void Wave (inout appdata v)
{
fixed Wave1_temp=2*pow(sinWavePart1(v.vertex.x,v.vertex.z),2)*_Wave1_A;
fixed Wave2_temp=2*pow(sinWavePart2(v.vertex.x,v.vertex.z),2)*_Wave2_A;
fixed Wave3_temp=2*pow(sinWavePart3(v.vertex.x,v.vertex.z),3)*_Wave3_A;
v.vertex.y=Wave1_temp+Wave2_temp+Wave3_temp;
fixed Wave_normal_x=2*_Direction.x*1*_Wave1_A*sinWavePart1(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.x,_Direction.y),fixed2(v.vertex.x,v.vertex.z))*1+_Time.y*1)
+2*_Direction.x*2*_Wave2_A*sinWavePart2(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.x,_Direction.z),fixed2(v.vertex.x,v.vertex.z))*2+_Time.y*2)
+3*_Direction.y*3*_Wave3_A*sinWavePart3(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.y,_Direction.z),fixed2(v.vertex.x,v.vertex.z))*3+_Time.y*3);
fixed Wave_normal_y=2*_Direction.y*1*_Wave1_A*sinWavePart1(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.x,_Direction.y),fixed2(v.vertex.x,v.vertex.z))*1+_Time.y*1)
+2*_Direction.z*2*_Wave2_A*sinWavePart2(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.x,_Direction.z),fixed2(v.vertex.x,v.vertex.z))*2+_Time.y*2)
+3*_Direction.z*3*_Wave3_A*sinWavePart3(v.vertex.x,v.vertex.z)*cos(dot(fixed2(_Direction.y,_Direction.z),fixed2(v.vertex.x,v.vertex.z))*3+_Time.y*3);
v.normal=fixed3(-Wave_normal_x,-Wave_normal_y,1);
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Specular = 0.2;
o.Gloss = 1.0;
}
ENDCG
}
FallBack "Diffuse"
}
当方向向量是从波中心到顶点的规范化向量时,该正弦波演化为圆形波,即: D ⃗ i ( x , y ) = ( x , y ) − C ⃗ i ∣ ( x , y ) − C ⃗ i ∣ \vec D_i(x,y)=\frac{(x,y)-\vec C_i}{|(x,y)-\vec C_i|} Di(x,y)=∣(x,y)−Ci∣(x,y)−Ci
效果如下:
代码如下:
Shader "GPU GEMS/Chapter 1/round Wave" {
Properties {
_EdgeLength ("Edge length", Range(2,50)) = 15
_MainTex ("Base (RGB)", 2D) = "white" {}
_Color ("Color", color) = (1,1,1,0)
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
_Wave1_A ("Wave 1 A",Range(0,2))=1
_Wave2_A ("Wave 2 A",Range(0,2))=1.5
_Wave3_A ("Wave 3 A",Range(0,2))=2
_Center ("Center",Vector)=(0.0,0.0,0.0,0.0)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:Wave tessellate:tessEdge nolightmap
#pragma target 4.6
#include "Tessellation.cginc"
struct appdata {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
float _EdgeLength;
fixed _Wave1_A;
fixed _Wave2_A;
fixed _Wave3_A;
fixed4 _Center;
fixed2 Center_vector(float x,float y){
fixed temp_v=pow((x-_Center.x)*(x-_Center.x)+(y-_Center.y)*(y-_Center.y),0.5);
return -fixed2((x-_Center.x)/temp_v,(y-_Center.y)/temp_v);
}
fixed sinWavePart1(float x,float y){
return sin(dot(Center_vector(x,y),fixed2(x,y))*2+_Time.z*0.5);
}
fixed sinWavePart2(float x,float y){
return sin(dot(Center_vector(x,y),fixed2(x,y))*0.2+_Time.y*0.1);
}
fixed sinWavePart3(float x,float y){
return sin(dot(Center_vector(x,y),fixed2(x,y))*0.8+_Time.w*0.7);
}
float4 tessEdge (appdata v0, appdata v1, appdata v2)
{
return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
void Wave (inout appdata v)
{
fixed Wave1_temp=2*pow(sinWavePart1(v.vertex.x,v.vertex.z),2)*_Wave1_A;
fixed Wave2_temp=2*pow(sinWavePart2(v.vertex.x,v.vertex.z),2)*_Wave2_A;
fixed Wave3_temp=2*pow(sinWavePart3(v.vertex.x,v.vertex.z),3)*_Wave3_A;
v.vertex.y=Wave1_temp+Wave2_temp+Wave3_temp;
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Specular = 0.2;
o.Gloss = 1.0;
}
ENDCG
}
FallBack "Diffuse"
}
圆形波并没有添加法线,法线与方向波类似。
对于池塘而言,上述形成的圆滑的波正合适。然而海洋的波比较粗犷,需要形成较尖的浪头和较宽的浪槽,于是我们使用Gerstner波。
波函数:
P ( x , y , t ) x = x + ∑ ( Q i A i × D ⃗ i . x × cos ( ω i D ⃗ i ⋅ ( x , y ) + ϕ i t ) ) P(x,y,t)_x=x+\sum(Q_iA_i\times\vec D_i .x\times \cos(\omega_i\vec D_i\cdot (x,y)+\phi_it)) P(x,y,t)x=x+∑(QiAi×Di.x×cos(ωiDi⋅(x,y)+ϕit))
P ( x , y , t ) y = y + ∑ ( Q i A i × D ⃗ i . y × cos ( ω i D ⃗ i ⋅ ( x , y ) + ϕ i t ) ) P(x,y,t)_y=y+\sum(Q_iA_i\times\vec D_i .y\times \cos(\omega_i\vec D_i\cdot (x,y)+\phi_it)) P(x,y,t)y=y+∑(QiAi×Di.y×cos(ωiDi⋅(x,y)+ϕit))
P ( x , y , t ) z = ∑ ( A i sin ( ω i D ⃗ i ⋅ ( x , y ) + ϕ i t ) ) P(x,y,t)_z=\sum(A_i\sin(\omega_i\vec D_i\cdot(x,y)+\phi_it)) P(x,y,t)z=∑(Aisin(ωiDi⋅(x,y)+ϕit))
法向量是各分量求偏导的结果。
最终效果如下:
代码如下:
Shader "GPU GEMS/Chapter 1/Gerstner" {
Properties {
_EdgeLength ("Edge length", Range(2,50)) = 15
_MainTex ("Base (RGB)", 2D) = "white" {}
_Color ("Color", color) = (1,1,1,0)
_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
_Wave1_A ("Wave 1 A",Range(0,2))=1
_Wave2_A ("Wave 2 A",Range(0,2))=1.5
_Wave3_A ("Wave 3 A",Range(0,2))=2
_Direction ("Direction",Vector)=(1.0,2.0,3.0,4.0)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:Wave tessellate:tessEdge nolightmap
#pragma target 4.6
#include "Tessellation.cginc"
struct appdata {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
float _EdgeLength;
fixed _Wave1_A;
fixed _Wave2_A;
fixed _Wave3_A;
fixed4 _Direction;
fixed sinWavePart1(float x,float y){
return sin(dot(fixed2(_Direction.x,_Direction.y),fixed2(x,y))*1+_Time.y*1);
}
fixed sinWavePart2(float x,float y){
return sin(dot(fixed2(_Direction.x,_Direction.z),fixed2(x,y))*2+_Time.y*2);
}
fixed sinWavePart3(float x,float y){
return sin(dot(fixed2(_Direction.y,_Direction.z),fixed2(x,y))*3+_Time.y*3);
}
fixed cosWavePart1(float x,float y){
return cos(dot(fixed2(_Direction.x,_Direction.y),fixed2(x,y))*1+_Time.y*1);
}
fixed cosWavePart2(float x,float y){
return cos(dot(fixed2(_Direction.x,_Direction.z),fixed2(x,y))*2+_Time.y*2);
}
fixed cosWavePart3(float x,float y){
return cos(dot(fixed2(_Direction.y,_Direction.z),fixed2(x,y))*3+_Time.y*3);
}
float4 tessEdge (appdata v0, appdata v1, appdata v2)
{
return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}
void Wave (inout appdata v)
{
fixed Wave1_temp=sinWavePart1(v.vertex.x,v.vertex.z)*_Wave1_A;
fixed Wave2_temp=sinWavePart2(v.vertex.x,v.vertex.z)*_Wave2_A;
fixed Wave3_temp=sinWavePart3(v.vertex.x,v.vertex.z)*_Wave3_A;
v.vertex.y=Wave1_temp+Wave2_temp+Wave3_temp;
v.vertex.x+=0.5*_Wave1_A*_Direction.x*cosWavePart1(v.vertex.x,v.vertex.z)+0.5*_Wave2_A*_Direction.x*cosWavePart2(v.vertex.x,v.vertex.z)+0.5*_Wave3_A*_Direction.y*cosWavePart3(v.vertex.x,v.vertex.z);
v.vertex.z+=0.5*_Wave1_A*_Direction.y*cosWavePart1(v.vertex.x,v.vertex.z)+0.5*_Wave2_A*_Direction.z*cosWavePart2(v.vertex.x,v.vertex.z)+0.5*_Wave3_A*_Direction.z*cosWavePart3(v.vertex.x,v.vertex.z);
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Specular = 0.2;
o.Gloss = 1.0;
}
ENDCG
}
FallBack "Diffuse"
}
需要注意的是:对波长相似的波进行叠加可以凸显水面活力。于是需要选择中等波长的波,然后从他的半波长到二倍波长之间产生任意波长。