跳转到内容

水

水有三部分。Water 着色器(builtin:water)是外观:折射、水深、反射、泡沫。WaterBody 说明哪里有什么水:海洋、湖泊、河流。WaterZone 把它区域内的所有水体烘成一张场,据此画出一个由波浪抬起的水面,并让代码能查询这个水面,所以脚本读到的就是画出来的。本章三者都讲,也讲还没有的部分。

水是一个可选模块:只有场景里有 WaterBody(或脚本导入了 esengine/water)时,包才会带上它,所以 2D 游戏不带任何水的代码。

给某个实体加一个 WaterBody。水体不会单独画出来:它是水体区域(水体区域)的输入,区域把它和其他所有水体一起烘成一个水面。实体决定水体的位置;它可以绕自己的上方向旋转,但不能倾斜。

字段 作用
form 海洋:没有边,在地面低于实体高度且与开阔海相连的地方处处都在这个高度,并一直延伸到区域之外。湖泊:以实体为中心、sizeX × sizeZ 的矩形,或 boundary 给出的轮廓,在实体的高度上。河流:沿同一实体上的 Spline。
bodyId 游戏给这个水体起的名字,在每次运行中都一样(实体编号不是):查询用它来称呼这片水。留空则用实体称呼。
sizeX、sizeZ 湖泊没有自己的轮廓时的尺寸。
boundary 湖泊的轮廓:它自己的 xz 平面上围一圈的三个或更多点。留空就是矩形。
shoreWidth 从岸边往里多远、水多深,波浪才长到完整高度,对所有形态都适用。0 表示波浪一直完整到岸边。
flowSpeed 河流的流速,世界单位每秒,指河面最宽处的流速。
waveHeight 最高那道波的高度,世界单位。波浪的波长、速度和方向属于区域,所有水体共用。
bedDepth 河流把河床挖进地形、河道中线处低于水面的深度。0 表示不改动地面。

移动水体、改它的字段、轮廓或样条,或者改它下面的地形,都会让区域重新烘焙,所以水立刻跟着变。

WaterZone 说明水画在哪里、画得多细,以及所有水体共用什么。把它放在一个带 Transform 的实体上:它的区域是 sizeX × sizeZ,以实体为中心(只看实体的 x 和 z)。

字段 作用
sizeX、sizeZ 烘焙的区域。区域外的水不画;海洋会延伸到区域之外。
resolution 区域较长一边上的纹素数:岸线和河流描得有多细。
material 整个水面使用的 Water 着色器材质。留空用内置的水。
waveLength、waveSpeed、waveDirection 所有水体共用的波浪(波浪)。
surf、surfLength 拍岸浪:浪头相对所在水体的波浪有多高(0 不画),以及拍岸浪的波长(拍岸浪)。
cellSize、cells、rings 水面的网格:最细一圈的格子大小、每圈一边多少格,以及有几圈(几何体)。
ripples、rippleSize、rippleSpeed、rippleDamping、foamFade 区域保留的涟漪(涟漪)。

区域把它范围内的所有水体烘成一张网格,每个纹素记着那里的水是什么样:

  • 水面的高度:湖的水位、河流曲线在最近处的高度、海的水位;
  • 到最近岸边的有符号距离,在水上为正:水面在哪里被裁掉、在哪里起泡沫;
  • 那里的波浪有多高:水体的 waveHeight,向岸边、随水变浅而变平静(以及保留下来的份额,它也让着色变平静);
  • 水流(河流的),以及水在地面之上的深度。

两个水体相接处,每个纹素按自己在各水体内部有多深来加权它们各自给出的值,过渡宽度是一个半纹素,所以数值从一个平滑过渡到另一个:河流流进湖里的地方就在湖的水位上,它的水流消失在静水里,河岸接上湖岸,没有接缝。湖泊和河流优先,海洋取它们剩下的,所以海边的湖归湖。没有任何水体占据的纹素,保留离它最近的水的高度,这样被岸线裁掉的水面边缘不会被拉低到零。

没有 WaterZone 的场景,会得到一个覆盖所有湖泊和河流(外加它们岸线的余量)、使用组件默认值的区域,每纹素是最细格子的一半(较长一边 32 到 1024 个纹素);它的格子会变大,让相机在区域里任何位置时水面都能覆盖整个区域。只有海洋的场景不需要区域:整个水面就是海洋。只使用一个区域:找到的第一个 WaterZone。

