[译文] 如何高效渲染庞大的地形 - Rendering large terrains
翻译:RyuZhihao123
时间:2017/5/16(大三下学期)
原文链接:Render large terrains - pheelicks
Rendering large terrains
Today we’ll look at how to efficiently render a large terrain in 3D. We’ll be using WebGL to do this, but the techniques can be applied pretty much anywhere.We’ll concentrate on the vertex shader, that is, how best to use the position the vertices of our terrain mesh, so that it looks good up close as well as far away.
To see how this end result looks, check out the live demo. The demo was built using THREE.js, and the code is on github, if you’re interested in the details.
今天,我们将谈一下如何高效渲染一个规模庞大的3D地形。我们将使用WebGL来做这项工作,但是如下的处理技术可以被应用到更多的地方(像博主,就是OpenGL党)。
我们将更多的关注vertex shader,它能更好的使用地形mesh中的顶点坐标,使得地形不管是近处还是远处的都看起来非常棒。
为了能够清楚认识庞大地形的渲染及其结果,可以先看看在线演示,这个demo使用THREE.js构建。如果你对它的实现细节感兴趣,那么它的源码可以在这个github链接中找到。
1. LOD – Level of detail (细节层次)
An important concept when rendering terrain is the “level of detail”. This describes how many vertices we are drawing in a particular region.
Take the terrain below, notice how the nearby mountain on the right fills a lot of the final image, while the mountains in the distance only take a small portion of the image.
LOD(细节层次)是渲染地形时的一个重要概念。它用来描述在一个特定的区域我们将要绘制多少个顶点。
观察下面的地形,能够发现近处的山占据了最终图像的大部分,然而远处的山仅仅占了在图像中占了很少的比例。
It makes sense to render nearby objects with a greater LOD, while those in the distance
with a lower LOD. This way we don’t waste processing power computing a thousand vertices that end up in the same pixel on screen.
可以简单理解为:近处的物体使用较高的细节层次(LOD)去渲染,而那些远处的物体则用较低的细节层次去渲染。使用这种方式,我们不会把太多的时间浪费在计算那些远处的数千个还只在屏幕上显示几个像素的顶点上。
2. Flat grid (平面网格)
An easy way to create a terrain mesh is to simply create a plane that covers our entire terrain, and sub-divide it into a uniform grid. This is pretty awful for the LOD, as distant features will have far too much detail, while those nearby not enough. As we add vertices to the uniform grid to improve the nearby features, we are wasting ever more on the distant features, while reducing the vertex count to bring down the vertices used to draw the distant features will impact the nearby features.
创建地形mesh的一个简单方法是:创建一个能够完全覆盖我们整个地形的平面(plane),并将它分割成若干规格一致的网格(grid)。(注:所谓规格一致,指的是每个grid的顶点数目、尺寸完全一致)。这是一个相当糟糕的LOD,远处的特征将会保留过多的细节,同时那些近处特征可能略显不足。这是因为,当我们试图向这些规格一致的网格中添增加顶点以提高近处的特征时,我们将不得不浪费更多的时间在那些远处特征上(因为规格一致,所以远处网格也要添加等量顶点)。当试图通过移除顶点数目,以减少用于绘制远处特征的顶点时,也会影响到近处的特征。
3. Recursive tiles (递归区块)
A simple way to do better is to split our plane into tiles of differing sizes, but of
constant vertex count. So for example, each tile contains 64×64 vertices, but sometimes these vertices are stretched over an area corresponding to a large distant area, while for nearby areas, the tiles are smaller.
The question remains how to arrange these tiles. There are more sophisticated methods for doing this, but we’ll stick with something simpler. Specifically, we’ll start with the area nearest to the camera and fill it with tiles of the highest resolution, say 1×1. We’ll then surround this area with a “shell” of tiles of double the size, 2×2. We’ll then add a 4×4 tile “shell”, and so on, until we have covered our map. Here’s how this looks, with each layer color-coded:
一种效果更好而又简单方法是:将我们的平面划分为不同尺寸的区块(tiles),但是每个tile都相同的顶点数目。举个例子:每个tile包含64x64个顶点,但是有时他们因为位于远方就会被拉伸变大,同时对于那些近处的区域,这些tiles的尺寸则要相应变小。(总而言之,tiles是具有相同顶点数,但是尺寸不同的区块,区块尺寸随着距离增加而变大)
重点在于,我们如何如何去组织这些tiles。这里有一些复杂的方法的解决方案,但是我们将继续使用一种更为简单的方式。我们从最靠近相机的区域开始,并用最高分辨率的tile来填充这一区域,我们称最高分辨率的tile的尺寸为1x1。接下来,我们以这个区域为中心,用几个2x2尺寸的tiles组成的“shell”来包围。再然后,我们用尺寸为4x4的tiles组成的"shell"继续包围已有的区域。直到完全覆盖我们的地图位置。下面是它的示意图,对于不同尺寸的tile用不同的颜色绘制(同种颜色的tiles组成一个shell):
Notice how the central layer actually has 4 extra tiles than the others, this is necessary, otherwise we’d end up with a hole in the middle. This tile arrangement is actually very nice, as each additional “shell” doubles the width and height of the area covered, while only required a constant number of additional tiles.
When viewed from above, this arrangement doesn’t seem particularly great, as all the tiles
are more or less the same distance from the camera. However, a more usual view is one where the camera is pointed at the horizon and hence nearby tiles are filling more of the final view:
通过上面的图片不难发现,中间绿色区域的tile数目比其他颜色的区域多了4个额外的tiles。这个是很有必要的,否则我们将在中间留下一个洞。这个tile的组织方法有非常好的实际效果,使得每个新增的“shell”都是它所包围区域的长和宽的两倍,同时每次只需要新增固定数目的tiles(这里,新shell需增加12个tiles)。
当然垂直从上方观察(上图),这种组织方式可能看起来没有想象中的那么好,那是因为所有的tiles和相机的距离几乎一样。但是,当我们站在正常的观察角度:相机水平摆放并顺着相机朝向看去(下图),那么这时距离近的tiles就会占据最终场景的大部分,效果是不是非常好了呢。
In this view it is clear that we’re already doing much better with our vertex distribution.
Each “shell” is represented roughly equally in the final image, which means that our vertex density is roughly uniform across the image.
从这种观察角度看,不难发现,我们的顶点分配工作已经做得非常好了。每个shell(即tile的组合)在最终图像看上去几乎相等,这意味着,我们的顶点密度在最终的渲染图像中的各处几乎一致。
4. Moving around (漫游)
Now that we have a suitable mesh, we need to address how what will happen as the camera moves around the terrain. One idea is to actually keep the mesh itself static, but let the terrain data “flow” through the mesh. Imagine that each vertex is part of a cloth that is being warped to the shape of the underlying terrain. As we move around the terrain, the cloth is dragged over it, being deformed into the correct shape below it.
The advantage of this approach is that we always have the correct LOD now matter where we move, as the terrain mesh is static relative to the camera.
现在我们已经有了一个合适的地形mesh的组织方案,我们需要进一步解决相机在地形中漫游将会发生的事。一种策略是:一直静态的保持地形mesh,但是让地形高度数据“滑过”mesh(所谓mesh,就是前面的recursive tiles中使用的平面的划分结果,顶点间距不均匀,反映了顶点的水平间隔;但是地形高度数据和mesh尺寸一直,但是顶点间距均匀,反映了地形的高度信息)。试着想象一下,我们把一件布(也就是mesh)平放在一个凹凸不平的地面上,但是这块布的各区域的顶点密度不同(就像前面的tiles,这块布就是由若干不同尺寸tiles组成的),当相机移动时,我们把这块布也拽着一起移动,这样布就会不断因地形的变化而改变自身的形状。这块布就是mesh的组织方案(当然,我们实际的做法可能是让地形移动,但是mesh(也就是布)相对于相机保持静止)。
这种方法的一个优点是:不管我们移动到哪里,我们都始终保持一个正确的LOD,因为地形mesh对于相机而言一直是静态不变的。
The problem is that as the terrain “flows” through the mesh, if the vertices are not sufficiently close together, they will not do a good job of sampling the terrain, in particular they will wobble as the terrain flows through them.
To illustrate why this happens, consider a region of the terrain where the vertex spacing is large, e.g. 1km. Imagine that the terrain we are displaying has alternating valleys and hills at this point, spaced 1km apart. As the terrain “flows” thorough the mesh, then sometimes the vertices will all be on the hills and other times they will be in the valleys. Now as this is a region that is far from the camera, we don’t care so much that the hills and valleys aren’t shown correctly, as either alone would be fine. The real issue is that the oscillation between the two creates flickering, which is visually distracting.
这种地形滑过mesh的方法会带来一个问题,如果顶点间并没有足够的紧密,他们就不能很好的去显示一个地形,尤其是当地形数据滑过他们时,会产生“晃动”的问题。
为了说明为什么会发生这样的情况,假设地形中的某一区域,它的顶点间距很大,比如1km。想象一下,我们现在展示的地形有间隔1km的交错的山谷和山脉。随着地形的“滑过”mesh,有时顶点将全部在山丘上,而其他时候它们又全在山谷中。现在,这是一个远离摄像机的区域,我们不太在乎山丘和山谷是否被正确地显示,因为它们都是可以接受的。但是真正的问题是,两者之间的振荡产生了闪烁,造成了视觉上的不连贯:
5. Grid snapping(网格对齐)
To solve this oscillation issue, we keep the same geometry as we had before, except that we make it so that when the terrain “flows” through the mesh, the vertices snap to a grid, with spacing equal to the vertex spacing for that tile. So in the hill/valley example from before, rather than having a vertex “flow” from the top of the hill to the bottom of the valley and then back up, it is instead snapped to the nearest hill-top. By making the snap grid be spaced at the same interval as the tiles vertex spacing, we will end up with a uniform grid again, except snapped to a fixed point on the terrain.
6. Morphing between regions
Ok, so we’ve solved one problem, but now we’re faced with another. As we’re snapping each tile according to it’s vertex spacing, where two tile layers meet we end up with seams, as shown below:
我们已经解决了一个问题,但是现在又出现了新的问题。当我们把每个tile根据它的顶点间距对齐时,两个tile之间的接缝处会出现裂缝,如下图所示:
To fix this we want to gradually blend one layer into another, so that we end up with a continuous mesh. One way to do this is to compute what we’ll call a “morph factor”, which determines how close we are to the boundary between two different LOD layers. When we are near the boundary with a layer that has a greater vertex spacing than ours, the value of the morph factor approaches 1, while when we are far away, it is 0.
We can then use this value to blend smoothly between one layer and the other. The way we do this is calculate what the position of the vertex would be if it were snapped to both it’s own grid and that of the next layer and then linearly blend the two positions according to the morph factor. Here’s some GLSL code that does just that:
为了解决这个问题,我们想要逐渐地将一层接合到另一层,这样我们就会得到一个连续的网格。一种方法是计算我们所说的“morph因子”,它决定了我们在两个不同的LOD层之间边界的距离。当我们在某层的边界附近,并且该边界附近某层的顶点间距比我所在层次的大,那么morph因子等于1;当我们远离这个边界时,morph因子取值为0。
然后我们可以使用这个值平滑地在一个层和另一个层之间进行接合。我们的方法是计算当顶点被对齐到它自己所在网格和另一层网格时的顶点的位置,然后根据morph因子线性混合两个位置。下面是一些GLSL代码:
// Snap to grid
float grid = uScale / TILE_RESOLUTION; // TILE_RESOLUTION:tile的分辨率
vPosition = floor(vPosition / grid) * grid;
// Morph between zoom layers
if ( vMorphFactor > 0.0 ) {
// Get position that we would have if we were on higher level grid
grid = 2.0 * grid;
vec3 position2 = floor(vPosition / grid) * grid;
// Linearly interpolate the two, depending on morph factor
vPosition = mix(vPosition, position2, vMorphFactor);
}
Here’s the terrain with the areas where the morph factor is non-zero highlighted in white.As before, each tile shell is color coded. Notice how each tile becomes completely white on the boundary with the next shell – as it morphs to have the same vertex spacing as the neighbouring tile.
下图是一个多个区域拼接而成的地形,对于morph因子非零的区域用白色高亮显示。和前面一样,每个tile都是不同颜色的。注意,每一个tile与下一个shell的边界上是完全白色的——tile发生变形与相邻的tile有相同的顶点间隔。
Finished~