HDR画像フォーマット


〜 The RADIANCE Picture File Format 〜






■はじめに

これだけHDR、HDRといわれてきた世の中で、「.hdr」の拡張子であらわされるHDR画像フォーマットを扱っていないのはもぐりでしょう。HDR画像フォーマットはエクスプローラでさえもプレビューできる世界です。
ということで、「.hdr」の画像を読み込んでテクスチャにRGBE(A)で保存するルーチンを作ってきました。
ま、実際問題、"The RADIANCE Picture File Format"のページのソースをDirectX用にまとめただけです。
HDR画像フォーマットのデータは、Paul Debevec によるLight Probe Image Gallery のページにあるので、そこから引っ張ってきました。
また、自分で撮影した画像からでも、HDR Shopを使って加工すれば、HDR画像フォーマットのデータを作成することができます(今回のリンクはPaul Debevecだらけだなこりゃ)。

で、いつものようにプログラムです。

プログラムでは、HDR画像フォーマットを読み込んで「Factored BRDF」のプログラムにフレネル付きの環境マップとして追加します。

ソースには、いつものように適当にファイルが入っています。
大事なソースは次のようになります。

hdr.h呼び出し側でインクルードするヘッダ
hdr.cppHDR画像フォーマットのファイルをでコードするソース
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース
hlsl.fxアプリケーションで使うHLSLプログラム

どうせ「hdr.h」と「hdr.cpp」をぱくって使うことが多くなるんでしょう…

■HDR画像フォーマット

さて、HDR画像フォーマットはどのようなものでしょうか?
中身をテキストエディタで除き見ると、次のようになっています。

#?RADIANCE
# Made with 100% pure HDR Shop
FORMAT=32-bit_rle_rgbe
EXPOSURE=          1.0000000000000

