“我们开始开发新游戏,我们需要凉水。 你能做到吗?”
-问我。 “是的,没问题! 我当然可以,”我回答,但是我的声音颤抖着。 “还有Unity?”-对我而言,很明显,还有很多工作要做。
所以,喝点水。 在那一刻之前,我还没有像C#一样见过Unity,所以我决定在我所知道的工具(C ++和DX9)上制作原型。 我当时知道并且能够在实践中实践的是用于形成表面的法线的滚动纹理,以及基于它们的原始位移映射。 立即有必要改变一切。 水表面的逼真的动画形状。 复杂(强烈)阴影。 泡沫产生。 摄像机附带的LOD系统。 我开始在互联网上寻找如何做所有这些信息。
当然,第一点是对
杰里·特森多夫 (
Jerry Tessendorf)的 《模拟海洋水》的理解。
带有一堆复杂公式的学术传呼机从来没有给过我太多,所以经过几次阅读后,我几乎一无所知。 基本原理很明确:每个帧都是使用快速傅立叶变换通过高度图生成的,该变换根据时间平滑地改变其形状以形成逼真的水面。 但是我不知道如何计数。 我慢慢地钻研了在D3D9中的着色器上计算FFT的智慧,以及源代码和Internet上某个地方的一篇文章,我试图找到一个小时,但无济于事(不幸的是),确实对我有所帮助。 得到了第一个结果(恐怖的核战争):
开始取得的成功令人高兴,并且向Unity的供水从竣工开始。
在有关海战的游戏中对水提出了一些要求:
- 逼真的外观。 像近距离和远距离的透视,动态泡沫,散布等一样美丽。
- 支持各种天气条件:平静,暴风雨和中等天气。 一天中时间的变化。
- 船舶在模拟表面上的浮力的物理原理,是漂浮的物体。
- 由于游戏是多人游戏,因此战斗中所有参与者的水量应该相同。
- 表面绘图:抽空岩心飞行的绘制区域,从岩心进入水中的泡沫。
几何形状
决定建造一个类似四叉树的结构,并以摄像机为中心,当观察者移动时,摄像机将被离散地重建。 为什么离散? 如果您使用摄影机平稳地移动网格或使用“
实时水绘制-介绍投影网格概念 ”一文中的使用屏幕空间重新投影,则在长期计划中,由于几何网格的分辨率不足,多边形将向上“跳跃”,下来。 这是非常惊人的。 图片在荡漾。 为了克服这一问题,必须要么大大提高水网格多边形的分辨率,要么在长距离上“展平”几何形状,或者构建并移动多边形以使这些移位不可见。 我们的水是进步的(呵呵),我选择了第三条路。 与任何类似技术一样(尤其是对于在游戏中创建地形的每个人都熟悉的技术),您需要摆脱细节层次过渡边界处的T型结。 为了解决这个问题,首先计算具有给定镶嵌参数的三种类型的四边形:

第一种类型用于那些没有过渡到较低细节的四边形。 双方的顶点数量均减少2倍。 第二种类型用于边界,但不用于四角形。 第三种是角边界四边形。 通过旋转和缩放这三种类型的网格来构造最终的水网格。
这就是LOD水位颜色不同的渲染的外观。
前几帧显示了两个不同级别的细节的连接。
视频作为一帧充满了四边形:
让我提醒您,这都是很久以前的事情了(不是真的)。 现在,可以直接在GPU上(GPU Pro5。GPU上的四叉树)进行更优化,更灵活的设置。 它将进行一次绘制调用,而镶嵌可以提高细节。
后来,该项目移至D3D11,但手中没有达到海洋渲染这部分的升级。
波形产生
为此,我们需要快速傅立叶变换。 对于选定的(必要的)波浪纹理分辨率(现在叫它,我将在这里解释什么数据),我们使用艺术家设置的参数(力,风向,波浪对风向的依赖性等)来准备初始数据。 所有这些都必须纳入所谓的公式中。 菲利普斯光谱 我们考虑到时间修改为每个帧获得的初始数据,并对它们执行FFT。 在输出中,我们在所有方向上得到一个纹理平铺,其中包含平面网格顶点的偏移量。 为什么不只是高度图? 如果仅存储高度的偏移量,那么结果将是不切实际的“冒泡”质量,该质量仅在某种程度上类似于海洋:
如果我们考虑所有三个坐标的位移,则将生成美丽的“清晰”逼真的波:
一个动画纹理是不够的。 可以看到拼贴,但在不久的将来没有足够的细节。 我们采用所描述的算法,不制作一个,而是制作3个fft生成的纹理。 首先是大浪。 它设置基本波形,并用于物理。 第二个是中波。 最后,最小的。 3个FFT发生器(第四个选项是最终混合):
各个层的参数彼此独立设置,并且生成的纹理在水着色器中混合为最终波形。 与偏移量平行,还生成每个层的法线贴图。
通过在战斗开始时同步海洋参数,确保所有战斗参与者中水的“均匀性”。 该信息由服务器传输到每个客户端。
物理浮力模型
由于不仅需要绘制精美的图片,而且还需要绘制船只的实际行为。 并且还考虑到游戏中应该存在波涛汹涌的海浪(大浪)这一事实,需要解决的另一项任务是确保物体在所产生的海浪表面上的浮力。 首先,我尝试使GPU回读波形纹理。 但是,很快就知道,所有海上战斗的物理必须在服务器上进行,或者必须在服务器上读取海,或者说,它的第一层(设置波形)必须在服务器上读取(而且很可能没有快速和/或兼容的GPU),因此决定以Unity的本地C ++插件的形式在CPU上编写GPU FFT生成器的完整功能副本。 我自己没有实现FFT算法,而是在Intel Performance Primitives(IPP)库中使用了它。 但是结果的所有绑定和后处理都是由我完成的,然后是对SSE的优化和线程的并行化。 这包括准备每帧FFT的数据阵列,以及将计算值最终转换为波偏移图。
该算法还有另一个有趣的功能,它基于水物理学的要求。 需要的是快速获得世界上给定点的波高的功能。 这是合乎逻辑的,因为这是构建任何物体浮力的基础。 但是,由于在FFT处理器的输出处得到的是偏移图,而不是高度图,因此从纹理中进行常规选择不会在必要时为我们提供波高。 为简单起见,请考虑2D选项:

要形成波浪,纹素(垂直线所示的纹理元素)包含一个矢量(箭头),该矢量设置平面网格顶点(蓝点)在其最终位置(箭头的尖端)方向上的偏移量。 假设我们获取了这些数据,并尝试从中提取我们感兴趣的水的高度。 例如,我们需要知道hB处的高度。 如果我们采用纹理像素矢量tB,那么我们将得到一个接近hC的点的偏移量,该偏移量可能与我们所需的相差很大。 解决此问题的方法有两种:在每个高度要求下,检查一组相邻的纹理像素,直到找到一个与我们感兴趣的位置偏移的像素。 在我们的示例中,我们发现texel tA包含最接近的偏移量。 但是这种方法不能被称为快速方法。 扫描纹素半径尚不清楚要花多长时间(无论是风雨如磐的海面还是平静的海面,位移都可能变化很大)。
第二种选择-在计算了偏移图之后,使用散射方法将其转换为高度图。 这意味着对于每个偏移矢量,我们将其设置的波的高度写入偏移的点。 这将是一个单独的数据数组,将用于获取感兴趣点的高度。 使用我们的插图,单元tB将包含从向量tA→hB获得的高度hB。 还有一个功能。 单元格tA将不包含有效值,因为没有向量可移动。 为了填充此类“漏洞”,进行了一段遍历,以用相邻的值填充这些漏洞。
如果使用向量(红色-大偏移量,绿色-小)对位移进行可视化显示,结果如下所示:
其余的很简单。 有条件水线的平面是为船设置的。 在其上,确定采样点的矩形网格,该网格定义了从船上推出水的力的施加位置。 然后,对于每个点,我们使用上述水高图来检查它是否在水下。 如果该点在水下,则在此点将垂直力施加到身体的物理外壳上,并按从该点到水表面的距离进行缩放。 如果在水之上,那么我们什么也不做,重力将为我们做一切。 实际上,公式有些复杂(所有这些都是为了微调船舶的行为),但是基本原理是这样。 在下面的浮力可视化视频中,蓝色立方体是样品的位置,从它们向下的线是推出水面的力的大小。
在服务器的实现中,还有另一个有趣的优化点。 如果不同的战斗实例在相同的天气条件(相同的FFT模拟器参数)下通过,则无需为不同的战斗实例模拟不同的水。 因此,合理的决定是建立一个模拟器池,战斗部队要满足这些要求,以获取具有给定参数的模拟水。 如果多个实例中的参数相同,则相同的水将返回它们。 这是使用Memor Mapped File API实现的。 创建FFT模拟器后,它会通过导出必要块的描述符来访问其数据。 服务器实例不是启动真正的模拟器,而是启动“虚拟”,该虚拟仅释放由这些描述符打开的数据。 有一些与此功能相关的有趣错误。 由于引用计数错误,模拟器被破坏了,但是内存映射文件至少在打开一个句柄时仍处于活动状态。 数据停止更新(没有模拟器),水“停止”。
在客户端,我们需要有关波形的信息,以计算原子核向波中的渗透并播放粒子和泡沫系统。 在服务器上计算损坏程度,并且还需要正确确定核心是否已进入水中(波浪可以使船关闭,尤其是在风暴中)。 在这里,已经有必要像在视差映射或SSAO效果中一样进行高度图跟踪。
底纹
原则上与其他地方一样。 巧妙地揉合反射,折射,地下散射,考虑到底部的深度,考虑到菲涅耳效应,考虑镜面反射。 我们考虑根据太阳的位置散射山脊。 泡沫的产生方式如下:在波峰上创建一个“泡沫点”(使用高度作为度量单位),然后将新创建的点应用于先前帧中的点,同时降低其强度。 因此,我们从运行的波峰获得了尾巴形式的泡沫斑点。
我们使用获得的“斑点”纹理作为蒙版,在其中混合气泡,污点等的纹理。我们在波浪表面上获得了相当逼真的泡沫动态图案。 该掩码是为每个FFT层创建的(我提醒您,我们有3个),并且在最终混合中将它们全部混合。
上面的视频显示了泡沫面罩。 第一和第二层。 我修改了生成器的参数,结果在纹理上可见。
以及一段有点笨拙的暴风雨大海的视频。 在这里,您可以清楚地看到波形,发生器功能和泡沫:
水画
用法图片:

用于:
- 标记,细胞核扩展区的可视化。
- 在岩心碰到水的地方吸取泡沫。
- 泡沫的船尾
- 在船下挤出水,以消除海浪淹没甲板和淹没的货舱的影响。
明显的基本情况是投影纹理。 已实施。 但是还有其他要求。 附近的物种-由于分辨率不足(您可以增加,但不能无限增加)而产生的肥皂,我希望这些在水面上的投影图清晰可见。 相同的问题在哪里解决? 是的,在阴影中(阴影贴图)。 她在那里怎么解决的? 右侧,级联(平行拆分)阴影贴图。 我们还将将该技术投入使用并将其应用于我们的任务。 我们将摄影机的视锥分成N个(通常为3-4个)亚视锥。 对于每一个,我们在水平面中构造一个描述矩形。 对于每个这样的矩形,我们建立一个正交投影矩阵,并为N个这样的正交相机中的每个绘制所有感兴趣的对象。 每个这样的相机都绘制成一个单独的纹理,然后在海洋着色器中,将它们组合为一张实心的投影图像。
所以我在海上放了一架带有国旗纹理的巨大飞机:

以下是拆分包含的内容:

除了通常的图片外,还必须以完全相同的方式绘制一个额外的泡沫面具(用于痕迹船只和原子核撞击的地方),以及一个用来挤出船下水的面具。 这是很多相机和许多过道。 最初,它如此制动,但是,在切换到D3D11之后,使用几何着色器中的几何“传播”并通过SV_RenderTergetArrayIndex将每个副本绘制到单独的渲染目标中,可以极大地加速这种效果。
改进和升级
D3D11在很多时候都是非常自由的双手。 切换到Unity 5之后,我在计算着色器上创建了FFT生成器。 在视觉上,什么都没有改变,但是变得更快了。 将来自单独的成熟相机渲染的反射纹理的错误计算转换为“
屏幕空间平面反射”技术,可以极大地提高性能。 我在上面写过关于优化水面对象的文章,但是我的双手并没有达到将网格转移到Quadtree GPU的目的。
可能可以做得更好,更简单。 例如,不要使用CPU模拟器围栏花园,而只需在具有WARP(软件)d3d设备的服务器上运行GPU选项即可。 那里的数据数组不是很大。
好吧,总的来说,以某种方式。 开发开始时,一切都是现代而酷的。 现在在某些地方已经不合适了。 有更多可用的资料,甚至还有与github类似的类似物:
Crest 。 大多数有海的游戏都使用类似的方法。