烘焙在 CPU 上进行,不需要渲染器,所以不画任何东西的服务器或测试也能查询水面在哪里。水体、区域或地形一变,就整张重烘,代价是纹素数乘以水体数。

四道波峰比波谷更尖的正弦波(exp(sin(theta) - 1)),相差几个方向,各自的波长、高度和速度是区域的 waveLength、waveSpeed、waveDirection 的份额。所有水体共用它们;它们在某处有多高,由场里那一处决定:所在水体的 waveHeight,乘以岸线留下的份额,再乘以天气的份额(驱动风的 Weather 会按水面迎风的开阔程度放大波浪:风浪随吹过的水面长度增长,所以海完全受天气影响,湖按其宽度与八个 waveLength 之比受影响,河几乎不受影响)。它们会移动几何体:着色器按波高抬起每个顶点,片元按同一个公式逐像素取斜率,所以被光照的表面就是画出来的表面,和天空交界的轮廓也随波浪起伏。

波浪在世界的 xz 平面上度量,把水面竖直向上抬起。参数以世界单位计:以米为单位的场景用默认值,以厘米为单位的要把长度相应缩放。

水与地面相接处,浪朝岸边推进。区域量出每一处沿地面到最近岸线的距离(海洋漫过沙滩和湖泊都是同一种量法),在最后四个 surfLength 的水面上,拍岸浪按这个距离立起来:朝岸的一面陡、背面长,以同波长深水波的速度乘 waveSpeed 向岸推进。水变浅时浪变高,在这段水域的靠岸部分破碎发白,浪峰过后白沫还会留一阵;过了岸线浪会冲上地面:那里也画出水面,略高于静水面,地面高过它的地方由深度测试裁掉,所以水线会爬上沙滩再退回来。浪头高度是所在水体的 waveHeight 乘 surf(含天气对波浪的缩放),再乘水面的开阔程度:拍岸浪是从开阔水面传来的涌浪,所以海岸会破浪,湖岸只是轻拍;有水流的地方没有拍岸浪,所以河流和河口保持平静。浪峰按自己的距离场排列:开阔水域的边缘(河里的水当作陆地)在一个 surfLength 范围内平滑,所以浪从开阔水面以直线涌来,近岸才转向贴合岸线,在河口处直线跨过;浪在哪里涌起、破碎、上滩仍按真实岸线。白水多少也由岸决定:区域烘焙时记录每段岸的陡度,浪拍在礁石和悬崖上碎成白沫,在缓坡沙滩上只冲出一条细线,上滩距离也很短。Waters.sample 也加上同样的拍岸浪,沙滩附近的漂浮物会随之起伏。

整个水面是一个网格:一圈圈格子数相同的网格(每边 cells 格),最细一圈是 cellSize 的格子,每圈的格子是里面那圈的两倍,中间留出里面那圈的洞,所以它从中心伸出 cells * cellSize * 2^(rings - 1) / 2 远,又把三角形花在近处。它跟着相机、落在所有圈都对齐的格点上,材质的原点同步移动,所以网格在水下面移动,而水留在世界里不动。它的顶点钩子在每个顶点读区域的场(高度逐纹素读取,先解码再混合),把顶点抬到那里的水面高度再加上波浪;场说那里没有水的像素会被裁掉。

粗的一圈画不出短的波:一个波长不到三格就会闪烁,所以它的顶点会淡掉比自己六格还短的波(着色仍然逐像素显示它们)。两圈相接处,粗的一圈沿边的顶点少一半,表面可能在顶点之间分开,分开多少取决于波浪在它们之间做了什么;让 waveLength 至少是看得到的最粗一圈的六格,接缝就看不出来。

湖泊是位于实体高度上的轮廓。区域取到轮廓的有符号距离,场景里有 Terrain 时还取水在地形之上的深度,由这两个量得出一个点保留多少波浪(在岸边或水很浅处一点不留,从 shoreWidth 往里全留)。水面按轮廓裁出(按区域的分辨率描出,而不是按网格的格子),几何体和着色都向岸边变平静,岸边还会起泡沫。

水填满低处,在山丘与它相交的地方被截断,波浪随变浅而消失。水体只在水面高于地面的地方才是水:湖泊轮廓或河流宽度之内、高出水面的山丘或河岸是陆地,画面和查询都如此。没有地形时,湖要多深有多深。轮廓不应自相交。

