城市级河流三维处理及展示的一些技术
本文是一些算法技术的初探分析,会陆续修订。
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---③
5、每一滴水的压力
在河流里面,每第一滴水都面临着很多压力。包括落差水压、后面水的推力和自己的惯性力。因此水的压力可以用下面公式进行表示(简化公式):
flow_base + steepness_map * flow_steepness + distance_map * flow_distance + pressure_map * flow_pressure
-
flow_base: 这是一个基础值,表示流动力的基础水平。
-
steepness_map * flow_steepness: 这个部分将倾斜度图(
steepness_map
)与一个表示倾斜影响的权重(flow_steepness
)相乘。它表示地形的倾斜程度对流动力的影响。 -
distance_map * flow_distance: 类似地,这个部分涉及一个距离图(
distance_map
),它与权重flow_distance
相乘。这个因素可能表示从某个点(例如源头)到当前计算点的距离对流动力的影响。 -
pressure_map * flow_pressure: 这一部分将压力图(
pressure_map
)与权重flow_pressure
相乘,表示压力对流动力的贡献。
当然,如果让你把整个河流的地形坡度计算出来,还是相对吃力(不是说难算,而是算了要存、读,流程麻烦)。因此我们仍然可以基于片元着色器的一些基本数据计算坡度。这需要知道一个前提条件。
在 Godot 中,
TANGENT
表示模型表面切线的方向,而BINORMAL
表示与切线相对应的副切线方向。这两个向量与顶点法线一起构成切线空间,允许在物体表面进行更灵活和细致的光照计算。
也就是说,任何一个三维点,我们都清楚其在视窗下的Normal、TANGENT和BINORMAL 。注意,我说的是视窗下。所以反推世界坐标系的Normal、TANGENT和BINORMAL就是乘以
反转的摄像机矩阵。比如Normal反推世界坐标系的normal为下面公式:
worldNormal=INV_CAMERA_MATRIX * vec4(normal, 0.0))
同理切向、副法线都能这样算法。那么坡向就worldTANGENT*radio1+worldBINORMAL*radio2。一般radio1取0.5,radio2取0.572。