土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

DXTnm形式とはなにか。あるいは、法線マップが青紫色な理由

「Unity #2 Advent Calendar 2017(https://qiita.com/advent-calendar/2017/unity2)」7日目になります。6日目はTeachさんによる「画像の色相を変更する.(https://qiita.com/Teach/items/52883127033885a95e82)」
でした。

自己紹介(飛ばしてOK)

 ちょっとだけ自己紹介しておきますと、土屋つかさはラノベ作家兼ゲームプランナー兼プログラマーです。元々は社内Webシステム開発系のSEでして、スクウェアエニックスにプランナー職で転職して数年経たのち専業のラノベ作家になりました。現在はゲーム会社さんに出向してプランナー/ライター/Unityプログラマーを兼ねたなんでも屋をやりつつ、別途文章のお仕事もさせていただいています。

 近年ではソシャゲの仕事が多いため、名前の出ている作品が減っているのですが、直近で一番大きな仕事は「シュタインズゲート・ゼロ」の企画協力/サブルートシナリオ執筆になります。アニメ化もすると発表されているので、よろしければ是非どうぞ。

 GitHubでRubyで動くゲームエンジンである「司エンジン(https://github.com/t-tutiya/tsukasa)」を開発してます。司エンジンは自己書き変え型ステートマシンを特徴としたゲームフレームワークライブラリ(と、その上で動作するオリジナルスクリプト言語)です。現在はこれのUnity移植を進めています。

 さて、今回はUnityにおけるシェーダーの話をして、そのまま土屋が作った同人誌の販売サイトに誘導するつもりだったのですが(!)、別途書きたいネタが出てきたので内容を変えました。同人誌については記事の末端にリンクを張っておきますんで、よろしければそちらもどうぞ。あ、表紙だけ先に見せておきます!

3Dリアルタイムレンダリング未経験者向け記事

 今回の記事はひたすら法線マップのフォーマットについて書いてまして、3Dリアルタイムレンダリング未経験の方にはさっぱり面白くないかもしれません。その場合は、もしよろしければこちらの記事もどうぞです。「GPU性能は上がり続けているものの、現実世界の光のシミュレーションはまだ全然できていない」という話です。

モダン3Dゲームグラフィックプログラミングについて色々誤解していたという話
d.hatena.ne.jp

謎のキーワード「DXTnm」

 法線マップ(Normal Map)は、テクスチャマップの中でもベースマップ(通常のテクスチャ画像)の次か、次の次くらいにポピュラーに使われているテクスチャマップです。「全体的に青紫っぽい画像」と言えば、わかる方も多いかと思います(※1)。

 法線マップは主にテクスチャの疑似的な凹凸をつけるために使用されています(※2)。Unityエディタで法線マップを選択すると、ビューに以下のように表示されます。

 「DXTnm」という文字列が書いてあるのがわかると思います。これ、一体なんなのでしょうか? これが、今回の記事のテーマです。これから皆さんと、「DXTnm」というアルファベット5文字が意味する所を理解するまでの、短くて長い旅に出かけましょう。

※1 ゲーム開発の現場では、「法線マップ」よりも、原語の読みである「ノーマルマップ」が使われる事が多い印象があります。けれど、土屋は「ノーマル」に「普通」の意味が強く含まれている為に「ノーマルマップ」と呼ぶのに違和感があり、「法線マップ」と呼んでいます。

※2 実際の開発では、凹凸表現よりは髪の艶を示すなどの目的で、モデルのエッジ近辺の法線を弄る用途の方が多いかもしれません。ここでは分かりやすさの為にほぼバンプマップと同じ用途としての法線マップを想定しています。

「法線マップのデータ構成」と「青紫色」

 法線マップは各テクセル(ピクセルと同じように、テクスチャの最小構成単位を示す用語です。ほぼピクセルと思って大丈夫)に、その位置の法線ベクトル(※1)の情報が格納されています。この値は、単位ベクトルのxyz成分として保存されています。

 テクセルはRGBA(xyzwとも書きます。内部的には同じ)各8bitの計32bit情報を持つことが可能で、RGBにxyzが格納されます(Aは使用しません)。また各要素は[0.0〜1.0]の値を格納できます。しかし、法線のxyzは[-1.0〜1.0]の範囲を取るので、そのままでは格納できません。そこで、元の値に1を足して0.5を掛けます。これによって値が[0.0〜1.0]の範囲に収まり格納できます。実際に使用する際には2倍して1を引けば元の値に戻ります(※2)。

 シェーダープログラミングでは、あるポリゴン表面に対して、そこに届く光源と、カメラとの角度を見て、それによって色を変化させます。この時、法線マップに値が設定されていると、その分だけ角度を補正して、結果として色が変わるので、擬似的に凹凸を表現できるわけです。

 余談ですが、なぜ法線マップが青紫色なのかの答えが、上記の話に含まれています。

 多くの場合において、凹凸を表現したいのはテクスチャ全体の一部であり、大部分は通常の法線ベクトル(=ポリゴン表面に対して垂直)を使うことになります。このような通常の法線ベクトルをxyz成分で表わすと[0,0,1]になります(法線空間ではZ方向が上)。法線マップとして保存する時には1を足して0.5を掛けるので、[0.5, 0.5, 1]になります。

 ここが重要なのですが、この[0.5, 0.5, 1]という値は、RGB値で言うと#8080FFにあたります。つまり、こんな色です。

 法線マップが青紫色に近いのは、[0.5, 0.5, 1]が基準になっているからなのでした。

※1 「法線ベクトル」と言ったら本来は常に接面に垂直なベクトルのことを指すだろうから、より正確には「法線ベクトルの補正方向を示すベクトル」なのかな?(よく知らない。詳しい方がいたら補足してもらえるとありがたいです)

※2 実際には精度が半分になっていますが、この後も出てくるようにことシェーダープログラミングにおいてはこのような処理が頻出します。近代ではより高い精度のデータが必要になっていることもあり、それに応じてハードウェアが拡張されている印象があります。

GPUメモリサイズ事情と圧縮

 さて、ゲームでは沢山のテクスチャマップを使用します。また、最近のゲームではベースマップ、法線マップ、反射マップなど、一つのマテリアルを表現する為に、テクスチャマップが何枚も必要になっています。これらのテクスチャは、演算速度を稼ぐ為に、基本的には全てGPUメモリに載っている必要があります。

 GPUメモリは近年こそギガ単位で搭載されていますが、テクスチャが高精細、高積載になればその分圧迫されるので、容量は常に足りません。そのためGPUメモリにアップするテクスチャは、出来る限り圧縮しておく事になります。個々のテクスチャが圧縮されてメモリサイズが小さくなれば、その分沢山のテクスチャをGPUメモリにアップできるためです。

 GPUメモリ上でのサイズを小さく保つ事が求められているので、当然そのテクスチャは圧縮されたままの状態でGPUメモリに配置されます。そのため、このテクスチャの解凍処理は、GPUのハードウェア処理によって行われることになります。そのため、圧縮方式はあらかじめ業界で決められている規格に準じています。

DXTCとDXT5

 DirectXでは、DXTC(Direct X Texture Compression)という規格で、テクスチャの圧縮形式を定義しています(※1)。DirectX9以降に対応しているGPUは、DXTC形式のテクスチャをハードウェア処理で解凍し、シェーダーコードからは無圧縮のテクスチャと区別せず透過的にアクセスできるようになっています。

 余談ですが、DXTCという呼び方はDirectX10から無くなりまして、現在はBCnという形式で呼ばれています(BCはブロック圧縮(Block Compression)の意味)。後述するDXT5はBC3に相当します。Unityはプラットフォーム互換性からDirectX9形式を基準にしていることもあって、今もDXTCでの呼び方なのかなと勝手に思っています。

 さて、DXTCには、用途や圧縮度合いに応じてDXT1からDXT5までの5種類があります。このうち、法線マップに使用されるのはDXT5なので、ここではDXT5の圧縮方法について概要を解説します(※2)。

 DXT5では、RGBA32bitのうち、RGBの24bitと、Aの8bitをそれぞれ分けて圧縮します。どちらも共通しているのは、テクスチャを4ドットx4ドットのブロックを単位に圧縮するという点、そして、色として保存するのは各ブロックでピックアップした基準色2つのみという点です。解凍時にはこの2つの基準色から他の色を機械的な補完式で生成します。

 RGB側では、基準色(元の値をR5G6B5の16ビットに変換して保存されています)から補間色2つを生成し、4つの色を使用します。ブロックの各要素は2ビットのインデックスを持ち、4つの色のいずれかを指定します。通常、24bitカラーが16ドット分ある場合、色を格納するのに384bitが必要なのですが、この方式であれば、インデックスの32bitと、基準色2つの32bitの計64bitで済み、全体で6分の1になります。

 A側は、基本はRGBと同じですが、基準色が8bitで保存され、インデックスに3bit用いる点が異なっています。この為、補間色は6個作られます。こちらは128bitが64bitになり、2分の1になります。RGB側とA側を合わせると、全体では512bit分の情報を128bitで表現表現しており、4分の1の圧縮が実現されています。

※1 いわゆるDDS(DirectDrawSurface)拡張子のファイルは、DXTC形式で圧縮されたテクスチャです

※2 ここではいくつかの説明を端折っているので、正確な解説ではありません。ちなみに、UnityのベースマップテクスチャはDXT1形式で圧縮されます

2つの値とDXT5nm

 法線マップもテクスチャなので、DXTC形式で圧縮しておき、GPUメモリ上にコピーされます。先述したように法線マップはDXT5形式で圧縮されます。なぜDXT5なのかと言うと、DXT5は2つの値のみを圧縮する場合において、RGB側とA側に1つずつ値を設定することで、圧縮時の品質を最大限に上げられるためです。この「2つの値」というのがポイントです。

 法線マップの各ドットにはXYZの3つの値が必要なのですが、格納するベクトル自体は単位ベクトルなので、XYの値が分かっていれば、Zは逆算で求められるのです(z = sqrt(1 - (x * x + y * y)))。そこで、xの値をA(xyzwで言えばw)に、yの値をG(xyzwで言えばy。Gには6ビット使われるので、R/Bよりも精度が高い)に、それぞれコピーします(RとBはyと同じ値が5bitで設定されます)。

 これによって、DXTC方式をそのまま用いて(つまり、GPU側で特殊な処理を実装することなく)法線マップに必要な二つの値を高精度で圧縮できました。このように、法線マップのxy成分をwy成分に設定してDXT5形式で圧縮したデータのことを、DXT5の特殊な形式として、"DXT5n"あるいは"DXT5nm"と呼んでいます(nmはNormal Map)。これが、冒頭で見た「DXTnm」の正体だったわけです(長い道のりでしたね!)。

 シェーダーコード上で法線マップの値を参照する際は、既に展開は終わっているので、無圧縮のテクスチャと区別なく、透過的に[0.0〜1.0]の範囲の値を取得できます。あとはこの値を2倍して1を引けば、必要な[-1.0〜1.0]の範囲の値を再現できたことになります。

マルチプラットフォーム開発とUnpackNormalメソッド

 「wzを2倍して1を引いてxyに設定し、xyの値からzの値を算出する」という作業は定型処理なので、UnityではUnpackNormalというヘルパーメソッドが用意されています。モバイルなどのプラットフォームのよっては、DXT5nm形式の圧縮展開に対応していない場合があり(今時のモバイルで対応してないってことはなさそうな気もしますが)、UnpackNormalメソッドではこのようなプラットフォーム間の差異も吸収してくれます。

 以下にUnpackNormalメソッドの実装を示します。このメソッドはUnityCG.cginc内で定義されています。

inline fixed3 UnpackNormal(fixed4 packednormal)
{
//DXT5nm非対応の場合
#if defined(UNITY_NO_DXT5nm)
    //無圧縮テクスチャとみなして値だけ変換
    return packednormal.xyz * 2 - 1;
#else
    //DXT5nm対応
    return UnpackNormalDXT5nm(packednormal);
#endif
}

inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
    fixed3 normal;
    //wyの値を[-1〜1]の範囲に変換
    normal.xy = packednormal.wy * 2 - 1;
    //xy成分からzの値を算出
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

 UnpackNormalメソッドでは法線マップからの値を受け取り、プラットフォームに応じてDXT5nm形式あるいは非圧縮のテクスチャであるとみなして法線ベクトルのxyz成分を返します。これによって、ユーザーはプラットフォームを意識せずに法線マップの情報を扱えるようになっています。

 以上です。お疲れ様でした!

終わりに

 いかがでしたでしょうか? ビューアーに書かれた「DXTnm」の文字列から、思えば遠くに来た物ですね。

 ここまで読んだ方にはおわかりかと思いますが、今回の話の中でUnityに固有の話は3%くらいしかありません(ホントゴメンナサイ)。しかし、シェーダーコードを読むためにはUnity固有ではない(=Unityのドキュメントには記載されていない)知識が大量に必要になるという事が、おわかりいただけたのではないかと思います。

 シェーダープログラミングでは、通常のUnityプログラミング(あるいは通常のゲームプログラミング)には出てこない用語やフォーマット、それに歴史的経緯から名前や方式が統一されていなかったり途中で変わってしまっている概念などが度々登場します。その上、日本語で読める資料が少なく、日本語圏のエンジニアが学習するのはなかなか面倒な分野だと思います。

 けれど、シェーダープログラミングは文字通りGPUの挙動を隅々まで制御できる技術であり、習得するほどに自身の表現力を向上できて、かつ、それを視覚的に認識できるという、プログラムの中でも面白い分野でもあると思います。これまでシェーダープログラミングを避けて通ってきた方も、これを機会にはじめてみてはいかがでしょうか。

それではここからは宣伝のコーナー!

 さあ読者の皆さんがシェーダープログラミングに興味を持ち始めたところで(ホントに?)、そんな皆さんに最適な同人誌の紹介です!(我ながら下衆いな……)
 土屋が執筆した同人誌「Unityシェーダープログラミングの教科書 ShaderLab言語解説編」のPDF版が、オンラインショップBOOTHさんでダウンロード販売中です。

Unityシェーダープログラミングの教科書 ShaderLab言語解説編(via BOOTH)
https://s-games.booth.pm/items/660001

 B5で本文106ページもあります。Unity専用のシェーダー言語「ShaderLab」について包括的にまとめてある日本語の資料は、他にないと思います(2017年12月現在)。Unityでシェーダープログラミングを始めるつもりの方は、傍らに置いておいて損はないかと思います(3Dリアルタイムグラフィックス初心者向けの本ではないのでご注意ください)。

以下ページサンプル。




 明日はKan_Kikuchiさんの担当回になります。それではまたどこかでお会いしましょう。土屋つかさでした。

参考サイト

法線マップ(Normal Map)(Bump mapping)
docs.unity3d.com

・公式ドキュメントの法線マップの記述です。

DXT5n(英語)
https://www.opengl.org/discussion_boards/showthread.php/167670-DXT5n
・OpenGLフォーラムでの、DXT5とDXT5nの違いについての(というか同じ物だという)議論。

DXTC(S3TC)圧縮のアルゴリズムとは?〜前編〜
www.webtech.co.jp

DXTC(S3TC)圧縮のアルゴリズムとは?〜後編〜
www.webtech.co.jp

・OPTPiXを開発しているWeb Technologyの技術ブログ。この記事が大変に参考になりました。正直土屋のエントリーよりも、このリンク先の記事を是非読んで欲しいです。