海洋也同样从地形取岸线:在区域内,地面高出海平面的地方被裁掉,波浪在 shoreWidth 的水深内变平静。它是区域边缘之外的开阔海:低地只有通过低地连到那条边缘时才是海,所以被更高地面围住、低于海平面的洼地保持干燥(或者装着自己水位的湖)。在区域外,海洋处处在它的水位上,波浪完整。

水体重叠时,湖泊优先于河流(汇入湖泊的河在湖里就是湖,取湖的水位,水流在静水中逐渐消失),两者都优先于海洋;一个地方属于哪个水体,与水体加入的顺序无关。水面不投射阴影:阳光穿过水面照亮水下,由焦散表现。

决定世界的湖泊和河流放在哪里是游戏(或它的生成器)的事;findBasins(grid, ground, { seaLevel }) 从网格上的一次优先级泛洪给出地面这一方的信息:

  • basins:每个洼地,带着水会把它填到的 level(它边缘的最低点)、最深点 deepest,以及溢出的出口 outlet;
  • filled:每个格子上水会到达的高度;basin:格子属于哪个盆地;
  • downstream 和 accumulation:每个格子排水流向哪里,一直到区域边缘,以及有多少水流经它,河流就在这里;drainPath(map, x, z) 从某点沿它走下去;
  • sea:给了海平面时,海从边缘到达的低地。

在盆地的 level 上按它的轮廓放一个湖就正好把它填满;从它的 outlet 沿 drainPath 放一条河,就流在水会流的地方。

河流沿着它自己实体上的 Spline(样条):宽度取曲线在那里的宽度,高度取曲线在最近处的高度,所以河流是顺着坡往下流的。在同一个实体上放一个 Spline 和一个 form 为 River 的 WaterBody,区域就会烘焙它。

  • 水流。 flowSpeed 是河面最宽处的流速;窄的河段更快、宽的河段更慢,范围在它的四分之一到两倍之间,和同样的水流过不同宽度时的情形一致。方向就是曲线的方向。它从河流起点起在一个河宽内渐强,流入别处时在一个河宽内渐弱,所以河口是静水。
  • 表面随水流动。 着色器把波浪和涟漪顺流带走:表面的两份拷贝每次沿水流前进一个周期,一份淡入的同时另一份淡出,所以其中一份被放回起点的那一刻被另一份遮住,河道拐弯处表面也不会被拉长。
  • 波浪。 河流有自己的 waveHeight,和其他水体一样在 shoreWidth 内向河岸变平静。Waters.sample 在 flow 里给出水流(世界单位每秒,沿河道方向;别处为零)。
  • 河床。 bedDepth 大于零时,河流把河床挖进地形:河道中线处低于水面 bedDepth,河边与水面齐平,河岸以相同的坡度继续升高直到接回原地面,最远两个河宽。它以地形修改器的形式铺设,每段河道一个,所以画出来的地面、查询和碰撞都跟随它;编辑样条只重做河道沿线的地面;比河床更低的地面保持不变。游戏自己刻好河道的地面(带河流一起生成的高度图)请把 bedDepth 留为 0。
  • 河岸与河口。 河岸是场里岸线距离过零的地方,按区域的分辨率描出。河流流进湖、海或另一条河的地方,两者在一个半纹素内混合,所以没有接缝,河口没有横跨的岸线,也没有高度台阶。

给一个有 RigidBody3D 和碰撞体的实体加上 Floating 组件,水就会托住它并带着它走。它通过作用在刚体上的力来工作,力来自询问水面(查询表面)在身体的几个点之下在哪里,所以不需要渲染器:不画任何东西的服务器、测试或构建,漂浮的结果也一样。

字段 作用
buoyancy 水的密度与物体密度之比。1 表示完全没入时刚好托住;2 表示漂在一半没入的位置。
drag 水对物体穿行的阻力,每秒。它让物体在静水里停下来,在河里随水流前进。
radius 物体上的一个点要在水面以下多深(世界单位)才算完全湿透。
points 物体自身空间里、世界单位的点,水会针对它们作答。把它们散开(比如船体的四个角),波浪就会让它倾斜。留空就是原点那一个。
wake 物体在会保留涟漪的水里(涟漪)激起多大的涟漪。0 表示不激起。

