本文是一些算法技术的初探分析,会陆续修订。
1、问题
河流是一种非常复杂的多边形。在二维地图可以采用多边形填充算法(DDA)对任意复杂的多边形进行绘制与填充。但是三维引擎只能采纳三角面进行渲染。但在如此复杂的多边形面前,简单的三角化算法不能解决问题。会出现计算错误或者三角面很长的情况。
2、网格切割
将河流根据经纬度或者某些度量单位进行切割成瓦片是一种可行办法。可采用开源软件Clipper2进行处理。Clipper2是一个开源免费软件库(用 C++、C# 和 Delphi Pascal 编写),用于执行线和多边形裁剪和偏移。可以预见被切割的河流虽然比较规整,但三角化的效果(错误率和长三角情况)稍微会好一些。
3、河流带方向
比较好的河流效果应该是高仿真的效果。如GitHub - Arnklit/Waterways: A tool to generate river meshes with flow and foam maps based on bezier curves.
godot的Waterways插件
它需要曲线引导水的流动方向。适合手绘并且具备较好DEM的情况。在这里河流片元着色器的颜色可以描述为下面简单的公式。
color=f(g(time,noise),h(waterDepth,direction),p(waterNormal,waterColorRamp))
如果基于该技术路线,那么还需要将矢量面的水面提取水面中心点。还有一点需要注意的是,该示例的多边形会根据与地面物体的碰撞,得到切割后的多边形。
4、计算水体深度
计算水体深度的代码非常有意思,我单独讲下。这里的水体深度不是严格的水面高度减去地形距离。而是通过显存中的深度值计算得到的。我附上源代码并进行简单解释:
float depth_tex = textureLod(DEPTH_TEXTURE, SCREEN_UV, 0.0).r;
//这行代码从名为 DEPTH_TEXTURE 的深度纹理中获取深度值。其中DEPTH_TEXTURE 是显存中已经计算的深度图。
//因为水面是后期处理,因此这个深度可以理解为DEM高度
float depth_tex_unpacked = depth_tex * 2.0 - 1.0;
float surface_dist = PROJECTION_MATRIX[3][2] / (depth_tex_unpacked + PROJECTION_MATRIX[2][2]);
//根据显存深度值得到真实的距离值
float water_depth = surface_dist + VERTEX.z;
//水体深度=眼睛到水底的距离+水平面的高度。
//比如眼睛到水底的距离是-3.2米,水平面的高度是4米,则水体深度是0.8米。
细心的读者会发现有几个比较难以理解的点。一是surface_dist的计算方法比较奇怪。二是眼睛到水底的距离为啥是负数。
先回答第二个问题,这是因为摄像机面朝负Z轴。对于位于摄像机前方的物体,其Z坐标将是负数。比如,一个物体在世界坐标系中的 Z 值为 -5,表示它在摄像机前方5个单位的地方。这种设计使得物体在视图空间中的深度易于理解和管理。
现在回答第一个问题。首先需要知道投影矩阵的公式,如下:
| f/aspect 0 0 0 |
| 0 f 0 0 |
| 0 0 (zFar + zNear)/(zNear - zFar) 2*zFar*zNear/(zNear - zFar) |
| 0 0 -1 0 |
其中 PROJECTION_MATRIX[3][2]表示 2*zFar*zNear/(zNear - zFar)。PROJECTION_MATRIX[2][2]表示(zFar + zNear)/(zNear - zFar) 。还需要知道一个物体在显存深度图的计算公式:
float distance = near * far / (far - depth * (far - near));
因为着色器并没有传递near和far,因此只需要证明下面两个公式是一致的即可。具体过程不再展开。
float distance = near * far / (far - depth1 * (far - near));--①
float distance2=pm32/(depth2-pm22)------②
depth2=depth1*2-1---③