この記事は、「Dynamic Resolution Rendering on OpenGL* ES 2.0」(http://software.intel.com/en-us/articles/dynamic-resolution-rendering-on-opengl-es-2) の日本語参考訳です。
ダウンロード
サンプルコード: dynamic-resolution.zip [ZIP 4MB] (https://software.intel.com/file/dynamic-resolutionzip)
高コストなピクセル処理
ゲームやグラフィックス・ワークロードのパフォーマンス解析を行う場合、フラグメント (またはピクセル) シェーダーの処理は、パフォーマンス・ボトルネックになりがちです。これは当然のことだと言えるでしょう。なぜなら、フラグメント・シェーダーではライティング、テクスチャー・サンプリング、および後処理効果の計算が行われるからです。画面上の各ピクセルの最終色の計算には、多大な処理と時間を要すため、必然的にコストが高くなります。さらに、新しいデバイスが登場するたびにモバイル・プラットフォームが高解像度になっていることも高コストに拍車をかけています。高解像度環境ではフラグメント・シェーダーを呼び出す機会が増えます。しかし、モバイル開発者にとって高解像度だけが課題ではありません。デバイスによって解像度が異なるという問題もあります。この記事の執筆時点 (2012 年 11 月 12 日) で市場に出回っているいくつかのデバイスを調べてみたところ、同じ OS を搭載したデバイスであっても、解像度が異なるものがありました。
- 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 S III: 1280 x 720、Adreno 225
一部のゲーム開発者は、増え続けるピクセル数だけでなく、さまざまな解像度への対応に取り組み始めています。まだ対応していない開発者にとっても、近い将来、これは避けて通ることができない課題です。また、別の問題として、グラフィックス・ハードウェアの向上に伴い、より多くのピクセル処理が発生することも避けられません。
ゲームの解像度に対応する方法
ゲームでさまざまな解像度に確実に対応する方法はいくつかあります。最も簡単な方法は、ネイティブ解像度でシーンを描画することです。ゲームのスタイルによっては、この方法を採用せざるを得ないことがあります。フラグメント・シェーダーがパフォーマンス・ボトルネックになるほど大量の作業を行っていないこともあります。この場合、さまざまな解像度でアートワークが正しく動作することを確認する必要はありますが、その他は何もすることはありません。
2 番目の方法は、ネイティブ解像度に関係なく、固定解像度を採用することです。この方法では、固定解像度に合わせて、アートワークとシェーダーを調整することができます。ただし、デバイスに合った最高のユーザー体験を提供できない可能性があります。
別の方法は、最初にプレーヤーが解像度を設定できるようにすることです。この方法は、プレーヤーが選択した解像度でバックバッファーを作成し、さまざまな解像度に対応します。プレーヤーは、デバイスに最適な解像度を選択することができ、開発者は、プレーヤーが選択可能な各解像度で、アートワークが正しく動作することを確認します。
3 番目の方法は、この記事で説明する動的な解像度のレンダリングです。これは、コンソールゲームや PC ゲームで一般的に採用されている方法です。ここで紹介する実装は、[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);
シーンがレンダリングされると、デフォルトのフレームバッファー (バックバッファー) が再度バインドされます。これで、シーンがオフスクリーン・カラー・テクスチャーに保存されます。次に、オフスクリーン・テクスチャーからサンプリングして、全画面を四分割したものをレンダリングします。以下にコード例を示します。
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 の値は、描画およびサンプリングするオフスクリーン・テクスチャーの幅と高さの割合 (%) を決定します。オフスクリーン・テクスチャーの描画の設定は簡単で、glViewport を呼び出すだけです。
// 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 を呼び出し、幅と高さに基づいて描画領域を設定します。そして、これをネイティブ解像度に合わせて、全画面を四分割したものとユーザー・インターフェイスを描画します。オフスクリーン・テクスチャーの更新された部分だけをサンプリングするには、分割した四画面の頂点をテクスチャーの座標に設定します。以下にコード例を示します。
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);
動的な解像度を使用する利点
セットアップが完了すると、シーンがオフスクリーン・テクスチャーに保存され、全画面を四分割したものが画面にレンダリングされます。シーンがレンダリングされる実際の解像度は、ネイティブ解像度とは関係ありません。シーンに対するピクセル処理量は動的に変えることができます。ゲームの種類とスタイルに応じて、画質をあまり劣化することなく、解像度を大幅に落とせます。以下は、サンプルをさまざまな解像度で描画した例です。
この例では、75% から 50% の間まで解像度を落とすことが妥当でしょう。それ以上落とすと、目に見えて画質が劣化します。主な影響として、エッジでエイリアスが発生します。この例では、ネイティブ解像度の 75% での描画が許容範囲ですが、ゲームによってはアートワークを 25% まで落とすこともできるでしょう。
動的な解像度のレンダリングにより、ピクセル処理量が減るのは明らかです。また、ピクセルあたりの作業量を増やすこともできます。フル解像度でレンダリングしないため、フラグメント・シェーダーを呼び出す回数が減り、各呼び出しでより多くの作業を行うことができます。ここでは、簡潔で理解しやすいサンプルコードにするため、単純なフラグメント・シェーダーを取り上げました。開発者にとって、パフォーマンスと画質のバランスを取ることは最重要課題の 1 つと言えるでしょう。
ここで紹介した実装
図 2. 実装の詳細
ここで提供するサンプルコードには、Android* プロジェクトのみが含まれていますが、図 2 に示すように設計されているため、ほかのモバイル・オペレーティング・システムに簡単に拡張できます。プロジェクトのコア部分は OpenGL* ES 2.0 向けに C で記述されており、Android* NDK が必要です。興味深いことに、C はクロスプラットフォーム開発に適した選択肢と言えます。システムの抽象化 (System Abstraction) は、ファイル I/O と、OS に依存する可能性があるその他の機能を指します。
結論
動的な解像度のレンダリングは、モバイルデバイスの画面解像度に関するさまざまな問題の解決に適した選択肢です。開発者とユーザーはパフォーマンスと画質のバランスをより適切に制御することができます。このバランスを上手く取るためには、動的な解像度のレンダリングの実装に伴うオーバーヘッドを理解する必要があります。レンダリング・ターゲットを作成し、各フレームでその切り替えを行うため、フレーム時間が増加します。このオーバーヘッドを考慮することで、この方法がゲームにとって適切であるかどうかを判断できるでしょう。
参考文献 (英語)
[Binks 2011] Binks, Doug.“Dynamic Resolution Rendering Article”.http://software.intel.com/en-us/articles/dynamic-resolution-rendering-article© 2013 Intel Corporation.無断での引用、転載を禁じます。
* その他の社名、製品名などは、一般に各社の表示、商標または登録商標です。
OpenGL および OpenGL ES ロゴは、アメリカ合衆国および / またはその他の国における Silicon Graphics, Inc. の商標または登録商標あり、Khronos の使用許諾を受けて使用しています。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。