在每个点上,没入水里的份额(水面处为 0,从 radius 往下为 1)给出一个逆着重力的浮力 mass * g * buoyancy * 份额 / 点数,作用在这个点上,还有一个对抗这个点相对水的速度的阻力(物体自己的速度加上转动在那里带来的,再减去水流)。这些力在每个固定步长、世界步进之前施加。buoyancy 为 2 且只有一个点的物体,中心会停在水面以下 radius / 2 处。

它需要 3D 物理模块,并且是带有施力函数(_physics3d_addForce、_physics3d_getBodyDynamics)的那一版;没有这些函数的构建会警告一次,什么也不会漂。漂浮是一个刚体在一种流体里:没有附加质量,没有船体形状带来的升力,完全没入的物体除了水流外不会被推向任何方向。

ripples 大于 0 时,区域会记住扰动水的东西。它保存水面的一个方形窗口(边长 rippleSize 世界单位,沿边 ripples 个格子),作为一张高度网格,用波动方程推进:每个格子被拉向它的邻居,并带着它原有的速度,所以放进去的一个凸起会以 rippleSpeed 的速度扩散成一圈圈环,每秒损失 rippleDamping 的高度,最后消失。网格旁边还保存着泡沫:扰动在哪里发生就往哪里加泡沫,泡沫按每秒 foamFade 消退,所以一次水花留下的白水会逗留、变薄,而不是随着环一起突然消失。

  • 谁来扰动。 脚本调用 Waters.disturb(x, y, z, radius, strength)(strength 是以世界单位计的高度,负数是下凹;没有水的地方什么也不做),Floating 物体也会自己做:落进水里、从水里升出或沿水面运动的东西,会按速度推动水(wake 是倍数)。
  • 它改变什么。 着色器把网格当纹理读:涟漪的斜率弯折法线,光照、反射和折射看到的就是这个法线;泡沫让表面变白。几何体不会被它抬起。代码读到的表面包含它们:Waters.sample 把涟漪的高度加到表面上,Waters.rippleAt(x, y, z) 单独给出它,所以一个漂浮的球会被从它下面经过的环托起。
  • 在哪里。 窗口以整格为单位跟随活动相机,所以任意大小的水,在看得见的地方都有涟漪,代价只是一张网格,由所有水体共用。窗口离开的地方被遗忘,新到的地方是静水。到达边缘的波会出去而不是被反射。网格在 CPU 上推进,子步数以保证波一步不跨过超过半个格子为准,并作为 RGBA8 纹理上传(红是高度,绿是泡沫):各后端一致。

网格最多能容纳 max(2 * 最高波浪, rippleSize / 50) 的高度,最高波浪是场里任何一处波浪能达到的最高高度乘以天气的份额:持续的扰动(运动的物体)会在这里饱和,而不会无限堆积。一个格子是 rippleSize / ripples 世界单位,所以窄于一两个格子的涟漪画不出来。代价是每个子步 ripples 的平方,加每帧一次纹理上传:128 是一万多个格子。

当活动相机在一个水体之下时,画面就是水的。水模块每帧报告相机在哪儿(来自 Waters.sample),后处理体积系统只要相机还在水下,就像天气带来雨一样,加上一个 Underwater 效果:

  • 按穿过的水雾化。 每个像素的光穿过了从眼睛到它出处的距离(天空没有表面,取水能被看穿的最长距离 u_range),水按颜色把光吸走,红色最先,用的是和水面材质绘制时相同的 u_absorption 和 u_density,再还给你它自己的颜色。水下一面白墙是蓝绿色的,远的比近的更甚。
  • 越深越暗。 水还回来的颜色随相机下沉的深度变暗(u_lightFalloff),因为到达它那里的光来自上方。
  • 横过镜头的水线。 每个像素都会被问:它在相机近平面上的位置,是不是在相机所在处的水面之下。所以镜头一半在水里的相机,上半是干的、下半是湿的,中间有一条柔和的线(u_band,近平面的十二分之一),穿过水面就是这条线在镜头上滑过,而不是硬切。从水面上方几个近平面的距离起,效果就开始作用。
  • 穿过波浪的阳光。 相机看到的东西,被波浪把阳光聚成的那张明亮细线网照亮:和水面从上往下画的是同一个图案(Water 材质上的 u_causticStrength、u_causticScale、u_causticSpeed,效果也会拿到),落在朝向太阳且不在它的阴影里的表面上,阳光穿过水到达那里走得越远就越弱。它跟随第一盏平行光,有阴影图时也用阴影图。
  • 光束。 沿每条视线的水把阳光散射向相机:图案沿视线分 24 步行进,每一步在阴影图说阳光能到达的地方才亮,并且按它上面的水能放过多少光来衰减,这就是光束(u_shaftStrength、u_shaftRange)。它在水里每个像素花 24 次采样。
  • 你写了就用你的。 在体积里自己写了 underwater 效果的场景,保留它的数值;只会填入相机的深度和它完全没入的程度。

