ビデオゲームでLUT(ルックアップテーブル)を使う方法
(blog.frost.kiwi)- LUT(ルックアップテーブル) は、事前計算した値を表のように参照する方式で、モノクロ熱画像のカラー化からゲームオブジェクトのバリエーション生成、映像のカラーグレーディングまで幅広く使われる
- WebGLの例では、ビデオフレームをGPUに 2Dテクスチャ として載せ、ピクセルごとの色を決めるFragment Shaderで tinting、1D LUT、3D LUT を適用する流れを示す
- 1D LUT は輝度値
[0.0 - 1.0]を色配列の座標として使い、モノクロ入力をRGBに変換し、GPUの双線形フィルタリングによって小さなLUTでも中間値を自然に補間する - Valveの Left 4 Dead の事例のように、tintingとLUTは限られたテクスチャメモリ内で車、ゾンビの肌、衣装の色バリエーションを作るのに活用される
- LUTは追加のテクスチャ読み出しコストを生むが、ガンマ補正のような高コスト計算を事前保存したり、外部のカラーグレーディングワークフローをリアルタイムグラフィックスに持ち込んだりできる実用的な道具でもある
LUTがグラフィックスプログラミングで果たす役割
- ルックアップテーブル(LUT) は、値をあらかじめ計算して行や表に保存し、必要なときに参照する方式
- グラフィックスプログラミングでは、この単純な構造がさまざまな視覚効果につながる
- モノクロ映像をカラー化する
- 色補正やスタイリングを適用する
- ビデオゲームでオブジェクトのバリエーションを効率よく作る
- 例では WebGL を使っているが、同じ手法は DirectX、OpenGL、Vulkan、Unity、科学データ可視化にも応用できる
- デモ映像は Panasonic GH6 と TESTO 890 熱画像カメラでキャプチャしたもので、処理は機器の外へ出さず GPUでローカル処理 される
WebGLパイプラインと tinting
- 熱画像カメラの出力は モノクロ動画 で、各フレームはWebGLを通じてGPUに2Dテクスチャとして渡される
- Fragment Shader は、出力の各 fragment に対して最終色を決めるコード
- 例では
texture2D(video, tex).rgbで現在位置のビデオ色を読み取る - WebGL 1.0 の最終出力は
gl_FragColorに RGBA として書き込まれる
- 例では
- tinting は、テクスチャ色に特定の色を掛けて色味を乗せる方式
- 例では
videoColor * vec3(1.0, 0.5, 0.0)でオレンジ色を掛ける - 白はオレンジになり、黒は 0 を掛けるので黒のままになる
- 例では
- この文脈では、tinting の乗算コストは測定しにくいことがある
- テクスチャをメモリから取得するコストは乗算よりはるかに大きい
- 複数スレッドが並列実行されるため、あるピクセルのテクスチャ読み出し待ちの間に別のピクセルの乗算が実行されることがある
- これは tinting が生む差の説明であり、全体の性能最適化が完了したという意味ではない
Valveによる tinting の活用
- Valve Software の Left 4 Dead は、車のバリエーションを作るために tinting を活用している
- 車種や色ごとに別テクスチャを持つ方式では、テクスチャメモリ予算を超えてしまう可能性がある
- Valve は追加の mask テクスチャ を使い、車の塗装領域をインスタンスごとに異なる色で tinting している
- 1つの色テクスチャを3つの車モデルで共有する
- 2枚分のテクスチャコストで、4つの車モデルと実質無制限の色バリエーションを作れる
- Source Engine では、車だけでなく本質的にはあらゆるものを tinting できる
1D LUTでモノクロ値を色に変える
- 1D LUT は数値配列であり、RGB画像であれば色の1次元配列になる
- モノクロ映像の輝度値をLUTのX軸座標として使い、色を取り出す
- ビデオの輝度値は
[0.0 - 1.0]の範囲を持つ 0.0の黒はLUT左端の色に、1.0の白はLUT右端の色にマッピングされる- 中間値は対応位置の色にマッピングされる
- ビデオの輝度値は
- WebGL 1.0 には1Dテクスチャがないため、高さ1pxの 2Dテクスチャ を使う
- 例のコードは
texture2D(lut, vec2(videoColor, 0.5))の形 - 入力は1D値で出力はRGBなので、“1D vector in, 3D vector out” の構造になる
- 例のコードは
- GPUの 双線形フィルタリング は、テクスチャ読み出し時に中間値を自動補間する
- 8ビットのモノクロ映像は256段階の輝度を持つ
- 1D LUT が32ピクセル幅でも、ピクセル間アクセスは線形補間される
- 8ピクセル分の情報だけでも、256ピクセル幅の多彩なグラデーションに近い結果を作れる
カラーマップの選択と知覚的均一性
- 例には matplotlib がサポートするカラーマップを1D LUTとして書き出した一覧が含まれる
- データに特定の構造がないなら、知覚的に均一なカラーマップ を使うか、それを基に色を設定するのが適している
- viridis 系列が代表例
- 知覚的に均一なカラーマップは、モノクロ出力にしても冷たい部分は暗く、熱い部分は明るいまま保たれる
- jet のように主に色相だけを変え、知覚的な明るさを無視するカラーマップでは、この性質は保証されない
- 色覚特性の異なる人でもデータを正しく解釈できることは、カラーマップ選択の重要な基準
1D LUTの性能と多項式近似
- 小さな1D LUTのコストは概して低い可能性があるが、tinting より考慮点は多い
- 主な性能上の懸念は 依存テクスチャ読み出し
- 1回目のテクスチャ読み出し結果で、2回目のテクスチャ読み出し座標を決める
- グラフィックスプログラミングでは、最適化可能な経路の一部を失わせることがあるパターン
- GPUにはテクスチャキャッシュがあり、小さなLUTはそこに収まるため、LUT読み出しコストが非常に低くなることもある
- 詳細な性能判断にはプラットフォームごとの解析ツールが必要
- Nvidia NSight はシェーダ段階ごとの性能解析を提供するが、OpenGL はサポートしない
- カラーマップを 多項式近似 で実装すれば、LUTテクスチャを扱わずに済む
- Matt Zucker の ShaderToy にある viridis 近似は Horner’s method の形で書かれている
c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))))という multiply-add 演算で色を計算する- 実際にLUTより速いかどうかは、プラットフォームごとの解析なしでは判断しにくい
Left 4 Dead 2 のゾンビバリエーション
- Left 4 Dead 2 は、単純な tinting だけでは不十分な肌や衣装の色バリエーションにLUTを活用している
- 単純な tinting では、色の輝度バリエーションを十分に提供できないことがある
- 肌と衣装にはいくつかのLUTをランダムに選び、多様な色バリエーションを作る
- アーティストが適切な color ramp を選び、最終ゲームに含める
- 1枚のテクスチャから明るいスーツと暗いスーツの両方のバリエーションを作れる
- 関連する完全版プレゼンは GDC の Shading a Bigger Better Sequel で確認できる
- “Exclusive Masking” は、1つのチャンネル内に2つのテクスチャ範囲を入れる方式
- Texture 1 は 0-128 の範囲
- Texture 2 は 128-256 の範囲
- 代償は色精度の低下
計算を事前保存する1D LUT
- 1D LUTは、グラフィックスプログラミングで 高コスト計算のキャッシュ としても使える
- Gamma correction は代表的な例
- 近似でないなら
pow()関数が必要 - 特に古いGPUでは
pow()が高コスト命令になりうる - 標準 sRGB の区分的曲線を使うと分岐経路も増える
- 近似でないなら
- ガンマ変換を1D LUTとして事前計算すれば、ピクセルごとの計算を省ける
- この場合は “1D vector in, 1D vector out” の構造
- 1D LUT は最大4つの色チャネルを出力できる
- Redshift は、夜間にモニターをオレンジ寄りに tinting して目の疲れを抑えるソフトウェアの例
- モニターの Red、Green、Blue 各チャネルの Gamma Ramp を変更する
- Kelvin Warmth → RGB と追加の Gamma 計算を3つの1D LUTに事前計算する
- 関連実装は Redshift のソースコード にある
- この方式では再マッピングがグラフィックカードではなくモニター側で行われるため性能への影響はないが、ハードウェアインターフェースのサポートは現在では不安定だったり壊れていたりする場合が多い
- Raspberry Pi のグラフィックススタックは、新しい更新で関連サポートを失った例がある
- Microsoft は SetDeviceGammaRamp の使用に注意を促している
3D LUT: RGB空間全体を再マッピングする
- 3D LUT は、RGB空間全体を1つのキューブとして表現し、あらゆる色を再マッピングする
- 入力RGBの3次元ベクトルを出力RGBの3次元ベクトルに変えるので、“3D vector in, 3D vector out” の構造になる
- Red、Green、Blue それぞれに1D LUTを適用すれば、色バランスは変更できる
- 3D LUT は、入力RGBの組み合わせを必要とする変換に必要
- 彩度変更
- 色相変更
- 特定色の調整
- 色分離
- 3D LUT は通常キューブ形状だが、ゲームでは strip や square 形状で保存され、映像編集ツールでは “Iridas/Adobe”
.cubeファイルとして使われる
WebGL 1.0で3D LUTを実装する
- 例の映像は Panasonic GH6 で撮影した Panasonic V-Log カラープロファイル映像
- V-Log は、より広いダイナミックレンジを保持し、Gamut と Gamma の定義が明確なログプロファイル
- WebGL 1.0 は3Dテクスチャをサポートしないため、3Dテクスチャ読み出しを2回の2Dテクスチャ読み出しと補間で実装する
- この手法は Gregg Tavares の Google I/O 2011 発表 と webglfundamentals.org の解説 に基づく
- 既存のサンプルコードにはキューブのZ軸計算の問題で色が青寄りになるミスがあり、2019年に 修正 された
- WebGL 1.0、OpenGLES 2、OpenGL 2.1 で
OES_texture_3Dなしに互換性を保つには、最新バージョンの3Dテクスチャサンプリングコードを使う必要がある
3D LUTでカラーグレーディングのワークフローを持ち込む
- 3D LUTに適用した補正は、そのLUTを通る映像やグラフィックスシーンにそのまま適用される
- 例では V-Log 映像とLUTを DaVinci Resolve に取り込んで処理する
- Panasonic の V-Log to V-709 3D-LUT を適用する
- コントラストと white point を補正し、白が full-bright になるようにする
- 補正したLUTを再エクスポートする
- 実際のビデオは元のままで、カラーグレーディングは WebGL でLUTを通してリアルタイム適用される
- アップロードするLUTは同じサイズを維持する必要がある
- 例の制約は 1024px × 32px
- 32³px のキューブ形状を維持しなければならない
ゲーム業界における3D LUTの使い方
- 3D LUTでゲームの色を外部ツールでスタイリングする方法は、ビデオゲーム業界でよく知られた標準ワークフロー
- 一般的な手順は次の通り
- カラーグレーディングしたいシーンのスクリーンショットを撮る
- スクリーンショットと初期化された3D LUTを Photoshop のようなツールで一緒に開く
- スクリーンショットとLUTに同じカラーグレーディングを適用する
- 3D LUT部分を切り出して書き出す
- Left 4 Dead も同じ方法を使っている
- Photoshop の色補正ツールは自由に使えるが、複数ピクセル間の関係を変えるフィルタは使えない
- blur、sharpen、emboss のような convolution 系は remapped color をぼかしてしまい、予期しない結果を招くことがある
複雑な色補正を1つのLUTに焼き込む
- DaVinci Resolve のようなカラーグレーディングツールは、複雑な色変換や color grade を作成して3D LUTとして書き出せる
- 例では、フィルムプリントルックを目指した Kodak 2383 LUT を適用する
- V-Log gamma 映像を Cineon Film Log に変換する
- 色空間を Rec.709 に変換する
- フィルムエミュレーションを適用し、gamma を再び Rec.709 に変換する
- 最後に white point を調整する
- 複雑な細部変換を自前で実装せず、1つのLUTに焼き込んでグラフィックスアプリケーションへ持ち込める
- 例の映像は記事互換性のため8ビット高圧縮形式だが、通常は10ビット footage に対して行われる作業
- LUTサイズは思ったより小さくて済む
- Panasonic 公式の V-Log to V-709 3D-LUT は 33³px サイズ
- Panasonic のカメラ内モニタリングLUTは 17³px を使う
- Panasonic の数千ドル帯シネマカメラでも 17³px が使われている
グラフィックス以外でのLUTの例
- LUTはグラフィックスプログラミング以外でも使われる
- Tomb Raider を Game Boy Advance に移植した事例でもLUTが活用されている
- オープンソースの OpenLara エンジン移植版は、Game Boy Advance に3D機能がないため、多くの部分をソフトウェアで処理している
- その1つが 整数除算を実装するLUT
- このLUTは load instruction を省くため、ROMアドレス空間の先頭に配置されている
1件のコメント
Hacker Newsのコメント
ルックアップテーブルがなければ、この効果はそもそも実装できなかったと思う: https://twitter.com/zeta0134/status/1756988843851383181
ポイントはルックアップテーブルを2つ使っていること。大きい方のテーブルには、プレイヤー周辺の設定可能な半径内にある円形照明の情報を保存していて、半径ごとにテーブルが丸ごと1つある。2つ目のテーブルには、背景行の疑似乱数順が入っている。松明の照明ルーチンが呼ばれるたびに、画面の1/20しか更新する時間がないが、順序を少しランダム化することで縁が滑らかに見え、本来見えてしまうラスタースキャンを隠せる。この順序は、更新から取り残される行が出ないようにするための grab bag であり、リアルタイム計算では遅すぎるのでテーブルを使っている
初めて本当に感銘を受けたルックアップテーブル効果は、テクスチャトンネルを作るのに使う方法だった
画面の各ピクセルについて、画面中心からの角度と距離が分かるルックアップテーブルを用意し、それを使って各ピクセル位置にどのテクセルを置くかを選ぶ。3Dジオメトリのトンネル内を移動しているように見えるが、コストが非常に低いため pico でも可能: https://www.lexaloffle.com/bbs/?pid=63818
最初は Stardust がこの効果を使っていたのだろうと思っていたが、今調べたら実際には背景で繰り返される6フレームアニメーションを再生していただけだった: https://codetapper.com/amiga/sprite-tricks/stardust/
https://web.engr.oregonstate.edu/~mjb/cs557/Projects/Papers/Parallax_Occlusion_Mapping.pdf
Wind Wakerが独特の見た目を作るために複数の LUT を使う方法についての動画がある。BoTW や ToTK も同じ手法を使っている
https://www.youtube.com/watch?v=mnxs6CR6Zrk
記事の作者です。質問があれば答えられる
ただ、LUT を使ったカラーグレーディングの紹介のしかたは少し意外だった。記事では L4D2 が使った気の利いたニッチな解法のように見えるが、実際にはずっと以前から業界標準で、私が関わったすべてのゲームで使われていた。AAAゲームの NFS (2015) から Lost in Random のような大規模インディー作品まで、どれもそうだった
[1] https://www.copetti.org/writings/consoles/game-boy-advance/#operating-system
“Here is every single colormap that matlibplot supports,...”
退屈なビジネスプロセスの領域でも、これは驚くほど有用だ
条件分岐だらけのコードベースを、きれいなルックアップテーブルで単純化できることがよくある。単純に見えるものでも、ルックアップテーブルが5万行になることがあるので、いつも発想に上らないのだと思う。エンドユーザーには多く見えるかもしれないが、コンピュータはあまり気にしない。しかもルックアップテーブルは反復的なので、たいてい管理しやすく、そうでない場合でもなお価値がある。コード変更なしでエンドユーザーが設定することもできる。全体として、ビジネスロジックをコーディングするときに繰り返し現れるさまざまなシナリオで特に有用な概念だ
レトロゲームではテーブルが本当によく使われていた。当時はメモリ速度が非常に速く、プロセッサは遅かったので、できるだけ多くの計算をテーブルに押し込むのが合理的だった。より賢くやるほど、より派手なゲームを作れた
あのチャンネルが好きだ。自分のModをより速く動かすためにMario 64のコード全体に手を入れ、安定した60FPSにしていた [0]
[0] https://www.youtube.com/watch?v=t_rzYnXEQlE
LUTを多く扱うなら、高度な色彩科学と変換作業を扱うMacアプリを作っている: https://videovillage.com/lattice
以前、NESホームブリューのシーンを少し掘っていたのだが、Boblというゲームで、そのシステムが計算できる水準をはるかに超える驚くべき物理効果を見た。調べてみると参照テーブルで、単純な道具で非常に複雑な過程のように見せられるのだと気づかされた
https://morphcatgames.itch.io/bobl
A Podcast Of Unnecessary Detailで、SNES版Doomの移植と、LUTを三角関数に使った方法を扱うエピソードが公開されていた。文脈としては、SNESにはグラフィックプロセッサがなかったという話だ
https://festivalofthespokennerd.com/podcast/series-3-episode-4/
https://github.com/RandalLinden/DOOM-FX
背景の回転やスケーリングも可能だった。Mario Kartを思い浮かべればよいMode 7がそれで、多くのゲームで見られるハードウェア透過もサポートしていた。さらにDoomやStarfoxでは、カートリッジに3D計算を処理するSuper FXチップが載っていた。つまりその特定タイトルでは、実質的にグラフィックプロセッサを2つ扱っていたようなものだ
SNESハードウェアを見事かつ痛いほど詳細に扱ったRetro Game Mechanics Explainedシリーズがある:
https://www.youtube.com/watch?v=57ibhDU2SAI&list=PLHQ0utQyFw5KCcj1ljIhExH_lvGwfn6GV&index=1
思いつく限りで自分が書いたLUTの用途は、大気散乱、スプライトの色付け、暗視照準器、FLIR照準器、白黒の「ビデオフィード」効果、グリッチ効果、高さマップのシェーディング、宇宙船の排気プルームのアルファ点係数、ウェブサイト訪問者のマウス滞留ヒートマップ、結晶質の効果、そして最後に生の色空間での後処理カラー化だった
LUTは、すでに分かっている値の配列を可視化したもので、驚くほど便利だ
http://www.effectgames.com/demos/canvascycle/
もともとはXboxがこれをあまりうまく扱えず、Photoshopも対応していなかったので人気がしぼんだ。だが最新のハードウェアなら、AESpriteで作ったパレットスワップを十分処理できる