-Y 1000 +X 1000
   レ 7\   [\ ZZZ\[[ [^[[[_[[`\[[]`eee]e[^^][[\\`[^[\\ [\ [ \\   ン    レ M   NN N+P NTOPNWNOXRNNTY```S_PUUTONQPYNUNNP NP N NN   ン    レ 7   ャ 活∴ 寫巣氣困来圖ュュュ豫除屁級柏、揣q 燕  a   ン    レ    b  。 b  b   ン    ホ \ ZZZ\\Z[c\c\fjmjlgmloopmsvoopoo

情報のコメントやフォーマットにサイズが続いてその後はごちゃごちゃとわけがわからなくなるように見えます。
HDR画像フォーマットは最初はテキストデータで、途中からバイナリデータに切り替わる特殊なデータです。
その中身は、空白しかない行をはさんで、ヘッダとデータに分かれる構造をしています。

+------------------+
|      ヘッダ      |
+------------------+
|    空白の1行    |
+------------------+
|      データ      |
+------------------+

ヘッダは、「#?」に続いてその種類を表す文字列「RADIANCE」が続きます(といっても、今回は「RADIANCE」しかサポートしません)。
その後で「***=***」の構文をしている行は、データの属性などを記述します(今回は、FORMATだけでさらにいえば32-bit_rle_rgbeしかサポートしません)。

#?RADIANCE
# Made with 100% pure HDR Shop
FORMAT=32-bit_rle_rgbe
EXPOSURE=          1.0000000000000

データ部は、最初に画像の大きさを表すテキスト文が一行あって、あとはバイナリデータになります。
画像の大きさの文のフォーマットは、「-Y M +X N」のようなYが前にあるときは、続くデータがN行のデータがM列ある構造になっていて、「+X N -Y M」の時には、M列のデータがN行あるデータの構造をしていることをあらわしていますが、今回は、「-Y M +X N」しか対応していません。なお、Yの前に-がついているのは、テクスチャのデータが下に伸びていくことを示しています(つまり、普通のテクスチャデータということです)。

-Y 1000 +X 1000
   レ 7\   [\ ZZZ\[[ [^[[[_[[`\[[]`eee]e[^^][[\\`[^[\\ [\ [ \\   ン    レ M   NN N+P NTOPNWNOXRNNTY```S_PUUTONQPYNUNNP NP N NN   ン    レ 7   ャ 活∴ 寫巣氣困来圖ュュュ豫除屁級柏、揣q 燕  a   ン    レ    b  。 b  b   ン    ホ \ ZZZ\\Z[c\c\fjmjlgmloopmsvoopoo

ちなみに、その後に続くバイナリのデータですが、バイナリエディタで開いてよく見ると、特定の文字列で区切られていることがわかります。

0x02 0x02 0x03 0xE8 RR RR RR RR GG GG GG GG GG GG GG GG BB BB BB BB EE EE EE EE EE
0x02 0x02 0x03 0xE8 RR RR RR RR RR RR GG GG GG GG GG GG BB BB BB BB BB BB BB BB EE EE EE
0x02 0x02 0x03 0xE8 RR RR RR RR GG GG GG GG BB BB BB BB EE EE EE EE
0x02 0x02 0x03 0xE8 RR RR RR RR RR RR RR RR RR RR GG GG GG GG GG GG BB BB BB BB BB BB EE EE EE EE

実際のデータでは、これらの列が画像の列数だけあることがわかると思います。
ここで、最初の2バイトの「0x02」はマジックナンバーです。古いバージョンのデータではこの部分が異なっていて、それらと区別をつけるために設けられています。
その後の2バイトは、画像の幅を2バイトで現したものです。この値と先ほどのテキストによる幅の違いからもデータの整合性をつけることができます。 なお、この幅は8〜0x7fffまでの値が許されていて、この大きさを超える(下回る)サイズの画像は、「古いバージョン」の読み方でサポートされます(それは、圧縮効率が悪い方法で格納されます)。
あとは、その列のデータになります。赤、緑、青、指数の4つのデータが連続してまとめられています。
指数は、128を基準として2のべき乗の強さを各色成分に与えます。つまり、この画像フォーマットによる色データは、

赤 = R * pow(2,E-128)
緑 = G * pow(2,E-128)
青 = B * pow(2,E-128)

になります。 ここで、R、G、Bは、画像データから得た色成分で、Eは画像データから得た指数成分です。

このデータは可変長で、ランレングス圧縮されています。 具体的な圧縮方法は、最初に読み込んだデータが128よりも大きければ、その次のデータを最初のデータから128を引いた数だけ続けます。

0x83 0x0a -> 0x0a 0x0a 0x0a

なお、読み込んだデータが128以下ならば、その後のデータを読み込んだデータ数だけそのまま出力します。つまり、圧縮されていないデータがこの後何個続くかを表現します。

0x03 0x0a 0x0b 0x0c -> 0x0a 0x0b 0x0c

あとは、その繰り返しで行数が尽きるまで続けます。

■プログラムのフロー

さて、以上の読み込みをどうやっているかということですが、今回は、下のような関数を定義して実装しました。

hdr.h
0013: namespace HDR{
0014:     //-----------------------------------------------------------------------------
0015:     // HDR ファイルの読み込み
0016:     //-----------------------------------------------------------------------------
0017:     HRESULT CreateTextureFromFile(
0018:         LPDIRECT3DDEVICE9 pDevice,
0019:         LPCTSTR pSrcFile,
0020:         LPCTSTR *pErr,
0021:         LPDIRECT3DTEXTURE9 *pTexture);
0022: 
0023: }// namespace HDR

pDevice はDirectXのデバイスで、pSrcFileは読み込むファイル名です。 pTexture は読み込んだデータを写し取る先のテクスチャで、pErr はエラーが発生したときに、そのエラーメッセージを吐き出します。

関数の中では次のように行動していきます。

ファイルを開く
  ↓
ヘッダを読み込む
  ↓
画像サイズを調べる
  ↓
D3DXCreateTexture でテクスチャを生成する
  ↓
テクスチャをロック
  ↓
ランレングスデータを読み込みながらテクスチャに書き込む
  ↓
テクスチャのロックを開放

具体的なコードの解説は、してもいいけどフォーマットの解説そのままだから省略(それでいいですか?)。

■HDR画像を使う

さて、このテクスチャをどのように使うのかということですが、 ピクセルシェーダ2_0でLDRのピクセルの色に変換することができます。
基本的な色の出力方法は、テクスチャで読み取ったアルファ成分から0.5のオフセットを引いて、2のべき乗して色成分に出力します。
0.5で引くのは、256段階で128の値として定義した指数の基準値のピクセルシェーダで読み込まれたときの値が0.5だからです。

hlsl.fx
0180: // -------------------------------------------------------------
0181: // HDRをLDRに変換するピクセルシェーダプログラム
0182: // -------------------------------------------------------------
0183: float4 HDR_PS(PS_INPUT In) : COLOR
0184: {   
0185:     float4 ldr = (float4)0;
0186:     
0187:     float4 hdr = tex2D( SampEnv, In.Tex );
0188:     
0189:     ldr.rgb = saturate(hdr * pow(2, 255*(hdr.a - 0.5) ));
0190:     
0191:     return ldr;
0192: }

今回のプログラムでは、HDR画像を環境マップで使います。
スフィア環境マップでHDR画像を読み込んで、あとはSchlickの近似によるフレネル項をかけてからLDRに戻して加算合成しました。
ここで、NvやVvは、ビュー座標系での法線ベクトルや視線ベクトルです。ビュー座標系での視線ベクトルは、頂点の位置を考えない近似としてz軸と同一視できるので、その近似を利用して計算量を減らしています(きちんと計算しても、違いはよくわかりませんよ)。

hlsl.fx
0157: float4 PS(VS_OUTPUT In) : COLOR
0158: {   
0159:     float4 v = tex2D( SampP, In.v );
0160:     float4 h = tex2D( SampP, In.h );
0161:     float4 l = tex2D( SampQ, In.l );
0162:     float4 gross = alpha * v * h * l * In.Intensity;
0163:     
0164:     // 環境マップ
0165:     float3 Nv = normalize(In.Nv);
0166:     float3 Vv = float3(0,0,-1);
0167:     float3 r = 2 * dot(Nv, Vv)*Nv - Vv;
0168:     float  m = 0.5 * rsqrt(dot(Nv+Vv,Nv+Vv));// 規格化定数
0169:     float2 env_coord = {m * r.x + 0.5, - m * r.y + 0.5};
0170:     float4 hdr = tex2D( SampEnv, env_coord );
0171:     
0172:     float F = 0.9*pow(1-dot(Nv,r), 4) + 0.1;// 補正されたフレネル項
0173:     
0174:     // HDRをLDRに変換
0175:     float3 ldr = saturate( F * hdr.rgb * pow(2, 255*(hdr.a - 0.5) ));
0176:     
0177:     // 最終的に合成
0178:     return gross + float4(ldr,0);
0179: }

なお、頂点シェーダプログラムは、次のようになります。 「Factored BRDF」のプログラムに、ビュー座標系での法線ベクトルを追加したものになります。

hlsl.fx
0119: // -------------------------------------------------------------
0120: // 頂点シェーダプログラム
0121: // -------------------------------------------------------------
0122: VS_OUTPUT VS(VS_INPUT In)
0123: {   
0124:     VS_OUTPUT Out = (VS_OUTPUT)0;            // 出力データ
0125:     
0126:     // 位置座標
0127:     Out.Pos = mul( In.Pos, mWVP );
0128:     
0129:     // 表面座標系の基底ベクトルを求める
0130:     float3 N = In.Normal;                   // 法線ベクトル
0131:     float3 B = float3(0,1,0);               // 従法線ベクトル
0132:     float3 T = normalize(cross(N,B));       // 接ベクトル
0133:     B = cross(T,N);
0134:     
0135:     // 必要なベクトルを計算する
0136:     float3 V = normalize(EyePos  -In.Pos.xyz);// 視点ベクトル
0137:     float3 L = normalize(LightPos-In.Pos.xyz);// ライトベクトル
0138:     float3 H = normalize(L+V);              // ハーフベクトル
0139: 
0140:     // 各ベクトルをテクスチャ座標へ変換する
0141:     Out.v = Q(V, B,T,N);
0142:     Out.l = Q(L, B,T,N);
0143:     Out.h = Q(H, B,T,N);
0144:     
0145:     // とりあえず、ライトに関して背面の処理を入れる
0146:     Out.Intensity = Out.l.a;
0147: 
0148:     // 位置座標
0149:     Out.Nv = mul( In.Normal, mWV_IT );
0150:     
0151: 
0152:     return Out;
0153: }

なお、アプリケーションプログラムでは、下のような構文でテクスチャを読み込んで、あとは普通のテクスチャと同じように取り扱います。

main.cpp
0189: HRESULT CMyD3DApplication::InitDeviceObjects()
0190: {
0191:     HRESULT hr;
0192:     LPCTSTR pErrMsg;
0193: 
0194:     hr = HDR::CreateTextureFromFile(m_pd3dDevice, "galileo_probe.hdr", &pErrMsg, &m_pEnvMap);
0195:     if(FAILED(hr)){
0196:         MessageBox( NULL, pErrMsg, "ERROR", MB_OK);
0197:     }
以下略

■最後に

はぁ〜、他人のプログラムを読むのは疲れますな。
まだまだ不完全なので、完全なプログラムに修正された方はどうぞください。

あと、ガンマ補正とか露光補正を全然していないので、その点おかしいと思います。





もどる

imagire@nifty.com