水面本身从下面看是翻过来的,光穿过它的方向也反了:从水里出来,超过临界角(约 49 度)的光线被整个反射,所以上面的水是头顶一扇天空的窗户,窗户之外是水下世界的镜子,菲涅尔项也随之改变。窗户的边缘并不是自己画出来的圆:它就是折射走到尽头的地方。镜子里映的是它下面的世界:反射光线沿着帧复制出来的场景画面一路走(20 步,步长递增),它碰到的东西的颜色就是水反射的颜色,所以水底的一个盒子会出现在它上面的水面里。光线在屏幕上没碰到任何东西的地方,镜子就是水自己的暗色。

从上面看,同样的焦散会穿过水面照亮水下的东西:Water 着色器找出折射光线在哪里结束、到那里的阳光光线在哪里穿过水面,再按那里的图案(以及阴影图)给它打光,所以这张网会随折射它的涟漪一起弯曲。

Spline 是数据,什么也不画:points(每个点是实体自身空间里的 x、y、z 和一个 width)和 closed。河流读取它实体上的那一个;任何沿路径走的东西都可以用 SplineCurve 读它:

const curve = new SplineCurve(spline.points, spline.closed);
curve.position(s); // 沿曲线走过 s 距离的那个点
curve.tangent(s); // 那里的前进方向
curve.width(s); // 那里的宽度,从一个点的宽度平滑过渡到下一个点的
curve.nearest(x, z); // 在 xz 平面上离它最近的地方:s、距离、在哪一侧

它是向心 Catmull-Rom 曲线(穿过每一个点,点在一头密、另一头疏时也不会打圈或出尖),按沿曲线的距离来度量,所以曲线上的位置是离起点 s 个单位,而不是在点靠得近的地方跑得很快的参数。开放曲线越过两端就停在端点;闭合曲线会绕回去。样条是一个可选模块(esengine/spline)。

Details 只显示水体这种形态用得到的字段:湖有尺寸和边界,河有流速,所有形态都有岸线宽度和波高。湖的 边界 一行说明它的轮廓有多少个点(或者说它就是按尺寸画出的矩形),并可以重置回矩形。WaterZone 的字段按组显示:区域、外观、波浪、细节和涟漪。

选中湖或河,它的点就成为视口里的手柄,和多边形碰撞体的顶点一样:

  • 湖。 轮廓每个点上一个方块,每条边中间一个圆。拖动点,在湖自己的平面上移动它;拖动边上的圆,在那里插入一个点;按住一个点、原地松开,删除它。湖至少保留三个点;没有点时,矩形的四个角就是这些点:拉动其中一个,轮廓就变成它自己的边界。
  • 河。 在它的 Spline 曲线上同样操作,每个点上还有第二个手柄,沿河的横向偏出宽度的一半:拖动它来设置那里的宽度。河至少保留两个点。最后按下的点,会在 Details 的 Spline 下显示它的宽度,可以直接输入。
  • 每次手势是一步撤销,点是边拖边写回的,所以水会跟着变。这些手柄是 Gizmo 菜单里的 Water 一项。

区域的范围没有手柄:在 Details 里设置它的尺寸。

水只花项目画质档位允许它花的。五个字段在每个档位的配置里(project.esproject,features.rendering.quality.profiles),项目可以改变一个档位的含义。项目设置 → 渲染 → 档位 · 水体为“正在编辑的档位”编辑它们;画质关闭或项目排除了水体模块时,这一组不显示:

字段 低 中 高 作用
waterWaves 2 3 4 四道波里累加几道:几何、着色以及 Waters.sample 的回答都按它。
waterRippleCells 32 64 128 涟漪网格每边最多多少格;区域自己的 ripples 会被限制到它,0 表示不保留。
waterDisplacement 关 开 开 波浪是否抬起几何体。关就是轻量水:位于水面静止高度的表面,波浪只在着色里。
waterMirror 关 开 开 从下面看水面时,是否映出它下面的世界(屏幕空间步进)。
waterCaustics 关 开 开 阳光的光网是否画在水下的东西上,以及从水下看时的光束。

