組み込み機器に対する3Dグラフィック最適化メモ(OpenGL)

# この記事について

この記事は、組み込み機器のUIなどに対するリアルタイム3Dレンダリングのパフォーマンス最適化のメモとなります。

## 組み込み機器での3Dグラフィックの特徴

ゲームなどの場合、ユーザーの入力や敵の動きなどランダム性があるため、事前に描画の最大負荷を見積もることが容易ではありません。実際に動作している際に描画負荷が上がってしまった場合、CPU上で優先度の低い要素を判定し、カリングなどの手法で描画処理を削減するなどで最適化することが多いかと思います。

一方で、組み込み機器でのUIなどで3Dグラフィックを利用する場合、動的要素がほとんど発生しないため、最大負荷を見積れることが多いと思います。

そのような場合、描画プロセスを工夫して、描画負荷を下げることで、3Dグラフィックアクセラレータの要求性能を下げることができ、コスト削減につなげることができます。

## 処理フロー

組み込み機器に限らず、3Dグラフィックを描画する場合のフローはおおむね以下のようになります。

CPUとGPUが協調して、それぞれの処理を行い、最終的にフレームバッファにレンダリングされた画像を出力することでグラフィックが描画されます。

| パイプライン             | 処理                       | 性能に影響するパラメータ                                |
| ------------------------ | -------------------------- | ----------------------------------------- |
| シーングラフの走査       | CPU                        | シーン内オブジェクト数<br>マテリアル総数  |
| Model Matrix             | CPU                        | シーン内オブジェクト数<br/>マテリアル総数 |
| モデリング・投影変換     | GPU                        | 頂点数                                  |
| 頂点単位での陰影処理     | GPU (頂点シェーダ)         | 頂点シェーダの複雑度                    |
| クリッピング・陰面処理   | GPU                        | Front / Back Culling                      |
| ラスタライズ             | GPU (ラスタエンジン)       | レンダリング解像度                      |
| Zバッファ作成            | GPU                        | enable / disable                          |
| 早期Zカリング            | GPU                        |                                           |
| ピクセル単位での陰影処理 | GPU (フラグメントシェーダ) | フラグメントシェーダの複雑度            |
| テクスチャ適用           | GPU (フラグメントシェーダ) |                                           |
| αテスト                  | GPU (バックエンド)         | enable / disable                          |
| ステンシルテスト         | GPU (バックエンド)         | enable / disable                          |
| Zテスト                  | GPU (バックエンド)         | enable / disable                          |
| αブレンディング          | GPU (バックエンド)         | enable / disable                          |
| アンチエイリアス(MSAA)   | GPU (バックエンド)         | enable / disable                          |

### 性能への影響

#### CPU/GPUの両方の負荷が高い傾向

- SceneグラフのNode数を減らす。
- SceneグラフのNode階層を減らす。
- Model Matrixを更新するNodeを減らす。
- 使用するMaterial数を減らす。
- 複数のモデルを一つにまとめる。
- CPUによるオクルージョンカリングを検討する

#### GPUの頂点処理パイプラインの負荷が高い傾向

- Vertexシェーダ内の処理を簡素化する。（e.g. 光源の数を減らす）
- Vertexシェーダ内のif/forを削除する
- Vertexシェーダに渡すuniform変数の数を減らす
- モデルの頂点数を減らす。（最も効果あり）

#### GPUのフラグメント処理パイプラインの負荷が高い傾向

- Fragmentシェーダ内の処理を簡素化する。(e.g. 頂点シェーダで陰影処理をする。)
- Fragmentシェーダ内のif/forを削除する。
- テクスチャ数を減らす。一つのテクスチャをまとめる。
- ポストプロセッシングを避ける。(Render Targetを使用しない。)
- Zバッファの使用を避ける。
- 半透明オブジェクトの使用を避ける。
- Cullingを有効にする。
- ステンシルバッファの使用を避ける。
- テクスチャを圧縮する。
- Render Targetのカラーバッファフォーマットを16bitにする。
- 固定解像度のRender Targetに描画し、screenに拡大して表示する。(画質劣化有)

#### CPUの負荷が高い傾向

- 使用するMaterialでSceneをソートする。（Materialの切り替えの発生を抑える）
- 複数のMaterialをまとめる

### 改善対策

#### 変化した部分だけ差分更新し、描画負荷を軽減

組み込み機器のUIなどへの適用の場合、ゲームのように全画面の再描画が必須にならない場合があります。

そのような場合、変化する部分を管理しておき、その部分のみ再描画することで描画に対する要求性能を抑えることができます。

### 描画性能

#### テクスチャ画像に対してもアンチエイリアジング(AA)をかける

