《GPU Gems》实践—用物理模型进行高效的水模拟(第一部分)

GPU Gems》实践—用物理模型进行高效的水模拟(第一部分)

综述

最近一直在阅读Real-Time Rendering 4th,看到烦闷之时拿起了GPU Gems翻了翻,虽然这本书如今看来已经有些年头了,但书中的案例都是非常经典的,而且很多案例如今看来也很不错。
GPU Gems第一部分是对自然效果的模拟,其中第一章介绍了如何用物理模型进行高效的水模拟。
这一章大体分为两个部分,前半部分介绍了如何用简单正弦波的加和以及其改进版本Gerstner波对顶点进行变换,并计算其法线。后半部分介绍了如何生成和使用纹理波。另外,书中还介绍了一部分运行时的处理方法。
在之前的学习中,我了解到,噪声纹理和快速傅里叶变换(FFT)也能达到水模拟的效果。
今天这篇文章是对前半部分的实现,也就是基于顶点变换的水模拟。使用的平台依旧是Unity以及其中的surface shader。

正文

关于细分

一个好的基于顶点变换的水波模拟,对于网格细分有一定的要求。理论而言,细分程度的越大的网格意味着对正弦函数有更大的采样率,当网格细分程度低于正弦波的Nyquist频率的二倍时,会导致不正确的波形的产生。事实上,当网格细分程度不够高时,虽然能够在网格层面满足波形的需要,但是在计算表面法线并生成表面光照时,在波峰处容易产生比较明显的锯齿和不正确的阴影。如图:
《GPU Gems》实践—用物理模型进行高效的水模拟(第一部分)_第1张图片
对于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(D i(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|} D i(x,y)=(x,y)C i(x,y)C i

效果如下:

代码如下:

    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波

对于池塘而言,上述形成的圆滑的波正合适。然而海洋的波比较粗犷,需要形成较尖的浪头和较宽的浪槽,于是我们使用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×D i.x×cos(ωiD i(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×D i.y×cos(ωiD i(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(ωiD i(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"
    }

需要注意的是:对波长相似的波进行叠加可以凸显水面活力。于是需要选择中等波长的波,然后从他的半波长到二倍波长之间产生任意波长。

你可能感兴趣的:(GPU,Gems)