Quantcast
Channel: Mali Developer Center
Viewing all articles
Browse latest Browse all 125

使用几何裁剪图进行地形渲染

$
0
0
terrain
简介
备注
本示例使用 OpenGL ES 3.0。

本示例的源文件可在 SDK 的 samples\advanced_samples\Terrain* 文件夹中找到。

备注
为完成目标,本示例涉及多个高级 OpenGL 技术:
  • 在顶点着色器中采样纹理
  • 实例化绘图
  • 常量缓冲区对象
  • 像素缓冲区对象
  • 2D 纹理阵列li>
  • 还应用了视域剔除技术(尽管它纯粹是数学方法)。
表示地形

地形表示取自 Losasso 和 Hoppe 的论文[1].

clipmap

地形的基本构建块是具有 N 乘以 N(在本实施中为 64 乘以 64)个顶点的棋盘形正方形。棋盘形正方形可以用单个三角形条有效表示

为块生成顶点数据:

GLubyte *pv = vertices;

// Block
for (unsigned int z = 0; z < size; z++)
{
for (unsigned int x = 0; x < size; x++)
{
pv[0] = x;
pv[1] = z;
pv += 2;
}
}

为块生成索引缓冲区:

static GLushort *generate_block_indices(GLushort *pi, unsigned int vertex_buffer_offset,
unsigned int width, unsigned int height)
{
// Stamp out triangle strips back and forth.
int pos = vertex_buffer_offset;
unsigned int strips = height - 1;

// After even indices in a strip, always step to next strip.
// After odd indices in a strip, step back again and one to the right or left.
// Which direction we take depends on which strip we're generating.
// This creates a zig-zag pattern.
for (unsigned int z = 0; z < strips; z++)
{
int step_even = width;
int step_odd = ((z & 1) ? -1 : 1) - step_even;

// We don't need the last odd index.
// The first index of the next strip will complete this strip.
for (unsigned int x = 0; x < 2 * width - 1; x++)
{
*pi++ = pos;
pos += (x & 1) ? step_odd : step_even;
}
}
// There is no new strip, so complete the block here.
*pi++ = pos;

// Return updated index buffer pointer.
// More explicit than taking reference to pointer.
return pi;
}

该块位于水平平面(XZ 平面)上,并在网格中拼接在一起以绘制出任意广阔的地形(如上所示)。

为避免绘图走样以及产生过多细节,绘制更远的地形需要较低的分辨率。这可通过二次幂扩展基本 N 乘 N 块来实现。

备注

与纹理映射级别相同,裁剪图级别 0 表示具有最高细节的级别。在本示例中,共使用了 10 个裁剪图级别。

在拼接裁剪图时,必须填充出现的小孔。可通过绘制较小的“修正”和/或“修剪”区域完成(如上所示)。还必须在裁剪图级别变化的边缘绘制退化三角形的条。

连接级别 N 和 N + 1 的退化三角形需使用级别 N 的顶点绘制。连接级别 N 和 N + 1 的修剪区域需使用级别 N + 1 的顶点绘制。

必须精心规划块的具体布局以确保无缝完美的地形。需要注意一个重点,即两个相邻的 N 乘 N 块之间的距离为 N – 1。示例代码中看到的大多数偏移量都使用此距离和偶尔的 2 纹素偏移量来表示水平和垂直修正区域的宽度。

在启动时,顶点缓冲区随索引缓冲区一次性上传到 GPU。

使地形贴紧网格

块的位置随摄像头以离散步长移动。以离散步长移动非常重要,因为它可以避免顶点“游移”效果。随着摄像头的移动,裁剪图的较低级别可以更改位置,而较高级别则不会变动,因此用来连接两个裁剪图级别的修剪区域可能需要更改,以便能够填满整个地形。

裁剪图级别偏移量在每一帧上的计算方法如下:

// The clipmap levels only move in steps of texture coordinates.
// Computes top-left world position for the levels.
vec2 GroundMesh::get_offset_level(const vec2& camera_pos, unsigned int level)
{
if (level == 0) // Must follow level 1 as trim region is fixed.
return get_offset_level(camera_pos, 1) + vec2(size << 1);
else
{
vec2 scaled_pos = camera_pos / vec2(clipmap_scale); // Snap to grid in the appropriate space.

// Snap to grid of next level. I.e. we move the clipmap level in steps of two.
vec2 snapped_pos = vec_floor(scaled_pos / vec2(1 << (level + 1))) * vec2(1 << (level + 1));

// Apply offset so all levels align up neatly.
// If snapped_pos is equal for all levels,
// this causes top-left vertex of level N to always align up perfectly with top-left interior corner of level N + 1.
// This gives us a bottom-right trim region.

// Due to the flooring, snapped_pos might not always be equal for all levels.
// The flooring has the property that snapped_pos for level N + 1 is less-or-equal snapped_pos for level N.
// If less, the final position of level N + 1 will be offset by -2 ^ N, which can be compensated for with changing trim-region to top-left.
vec2 pos = snapped_pos - vec2((2 * (size - 1)) << level);
return pos;
}
}

