Unreal中的水

水体系统

Posted by Bob on February 23, 2021
水体系统背景
年份 游戏 实现方式 截图
1996年 《Wave Race 64》 Sin,屏幕分辨率小效果ok image
1998年 《雷神之锤》 动态纹理制作的半透水 image
2002年 《Super Mario Sunshine》 静态的几何波,实现比较基础的反射折射 用Mipmap技巧表现表面的Lod和模糊效果 image
2006年 《FARCRY》 达到了本世代表现,有波纹效果。更准确的吸收、散射、折射 image
2008年 《恐水症》 水和动态物体的交互。基于高度场的流体模拟系统 image
2013年 BattleField 4 Tile离散傅里叶变换纹理 image
2014年 《看门狗》 形变的频率与Gameplay的相互作用 image
2016年 《神秘海域4》 改善了河流的几何流动模拟 image
2018年 《盗贼之海》 虚幻引擎制作,离散傅里叶驱动波动,叠加程序化的泡沫和飞溅 image
2018年 《刺客信条:奥德赛》 海岸线分层的波浪 image
UE4.26之前的水

UE引擎没有内置完整水系统,都是基于自制。常见做法使用一个平面mesh,指定一个水材质。而UE希望有网格的处理,有物理、模拟,能和地形进行交互,有完整工具链。

  1. 《Learning Water Graphics in UE4》 Hiroyuki Kobayashi

  2. 《VR Editor Demo Beach Scene》 Ryan Brucks et.al

UE4.26 水
  1. 统一的编辑工具
    • 流动的河流
    • 有深度的湖泊
    • 海洋
  2. 高效且出色的渲染
    • 模型
    • 着色
  3. 具有确定性的波浪
Water插件

实验版本插件开启 image 非破坏性的修改地形 image 多种默认水体,基于(闭环)样条线的水体(同一高度上的样条线)创造工具,海洋(同一高度)、河流(有起点终点,可以不同高度,不同的河段有不同的流速)、湖泊(同一高度)能无缝连接。水体有水深、流速可和游戏玩法相结合。Body Custom只存储信息,不定义形状,用户可以去指定一个Static Mesh。 image Water Body Actor只是编辑工具并不生成Mesh,Mesh由Water Mesh Actor生成并存储其中。 image

编辑工具

海洋是有距离限制的,默认大小是3平方公里,如果想让海洋继续延到地平线消失,可以在FarDistance中指定材质和延展距离,会在外部生成简化的低面数的Far Mesh并使用指定的材质。 image

波浪使用资产存储,cpu和gpu都可以访问这同一份数据。 可以调整波层数、波数默认是16层,可以对海洋波浪进行不错的模拟,堡垒之夜风格化渲染使用的6层,移动端可简化为4层 。每层都可以单独设置波长、振幅、方向、陡峭度。

image

GerstnerWave计算相对简单,状态独立的。(x、z坐标向波峰靠拢,y轴坐标做sin波计算。所以看起来波峰更尖锐,波谷更广阔(因为顶点向波峰靠拢了,波谷的顶点距离变更大了,看起来就更宽阔。)) 基本原理参见《GPU Gems的官方文档》.

  • Qi是控制波浪陡度的参数,i是第i个波浪(也可以只有一个波浪,更好理解)。如果Qi是0,则是一般的正弦波。当Qi = 1/(wi*Ai)时,波峰最尖锐.应该避免Qi的值过大,不然会形成顶部的水波穿插,形成错误的网格顶点位置,甚至露出到水面背面的网格面。
  • L(WaveLength)是世界坐标上波峰与波峰之间的距离。
  • 参数wi是频率,是一个常数,wi = 2 π /L.根据正弦方程的标准函数f(x)=Asin(wx+β),最小正周期L就是L=2π/w。所以 w = 2π/L。
  • Ai是水平面到波峰的高度。
  • Di是波峰移动的水平向量。
  • tφi 是初相,是一个常数。根据正弦型函数,ωx+φ叫做相位,φ叫做初相(即当x=0时的相位 )。t φi 决定了所有的点是否向左(t φi >0)或向右(t φi <0)平行移动t φi 个单位.t是时间。
  • Di点乘(x,y)是可知两者的夹角方向,等于0为90度,大于0夹角是锐角。小于0夹角是钝角。(x,y)是水平位置(horizontal position)。 image
Unity C#实现GerstnerWave

public float height;
[Range(0, 1f)]
public float sharp = 0.5f;//尖锐
[Range(0.5f, 10f)]
public float speed = 2f;//初相
[Range(1, 50)]
public int waveT;//周期
public Vector2 WaveDir = Vector2.left;
private Vector3[] baseVertices;

private void OnEnable()
{
        mesh = GetComponent<MeshFilter>().mesh;
        baseVertices = mesh.vertices;
}

