借助 OpenGL* ES 2.0 实现动态分辨率渲染

下载

借助 OpenGL* ES 2.0 实现动态分辨率渲染 [PDF 677KB]
代码样本: dynamic-resolution.zip [ZIP 4MB]

像素处理成本昂贵

当在游戏和显卡工作负载上执行性能分析时,似乎处理片段或(像素)着色器是主要的性能瓶颈。 当然这也在情理之中,因为照明计算、纹理采样和后期处理效果等计算均在片段着色器中执行。 计算屏幕上每个像素的最终颜色需要大量处理能力和时间,且可能会非常昂贵。 此外,每次新发布的移动平台均采用更高的分辨率,从而提高了总体成本。 支持高分辨率需要增加片段着色器的调用次数。 但是,高分辨率并非移动开发人员的唯一问题。 如何确定分辨率不同的设备也是一个问题。 在写这篇文章时,我对市场上的一些设备做的快速调查显示分辨率的差异非常大 — 即使是运行相同操作系统的设备。

  • Apple iPhone* 5: 1136 x 640,PowerVR* SGX543MP3
  • Apple iPhone 4S: 960 x 640,PowerVR SGX543MP2
  • Nexus* 4,1280 x 768, Adreno* 320
  • Galaxy Nexus, 1280 x 720, PowerVR SGX540
  • Motorola RAZR* i, 960 x 540, PowerVR SGX540
  • Samsung Galaxy SIII, 1280 x 720, Adreno 225

不仅要清楚地找到不断提高的分辨率还要找到不同的分辨率是游戏开发人员正在面临或即将面临的问题。 另一个问题是,即使显卡硬件不断改进,它也会不可避免地因为处理更多像素而被消耗掉。

渲染游戏画面的几种方法

有许多合理且业经验证的方式来处理游戏中的各种分辨率。 最简单的方式是将场景绘制为原始分辨率。 在某些类型的游戏中,您可能会一直需要用到这种方法。 或者片段着色器可能能力不够强大而成为性能的瓶颈。 如果您遇到这种情况,多半会受到局限,但是仍然需要确保游戏开发资源(art asset)能够在各种重要分辨率中使用。

第二种方法是确定固定分辨率而非原始分辨率。 这种方法支持您将游戏开发资源(art asset)和着色器调整为固定分辨率。 但是,它可能无法为用户提供最佳的游戏体验。

另一种常见的方法是让用户在开始时设置所需的分辨率。 这种方法可以使用玩家选择的分辨率生成后台缓冲,并能有效解决分辨率各异的问题。 它支持玩家选择其设备上最适合的分辨率。 此外,您还需要确认游戏开发资源(art asset)是否能够在用户可选的分辨率中使用。

本文中介绍的第三种方法名为动态分辨率渲染。 这是单机游戏和高级电脑游戏上的一种常用技术。 本文中介绍的实施情况是从 [Binks 2011] 中的 DirectX* 版本中获取,并用于配合 OpenGL* ES 2.0 使用。 借助动态分辨率渲染,后台缓冲是原始分辨率的尺寸,但是场景使用了固定分辨率绘制为屏幕外纹理。 如图 1 所示,画面被绘制为屏幕外纹理的一部分,然后被采样填充后台缓冲。 UI 元素在原始分辨率下绘制。



图 1. 动态分辨率渲染

绘制为屏幕外纹理

第一步是创建屏幕外纹理。 在 OpenGL ES 2.0 中,使用所需的纹理尺寸创建 GL_FRAMEBUFFER。 下列是完成此操作的代码:

glGenFramebuffers(1, &(_render_target->frame_buffer));
glGenTextures(1, &(_render_target->texture));
glGenRenderbuffers(1, &(_render_target->depth_buffer));

_render_target->width = width;
_render_target->height = height;

glBindTexture(GL_TEXTURE_2D, _render_target->texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _render_target->width, _render_target->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);

glBindRenderbuffer(GL_RENDERBUFFER, _render_target->depth_buffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _render_target->width, _render_target->height);

glBindFramebuffer(GL_FRAMEBUFFER, _render_target->frame_buffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _render_target->texture, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _render_target->depth_buffer);

调用 glTexImage2D 可创建我们将要渲染的纹理,调用 glFramebufferTexture2D 可将着色的纹理绑定至帧缓冲。 然后,在帧缓冲绑定后按照下列方式渲染场景:

// 1. SAVE OUT THE DEFAULT FRAME BUFFER
static GLint default_frame_buffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &default_frame_buffer);