代码读到的跟画出来的一致:没有位移的档位里,Waters.sample 看到的是静止的水面,漂浮物停在它上面,波数少对两者同样生效。有画质策略运行时由当前档位决定;没有时,小游戏得到低档的水(它能花的最少),其他情况得到全部。轻量水不为波浪做任何顶点计算,着色仍然在动,所以远看仍是水。

Waters.sample(x, y, z)(资源 WatersRes)从区域的场作答,和画水面用的是同一组数:

  • entity:那里占水最多的水体(两个水体相接处,是这个点在其内部更深的那个),以及 bodyId,游戏给它起的名字;
  • depth:这个点高出表面多少(在下面则为负),世界单位;
  • normal:表面在那里的法线,世界空间;
  • surface:这个点正上方或正下方、表面上的世界坐标;
  • inside:这个点是否在水上:只要回答不是 null,就总是;
  • flow:水在那里怎么流动,世界单位每秒(河流的水流;静水为零)。

它读的是着色器画的同一个公式,取这一帧的时间,带上那里波浪的高度和涟漪。Waters.heightAt(x, z) 只给出水面的高度。点上方没有水(在所有岸线之外,或在区域之外且没有海洋)则返回 null。Waters.bodyAt(x, z) 说明一个地方属于哪个水体:它的实体、bodyId、形态和静止水位。Waters.shoreAt(x, z) 是一个地方沿地面到水边的距离:干燥的陆地上为正,水面上为负。

代码读到的是完整的波浪和;远处的粗圈画得少一些(见上),所以在接近地平线处两者可能相差那些短波,它们本来就很小。

builtin:water 也可以单独用在任何网格上,这是组件出现之前做一块平水面的方式。u_waveSpace 取默认值(0)时,波浪按世界的 xz 平面度量,什么也不抬起,表面被当作是平的。区域把它设为 1:网格的 uv 是它的 x 和 z,u_origin 是网格跟随相机移到的位置,顶点被抬到区域的水面和它的波浪上。

  • 焦散。 u_causticStrength(0 关掉它)、u_causticScale(一格的大小,世界单位)和 u_causticSpeed 决定水下东西上那张光网的样子,从上面看是这样,通过效果从下面看也是这样。
  • 波浪。 u_waveLength、u_waveHeight、u_waveSpeed、u_waveDirection;风吹起的涟漪(u_rippleStrength、u_rippleLength)骑在它们上面:围绕风向的六道短波,从 u_rippleLength 到它的五分之一,每道在会闪烁之前随距离淡出。
  • 雨。 Weather 驱动雨时,雨滴在水面上激起环纹(u_rain,由区域写入):向外扩散然后消失的圆环,每个 u_rippleLength 里有几个,雨越大越多,水面也稍微变得不那么锐利。它们是画出来的而不是模拟的:不会往区域的涟漪里放任何东西,也不产生泡沫,并随距离淡出。在区域的水面上,这四个由区域每帧写入(它自己的波长、速度和方向,高度则是天气的份额,再乘以场里每一处的高度),所以材质里这四个值在那里不起作用。
  • 碎闪。 阳光在细小坡面上的亮点来自两张可平铺、在表面上滚动的法线图,它们打包在材质唯一容得下的那一张纹理 u_detailMap 里:红绿是第一张的坡度(x 和 z,0.5 为平),蓝和透明是第二张的,按数据读取。每张有自己的 u_detailTile(重复一次的世界单位)、u_detailStrength(比图上画的陡多少倍;0 表示不用,没设置的材质因此保持原样)和 u_detailFlow(每秒漂移的世界单位)。亮点是这些坡面受光后的镜面反应,不是画上去的白色贴图,所以只出现在太阳、眼睛和坡面对齐的地方,光变弱它们也跟着淡出。它们只在片元里计算:几何体和 Waters.sample 不带它们。
  • 折射与水深。 水下看到的是不透明场景的画面,被波浪按水量多少偏移。水深是量出来的:绘制水之前,帧会拷贝一份场景每个像素离眼睛多远,水在某个像素处的深度就是那个距离与水面自己的距离之差。光沿着这段水柱被吸收——红色最先(u_absorption、u_density)——水再还给你它自己的颜色,随水变深从 u_shallowColor 过渡到 u_deepColor。
  • 反射。 天空、灯光和环境按菲涅尔项反射(水是 1.333),所以俯视时清澈、掠射时像镜子。想要场景本身的倒影,在体积里加上屏幕空间反射。
  • 泡沫。 水柱浅于 u_foamWidth 的地方(岸边、礁石、桥墩脚下)和波峰上(u_foamCrest)出现泡沫,由流动的噪声打散。在区域的水面上,烘好的岸线也起泡沫,范围是离岸边或浅水 u_foamWidth 以内,所以量不出深度的地方岸边也有泡沫。
  • 水流。 u_flowCycle 是沿水流带走的两份表面拷贝,每份存活多少秒后由另一份接替。
  • 区域的数值。 下面这些由区域写入,不是给作者设的:u_zoneOn、u_zoneHeights 和 u_zoneField(烘好的场,两张纹理),u_zoneOrigin、u_zoneSize、u_zoneHeight 和 u_zoneRange(区域在哪里、纹理按什么范围编码),u_oceanOn、u_oceanLevel 和 u_oceanWaves(区域之外的海洋),u_cellSize(最细一圈的格子,用来算粗圈要淡掉的波),u_geoWaves(轻量水为 0),以及涟漪和画质相关的(u_rippleOn、u_rippleMap、u_rippleOrigin、u_rippleSize、u_rippleTexel、u_rippleRange、u_waveCount、u_mirror、u_causticOn)。