void Update()
{
            Vector3[] vertices = this.mesh.vertices;
            for (int i = 0; i < vertices.Length; i++)
            {
                Vector3 vertice = this.baseVertices[i];
                float A = this.height;
                float w = (float)(2 * Math.PI / waveT);
                float Qi = sharp / w * A;
                float cosNum = Mathf.Cos(Time.time * speed + w * Vector2.Dot(WaveDir, new Vector2(vertices[i].x, vertices[i].z)));
                float sinNum = Mathf.Sin(Time.time * speed + w * Vector2.Dot(WaveDir, new Vector2(vertices[i].x, vertices[i].z)));
 
                vertice.x += Qi * A * WaveDir.x * cosNum;
                vertice.z += Qi * A * WaveDir.y * cosNum;
                vertice.y = sinNum * A;
 
                vertices[i] = vertice;
            }
            this.mesh.vertices = vertices;
            this.mesh.RecalculateNormals();//重新计算法线
}
Unity Shader实现GerstnerWave
Shader "Unlit/GerstnerWave"
{
    Properties
    {
		_Color("Color", Color) = (1,1,1,1)
		_WaveLength("WaveLength", float) = 4//水波长度,世界空间中波之间的波峰到波峰的距离
		_WaveAmplitude("WaveAmplitude", float) = 0.4//振幅,从水平面到波峰的高度
		_WindDirection("WindDirection", Range(0, 360)) = 70//风方向
		_WindSpeed("WindSpeed", float) = 0.4//风速系数
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100
		ZWrite On
		ZTest On
		Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

			fixed4 _Color;
			float _WaveLength;
			float _WaveAmplitude;
			float _WindDirection;
			float _WindSpeed;

			struct Wave {
				float3 vertex;
				float3 normal;
			};
			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 normal : NORMAL;
			};

			Wave GerstnerWave(half2 pos, float waveCount, half waveLen, half amplitude, half direction, half windSpeed) {
				Wave waveOut;
				float time = _Time.y;
				direction = radians(direction);
				half2 D = normalize(half2(sin(direction), cos(direction)));//方向(D):垂直于波峰传播的波阵面的水平矢量。
				half w = 6.28318 / waveLen;
				half L = waveLen;//波长(L):世界空间中波之间的波峰到波峰的距离。
				half A = amplitude;//振幅(A):从水平面到波峰的高度。
				half S = windSpeed * sqrt(9.8 * w);//速度(S):波峰每秒向前移动的距离。
				half Q = 1 / (A * w * waveCount);//陡度 (Q) : 控制水波的陡度。

				half commonCalc = w * dot(D, pos) + time * S;
		
				half cosC = cos(commonCalc);
				half sinC = sin(commonCalc);
				waveOut.vertex.xz = Q * A * D.xy * cosC;
				waveOut.vertex.y = (A * sinC) / waveCount;
				half WA = w * A;
				waveOut.normal = half3(-(D.xy * WA * cosC), 1 - (Q * WA * sinC));
				waveOut.normal = waveOut.normal/waveCount;
				return waveOut;
			}
			Wave GenWave(float3 vertex) {
				half2 pos = vertex.xz;
				Wave waveOut;
				uint count = 4;
				for (uint i = 0; i < count; i++) {
					Wave wave = GerstnerWave(pos, count, _WaveLength, _WaveAmplitude, _WindDirection, _WindSpeed);
					waveOut.vertex += wave.vertex;
					waveOut.normal += wave.normal;
				}
				return waveOut;
			}

            v2f vert (appdata v)
            {
                v2f o;
				Wave wave = GenWave(v.vertex.xyz);
				v.vertex.xyz += wave.vertex;//叠加偏移
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.normal = mul(wave.normal, (float3x3)unity_WorldToObject);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
				//漫反射公式
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
				fixed3 worldNormal = normalize(i.normal);
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 color = ambient + diffuse;
                return fixed4(color, _Color.a);
            }
            ENDCG
        }
    }
}
UE水体渲染
  1. 渲染-生成网格体 image
  • 水面按照四叉树网格生成grid
    • 每次重启引擎或者需要重建时重新生成
    • 每次更新所有水体耗
    • 时非常短
  • 不同的WaterBody生成的grid不合并
    • 命令行 r.Water.WaterMesh.ShowTileBounds 可以看到不同颜色的方格
    • 每种颜色对应不同的WaterBodyType
    • 河流可以与海洋和湖泊交汇,交汇处生成的grid不同
  • Tile
    • 每个Tile只需要渲染自己的WaterBodyType的材质
    • Culling是基于Tile
    • 剔除后的tile,同材质同LOD的可以instancing
    • 命令行 stat watermesh 可以查看面数,生成的grid数和drawcall
  • LOD
    • 变形逻辑是基于到摄像机的距离
    • 近处高细节
    • 远处简化
    • 遍历四叉树时使用相同的距离
    • morph完成后,将精确切换 tiled LOD
  • 平滑的过度
  1. 渲染-着色
    • Material:一个主材质
    • ShaderingModel:水体、大气、其他胶状物体有点类似,有散射,在次表面中比较常见。
    • SingleLayerWater:,可见光有些波长优先被吸收掉了(红色),蓝光走的远 image
    • Scattering&AbsorptionCoeffs:消光指数,光在水中如何传播。
    • PhaseG:各向异性散射系数
    • ColorScaleBehindWater:调整水后面的cene color,更好表现焦散效果。 * 独立的管线
    • 降采样 scene color/depth
    • 渲染SingleLayerWater物体到GBuffer
    • Tile SingleLayerWater MaterialID
    • 为SingleLayerWater的像素计算SSR
    • composite reflection captures, sky & SSR
  2. UnderWater PP

  3. 模拟-浮力:Buodancy Component

  4. 模拟-流体Fluid
    • Ripple Solver:No Velocity,相对cheap
    • Shallow Solver:不稳定,有速度。

目前还是实验版本,不是很稳定和完善。