740 likes | 974 Views
Practical Implementation of High Dynamic Range Rendering. Masaki Kawase BUNKASHA GAMES BUNKASHA PUBLISHING CO.,LTD http://www.daionet.gr.jp/~masa/ http://www.bunkasha-games.com/. 本日の内容. HDR で何が変わるのか HDR レンダリングのトピック DX8 ハードウェアでの HDR 実装 DX9 ハードウェアでの HDR 実装 グレア生成 露光調整 ゲームでの HDR 表現について
E N D
Practical Implementation of High Dynamic Range Rendering Masaki Kawase BUNKASHA GAMES BUNKASHA PUBLISHING CO.,LTD http://www.daionet.gr.jp/~masa/ http://www.bunkasha-games.com/
本日の内容 • HDRで何が変わるのか • HDRレンダリングのトピック • DX8ハードウェアでのHDR実装 • DX9ハードウェアでのHDR実装 • グレア生成 • 露光調整 • ゲームでのHDR表現について • 参照
HDRで何が変わるのか • 眩しい光の表現 • 眩しい反射光の表現 • フレネル反射 • 正面方向への明るい光源の反射 • 露光調整の演出 • リアルな被写界深度表現 • リアルなモーションブラー HDRのコストパフォーマンス
HDRフレネル 低い反射率での 明るい映り込み
HDR被写界深度処理 将来の実装に向けて
HDRモーションブラー 将来の実装に向けて
HDRレンダリングのトピック • ダイナミックレンジ • HDRバッファレンダリング • グレア生成 • 露光調整 • トーンマッピング
ダイナミックレンジ • 最も大きい値と最も小さい値の比 • ディスプレイアブルイメージ • 28 ローダイナミックレンジ(LDR) • 絶対輝度のフレームバッファ • シーンを絶対的な輝度でレンダリング • 232以上 全ての輝度を絶対的に表現 • 相対輝度のフレームバッファ • シーンを露光でスケーリングしてレンダリング • 215~16以上 輝度の低い部分は重要ではない
HDRバッファレンダリング • フレームバッファ • グレア生成のため • 環境マップ • 反射率が低くても輝度をクランプさせないため • 眩しい反射表現のため • 自己発光テクスチャ • レンダリング後グレア生成のため • デカールテクスチャはHDR不要
フレームバッファのHDR • グレア生成のため • 相対輝度でのレンダリングなら • 理想的には215~16以上 • ゲーム映像では • 212~13(4000~10000)程度で許容できる
環境マップのHDR • 非常に重要 • リアルな鏡面反射表現のため • 眩しい鏡面反射表現のため • 非金属の鏡面反射率 • 正面への反射率は4%以下がほとんど • 明るい光源は4%以下の反射後も充分に眩しい • 1~4%程度の反射後も眩しさを維持したい • ダイナミックレンジで10000~20000以上
DX8ハードウェアでのHDR実装 • 選択の余地は無い • ピクセルシェーダ1.x • 整数演算のみ • HDR用バッファフォーマット • 低精度バッファのみ • αチャネルを輝度情報として利用 • フェイクでそれらしく表現 • 正確な演算は不可能
光沢反射マテリアル フェイクHDRピクセルシェーダ ps_1_1 text0 text1 text2 madr0.rgb, v0, t2, v1 // プライマリ拡散反射カラーを // シャドウ/ライトマップでスケール // その他のライティング結果を加算 +mult0.a, v1.a, t0.a // 鏡面反射率をグロスマップでスケール mulr0.rgb, t0, r0 // 拡散反射カラーをデカールテクスチャで変調 +mulr0.a, t0.a, t1.a // r0.a = 鏡面反射率 * 環境マップ輝度 mult1.rgb, t1, c0 // 環境マップカラーを鏡面反射カラーで変調 +mulr1.a, r0.a, t1.a // 環境マップ輝度がどの程度高いかを示す値 // r1.a = 鏡面反射率 * 環境マップ輝度 * グロスマップ lrpr0.rgb, t0.a, t1, r0 // 鏡面反射率で環境マップを反射 mulr1.a, r1.a, c0.a // 環境マップ輝度がどの程度高いかを示す値 // r1.a = 鏡面反射率 * 環境マップ輝度 * グロスマップ * Clamp(光沢 * 2 , 0, 1) lrpr0.rgb, r1.a, t1, r0 // 出力カラー // 環境マップ輝度がどの程度高いか(r1.a)によって // 環境マップカラーとLDRの計算結果を補間 +lrpr0.a, r1.a, t1.a, r0.a // 出力輝度 // v0.rgb : プライマリライト拡散反射カラー // v1.rgb : その他のライト/環境光カラー // ライティング結果は露光 * 0.5 で // スケーリング済み // // v1.a : 鏡面反射率(フレネル考慮済み) // // t0.rgb : デカールテクスチャ(拡散反射スケール) // t0.a : グロスマップ(鏡面反射率スケール) // t1.rgb : 環境マップカラー // t1.a : 環境マップ輝度 // t2.rgb : シャドウ/ライトマップ // // c0.rgb : 鏡面反射カラー // c0.a : Clamp(光沢 * 2, 0, 1)
自己発光マテリアル フェイクHDRピクセルシェーダ ps_1_1 text0 text1 text2 madr0.rgb, v0, t2, v1 // プライマリ拡散反射カラーを // シャドウ/ライトマップでスケール // その他のライティング結果を加算 +mult0.a, t0.a, c1.a // 自己発光の強さをスケール mulr0.rgb, t0, r0 // 拡散反射カラーをデカールテクスチャで変調 mulr1.rgb, t0, c1 // 自己発光カラーはデカールテクスチャ * 自己発光カラー lrpr0.rgb, t0.a, r1, r0 // 出力カラー // 自己発光を拡散カラーに追加 // 輝度情報が高い程拡散カラーから // 自己発光カラーに近づける +movr0.a, t0.a // 自己発光が高い程輝度情報も高く // v0.rgb : プライマリライト拡散反射カラー // v1.rgb : その他のライト/環境光カラー // ライティング結果は露光 * 0.5 で // スケーリング済み // // t0.rgb : デカールテクスチャ(拡散反射スケール) // t0.a : 自己発光輝度(どの程度強く発光しているか) // t2.rgb : シャドウ/ライトマップ // // c1.rgb : 自己発光カラー(t0.rgb の変調) // c1.a : 発光の強さ(t0.a のスケール) // 露光 * 0.5 でスケーリング済み
ディスプレイイメージの生成 • グレア生成用の高輝度成分抽出 Threshold 0.4~0.5程度のしきい値 • グレア生成 • ディプレイアブルイメージ生成 • フレームバッファから輝度を計算 • 輝度にグレア生成結果を加算
高輝度成分抽出シェーダ // 高輝度成分抽出ピクセルシェーダ // Out.rgb = FrameBuffer.rgb * ( (1 - Threshold) / 16 + FrameBuffer.a ) // t0.rgb : フレームバッファカラー // t0.a : フレームバッファ追加輝度 // // c0.a : 輝度バイアス // (1 - Threshold) / 16 // (0.5~0.6)/16 程度 ps_1_1 text0 // フレームバッファ addr0.a, t0.a, c0.a // r0.a = (1 - Threshold) / 16 + Buffer.a mulr0.rgb, t0, r0.a // r0.rgb = Buffer.rgb * ( (1 - threshold) / 16 + Buffer.a )
グレア生成 • 参照: • Kawase, Masaki. “Frame Buffer Postprocessing Effects in DOUBLE-S.T.E.A.L (Wreckless)”
輝度計算/グレア合成 // トーンマップ/グレア合成ピクセルシェーダ // Out.rgb = (FrameBuffer.rgb + FrameBuffer.rgb * FrameBuffer.a^2 * 16) * 2 + Glare.rgb * Glare.rgb // t0.rgb : フレームバッファカラー // t0.a : フレームバッファ追加輝度 // t1.rgb : グレア生成結果 ps_1_1 text0 // フレームバッファ text1 // グレア mul_x4r0.a, t0.a, t0.a // r0.a = FrameBuffer.a^2 * 4 mul_x4r1.rgb, t0, r0.a // r1.rgb = FrameBuffer.rgb * FrameBuffer.a^2 * 16 add_x2r0.rgb, t0, r1 // (FrameBuffer.rgb + FrameBuffer.rgb * FrameBuffer.a^2 * 16) * 2 madr0.rgb, t1, t1, r0 // グレアを加算
DX8のHDR実装について • 正確な計算は不可能 • フェイクで如何にそれらしく表現するか • 理論よりも見た目で実装
DX9ハードウェアでのHDR実装 • 現状ではさまざまな制限 • 状況に応じて実装を使い分ける • ピクセルシェーダ • ピクセルシェーダ2.0以降 • ピクセルシェーダ1.x • HDR用バッファフォーマット • 高精度整数/浮動小数点バッファ • 低精度整数バッファ
高精度バッファの問題点 • メモリ使用量 • A16B16G16R16 / A16B16G16R16F • 64bpp(Bits Per Pixel) • A8R8G8B8 の2倍 • A32B32G32R32 / A32B32G32R32F • 128bpp • A8R8G8B8 の4倍 • 少なくとも従来のフルカラーのバッファの2倍以上
高精度バッファの問題点 • 機能の制限 • αブレンディングを利用できない • シーンのレンダリングに影響 • 浮動小数点フォーマットではフィルタリングを利用できない • 環境マップ/自己発光テクスチャのクォリティに影響 • サポート状況 • 高精度バッファを利用できない環境もある
高精度バッファの実用性 • 現在のハードウェアでは問題点が多い • 現状での高精度バッファの利用 • A16B16G16R16 / A16B16G16R16F • ハードウェアがサポートしている • メモリに余裕がある • αブレンドを利用できなくても良い • 条件は厳しい…
低精度バッファの利用 • 低精度バッファの利用 • A8R8G8B8 / A2R10G10B10 etc. • メモリ使用量の軽減 • αブレンディング可能 • 演算は不正確だが大きな問題にはならない
トーンマップによる圧縮 • ディスプレイアブルフォーマットでレンダリング • 最終出力映像への劣化が無い • フィルム/網膜の非線形感度を近似 • 充分高いダイナミックレンジを保持できる • 参照: • Reinhard, Erik, Mike Stark, Peter Shirley, and Jim Ferwerda. “Photographic Tone Reproduction for Digital Images” • αチャネルを利用しない • 別の用途に利用可能
低精度バッファフォーマット • A8R8G8B8(8 bits per color channel) • αチャネルを別の用途に利用可能 • 全ての環境で動作可能 • RGB精度に多少の不安あり • A2B10G10R10 A2R10G10B10(10 bits per color channel) • より高いカラーチャネル精度 • αビットが少ない • 他の情報には不向き • サポートされない環境も多い
環境マップフォーマット • 理想的なフォーマット • 10000~20000以上のダイナミックレンジ • サンプリングフィルタを利用可能
環境マップフォーマット • 環境マップの解像度は比較的低い • αチャネルやαブレンドはあまり重要ではない • メモリに余裕があるならfx16(16bit 整数)を利用 • A16B16G16R16 * (256~512)として使用 • エンコード/デコードが高速 • サンプリングフィルタを利用可能 • 将来的には • A16B16G16R16F(fp16)で全てを処理
低精度環境マップのHDR • 低精度環境マップを利用するケース • 高精度整数バッファがサポートされていない • メモリに余裕が無い • フィル性能に余裕が無い場合 • DX8用フェイクHDRと同じフォーマット • フィル性能に充分な余裕がある場合 • トーンマップと同様のカラー圧縮 • αチャネルに指数を格納 • より正確な演算が可能 • αチャネルをスケーラに使うだけでは不充分 • DX8用のフェイクの方がインパクトがある
トーンマップ同様のカラー圧縮 • 環境マップレンダリング時にエンコード Offset輝度カーブを制御する調整値(2~4程度) • Offsetを大きくすると • 高輝度領域の精度が上がる • 低輝度領域の精度が下がる • フレームバッファレンダリング時にデコード • フェッチした環境マップからデコード δ ゼロ除算を避ける小さな調整値
トーンマップ同様のカラー圧縮 • 高輝度領域の解像度が犠牲になる • 広く明るい領域の反射表現の劣化 • 面積の小さい光源ではあまり問題ない
E8R8G8B8 • αチャネルにRGB共通の指数を格納 • 底を1.04~1.08程度に • 底=1.04のダイナミックレンジは約23000(1.04256) offset 64~128程度 • 底をより大きくすると • ダイナミックレンジは高くなる • 分解能は低くなる • マッハバンドが現れる • 環境マップレンダリング時にエンコード • フレームバッファレンダリング時にデコード • フェッチした環境マップからデコード
E8R8G8B8エンコード(HLSL) // a^n = b #define LOG(a, b) ( log((b)) / log((a)) ) #define EXP_BASE (1.06) #define EXP_OFFSET (128.0) // ピクセルシェーダ6命令 // rgb は露光によるスケーリング済み float4 EncodeHDR_RGB_RGBE8(in float3 rgb) { // 共通の指数計算 float fLen = dot(rgb.rgb, 1.0) ; float fExp = LOG(EXP_BASE, fLen) ; float4 ret ; ret.a = (fExp + EXP_OFFSET) / 256 ; ret.rgb = rgb / fLen ; return ret ; } // より正確なエンコード #define EXP_BASE (1.04) #define EXP_OFFSET (64.0) // ピクセルシェーダ13命令 float4 EncodeHDR_RGB_RGBE8(infloat3 rgb) { float4 ret ; // 共通の指数を最も明るいカラーチャネルから計算 float fLen = max(rgb.r, rgb.g) ; fLen = max(fLen, rgb.b) ; float fExp = floor( LOG(EXP_BASE, fLen) ) ; float4 ret ; ret.a = clamp( (fExp + EXP_OFFSET) / 256, 0.0, 1.0 ) ; ret.rgb = rgb / pow(EXP_BASE, ret.a * 256 - EXP_OFFSET) ; return ret ; }
// ピクセルシェーダ5命令 float3 DecodeHDR_RGBE8_RGB(infloat4 rgbe) { float fExp = rgbe.a * 256 - EXP_OFFSET ; float fScaler = pow(EXP_BASE, fExp) ; return (rgbe.rgb * fScaler) ; } エンコード/デコードは部分精度で充分 テクスチャフォーマットの誤差の方が遥かに大きい E8R8G8B8デコード // R16F テクスチャフォーマットを利用可能な場合 // αチャネルからスケールにテクスチャで変換しても良い float3 DecodeHDR_RGBE8_RGB(infloat4 rgbe) { // samp1D_Exp: 256x1 の一次元浮動小数点テクスチャ // pow(EXP_BASE, uCoord * 256 - EXP_OFFSET) float fScaler = tex1D(samp1D_Exp, rgbe.a).r ; return (rgbe.rgb * fScaler) ; }
自己発光テクスチャ • 自ら発光するオブジェクトのテクスチャ • αチャネルをRGB共通のスケールに scaler 16~128程度 • グレア生成に反応する明るさにすると魅力的 • オフラインでエンコード • デコードは高速
光沢反射マテリアル トーンマップ付きレンダリング float4 PS_GlossReflect(PS_INPUT_GlossReflect vIn) : COLOR0 { float4 vDecalMap = tex2D(samp2D_Decal, vIn.tcDecal) ; float3 vLightMap = tex2D(samp2D_LightMap, vIn.tcLightMap) ; float3 vDiffuse = vIn.cPrimaryDiffuse * vLightMap + vIn.cOtherDiffuse ; vDiffuse *= vDecalMap ; // 環境マップの HDR デコード float3 vSpecular = DecodeHDR_RGBE8_RGB( texCUBE(sampCUBE_EnvMap, vIn.tcReflect) ) ; float3 vRoughSpecular = texCUBE(sampCUBE_DullEnvMap, vIn.tcReflect) ; float fReflectance = tex2D( samp2D_Fresnel, vIn.tcFresnel ).a ; fReflectance *= vDecalMap.a ; vSpecular = lerp(vSpecular, vRoughSpecular, fShininess) ; float3 vLum = lerp(vDiffuse, vSpecular, fReflectance) ; // HDR トーンマップエンコード float4 vOut ; vOut.rgb = vLum / (vLum + 1.0) ; vOut.a = 0.0 ; return vOut ; } struct PS_INPUT_GlossReflect { float2 tcDecal : TEXCOORD0 ; float3 tcReflect : TEXCOORD1 ; float2 tcLightMap : TEXCOORD2 ; float2 tcFresnel : TEXCOORD3 ; // 以下は露光スケール済みのライティング結果 // クランプを避けるために TEXCOORD を利用 float3 cPrimaryDiffuse : TEXCOORD6 ; float3 cOtherDiffuse : TEXCOORD7 ; } ;
自己発光マテリアル トーンマップ付きレンダリング float4PS_SelfIllum(PS_INPUT_SelfIllum vIn) : COLOR0 { float4 vDecalMap = tex2D(samp2D_Decal, vIn.tcDecal) ; float3 vLightMap = tex2D(samp2D_LightMap, vIn.tcLightMap) ; float3 vDiffuse = vIn.cPrimaryDiffuse * vLightMap + vIn.cOtherDiffuse ; vDiffuse *= vDecalMap ; // 自己発光テクスチャの HDR デコード // fEmissiveScale : 自己発光輝度 * 露光 float3 vEmissive = vDecalMap.rgb * vDecalMap.a * fEmissiveScale ; // 自己発光を加算 float3 vLum = vDiffuse + vEmissive ; // HDR トーンマップエンコード float4 vOut ; vOut.rgb = vLum / (vLum + 1.0) ; vOut.a = 0.0 ; return vOut ; } struct PS_INPUT_SelfIllum { float2 tcDecal : TEXCOORD0 ; float2 tcLightMap : TEXCOORD2 ; // 以下は露光スケール済みのライティング結果 // クランプを避けるために TEXCOORD を利用 float3 cPrimaryDiffuse : TEXCOORD6 ; float3 cOtherDiffuse : TEXCOORD7 ; } ;
ディスプレイイメージの生成 • グレア生成用の高輝度成分抽出 Threshold 0.5~0.8程度のしきい値 • 正規化のため1- Threshold で除算 • グレア生成 • テクスチャフィルタのため整数バッファで • 理想は浮動小数点バッファ • ディプレイアブルイメージ生成 • フレームバッファにグレアを加算
struct PS_INPUT_Display { float2 tcFrameBuffer : TEXCOORD0 ; float2 tcGlare : TEXCOORD1 ; } ; float4PS_Display(PS_INPUT_Display vIn) : COLOR0 { float3 vFrameBuffer = tex2D(samp2D_FrameBuffer, vIn.tcFrameBuffer) ; float3 vGlare = tex2D(samp2D_Glare, vIn.tcGlare) ; // グレアを加算 float4 vOut ; vOut.rgb = vFrameBuffer + vGlare * vGlare ; vOut.a = 0.0 ; return vOut ; } グレア合成
DX9のHDR実装について • 高精度バッファを利用 • メモリ使用量が多い • ブレンディングを行えない • 近い将来には実用的に… • 低精度バッファを利用 • ピクセルシェーダ負荷が高い • DX8同様のフェイク処理も検討すること • パフォーマンスが高い • メモリ使用量が少ない • 効果が大きい
グレア生成 • グレア生成の注意点 • 複数のガウスフィルタの合成によるブルーム • イメージプロセスとスプライトによるグレア表現
グレア生成の注意点 • かなりのマルチパスフィルタ • 整数バッファの精度に注意 • 値のクランプ • 丸め誤差 • fx16でも充分では無い • 常に値の分解能と範囲に注意すること
複数のガウスフィルタの合成 • ブルーム生成 • 単独のガウスフィルタではあまり良い結果を得られない • フィルタ半径が小さい • にも関わらず光源中央付近の鋭さは足らない • 複数のガウスフィルタの合成 • 半径の異なる複数のガウスフィルタを合成 • より広く、なおかつ鋭いグレアを表現できる
複数のガウスフィルタの合成 元のイメージ
複数のガウスフィルタの合成 • フィルタ半径が大きいと負荷が高い • 縮小バッファの活用 • 半径の大きなフィルタほどローパス • 低い解像度でガウスブラーを適用してからバイリニアフィルタで拡大しても誤差は少ない(目立たない) • フィルタ半径ではなく画像解像度を変える • 1/4 x 1/4 (負荷1/16) • 1/8 x 1/8 (負荷1/64) • 1/16 x 1/16 (負荷1/256) • 1/32 x 1/32 (負荷1/1024) • … • 数百ピクセル四方の巨大なフィルタカーネルも高速に適用可能
縮小バッファのガウスフィルタ 1/4 x 1/4 (256x192 pixels) 1/8 x 1/8 (128x96 pixels) 1/16 x 1/16 (64x48 pixels) 1/32 x 1/32 (32x48 pixels) 1/64 x 1/64 (16x12 pixels)