[WebGL] HDRレンダリングの様々な実現方法の比較

[WebGL Advent Calendar 2015](http://qiita.com/advent-calendar/2015/webgl) 7日目の記事です。

-----------

2015年10月、Unityは[Web Playerを非推奨とすることを発表](http://blogs.unity3d.com/jp/2015/10/08/unity-web-player-roadmap/)しました。Flashについても[終わりが見え始めています](http://www.ka-partner.jp/blog/google%E3%81%8Cflash%E3%82%92%E9%9D%9E%E6%8E%A8%E5%A5%A8%E3%80%81html5%E3%81%A7%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E6%8E%A8%E5%A5%A8%E3%81%99%E3%82%8B%E3%81%A8/)。また、最近はiOSやAndroidを搭載したモバイル端末の利用率が増加していますが、これらはUnity Web PlayerもFlashも対応していません。このため、以前はWebGLは軽量な3Dコンテンツ程度にしか使用されていませんでしたが、これまでUnity Web PlayerやFlash Stage3Dが担当していたような本格的な3Dのブラウザゲーム(もしくはインタラクティブコンテンツ)も、そのほとんどがWebGLに移行していくと予想されます。

現代のPC/コンソール向けゲームでは、HDRレンダリングやリニアブレンディングが当然のように使用されています。しかしながら、WebGLで同じようなことをしようとすると、GPUの限界はもちろん、[ブラウザの機能の制限](http://webglstats.com/)、パフォーマンスなど、様々な壁に直面することになります。

本記事では、皆様がWebGLでレンダリングエンジンを作る際に使える、HDRの様々な実現方法を紹介します。

HDRレンダリングとは
------------------

![](https://qiita-image-store.s3.amazonaws.com/0/38578/759a966b-cd39-76f7-9a82-4607f1f32998.png)

HDR (High Dynamic Range) レンダリングは、十分な計算精度を保ったまま幅広い階調(ダイナミックレンジ)を扱うことができるようなレンダリング手法のことを指します。例えば、下のシーンをご覧下さい。

![](https://qiita-image-store.s3.amazonaws.com/0/38578/0954649f-6bf0-8b43-3939-e3c14e937443.jpeg)

後ろの2個の点光源から、画像中央の[Stanford Dragon](https://en.wikipedia.org/wiki/Stanford_dragon)が照らされ、反射しています。試しにこのシーンの光量を変えて(露光補正して)みましょう。

![EV_3.jpg](https://qiita-image-store.s3.amazonaws.com/0/38578/611a2fd6-2cb1-724c-2bd5-3a225895e3d6.jpeg)

**x 1/16** の方を見てみましょう。ほとんどの部分が暗くなって見えなくなりますが、緑色の照明やドラゴンの光沢はもともと非常に明るいので、程々の明るさになって形がはっきり見えるようになっています。

次に **x16** の方を見てみると、大部分が白飛びしてしまいます。しかし、元の画像では暗くてよく見えなかった背景の建物の隅のほうや、ドラゴンの陰のあたりの様子(胴に右手が反射している、など)がよく見えるようになっています。

ここから分かるように、3Dのシーンには、最終的に画面に表示できるもの(こちらはHDRに対して、LDRと呼びます)よりもはるかに幅広い明暗の差(ダイナミックレンジ)が含まれていることが分かります。このように幅広い明暗を保ったまま中間処理を行うことにより、より現実世界の物理法則に忠実なレンダリングを行う手法をHDRレンダリングと呼びます。


### トーンマッピング

HDRレンダリングを行った後、その結果を画面に表示する必要があるのですが、画面はLDRですので、そのまま表示することはできません。そこで、HDRからLDRに変換する処理が必要となりますが、この処理をトーンマッピングと言います。


リニアレンダリング
---------------

シェーダーで0.5という値を出力しても、実際に画面に表示されたときの明るさは、最大値の50%にはならないということはご存知でしょうか? (各種誤差は気にしないものとします)

シェーダの出力値は確かにちゃんとフレームバッファに出力され、128(か127)になっています。試しに画面上の色を抽出できるソフトを使用してみても、ちゃんとそれと同じ値になっています。では何が原因なのでしょうか?

![Linear-2-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/025db106-cca1-5bcc-2242-555f2cfe9fd3.png)

実は、モニタが犯人なのです。(現在ではあまり見かけませんが)CRTモニタは入力電圧に対する出力が非線形となる特性があります。現在主流のLCDモニタには、本来はこのような特性はありませんが、同じような特性を示すように調整されています。

![Linear-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/05303202-d3ee-c7f9-e1c9-d640b0e1a689.png)

このため、この特性を考慮せずに処理を行ってしまうと、物理的に不正確な結果となってしまうことがあります。例えば、2つの光が重なった時、明るさは元の明るさの和となるはずです。ですから、シェーダーで0.5という値を出力したときのモニタの輝度は21.8%になるので、それが2つ重なれば43.6%となるはずです。ところが、単純に加算ブレンディングを行うと、フレームバッファの値は255 (最大値)となり、これをモニタの輝度に換算すると100%となってしまいます。

![Linear-3-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/cabdf29d-93b5-e8d9-5044-e448d79e5ad2.png)

そこで、上図のようにレンダリングはモニタの輝度に対し線形な空間で行い、最後のトーンマッピングでモニタに合わせてガンマ補正を行うという手段が最近ではよく用いられます。

しかし、人の目は暗部の差異には敏感で、モニタのガンマ補正も暗部に高精度な領域が集中するようになっているので、単純にこの方法を使うと暗部の精度が不足してしまいます。これに対する対処法として、3つの選択肢があります。

 1. リニアブレンディングをあきらめる。
 2. WebGL拡張 `EXT_sRGB` を使用すると、モニタの特性に合わせて暗部の精度を高めたエンコーディングを使用したフレームバッファ・テクスチャを作成できるので、これを利用する。
![sRGB-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/3e55f535-ab90-ce71-d529-81abac698f5b.png)
 3. HDRレンダリングを実装する。




HDRを使うことで効果的になるエフェクト
--------------------------------------------

HDRレンダリングをによりジオメトリを描画してただ単純にその結果をトーンマッピングしたとしても、LDRレンダリングしたのとあまり変わりありません。HDRレンダリングを意味のあるものとするためには、HDRを活かせるような各種エフェクトを使用することが重要となります。

### ブルームエフェクト

<img width="1150" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/38578/4d78cbb4-6585-a2b8-e1d6-fd3225a6bf8f.jpeg">

ブルームエフェクトは、上に示した画像のように、画像の非常に明るい部分をにじませることにより、「明るい」という感じなどを表現するエフェクトです。一般的には次のような流れで処理が行われます。

1. 入力画像の明るい部分のみを抽出した画像を生成する。(ソフトなしきい値処理や、ガンマ補正などを使用)
2. その画像にぼかしを掛ける。
3. 元の画像に合成する。

ブルームエフェクトは2004年頃から使用されていたと言われています。当時はLDRレンダリングがメインであったため、上記の手法をそのまま適用しても写実性の高い画像を生成するのは困難でした。ひとつの要因としては、LDRでは非常に明るい部分があったとしても最大値で飽和してしまう為、それよりも若干低い値を「明るい部分」のしきい値として設定する必要があり、結果として不必要なところまでブルームエフェクトが掛かってしまい、全体としてぼやけた映像になってしまうことがあります。また、しきい値処理やガンマ補正などは非線形な処理であるため、カラーチャンネル毎に掛かり方が変わり、あるいは入力の段階ですでに明るい部分が飽和してしまっていることが原因で、不自然な色合いとなってしまうこともあります。

そもそも、ブルームエフェクトは、非常に明るい光がレンズに入った際にわずかに散乱する光学現象をシミュレートしたものなので、非線形現象が起きる、つまり"入力光の強さによって掛かり方が変化する"ようなことは決して有り得ないはずなのです。つまり、単純に入力を定数倍するのが物理的に正確な方法なのです[^4]。

LDRレンダリングでは入力に"非常に明るい"部分は存在しないので、入力画像を定数倍して暗くすると全部暗くなってしまい、ほとんどブルームエフェクトの効果が無くなってしまいますが、HDRレンダリングでは"非常に明るい"部分だけは残るので、自然なブルームエフェクトが可能となります。


### モーションブラー

![Motion Blur_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/38578/3f1afc7e-279e-5a8e-1b5a-53ace91cd8f0.jpeg)

モーションブラーもHDRの効果が出やすいエフェクトです。LDRレンダリングの場合、高輝度成分は完全に失われてしまいますので、自己発光するマテリアルの物体にモーションブラーを掛けても「明るい色の物体」程度にしか見えません。HDRレンダリングであれば、輝度を保ったままモーションブラーを掛けることができ、その結果「光っている感じ」を出すことが出来ます。

### その他

他にも、次のようなエフェクトがHDRと非常に相性が良いようです。

* 被写界深度
* 反射 (スクリーンスペース系の手法など)
* アルファブレンディング


HDRレンダリングの実現方法
---------------------

### 浮動小数点フレームバッファ

最近のGPUでは、浮動小数点バッファに直接レンダリングを行う機能が搭載されているものが多いです。WebGLでも拡張 `EXT_color_buffer_half_float` および `WEBGL_color_buffer_float` を使用すると浮動小数点テクスチャへのレンダリングが可能になるので、これによりHDRレンダリングを簡単に実現できます。

浮動小数点フレームバッファをバインドしている時は、シェーダーの出力値(`gl_FragColor`)がそのまま([0, 1]に制限されずに)フレームバッファに書き込まれます。ブレンディングも同様に行われます。逆に、レンダリング結果を利用するときも、普通に`texture2D`を呼び出せば書き込んだ値がそのまま返ってきます。 簡単ですね! 

浮動小数点フレームバッファの作成は非常に簡単で、普通にRender-to-Textureをするときとほとんど変わりありません。

```js:fp_framebuffer.js
var gl;
var texture, fb;

function createFPFramebuffer(width, height)
{
    // 1. まず最初に浮動小数点テクスチャを作成します。
    // ここでは、半精度浮動小数点テクスチャを使用したいので、
    // OES_texture_half_float 拡張を使用します。
    var ext = gl.getExtension('OES_texture_half_float');
    
    texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    
    // テクスチャのデータ型にはHALF_FLOAT_OESを指定します。
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
                  gl.RGBA, ext.HALF_FLOAT_OES, null);
    
    // 2. 続いてフレームバッファを作成します。
    // ここから先は通常と何も変わりありません。
    fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
                            gl.TEXTURE_2D, texture, 0);
}
```

浮動小数点フレームバッファを使う場合、一つ注意点が有ります。それは、NaN、Infinityといった特別な値もそのままフレームバッファにバッファに書き込まれる場合があるということです。こういった特別な値は、数値的に不安定な計算や非常に大きな/小さな値を扱うシェーダーでよく発生するものですが、これらを入力として計算を行うと出力もNaNまたはInfinityになってしまい、それをまた入力とするとーーつまり、簡単に言うとNaNとInfinityは「伝染」します。例えばブルームエフェクトでは画面の大部分を覆うような大きさのカーネルのぼかしを使用することがありますが、入力に1ピクセルでもNaNがあると、そのNaNが画面の大部分に伝染し、結果として画面全体が一瞬黒く点滅することになります。

NaN/Infinityを検出できれば、伝染を早い段階で食い止めることができるかもしれません。しかし、これらを検出できる標準関数 `isnan`, `isinf` はGLSL 1.30には存在するのですが、残念なことにWebGLでは利用することができません。自作関数で実現しようとしても確実に検出することは難しい[^7]ようですので、出来る限りこういった値が生成されないように計算を工夫するしか解決策はないようです。

もう一つの注意点として、浮動小数点フレームバッファは通常の整数フレームバッファよりも2倍以上のメモリ帯域を必要とするという点があります。このため、非力なGPU(特にIntel HD GraphicsのようなCPU内蔵のもの)では少し厳しいかもしれません。

### 整数フレームバッファをスケーリングして使用する

![Limited HDR Diagram_2-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/55caef80-6b88-0011-b206-653e79e6d060.png)

通常、整数フレームバッファでは[0, 1]の範囲の値を格納できるものとして扱いますが、シェーダで値を出力する前にスケーリングを行うことで、これよりも広い範囲の値を格納したことにするという手法です。例えばCall of Duty: Black Opsでは、この手法で[0, 4\] (ガンマ空間では[0, 2])の範囲の値を格納しています[^6]。

```glsl:limited_hdr.fs
void main()
{
    /* ... */
    gl_FragColor *= 1.0 / 2.0;
}
```

この方法は、浮動小数点フレームバッファよりも必要なメモリ帯域が少ない点では優れています。しかしながら、表現できる範囲が著しく限られるのと、暗部の色精度がかなり落ちるという問題点があります。

R10G10B10A2のような高精度な整数フレームバッファのフォーマットを使用すれば、精度の問題はある程度解決できます。しかし、残念なことにWebGLでは利用できません。

### ソフトウェアにより特殊な色表現を実装する

(注: CPUで実装するという意味ではありません。浮動小数点フレームバッファのようにハードウェアの機能を利用するのではなく、プログラマブルシェーダーという一種のソフトウェアで実現するという意味です。)

フレームバッファに出力する前に、シェーダ内で出力値を特殊な方法でエンコードすることにより、通常格納できるものよりも幅広い値を扱えるようにするという手法です。

エンコード方法は様々なものが提案されています。

* **RGBM** RGBの最大値をスケーリングしたものをアルファチャンネルに格納し、RGBはその値で除算して格納します。[^10]
* **RGBD** アルファチャンネルには最大値の逆数を格納します。[^9]
* **RGBE** アルファチャンネルには最大値の対数を格納します。[^8]

要するに、余ったアルファチャンネルを利用してRGB値の表現範囲を広げるというのがこの手法です。(逆に言えば、アルファチャンネルが余っていなければ適用できません。)

この手法は浮動小数点フレームバッファほどメモリ帯域を必要としない点は優れています。しかしながら、エンコード・デコード処理のために若干ALUに負荷が掛かるのと、他にも以下の問題点があります。

* **ブレンディングが使用できない。** GPUのブレンディング機能は、通常の色表現、別の言い方をするとハードウェアでもともと対応している値の表現に対してのみ使用できるので、このようにソフトウェアで勝手に定義したエンコーディングに対しては使用することができません。もし無理に使用したとしても、無茶苦茶な出力結果となります。
  * Deferred Shadingを行う場合、Light Accumulation Bufferに加算ブレンディングを行うことが必要となります。ブレンディング以外の方法でも実現することは可能ですが、劇的に遅くなってしまいます。(この辺は研究の余地があるかもしれませんね!)
  * Forward Shadingを行う場合でも、マルチパスレンダリングを行う場合、ブレンディングが必要となります。これもDeferred Shadingの時と同様、ブレンディング以外の方法ですと遅くなってしまいます。
  * ちなみに、OpenGL ESでは拡張 `EXT_shader_framebuffer_fetch ` を利用してブレンディングもソフトウェアで実装することができますが、WebGLでは利用できないようです。そもそも対応GPUがMaliやPowerVRに限られますが…
* **正しく線形補間できない。** RGBMなどを用いて描画を行った後、その結果をポストエフェクトに通したりするかと思います。ポストエフェクトでは、入力画像のテクスチャをサンプリングし、その後にRGBMをデコードして様々な処理を行う訳ですが、テクスチャをサンプリングするときに線形補間をしてしまうと、滑らかに補間できなかったり、あるいは最悪の場合には「元の画像には存在し得ない値」が得られてしまうことがあります。
  * 線形補間を利用することにより半分程度のテクスチャフェッチ回数でガウシアンブラーを実現する手法が知られています[^11]が、この方法は適用できなくなります。

### 露出値ごとに別の画像にレンダリング

露出の異なる画像を複数回レンダリングするという、写真でもよく見られる方法を利用したものです。

![MultiEV_2-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/a148e523-6d6e-5149-7519-7f2f56e9299a.png)

次のような流れで行います。

1. 異なる露出で複数回レンダリングします。このときは通常LDRレンダリングする時とほとんと同じようにレンダリングしますが、フラグメントシェーダーの最後で露光補正を行えるようにしておきます。

    ```glsl:multi_exposure.fs
    uniform float u_exposure;
    
    void main()
    {
        /* ... */
        gl_FragColor.xyz *= u_exposure;
    }
    ```

    このとき、特別気にすべきことはありません(一つありますが後で述べます)。ブレンディングなどを行っても問題ありません。
    
    レンダリング回数は必要なダイナミックレンジを全てカバーできるだけの枚数が必要となりますが、2枚程度あれば十分かと思われます。露出値の組み合わせは、`EXT_sRGB`が利用できる場合は×2, ×1/64、利用できない場合は×1, ×1/16あたりがお勧めです。

2. 1.でレンダリングした画像のうち、一番暗い画像(一番露出値が低いもの)以外の画像について、オーバーフローしたピクセルを検出します。オーバーフローしているということはその露出値では明るすぎて正確に色を表現できていないということですので、「一段階露出値が低い画像の値を使用しなければならない」という意味になります。

3. HDR合成を行います。各ピクセルについて、2.の結果に基づいて最適な(つまり、オーバーフローしておらず、十分な精度が確保できている)入力画像を選択し、HDR画像を生成します。

4. HDR合成の結果を出力します。
   1. ここでトーンマッピングを行い、それ移行はLDR処理をする(もしくはそのまま画面に表示する)という選択肢があります。
   2. あるいは、とりあえず先に述べたようなRGBMやRGBEなどの方法でエンコーディングを行ってテクスチャに描画しておき、これをポストプロセッシングの際の入力にすることもできます。

このうち2, 3, 4は一つのシェーダーでまとめて実行したほうが簡単です。露出値が2種類の場合、HDR合成を行いRGBEエンコードして出力するシェーダーは次のようになります。

```glsl:hdr_composite.fs
uniform texture2D u_darkImage;
uniform texture2D u_brightImage;

varying highp vec2 v_texCoord;

const float DarkImageExposure = 1.0 / 64.0;
const float BrightImageExposure = 2.0;

// (RGBEエンコーダの実装はここでは省略します)
vec4 encodeRGBE(in vec3 rgb);

void main()
{
    // 各露出値のレンダリング結果をサンプリング
    vec3 dark = texture2D(u_darkImage, v_texCoord);
    vec3 bright = texture2D(u_brightImage, v_texCoord);
    
    // ステップ2: 露出オーバーしているか検出する
    float brightLum = max(max(bright.x, bright.y), bright.z);
    float brightOverflowness = smoothstep(0.8, 1.0, brightLum);
    
    // 各レンダリング結果の実際の輝度(露出補正前の値)を計算
    dark *= 1. / DarkImageExposure;
    bright *= 1. / BrightImageExposure;
    
    // ステップ3: HDR合成
    // brightが露出オーバーしているときは、darkに切り替える。
    vec3 composited = mix(bright, dark, brightOverflowness);
    
    // ステップ4: 出力
    gl_FragColor = encodeRGBE(composited);
}
```

実は、この方法にはブレンディングに起因する問題点があります。

![MultiEVBlending_4-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/eb8fbc27-a659-54f5-07b6-700e5f11a29c.png)

上の図の左側をご覧ください。ものすごく明るい緑色(4, 5, 4)の正方形の上に、不透明度80%の赤色(0.5, 0, 0)の正方形が乗っています。重なりあった部分の色を計算すると(1.3, 1, 0.8)となり、明らかにLDRの範囲をオーバーフローしているため、この部分は「露出オーバー」として判定されてほしいものです。

ところが、実際にLDRフレームバッファで描画すると(右側)、ものすごく明るい緑色(4, 5, 4)はLDRで表現できる範囲に制限され、ただの白色(1, 1, 1)になってしまいます。そうするとこの赤色の正方形が重なった部分の色は(0.6, 0.2, 0.2)となってしまい、露出オーバー判定されなくなってしまいます。

これに対する対処法としては、露出値が低い画像を用いて、露出値が高い画像の露出オーバー箇所を予測するというものがあります。シェーダーは次のようになります。

```glsl:hdr_composite_improved.fs
uniform texture2D u_darkImage;
uniform texture2D u_brightImage;

varying highp vec2 v_texCoord;

const float DarkImageExposure = 1.0 / 64.0;
const float BrightImageExposure = 2.0;

// (RGBEエンコーダの実装はここでは省略します)
vec4 encodeRGBE(in vec3 rgb);

void main()
{
    // 各露出値のレンダリング結果をサンプリング
    vec3 dark = texture2D(u_darkImage, v_texCoord);
    vec3 bright = texture2D(u_brightImage, v_texCoord);
    
    // ステップ2: 露出オーバーしているか検出する
    // ここで、brightが露出オーバーしているかを検出するのに、
    // darkの方を見ているところが重要。
    float darkLum = max(max(dark.x, dark.y), dark.z);
    const float BrightLimitInDark = DarkImageExposure / BrightImageExposure;
    float brightOverflowness = smoothstep(
        BrightLimitInDark * 0.8, 
        BrightLimitInDark, darkLum);
    
    // 各レンダリング結果の実際の輝度(露出補正前の値)を計算
    dark *= 1. / DarkImageExposure;
    bright *= 1. / BrightImageExposure;
    
    // ステップ3: HDR合成
    // brightが露出オーバーしているときは、darkに切り替える。
    vec3 composited = mix(bright, dark, brightOverflowness);
    
    // ステップ4: 出力
    gl_FragColor = encodeRGBE(composited);
}
```

この手法の別の問題点として、複数回レンダリングを行うため、レンダリング負荷(ドローコール, 頂点処理, フラグメント処理)が激増するという問題点があります。これを解決した手法が次のExposure Mosaicです。

### Exposure Mosaic

前の手法では、異なる露出の画像を複数回のレンダリングによって得ていました。この手法では、それを1回のレンダリングだけで得ることができます[^12]。

![](https://qiita-image-store.s3.amazonaws.com/0/38578/0c2bf51b-7b29-d1a7-aa80-f3f5d9ef7e41.png)

2x2のディザパターンを使用し、それぞれのピクセルに対して異なった露出値を適用します。レンダリング完了後、各露出値について、周辺の4ピクセルの情報を平均することにより、その露出値の画像を復元することができます(Demosaic処理)。

Demosaic処理によってディザパターン状のアーティファクトが生じてしまう場合がありますが、次のようなカーネルのぼかしを掛けたり、Temporal AAを適用することにより軽減できることが分かっています。(鮮鋭度は犠牲になってしまいますが…)

```math
\begin{pmatrix}
0 & 1/4 & 0 \\
1/4 & 1 & 1/4 \\
0 & 1/4 & 0
\end{pmatrix}
```

![EVMosaicEx2.png](https://qiita-image-store.s3.amazonaws.com/0/38578/402dab1d-9126-871f-7408-2f9ae649222e.png)

(左から順にExposure Mosaicのレンダリング結果、Demosaic後、ぼかし + Temporal AA適用後)


現状のWebGL実装における問題点
---------------------------

### 浮動小数点フレームバッファのサポート状況

HDRレンダリングを実現するための方法の一つとして浮動小数点フレームバッファへのレンダリングがありますが、このために必要なWebGL拡張 `EXT_color_buffer_half_float` の対応状況はあまり良いとは言えません。

現在のほとんどの環境では、GPU側ではこの機能に対応しています。しかしながらブラウザ側でサポートしていない場合が多く、2015年12月現在においてもわずか9.1%のユーザーしかこの拡張を利用することができないという状況です[^5]。

| 環境 | 対応しているユーザーの割合 | 備考  |
|:---:|:----:|:-----|
| PC  | 10.3% | 主要ブラウザの中ではFirefoxのみ対応。 |
| Android | 1.1% | こちらもFirefoxのみ対応している模様。 |
| iOS | 0.1% | |

特に、PCのブラウザで最もシェアが大きいGoogle Chromeで対応していないというのは致命的です。さらに、モバイル系については全くサポートされていないと言っても良い状況ですので、今のところは `EXT_color_buffer_half_float` は使えないことを前提にして設計するのが得策であるように思えます。

一応Firefoxでは動作するので、「リファレンス実装」として `EXT_color_buffer_half_float` を使用したものも実装しておくのも良いかもしれません。

**2015年12月18日追記** Chromeでは `EXT_color_buffer_half_float` がサポートされていないにも関わらず、実際には浮動小数点フレームバッファへのレンダリングには対応しており(規格違反の動作)、大部分のPCで実際に動作するようです。すなわち、PCではほぼ確実に使えるということになります[^13]。しかし、モバイル環境での対応状況は不明です。試しに当方の環境で試したところ、iPhone 6 + iOS 9では動きませんでした。

[^13]: [Steamの統計](http://store.steampowered.com/hwsurvey/videocard/)によると、96%近いユーザーがDirectX 10.x以上に対応したGPUを使用しており、Direct 3D Feature Level 10_0では浮動小数点フォーマットが完全に扱えることが要件に含まれている[^14]ので、(母集団の偏りを無視すれば)大部分のユーザーが浮動小数点フレームバッファを使えると言えます。

[^14]: Chuck Walbourn, "[Direct3D Feature Levels](http://blogs.msdn.com/b/chuckw/archive/2012/06/20/direct3d-feature-levels.aspx)."

### Chrome for OS XおよびSafariでは `EXT_sRGB` が正常に動作しない

まだあまり知られていないようなのですが、Google Chrome 45〜47のOS X版とSafari (おそらく9とそれ以前の全バージョン?)において、WebGL拡張 `EXT_sRGB` が正常に動作しないというバグがあるようです。

これらのブラウザでこのバグを回避する手段は、現時点ではまだ見付かっていません。HDRは要らないけどリニアブレンディングをしたいといった場合に `EXT_sRGB` はよく使用されますものですが、この拡張が利用できない場合はHDRレンダリングのものと似た手法を利用せざるを得なくなります。

まとめ
-----

いくつかの観点から本記事で紹介したHDRの手法を評価し、表にまとめてみました。

|                手法       | パフォーマンス   | ブラウザサポート   | 精度・DR  | ブレンディング |
| :------------------------ | :-----------: | :--------------: | :--: | :---------: | 
| 浮動小数点フレームバッファ    |       △        |        △         | ○    |     ○       |
| 特殊な色表現                |      △         |      ○          |  ○   |     ×       |
| スケーリング                |      ○         |       ○         |  ×   |     ○       | 
| 露出値ごとに別の画像         |       ×       |        ○       |   △  |       △       | 
| Exposure Mosaic           |      △         |        ○         |  △   |     △       | 

**パフォーマンス**: エンコード・デコードのALU処理量や、VRAMの帯域幅について評価しました。「露出値ごとに別の画像」についてはシーン全体を複数回レンダリングする必要があり、描画コールも増加しパフォーマンスに大きく影響すると考えたため、×としています。
**ブラウザサポート**: 浮動小数点フレームバッファはPCではほぼ確実に使えますが、モバイル環境の対応状況が不明です。
**精度・DR** (ダイナミックレンジ): シェーダー内で通常扱う色表現が16ビット浮動小数点相当ですので、これに近い精度が扱える上の2つについては○としました。「スケーリング」に関してはそもそもダイナミックレンジは増加していないので、×としています。
**ブレンディング**: 「特殊な色表現」は上で述べたようにハードウェアでのブレンディングは不可能と言える状況なので、×と判断しました。下の2つについては注意が必要であり、上で述べた解決策も計算精度の不安があるので△としていますが、実用上はあまり問題ないと推測しています。

HDRの手法はそれぞれ長所と短所があるため、目的に応じて使い分けるのが望ましいと言えます。また単一の手法のみを用いるのではなく、レンダリングパイプライン中で、複数の手法を併用することも考えられます。どのように使い分けるかについては、次に紹介するHYPER WebGL Rendererを参考にすると良いでしょう。


HYPER WebGL Renderer (仮称)
------------------------------

HYPER WebGL Rendererは現在開発途中のWebGL向けのDeferred Shadingベースのレンダリングエンジンです。このエンジンでも、本記事で紹介したようなHDR手法を活用しています。

![HYPER_2-01.png](https://qiita-image-store.s3.amazonaws.com/0/38578/068ecbdc-0be5-d6e1-ce44-6ca6c8b47168.png)

1. Geometry Passが終わった後、Light Accumulation Bufferに各ライトの計算結果を蓄積していく訳ですが、この際に加算ブレンディングが必要となるので、ここではExposure Mosaicを使用します。
2. この後に各種ポストプロセスを適用するのですが、Exposure Mosaicのままでは扱いづらい為、ここで一旦RGBE-likeなエンコードに変換します。
3. ポストプロセスの一つにブルームエフェクトがあります。ブルームエフェクトには画像の暗部はほとんど寄与せず、またガウシアンブラーやリサンプリングを多用するため、リニアなバッファ(本記事の「整数フレームバッファをスケーリングして使用する」に相当)に変換したものを用います。

このように、用途に応じ最適な手法を使い分けるようにしています。また、`EXT_color_buffer_half_float` が利用できるような環境では、浮動小数点バッファに切り替えられるような仕組みとなっています。


クレジット
-----------------

本記事の画像では、以下の素材を使用しています。

* Stanford University Computer Graphics Laboratory, [Dragon](http://graphics.stanford.edu/data/3Dscanrep/)
* Willem-Paul van Overbruggen, Suzanne Blender Monkey
* [Humus](http://www.humus.name/)氏作成のキューブマップテクスチャ。(背景および反射)

参考文献等
-----------------

[^3]: [Elephants Dream](https://orange.blender.org/)の[ワンシーン](https://commons.wikimedia.org/wiki/File:Elephants_Dream_-_Emo_and_Proog.jpg)
[^4]: Larson, Greg Ward., H. Rushmeier, C. Piatko, "[A Visibility Matching Tone Reproduction Operator for High Dynamic Range Scenes](http://radsite.lbl.gov/radiance/papers/lbnl39882/tonemap.pdf)," IEEE Transactions on Visualization and Computer Graphics, Vol. 3, No. 4, December 1997.
[^5]: [WebGL Stats](http://webglstats.com/)
[^6]: Dimitar Lazarov, "[Physically-based lighting in Call of Duty: Black Ops](http://advances.realtimerendering.com/s2011/)," SIGGRAPH2011, Advances in Real-Time Rendering in 3D Graphics and Games.
[^7]: "[How to deal with NaN or inf in OpenGL ES 2.0 shaders](http://stackoverflow.com/questions/11810158/how-to-deal-with-nan-or-inf-in-opengl-es-2-0-shaders)," Stack Overflow.
[^8]: Malte Clasen, "[HDR values in 8 bit RGBA pixels](http://www.malteclasen.de/zib/index4837.html?p=37)."
[^9]: Claes Johanson, "[RGBdiv8 texture/framebuffer HDR compression](http://vemberaudio.se/graphics/RGBdiv8.pdf)."
[^10]: Brian Karis, "[RGBM color encoding](http://graphicrants.blogspot.jp/2009/04/rgbm-color-encoding.html)."
[^11]: Daniel Rákos, "[Efficient Gaussian blur with linear sampling](http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/)."
[^12]: Niklas Smedberg, Timothy Lottes, "[Next-gen Mobile Rendering by Epic Games](https://de45xmedrsdbp.cloudfront.net/Resources/files/GDC2014_Next_Generation_Mobile_Rendering-2033767592.pdf)," GDC2014