// 2. RENDER TO OFFSCREEN RENDER TARGET
glBindFramebuffer(GL_FRAMEBUFFER, render_target->frame_buffer);
glViewport(0, 0, render_target->width * resolution_factor, render_target->height * resolution_factor);
glClearColor(0.25f, 0.25f, 0.25f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/// DRAW THE SCENE ///
// 3. RESTORE DEFAULT FRAME BUFFER
glBindFramebuffer(GL_FRAMEBUFFER, default_frame_buffer);
glBindTexture(GL_TEXTURE_2D, 0);

// 4. RENDER FULLSCREEN QUAD
glViewport(0, 0, screen_width, screen_height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

渲染完场景后,默认的帧缓冲(后台缓冲)将会被再次绑定。 此时,场景绑定至屏幕外着色纹理。 下一步是渲染从屏幕外纹理采集的全屏四边形(full-screen quad)。 下列代码介绍了如何完成该操作:

glUseProgram(fs_quad_shader_program);
glEnableVertexAttribArray( fs_quad_position_attrib );
glEnableVertexAttribArray( fs_quad_texture_attrib );

glBindBuffer(GL_ARRAY_BUFFER, fs_quad_model->positions_buffer);
glVertexAttribPointer(fs_quad_position_attrib, 3, GL_FLOAT, GL_FALSE, 0, 0);

glBindBuffer(GL_ARRAY_BUFFER, fs_quad_model->texcoords_buffer);

const float2 texcoords_array[] =
{
    { resolution_factor, resolution_factor },
    { 0.0f,              resolution_factor },
    { 0.0f,              0.0f              },
    { resolution_factor, 0.0f              },
};

glBufferData(GL_ARRAY_BUFFER, sizeof(float3) * fs_quad_model->num_vertices, texcoords_array, GL_STATIC_DRAW);
glVertexAttribPointer(fs_quad_texture_attrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, fs_quad_model->index_buffer );
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, render_target->texture);
glUniform1i(fs_quad_texture_location, 0);
glDrawElements(GL_TRIANGLES, fs_quad_model->num_indices, GL_UNSIGNED_INT, 0);

分辨率因素

上述展示的大部分代码片段是 OpenGL 设置代码。 比较重要的是使用变量 resolution_factor 的内容。 resolution_factor 的值决定了用于绘制和采样的屏幕外纹理的宽、高比例。 设置用于绘制的屏幕外纹理非常简单,可以通过调用 glViewpor 完成。

// 1. SAVE OUT THE DEFAULT FRAME BUFFER

// 2. RENDER TO OFFSCREEN RENDER TARGET
glBindFramebuffer(GL_FRAMEBUFFER, render_target->frame_buffer);
glViewport(0, 0, render_target->width * resolution_factor, render_target->height * resolution_factor);

/// DRAW THE SCENE ///

// 3. RESTORE DEFAULT FRAME BUFFER

// 4. RENDER FULLSCREEN QUAD
glViewport(0, 0, screen_width, screen_height);

绑定帧缓冲后,调用 glViewport 可根据宽和高设置要绘制的区域。 然后,这将重置为原始分辨率来绘制全屏四边形(full-screen quad)和用户界面。 如只采集更新的屏幕外纹理,请为全屏四边形(full-screen quad)的顶点设置纹理坐标。 下列代码可完成该操作:

glBindBuffer(GL_ARRAY_BUFFER, fs_quad_model->texcoords_buffer);

const float2 texcoords_array[] =
{
    { resolution_factor, resolution_factor },
    { 0.0f,              resolution_factor },
    { 0.0f,              0.0f              },
    { resolution_factor, 0.0f              },
};

glBufferData(GL_ARRAY_BUFFER, sizeof(float3) * fs_quad_model->num_vertices, texcoords_array, GL_STATIC_DRAW);

使用动态分辨率的优势

设置完成后,场景将会保存为屏幕外纹理并渲染至全屏四边形(full-screen quad)的屏幕。 渲染场景的实际分辨率将不再绑定在原始分辨率。 针对该场景处理的像素数量可以进行动态更改。 根据游戏的类型和风格,可在不退化图像的前提下大量降低分辨率。 下列是几种不同分辨率的样本示例:

在这种情况下,可先将分辨率降低至 75% 和 50% 之间,这一分辨率不会导致图像质量明显恶化。 将要显示的主要图形边缘混叠。 对于这种情况,使用 75% 的原始分辨率也可行,但是具体取决于您的游戏,您也可以为了达到有趣的艺术风格而采用 25% 的分辨率。

很显然,动态分辨率渲染提供了一种明确的方式来降低要处理的像素数量。 而且,它还支持在每个像素上处理更多任务。 因为您不再以全分辨率进行渲染,片段着色器调用已降低,以便能够在每次执行时处理更多任务。 如要确保样本代码清晰、可读,片段着色器是合适的选择,因为它非常简单。 作为开发人员,实现性能和图像质量的平衡是最具挑战性的任务之一。

我们的实施



图 2. 实施详图

可下载的实施仅包括 Android* 项目,且仅以图 2 中呈现的格式布局以便能够轻松扩展至其他移动操作系统。 项目核心以 C 语言编写,以 OpenGL ES 2.0 为目标且需要 Android NDK。 值得注意的是, C 非常适合跨平台开发。 系统抽象是指文件 I/O 和其他不受 OS 影响的功能。

结论

动态分辨率渲染非常适合解决多种与移动设备屏幕分辨率有关的问题。 借助它,开发人员和用户能够更好地控制性能和图像质量之间的比率。 调整该比率还应考虑到实施动态分辨率渲染的费用。 创建渲染目标和更改每帧的渲染目标将会增加每帧的处理时间。 了解并计算该费用可以帮助您确定这一技术是否适合您的游戏。

参考

[Binks 2011] Binks, Doug. “动态分辨率渲染文章”。 http://software.intel.com/zh-cn/articles/dynamic-resolution-rendering-article

英特尔公司 © 2013 年版权所有。 所有权利受到保护。

*其他的名称和品牌可能是其他所有者的资产。

OpenGL 是注册商标,OpenGL ES 标识是 Khronos 授权 Silicon Graphics Inc. 使用的商标。

有关编译器优化的更完整信息,请参阅优化通知