裁剪图级别中的块偏移量与裁剪图级别偏移量有关。

在顶点着色器中采样纹理

OpenGL ES 3.0 增加了对顶点着色器中进行纹理采样的可靠支持。这使应用程序能够动态地更新顶点数据,而旧方法执行此操作则非常昂贵。顶点缓冲区是固定的,绝不需要更新。(请参见 advanced_samples/Terrain/jni/shaders.h)

虽然顶点缓冲区代表水平平面内的固定网格结构,垂直 Y 分量则为动态且会从高度图纹理中采样。

备注
不能在顶点着色器(无导数)中使用自动纹理映射。如果要从纹理映射的纹理中采样,则必须通过使用textureLod等提供明确的细节级别。然而,本示例中未使用纹理映射的纹理,因此直接使用texture 是安全的。
高度图表示

每个裁剪图级别均由它自己的 255×255 纹理提供支持。由于每个级别的大小相同,使用由 OpenGL ES 3.0 推出的 2D 纹理阵列功能非常方便且有效。如果使用纹理阵列,则无需绑定不同的纹理来绘制不同的裁剪图级别,因而降低了所需的绘制调用数。

GL_CHECK(glGenTextures(1, &texture));
GL_CHECK(glBindTexture(GL_TEXTURE_2D_ARRAY, texture));

// Use half-float as we don't need full float precision.
// GL_RG16UI would work as well as we don't need texture filtering.
// 8-bit does not give sufficient precision except for low-detail heightmaps.
// Use two components to allow storing current level's height as well as the height of the next level.
GL_CHECK(glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RG16F, size, size, levels));

GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST));

// The repeat is crucial here. This allows us to update small sections of the texture when moving the camera.
GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT));

GL_CHECK(glBindTexture(GL_TEXTURE_2D_ARRAY, 0));

高度图更新

使用裁剪图方法时,LOD 中一次只能看见部分地形。由于摄像头四处移动,必须在高度图纹理中更新新的区域,这样才能提供无缝、不中断的地形假象。

可通过从预先计算的高度图上传新数据来更新高度图,或使用具有着色器的帧缓冲区对象逐步更新高度图。

void Heightmap::update_region(vec2 *buffer, unsigned int& pixel_offset, int tex_x, int tex_y,
int width, int height,
int start_x, int start_y,
int level)
{
if (width == 0 || height == 0)
return;

// Here we could either stream a "real" heightmap, or generate it procedurally on the GPU by rendering to these regions.

buffer += pixel_offset;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
buffer[y * width + x] = compute_heightmap(start_x + x, start_y + y, level);

UploadInfo info;
info.x = tex_x;
info.y = tex_y;
info.width = width;
info.height = height;
info.level = level;
info.offset = pixel_offset * sizeof(vec2);
upload_info.push_back(info);

pixel_offset += width * height;
}

该样品通过从预先计算的 1024×1024 缓冲区(由带通滤波白噪声生成)复制样本,实施了高度图更新。该副本通过使用像素缓冲区对象异步完成。

该裁剪图渲染代码使用 GL_REPEAT 纹理环绕功能,以确保每次摄像头移动时只需更新一小部分的纹理。

重复预先计算的高度图,以使地形无穷远。

备注
除高度图以外,还通常使用对应的法线贴图。为清楚起见,已省略其内容。法线贴图可以通过采样高度图在顶点着色器中即时计算,也可与高度图一起更新。本示例中的片段着色器根据顶点的高度分配颜色。
高度图混合

裁剪图的分辨率会突然更改级别之间的分辨率。这种细节的不连续性会造成失真。为避免这种情况,两个高度图级别(当前级别和下一个级别)会在顶点着色器中进行采样并混合。

为避免从下一个裁剪图级别过滤高度图值,过滤版高度图会进行预先计算并随附在当前级别的高度中。