还没有的部分,免得让人意外:

  • 它需要与玻璃相同的、由引擎拷贝的场景(有后处理链在运行),以及渲染目标能存浮点数的设备来存深度。没有链时,水作为半透明表面画在场景上;没有浮点目标时,深度处处是 u_fallbackDepth,所以颜色不随水深变化,只有区域烘好的岸线会起泡沫。
  • 混合(半透明)表面(玻璃板、粒子、叶片卡片)与水和玻璃是按像素排序的:一帧里两者都有时,场景分层绘制(不透明场景;在其上画混合物体,作为水折射的画面;把不透明场景放回去再画水;最后把混合物体按水的深度再画一遍)。水下的混合物体能透过水看到,水前的躺在水面上,没有水的地方只混合一次。代价是多两次全屏场景拷贝和对混合物体的第二遍绘制,只在同时有两者的帧里发生。没有深度测试的混合物体,或横跨水面的,在水面两侧都整个画出;水前面的物体会因为水折射的画面里包含它,而在水里被淡淡地再看到一次。
  • 水是水平的:水体可以绕自己的上方向旋转,但不能倾斜;水面在每一处只有一个高度,所以没有水上之水。
  • 只使用一个区域,即找到的第一个 WaterZone,它的所有水共用一个材质;也只使用一个海洋,即找到的第一个。区域范围之外的水不画,海洋除外。岸线和河流按区域的分辨率描出:只有一两个纹素宽的河,或比这更细的轮廓细节会丢失,大的区域需要高的 resolution。
  • 水面从相机伸出 cells * cellSize * 2^(rings - 1) / 2 远;自己放了 WaterZone 时,要让这个距离覆盖应该看到的范围(不是你放的区域会自己把格子变大来做到这一点)。
  • 河流只有在 bedDepth 大于零时才会切削地面(见上文「河床」);急流和瀑布不会按水流的快慢或陡度起泡沫。湖泊的轮廓是平面上的多边形:它不跟随弯曲的岸线。
  • 东西可以漂浮(见漂浮),也会激起尾迹(见涟漪);还没有东西能游泳。
  • 焦散和光束只跟随第一盏平行光,来自一个图案而不是模拟:这张网并不响应波浪的真实形状。从上面看时,焦散把水下的东西当作朝上的面(画面里那里没有法线),所以水面下的一面墙会像地板一样被照亮。
  • 从下面看的镜子是屏幕空间的:它只显示帧里在屏幕上的东西,射线离开画面或什么也没碰到时,找到的是水自己的颜色。
  • 涟漪只弯折着色并携带泡沫:不会抬起几何体;它们是跟随相机的一个窗口,所以离相机远的水什么也不保留,分屏时两个相机共用第一个的。涟漪留下的泡沫不会被河流的水流带走,抬起几何体的波浪也不会(只有着色里的会)。
  • 波浪是这四道波峰尖的正弦,不是波谱,并且只把表面抬起,不会把它推向一侧。