一般的なGPUのハードウェアで実行されるアンチエイリアジング(e.g. MSAAなど)は、テクスチャマッピングされたイメージに対してAAをかけることはできません。

また、Zバッファを利用するため、解像度が高い場合、メモリ帯域を圧迫する可能性があります。

Post-processing型のAAでは、上記の問題を解決することができます。
(※注意：性能はFragmentシェーダの処理能力に左右されることに注意が必要です。)

#### High Dynamic Range(HDR)レンダリングをシミュレートする

OpenGL ES等でfloatingバッファが使いない場合、基本的に8bitのLDRレンダリングとなります。シェーダを使い、α成分にダイナミックレンジ成分を格納することで、疑似的にHDRをシミュレートできるテクニックとして、疑似HDRがあります。

この方法では、HDR値→疑似HDRバッファ→LDR値のエンコード、デコードを行うため、最低でも２パスのマルチパスレンダリングを行う必要があります。

- 1 pass
    - Fragmentシェーダで通常のPhongシェーディングを行い、vec4カラー値rgbaを算出する
    - rgbaをLDRバッファに格納するためエンコードする
- 2 pass
    - エンコードされたLDRバッファをsampler2D型のuniform変数で受け取る
    - LDRバッファをデコードする
    - トーンマッピングを行い、HDR値をLDR値に変換する
    - トーンマッピング後のLDR値をgl_FragColorに出力する


**HDRエンコード/LDRデコード方法の例**

```
const float HDR_MIN = 0.25;
vec4 encodeHdr(vec4 col)
{
   float v = max( max(col.r, col.g), max(col.b, HDR_MIN) );
   vec4 hdr = col/v;
   hdr.a    = HDR_MIN/v;
   return hdr;
}

vec4 decodeHdr(vec4 hdr)
{
   hdr   = (hdr/hdr.a * HDR_MIN);
   hdr.a = 1.0;
   return hdr;
}

// 引数
//  hdr: HDR値
//  ex: シーンの平均輝度
//  bright_max: シーンの最大輝度
vec4 tonemapping(vec4 hdr, float ex, float bright_max)
{
   float Lm = bright_max+0.0001;
   float Yd = ex * (ex/Lm + 1.0) / (ex + 1.0);
   return hdr*Yd;
}

```

#### 効率よくぼかしを適用する

ぼかし処理、ブラー処理はガウスフィルタを用いる方法が一般的ですが、このフィルタを通常通りにxy方向に適用すると高負荷となります。ガウスフィルタは変数分離可能なため、x方向、y方向を2パスで適用することができます。

- 1 pass
    - Fragmentシェーダでx方向のみガウスフィルタをかけたイメージを生成し、Render Targetに描画する。
- 2 pass
    - Fragmentシェーダで①のRender Targetをsampler2D型のuniform変数に読み込み、y方向のガウスフィルタをかけたイメージを生成する


**ガウスフィルタの例**

```c
// 引数
//   texture: ガウスフィルタ対象のイメージ
//   texcoord: vertexシェーダからの座標情報
//   blurSizeX: 画像の1/Widthの値
//   blurSizeY : 画像の1/Heightの値
// 
// 戻り値
//   フィルタ適用後の座標texcoordのカラー値

vec4 GaussFilter( sampler2D texture, vec2 texcoord, float blurSizeX, float blurSizeY )
{ 
   vec4 sum = vec4(0.0);
   sum += texture2D(texture, vec2(texcoord.x - 4.0*blurSizeX, texcoord.y - 4.0*blurSizeY)) * 0.05;
   sum += texture2D(texture, vec2(texcoord.x - 3.0*blurSizeX, texcoord.y - 3.0*blurSizeY)) * 0.09;
   sum += texture2D(texture, vec2(texcoord.x - 2.0*blurSizeX, texcoord.y - 2.0*blurSizeY)) * 0.12;
   sum += texture2D(texture, vec2(texcoord.x -     blurSizeX, texcoord.y -     blurSizeY)) * 0.15;
   sum += texture2D(texture, vec2(texcoord.x,                 texcoord.y                )) * 0.16;
   sum += texture2D(texture, vec2(texcoord.x +     blurSizeX, texcoord.y +     blurSizeY)) * 0.15;
   sum += texture2D(texture, vec2(texcoord.x + 2.0*blurSizeX, texcoord.y + 2.0*blurSizeY)) * 0.12;
   sum += texture2D(texture, vec2(texcoord.x + 3.0*blurSizeX, texcoord.y + 3.0*blurSizeY)) * 0.09;
   sum += texture2D(texture, vec2(texcoord.x + 4.0*blurSizeX, texcoord.y + 4.0*blurSizeY)) * 0.05;
   return sum;
}
```