// Compute the height at texel (x, y) for cliplevel.
// Also compute the sample for the lower resolution (with simple bilinear).
// This avoids an extra texture lookup in vertex shader, avoids complex offseting and having to use GL_LINEAR.
vec2 Heightmap::compute_heightmap(int x, int y, int level)
{
float height = sample_heightmap(x << level, y << level);
float heights[2][2];
for (int j = 0; j < 2; j++)
for (int i = 0; i < 2; i++)
heights[j][i] = sample_heightmap(((x + i) & ~1) << level, ((y + j) & ~1) << level);

return vec2(
height,
(heights[0][0] + heights[0][1] + heights[1][0] + heights[1][1]) * 0.25f);
}

视域剔除

绘制地形时,大部分地形将不可见。为提高性能,避免绘制从不显示的块非常重要。

本地形示例根据轴对齐的边界性框体实施简单的视域剔除。

该视域剔除实施的想法是将摄像机视域的所有平面表示为平面方程。在测试轴对齐框体的可见性时,我们会一次针对一个视域平面来检查边界性框体的每一个角。

bool Frustum::intersects_aabb(const AABB& aabb) const
{
// If all corners of an axis-aligned bounding box are on the "wrong side" (negative distance)
// of at least one of the frustum planes, we can safely cull the mesh.
vec4 corners[8];
for (unsigned int c = 0; c < 8; c++)
{
// Require 4-dimensional coordinates for plane equations.
corners[c] = vec4(aabb.corner(c), 1.0f);
}

for (unsigned int p = 0; p < 6; p++)
{
bool inside_plane = false;
for (unsigned int c = 0; c < 8; c++)
{
// If dot product > 0, we're "inside" the frustum plane,
// otherwise, outside.
if (vec_dot(corners[c], planes[p]) > 0.0f)
{
inside_plane = true;
break;
}
}

if (!inside_plane)
return false;
}

return true;
}

如果边界性框体的每个角均在平面的“错误”一侧(负距离),即可证明包含在框体内的网格将永远不用绘制。因此,如果我们能够证明至少有一个视域平面不可见,即可剔除该网格。

为了在世界空间中获取视域的平面方程,完成从剪贴空间的反转变换。

Frustum::Frustum(const mat4& view_projection)
{
// Frustum planes are in world space.
mat4 inv = mat_inverse(view_projection);

// Get world-space coordinates for clip-space bounds.
vec4 lbn = inv * vec4(-1, -1, -1, 1);
vec4 ltn = inv * vec4(-1, 1, -1, 1);
vec4 lbf = inv * vec4(-1, -1, 1, 1);
vec4 rbn = inv * vec4( 1, -1, -1, 1);
vec4 rtn = inv * vec4( 1, 1, -1, 1);
vec4 rbf = inv * vec4( 1, -1, 1, 1);
vec4 rtf = inv * vec4( 1, 1, 1, 1);

// Divide by w.
vec3 lbn_pos = vec_project(lbn);
vec3 ltn_pos = vec_project(ltn);
vec3 lbf_pos = vec_project(lbf);
vec3 rbn_pos = vec_project(rbn);
vec3 rtn_pos = vec_project(rtn);
vec3 rbf_pos = vec_project(rbf);
vec3 rtf_pos = vec_project(rtf);

// Get plane normals for all sides of frustum.
vec3 left_normal = vec_normalize(vec_cross(lbf_pos - lbn_pos, ltn_pos - lbn_pos));
vec3 right_normal = vec_normalize(vec_cross(rtn_pos - rbn_pos, rbf_pos - rbn_pos));
vec3 top_normal = vec_normalize(vec_cross(ltn_pos - rtn_pos, rtf_pos - rtn_pos));
vec3 bottom_normal = vec_normalize(vec_cross(rbf_pos - rbn_pos, lbn_pos - rbn_pos));
vec3 near_normal = vec_normalize(vec_cross(ltn_pos - lbn_pos, rbn_pos - lbn_pos));
vec3 far_normal = vec_normalize(vec_cross(rtf_pos - rbf_pos, lbf_pos - rbf_pos));

// Plane equations compactly represent a plane in 3D space.
// We want a way to compute the distance to the plane while preserving the sign to know which side we're on.
// Let:
// O: an arbitrary point on the plane
// N: the normal vector for the plane, pointing in the direction
// we want to be "positive".
// X: Position we want to check.
//
// Distance D to the plane can now be expressed as a simple operation:
// D = dot((X - O), N) = dot(X, N) - dot(O, N)
//
// We can reduce this to one dot product by assuming that X is four-dimensional (4th component = 1.0).
// The normal can be extended to four dimensions as well:
// X' = vec4(X, 1.0)
// N' = vec4(N, -dot(O, N))
//
// The expression now reduces to: D = dot(X', N')
planes[0] = vec4(near_normal, -vec_dot(near_normal, lbn_pos)); // Near
planes[1] = vec4(far_normal, -vec_dot(far_normal, lbf_pos)); // Far
planes[2] = vec4(left_normal, -vec_dot(left_normal, lbn_pos)); // Left
planes[3] = vec4(right_normal, -vec_dot(right_normal, rbn_pos)); // Right
planes[4] = vec4(top_normal, -vec_dot(top_normal, ltn_pos)); // Top
planes[5] = vec4(bottom_normal, -vec_dot(bottom_normal, lbn_pos)); // Bottom
}

