水体渲染+GPUI(一)
一个简单demo,其中大部分资源以及实现都来自Unity官方项目BoatAttack以及GPUI插件。本系列文章讲一下大致实现思路,想详细了解的可以下载工程查看(800m大小)。附上工程链接链接:https://pan.baidu.com/s/1RQLLoVQhGAToZoCMvhe9QQ?pwd=mrzz提取码:mrzz
需要的工具:Unity2020,VS2019
目录:
一、水体渲染
二、水体交互
三、岩石、植被渲染
四、GPUI渲染大量实例
<hr/>
演示 水体渲染 和 gpu实例化
https://www.zhihu.com/video/1538310165103468545
<hr/>水体渲染
水体主要实现的效果:
水体渲染效果1
水体渲染效果2
一、波浪
创建一个空物体,挂上脚本ASE_Water.cs
1、LOD
波浪使用到的资源在Assets\ASE\Meshes\ASE_WaterMesh.fbx。
在Unity的Scene面板中的左上角,切换至网格渲染,该模型组如下所示,中间的网格顶点密集,边上的顶点稀疏。
水面网格
将该模型组添加到ASE_Water的Water Mesh列表中。
然后我们需要在Update中实时更改其位置,使其始终保持在摄像机朝向的下方。
更改水面位置
现在就得到了一个可以跟随相机的水面了。
2、顶点动画
现在的水面是一个平面上的网格,需要对顶点进行偏移才能模拟波浪。
代码部分在Assets\ASE\Shaders\ASE_Water.shader 实现。
2.1、单个sin函数
顶点高度函数:
W_i(x,y,t) = A_i \times sin(D_i \cdot (x,y) \times w_i + t \times \phi_i)
W_i :修改后的顶点位置。输入为x,y坐标(水面顶点的x,z)以及时间t,输出为变换后的z坐标(水面顶点的y)
A_i :控制水的振幅
D_i :波浪移动的方向,与(x,y)点乘得到某一方向上的分量
w_i :频率,和波长互为倒数,控制水的波长
\phi_i :与t相乘代表波峰的移动距离,控制水的移动速度
单个sin函数生成波浪
法线函数:
首先定义顶点坐标函数:
P(x,y,t) = (x,y,W_i(x,y,t)) ,其中 W_i(x,y,t) 即刚才的顶点高度
一个顶点的法线怎么求得?
求出X和Y方向上的偏导 B 和 T ,再用 B \times T得到顶点的新法线 N 。
对X偏导,得到 B :
\begin{aligned} B(x,y,t) &= [\frac{\partial x}{\partial x},\frac{\partial y}{\partial x},\frac{\partial}{\partial x}(W_i(x,y,t))] \\&= \end{aligned}
同理,对Y偏导,得到 T :
T(x,y,t) =
B \times T得到 N :
N(x,y,t) = [-\frac{\partial}{\partial x}(W_i(x,y,t)),-\frac{\partial}{\partial y}(W_i(x,y,t)),1]
其中 \frac{\partial}{\partial x}(W_i(x,y,t)) =w_i \times D_i.x \times A_i \times cos(D_i \cdot (x,y) \times w_i + t \times \phi_i)
//单个Sin波浪
WaveStruct SinWave(half2 pos,float waveCountMulti, half amplitude, half angle, half wavelength)
{
WaveStruct waveOut;
float time = _Time.y;
half w = 6.28318 / wavelength;
half wSpeed = sqrt(9.8 * w);
angle = radians(angle);
half2 direction = half2(sin(angle), cos(angle));
half dir = dot(direction, pos);
half calc = dir * w + time * wSpeed; // the wave calculation
half cosCalc = cos(calc);
half sinCalc = sin(calc);
waveOut.position = 0;
waveOut.position.y = amplitude * sinCalc*waveCountMulti;
waveOut.normal = normalize(float3(
-w*direction.x*amplitude*sinCalc,
1,
-w*direction.y*amplitude*sinCalc
)) * waveCountMulti;
return waveOut;
}2.2、叠加sin函数
顶点高度函数:
H(x,y,t) = \sum_{i}{W_i(x,y,t)}
法线函数:
此时得到新的顶点坐标函数:
P(x,y,t) = (x,y,H(x,y,t))
同理求得法线 N:
N(x,y,t) = [-\frac{\partial}{\partial x}(H(x,y,t)),-\frac{\partial}{\partial y}(H(x,y,t)),1]
其中 \frac{\partial}{\partial x}(H(x,y,t)) =\sum_{i}{(w_i \times D_i.x \times A_i \times cos(D_i \cdot (x,y) \times w_i + t \times \phi_i))}
多个sin叠加
2.3、Gerstner叠加波浪
Sin函数的变种,可以使得波谷更加平坦,波峰更陡峭,更好地模拟波浪。
Sin函数中的 P(x,y,t) 只会修改其z值,而Gerstner中还需要将x,y坐标向波峰位置移动。
新的顶点坐标函数:
P(x,y,t) =\begin{bmatrix} x + \sum_{i}{(w_i \times D_i.x \times A_i \times cos(D_i \cdot (x,y) \times w_i + t \times \phi_i))} \\ y+\sum_{i}{(w_i \times D_i.y \times A_i \times cos(D_i \cdot (x,y) \times w_i + t \times \phi_i))} \\ \sum_{i}{(A_i \times sin(D_i \cdot (x,y) \times w_i + t \times \phi_i))} \end{bmatrix}
其中,x,y分别向各自的偏导方向移动,即波峰位置
同理,求偏导得到 B 和 T ,再用 B \times T 得到顶点的新法线 N 。
多个Gerstner函数叠加
到这里,波浪生成的部分就结束了。
<hr/>二、折射
1、_CameraDepthTexture
该纹理由渲染管线在渲染完不透明物体之后生成,在Renderer中勾选OpaqueTexture即可。
2、UV扰动
对_CameraDepthTexture采样,不过需要添加一定的UV扰动,(扰动值是根据顶点法线算出来的)。
此时可以得到效果如下:
注意到黄色框内的错误,在水面上的物体也进行了uv扰动,这并不应该,需要对其修正。在4、修正讲解
3、WaterDepth
在shader中定义二维向量WaterDepth,其中x存储了视角方向上的水的深度,y存储了水的竖直深度。
WaterDepth示意图
WaterDepth.x 对_CameraDepthTexture采样,由采样得到的深度 减去 相机到水面顶点的距离
WaterDepth.x
WaterDepth.y 添加一个垂直水平面的相机,生成深度纹理
WaterDepth.y
最明显的差别就在于视角深度会随着观察角度改变,上面视角深度中的胶囊体靠近水面部分为红色,就是因为在这个角度观察,会得到较小的视角深度。
4、修正
对_CameraDepthTexture进行UV扰动时,我们可以对WaterDepth.x进行同样的的扰动,得到如下效果:
WaterDepth.x(修正)
这时候,用扰动过后的WaterDepth.x判断该点是否在水平面以上。若是的话则取消扰动,直接用screenUV进行采样。
此时,就能得到一个较好的折射效果:
折射(修正)
可以看到,树叶以及在水面上方的方块、胶囊体都没有产生扰动了。
5、sss吸收
同时,随着水的WaterDepth.x变得越大,我们能从水中观察到的折射部分会越少。
在demo里,自定义了一条吸收色带。
当WaterDepth.x较小时,我们能够透过水面看到水下的物体,对应了色带左边的白色;
当WaterDepth.x较大时,我们基本看不到水下的物体,随影了色带右边的黑色。
模拟水对光线吸收
模拟水对光线的吸收
将上述水对光线的吸收与折射部分相乘,即可得到整个折射部分的效果:
折射+sss(吸收)
<hr/>三、反射
1、翻转摄像机
水面反射物体的倒影,是将摄像机关于水平面做了一次翻转(位置、朝向都关于水平面对称)
对应到摄像机就是把红框内两个属性取反
位置和朝向取反
下面试一试,将摄像机翻转后的效果:
此时若直接将反射纹理采样到水平面上,会出现问题:
直接翻转摄像机
是因为我们把摄像机进行翻转后,水下的物体也能被看到,跟着一起反射了。
2、斜面裁剪
所以我们需要对其进行一次斜面裁剪,将水面以下的物体给裁剪掉:
左边为常规的裁剪面,右边为斜面裁剪
Unity里可以很方便地实现斜面裁剪,调用CameraSpacePlane(),设置好裁剪面即可。
设置裁剪面
现在看看斜面裁剪之后的水面:
反射(斜面裁剪)
3、UV扰动
同样根据顶点法线添加扰动,不过在demo里还有一张水的表面贴图:
表面贴图
可以通过该贴图使得法线扰动更加细节,后续的焦散效果也用到了该贴图。
左图根据顶点法线进行扰动,右图结合了顶点法线和表面贴图
<hr/>四、菲涅尔项
当观察者和反射平面的夹角不同时,其反射和折射所占比例各不相同,菲涅尔效应即描述该现象。
通过viewDir(相机位置 - 水面顶点)和水面顶点法线相乘,可以得到夹角大小,然后进行幂运算可得到更好的效果。
菲涅尔项
可以看到,近处较暗,容易看到折射部分;远处较亮,容易看到反射部分。
将折射和反射按照菲涅尔项进行混合:
折射与反射混合
按菲涅尔项混合折射与反射
<hr/>五、sss散射
在折射部分,已经模拟了水对光的吸收,现在还需要模拟水对光线的散射(更好的模拟出水体通透的效果)。散射部分的色带与吸收相反:
水对光线的散射,主要是模拟直接光照和环境光,当WaterDepth越大时,会有更多的光散射出来,而不是被吸收(吸收的部分是要从水下折射出来的光,散射是水上反射的光)。
模拟水对光线的散射
然后乘上环境光(环境光用的Unity提供的球谐采样)和直接光照:
sss散射
现在将sss散射和之前的效果叠加:
折射+反射+菲涅尔+sss
<hr/>六、高光反射
设置好水面的brdf,然后将顶点法线,viewDir和lightDir作为输入,即可得到高光反射部分:
高光反射
将其叠加到之前的效果上:
折射+反射+菲涅尔+sss+高光
<hr/>七、浮沫
1、深度采样
靠近岸边以及靠近物体的地方都需要产生浮沫,所以需要结合WaterDepth的x和y分量。
靠近岸边的的WaterDepth.y一般比较小,靠近中间悬浮物的WaterDepth.x一般比较小。
两者取反之后,再取最大值,即可得到下图所示的浮沫产生区域。
浮沫区域
还有在波峰的位置也可以添加一点浮沫,采用了frac(sin(x))用以产生随机数,使得波峰位置产生的浮沫具有一定随机性(其实好像没有用)。
波峰浮沫区域
将浮沫纹理采样,结合浮沫产生的区域,即可得到下图所示的浮沫遮罩:
浮沫遮罩
2、计算光照
demo里的浮沫本身没有颜色,需要通过浮沫遮罩计算光照得到:
不同光照对应了不同颜色的浮沫
通过浮沫遮罩将水面和浮沫进行混合:
折射+反射+菲涅尔+sss+高光+浮沫
<hr/>最后可以加上一些后处理效果,调整场景中Post-process Volume的Volume组件即可。Bloom:
Bloom后处理
暂时先更新这部分的内容,下一节讲讲水体的交互。因为焦散和水体的物理交互都是用RenderFeature实现的,所以安排到下一节一起。
感谢以下文章提供的帮助:
参考链接:
lionheart:BoatAttack_水效果分析2_水的细节作色效果(折射,sss,高光)
jaleco:Boat Attack 项目海水技术解析
https://github.com/Unity-Technologies/BoatAttack 对计算机图形学很感兴趣,只是不知道自己能力够不够[可怜] 够的,加油 妈呀差点吓死我[捂脸]我是学建筑的,我还以为这是可以针对我们出图的那种直接上材质和参数的渲染教程[飙泪笑]看见那么多高深的东西一下子自我怀疑了[捂脸]还好不是 ue里有自带的水材质,调调参数就行。不知道能不能导入twinmotion
页:
[1]