感觉国庆到处跑比上班还忙,终于闲下来了,五天没写代码了怕手生,刚好无聊顺便写一下Gerstner计算。
Gerstner是早期图形算法中模拟水面波浪的一种拟真算法,是基于正余弦波的一种组合叠加,具体详细的解释,首先上官方:
trochoidal_wave
wind_wave
wave
那具体是怎么样的一个叠加形式呢?首先我们写一个正弦波的效果:
网格构建:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class WaveRectanglePanel : MonoBehaviour
{
public int horizonCount = 50;
public int verticalCount = 50;
[Range(0.1f, 100f)]
public float cellDistance = 1f;
public bool isUpdate = false;
private MeshRenderer meshRender;
private MeshFilter meshFilter;
private Mesh mesh;
void Start()
{
meshRender = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
mesh = new Mesh();
}
void Update()
{
if (isUpdate)
{
UpdateMesh();
isUpdate = false;
}
}
private void UpdateMesh()
{
if (mesh != null)
{
mesh.Clear();
}
Vector3[] vertices = new Vector3[(horizonCount + 1) * (verticalCount + 1)];
Vector3[] normals = new Vector3[(horizonCount + 1) * (verticalCount + 1)];
Vector2[] uvs = new Vector2[(horizonCount + 1) * (verticalCount + 1)];
int[] triangles = new int[horizonCount * verticalCount * 6];
int triindex = 0;
for (int y = 0; y <= verticalCount; y++)
{
for (int x = 0; x <= horizonCount; x++)
{
int index = (horizonCount + 1) * y + x;
vertices[index] = new Vector3(x * cellDistance, 0, -y * cellDistance);
normals[index] = new Vector3(0, 1, 0);
uvs[index] = new Vector2((float)x / (float)horizonCount, (float)y / (float)verticalCount);
if (x < horizonCount && y < verticalCount)
{
int topindex = x + y * (horizonCount + 1);
int bottomindex = x + (y + 1) * (horizonCount + 1);
triangles[triindex] = topindex;
triangles[triindex + 1] = topindex + 1;
triangles[triindex + 2] = bottomindex + 1;
triangles[triindex + 3] = topindex;
triangles[triindex + 4] = bottomindex + 1;
triangles[triindex + 5] = bottomindex;
triindex += 6;
}
}
}
mesh.vertices = vertices;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.uv = uvs;
meshFilter.sharedMesh = mesh;
}
}
接下来实现sin wave效果
Shader "GerstnerWave/SinWaveShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaveSpeed("Wave Speed",Range(0.1,10)) = 1
_WavePower("Wave Power",Range(0.1,10)) = 1
_WaveRange("Wave Range",Range(0.1,10)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _WaveSpeed;
float _WavePower;
float _WaveRange;
v2f vert (appdata v)
{
v2f o;
//已x或z轴做sin入参系数控制波浪
float s = sin(v.vertex.x*_WavePower+_Time.y*_WaveSpeed)*_WaveRange;
v.vertex += float4(0,s,0,0);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
首先我们创建y轴为0的水平平面网格,所以shader中通过网格顶点的x和y值进行正弦波入参计算就能得到波浪的效果。
但现实中波浪波峰明显不是这种圆滑的,而是尖锐的波峰,如下图:
官方数学示意图:
ps:上部位sin采样,下部为gerstner采样。
同时我们看得出来gerstner采样就是在sin采样基础上叠加(或消减)了一个x(或z)轴的cos采样。我们可以在脑海中想一下,对比上下图形的黑色原点,看得出来原点在y轴上坐标没有变化,而在x(或z)轴上有cos的周期变化(sinx = cos(90-x))
那么为了验证我们的想法,实现一下就看得出来了。
Shader "GerstnerWave/GerstnerWaveShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaveRadius("Wave Circle Radius",Range(0.1,10)) = 1
_WaveSpeed("Wave Speed",Range(0.1,10)) = 1
_WaveRange("Wave Range",Range(0.1,10)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _WaveRadius;
float _WaveSpeed;
float _WaveRange;
v2f vert (appdata v)
{
v2f o;
//angle入参
float ang = v.vertex.x + _WaveSpeed*_Time.y;
//y轴sin叠加
float y = _WaveRange*sin(ang)*_WaveRadius;
//x轴cos消减
float x = _WaveRange*cos(ang)*_WaveRadius;
v.vertex += float4(x,y,0,0);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
可以看得出来这样的水波就较为真实了。当然了,水波也不是一个固定x或z轴方向的,我们如果能增加一个二维(xz轴)的波动方向就好了,如下:
可以看得出来这时候波峰因为朝向(dir)的影响,波动会对x和z轴造成运动,那么我们可以想象xoz平面顶点与dir的点积越大(即x’oz’与dir的夹角越小),则表明此vertex越接近波峰,则参与正余弦计算的入参越大,同时x和z波动横纵移动的分量也需要根据dir的分量x、y进行运算。
Shader "GerstnerWave/GerstnerWaveDirectionShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaveDirection("Wave Direction",vector) = (1,0,0,0)
_WaveRadius("Wave Circle Radius",Range(0.1,10)) = 1
_WaveSpeed("Wave Speed",Range(0.1,10)) = 1
_WaveRange("Wave Range",Range(0.1,10)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float2 _WaveDirection;
float _WaveRadius;
float _WaveSpeed;
float _WaveRange;
v2f vert (appdata v)
{
v2f o;
//直接输入单位化向量更好
float2 dir = normalize(_WaveDirection);
//dot(dir,v.vertex.xz)得到vertex建模空间xz二维向量与dir向量的点积(权重)
//用来判断vertex的波峰权重(即正余弦函数入参)
float ang = dot(dir,v.vertex.xz) + _WaveSpeed*_Time.y;
//x方向的叠加和z方向的叠加需要根据dir分量进行计算
float x = dir.x*_WaveRange*cos(ang)*_WaveRadius;
float y = _WaveRange*sin(ang)*_WaveRadius;
float z = dir.y*_WaveRange*cos(ang)*_WaveRadius;
v.vertex += float4(x,y,z,0);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
可以看得出实现了效果,接下来我们还得做一下多波叠加,水面确实是很多波浪叠加出的效果。
Shader "GerstnerWave/GerstnerMultiWaveShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaveDir1("Wave Direction 1",vector) = (1,0,0,0)
_WaveDir2("Wave Direction 2",vector) = (1,0,0,0)
_WaveDir3("Wave Direction 3",vector) = (1,0,0,0)
_WaveRadius("Wave Circle Radius",Range(0.1,10)) = 1
_WaveSpeed("Wave Speed",Range(0.1,10)) = 1
_WaveRange("Wave Range",Range(0.1,10)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float2 _WaveDir1;
float2 _WaveDir2;
float2 _WaveDir3;
float _WaveRadius;
float _WaveSpeed;
float _WaveRange;
float4 getGerstnerWave(float4 vtx,float2 dir,float wavespd,float waverag,float waverad)
{
float ang = dot(dir,vtx.xz) + wavespd*_Time.y;
float x = dir.x*waverag*cos(ang)*waverad;
float y = waverag*sin(ang)*waverad;
float z = dir.y*waverag*cos(ang)*waverad;
return float4(x,y,z,0);
}
v2f vert (appdata v)
{
v2f o;
float2 dir1 = normalize(_WaveDir1);
float4 vtx1 = getGerstnerWave(v.vertex,dir1,_WaveSpeed,_WaveRange,_WaveRadius);
float2 dir2 = normalize(_WaveDir2);
float4 vtx2 = getGerstnerWave(v.vertex,dir2,_WaveSpeed,_WaveRange,_WaveRadius);
float2 dir3 = normalize(_WaveDir3);
float4 vtx3 = getGerstnerWave(v.vertex,dir3,_WaveSpeed,_WaveRange,_WaveRadius);
v.vertex += (vtx1+vtx2+vtx3);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
可以看得出来多波叠加功能是实现了,不过总觉得差点什么,那就是随机,因为我们除了direction是三个随机的,其他参数全是一样的,所以还得修改增加入参的设置功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GerstnerMultiRandomWave : MonoBehaviour
{
[System.Serializable]
public class WaveParams
{
public float WaveDirX;
public float WaveDirY;
public float WaveRadius;
public float WaveSpeed;
public float WaveRange;
}
public Material waveMat;
public bool updateParam = false;
public WaveParams[] waveParams;
void Start()
{
}
private void Update()
{
if (updateParam)
{
UpdateWaveParam();
updateParam = false;
}
}
private void UpdateWaveParam()
{
int count = waveParams.Length;
waveMat.SetInt("_WaveCount", count);
waveMat.SetFloatArray("_WaveParams", GetWaveParamFloats());
}
private List<float> GetWaveParamFloats()
{
List<float> floatlist = new List<float>();
for (int i = 0; i < waveParams.Length; i++)
{
WaveParams param = waveParams[i];
floatlist.Add(param.WaveDirX);
floatlist.Add(param.WaveDirY);
floatlist.Add(param.WaveRadius);
floatlist.Add(param.WaveSpeed);
floatlist.Add(param.WaveRange);
}
return floatlist;
}
}
shader修改:
Shader "GerstnerWave/GerstnerMultiParamWaveShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
const int _WaveCount;
float _WaveParams[30]; //最多6个
float4 getGerstnerWave(float4 vtx,float2 dir,float wavespd,float waverag,float waverad)
{
float ang = dot(dir,vtx.xz) + wavespd*_Time.y;
float x = dir.x*waverag*cos(ang)*waverad;
float y = waverag*sin(ang)*waverad;
float z = dir.y*waverag*cos(ang)*waverad;
return float4(x,y,z,0);
}
v2f vert (appdata v)
{
v2f o;
for(int i=0;i<_WaveCount;i++)
{
int index = i*5;
float2 dir = float2(_WaveParams[index],_WaveParams[index+1]);
float spd = _WaveParams[index+2];
float rag = _WaveParams[index+3];
float rad = _WaveParams[index+4];
float4 wvtx = getGerstnerWave(v.vertex,dir,spd,rag,rad);
v.vertex += wvtx;
}
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
效果好不好就只能靠调整参数了,我就随便调了几个值。
还没完,接下来我们还得处理着色方面的问题,那就是法向量的重计算,如果我们不重计算法向量,就会是这样:
我添加了specular分量计算,发现高光也是一块板,如果我们增加normal向量的重计算,才能正常显示specular高光。
那么怎么重计算呢?我们原本vertex的法向量是(0,1,0),然后vertex经过xyz三个轴向上波动(移动),就成了如下的情况:
而此时新的法向量n‘比较简单的计算方法是通过切线和副切线来的叉积计算,因为切线和副切线还是挺好计算的,如下:
我们可以理解为仿射坐标系tbn(tangent、bitangent、normal)经过旋转平移到达了t’b’n’,当然我们这里法向量不用计算平移,只考虑旋转就行了。所以我们通过z得到tangent、bitangent绕y轴在xz上的旋转后,再加上(0,y,0)向量得到tangent’、bitangent‘,通过叉积就能得到normal‘。
当然也不必通过矩阵旋转,直接向量计算也可以。
在当然,如果我们波动朝向使用的角度入参,那么矩阵旋转就更好。
下面我们以“一次波动”为例,画出每次“波动”后tangent、bitangent变换到t’、bt‘的过程:
想象xz平面上,tangent、bitangent通过旋转变换到t1、bt1,然后xyz三维空间中,加上(0,y,0)得到t’、bt‘,下面测试一下:
Shader "GerstnerWave/GerstnerMultiParamWaveShader"
{
Properties
{
_MainColor("Main Color",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
[Toggle]_IsCalNorm("Calculate Normal?",int) = 0
_LightFactor("Lighting Factor",Color) = (1,1,1,1)
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_SpecFactor("Specular Factor",Color) = (1,1,1,1)
_SpecGloss("Specular Gloss",Range(0,500)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 wdNorm : TEXCOORD1;
float3 wdP2S : TEXCOORD2;
float3 wdP2V : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainColor;
int _IsCalNorm;
float4 _LightFactor;
float4 _DiffuseFactor;
float4 _SpecFactor;
float _SpecGloss;
const int _WaveCount;
float _WaveParams[30]; //最多6个
float4 getGerstnerWave(float4 vtx,float2 dir,float wavespd,float waverag,float waverad,inout float3 tang,inout float3 bitang)
{
float ang = dot(dir,vtx.xz) + wavespd*_Time.y;
float x = dir.x*waverag*cos(ang)*waverad;
float y = waverag*sin(ang)*waverad;
float z = dir.y*waverag*cos(ang)*waverad;
float3 t1 = normalize(tang+float3(0,0,z));
tang = normalize(t1+float3(0,y,0));
float3 bt1 = normalize(bitang+float3(z,0,0));
bitang = normalize(bt1+float3(0,y,0));
return float4(x,y,z,0);
}
v2f vert (appdata v)
{
v2f o;
//原tangent、bitangent
float3 otan = float3(1,0,0);
float3 obitan = float3(0,0,1);
for(int i=0;i<_WaveCount;i++)
{
int index = i*5;
float2 dir = float2(_WaveParams[index],_WaveParams[index+1]);
float spd = _WaveParams[index+2];
float rag = _WaveParams[index+3];
float rad = _WaveParams[index+4];
float4 wvtx = getGerstnerWave(v.vertex,dir,spd,rag,rad,otan,obitan);
v.vertex += wvtx;
}
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
if(_IsCalNorm){
//计算world space normal
o.wdNorm = UnityObjectToClipPos(cross(otan,obitan));
}else{
o.wdNorm = UnityObjectToClipPos(v.normal);
}
o.wdP2S = WorldSpaceLightDir(v.vertex);
o.wdP2V = WorldSpaceViewDir(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _MainColor;
float3 wdnorm = normalize(i.wdNorm);
float3 wdp2s = normalize(i.wdP2S);
float3 wdp2v = normalize(i.wdP2V);
float3 hdir = normalize(wdp2s+wdp2v);
float ndotl = dot(wdnorm,wdp2s);
float ndoth = dot(wdnorm,hdir);
//计算光照
fixed4 light = _LightColor0*_LightFactor;
float diff = max(0,ndotl);
fixed4 diffcol = diff*_LightColor0*_DiffuseFactor;
float spec = pow(max(0,ndoth),_SpecGloss);
fixed4 speccol = spec*_LightColor0*_SpecFactor;
col*=(light+diffcol+speccol);
return col;
}
ENDCG
}
}
}
效果如下:
可以看得出来问题不大,法向量看上去是计算对了。好了,后面有时间继续。聊一下我写的一个可交互水体的功能。