使用统一的缓冲区

OpenGL ES 3.0 引入了将常量数据传递到着色器的新方式。在常量数据更改时无需多次调用 glUniform*,可以采用由常规 OpenGL 缓冲区对象支持常量数据的做法。

此做法 (GLSL) 一个非常简单的示例是:

#version 300 es
layout(std140) uniform; // Use std140 packing rules for uniform blocks.
in vec4 aVertex;
uniform VertexData
{
mat4 viewProjection; // Let the view-projection matrix be backed a buffer object.
} vertex;

void main()
{
gl_Position = vertex.viewProjection * aVertex;
}

备注
需要注意的是,虽然常量缓冲区数据通过名称 vertex进行访问, 从而与其他着色器和/或 OpenGL 进行通信,实际使用的是块名称VertexData

可以使用着色器内的多个统一块,这就需要依据索引来访问统一块。

链接着色器程序之后,可以使用以下命令查询统一块:

GLuint block = glGetUniformBlockIndex(program, "VertexData"); // Note the use of VertexData and not vertex.

现在,我们要指定常量缓冲区将从哪个源位置抽取数据:

glUniformBlockBinding(program, block, 0);

常量缓冲区现在从绑定 0 获取源数据。要绑定供着色器使用的缓冲区,需要使用新的索引版b>glBindBuffer.

glBindBufferBase(GL_UNIFORM_BUFFER, 0, buffer_object); // Binds the entire object.
glBindBufferRange(GL_UNIFORM_BUFFER, 0, buffer_object, offset, size); // Binds a range of the object.

备注
这些新调用仅设置着色器的状态。如果缓冲区用于上传数据等常规 OpenGL 调用,则仍使用通用glBindBuffer
// Initialize an UBO.
glBindBuffer(GL_UNIFORM_BUFFER, buffer_object);
glBufferData(GL_UNIFORM_BUFFER, size, NULL, GL_DYNAMIC_DRAW);

备注
glBindBufferRange 需要对偏移量和大小进行一定的校准。要准确查询所需的校准,可以调用 glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, …)dd>
使用实例化绘图绘制地形

裁剪图包含绘制多次的简单构建块。为了大幅减少所需的绘制调用数,可以使用由 OpenGL ES 3.0 推出的实例化绘图功能来绘制块。

{常量缓冲区对象用于访问顶点着色器中基于实例的数据。在绘制实例化绘图时,内置变量 gl_InstanceID 可供使用, 它可用来访问缓冲区对象中基于实例的数据阵列:render_draw_list()

void GroundMesh::render_draw_list()
{
for (std::vector<DrawInfo>::const_iterator itr = draw_list.begin(); itr != draw_list.end(); ++itr)
{
if (!itr->instances)
continue;

// Bind uniform buffer at correct offset.
GL_CHECK(glBindBufferRange(GL_UNIFORM_BUFFER, 0, uniform_buffer,
itr->uniform_buffer_offset, realign_offset(itr->instances * sizeof(InstanceData), uniform_buffer_align)));

// Draw all instances.
GL_CHECK(glDrawElementsInstanced(GL_TRIANGLE_STRIP, itr->indices, GL_UNSIGNED_SHORT,
reinterpret_cast<const GLvoid*>(itr->index_buffer_offset * sizeof(GLushort)), itr->instances));
}
}

void GroundMesh::render()
{
// Create a draw-list.
update_draw_list();

// Explicitly bind and unbind GL state to ensure clarity.
GL_CHECK(glBindVertexArray(vertex_array));
render_draw_list();
GL_CHECK(glBindVertexArray(0));
GL_CHECK(glBindBuffer(GL_UNIFORM_BUFFER, 0));
GL_CHECK(glBindBufferBase(GL_UNIFORM_BUFFER, 0, 0));
}

参考资料

[1] http://research.microsoft.com/en-us/um/people/hoppe/geomclipmap.pdf


Viewing all articles
Browse latest Browse all 125

Trending Articles