ノーマルマップ変換 · OpenGL ↔ DirectX(緑を反転)
2つの規約の違いは緑チャンネルだけです。変換は255 − gで、それ自体が逆変換なので、変換したマップをもう一度変換すると同じバイトに戻ります。
OpenGL方式のノーマルマップ(Y+、緑が上向き — Unity・Godot・glTFが期待する方式)とDirectX方式(Y−、緑が下向き)の違いは緑チャンネルだけです。一方から他方への変換は全テクセルでg → 255 − gとするだけで、赤・青・アルファは変わらず、2回行うと元のバイトに戻ります。Nerulioの変換はブラウザでPNGにまさにこの計算を適用し、単位長のチェックを表示します。ファイルがどちらの規約かは推測せず、その判定は測定の裏付けがあるStudioのテクスチャ作業画面の判定機能が行います。

- Channel PNGs測定確認· numpy
- Normal map測定確認· numpy
- Packed mask / ORM測定確認· Pillow
- Engine import未検証
緑チャンネルの意味と、反転で起きること
接空間ノーマルマップはテクセルごとに単位ベクトルを1つ保存します。赤がX(画像の右方向)、緑がY、青がZ(面の外向き)で、それぞれ(n + 1) / 2 × 255でエンコードされます。平らなテクセルは(128, 128, 255)です。2つの規約はXとZでは一致し、画像上で+Yがどちらを向くかだけが違います。OpenGLは上、DirectXは下です。
逆の規約のマップを使うと、縦方向の傾きがすべて反転します。上から照らすと凹凸の上側ではなく下側の縁が明るくなり、凸が凹に見えたり、光が下から来ているように見えたりします。横方向の傾きは正しいので、テクスチャではなくライティングの不具合のように見えることもあります。
直し方は焼き直しではなく算数です。y → −yはg → 255 − gで、この操作はそれ自身が逆操作です。UnityのマニュアルはUnityがY+(OpenGL)のノーマルマップを使うと書き、GodotのマニュアルはX+・Y+・Z+(「OpenGL方式」)を求め、glTF 2.0は+Xを右・+Yを上と定めています。UnrealについてはEpicが間接的にしか示していないため、ラボはDirectX(−Y)に「推定」と表示します。
このファイルはどちらの規約? どちらも正しいノーマルマップなので、ラボは推測しません。Studioのテクスチャ作業画面は面そのもの(傾き同士が矛盾しないか、スプライトならシルエットも)から推定して信頼度とともに表示し、確認するまで判定を適用しません。この確認はOpenGLかDirectXかを参照してください。
- 接空間
- 面を基準にしたノーマル。XはテクスチャのU、YはV方向、Zは面の外向き。
- OpenGL (Y+)
- 画像上で緑が上向き。Unity・Godot・glTF。
- DirectX (Y−)
- 画像上で緑が下向き。
- 単位長
- 正しいノーマルは長さ1のベクトルにデコードされます。ラボは平均と最大誤差を報告します。
例:1テクセルと、判定の精度
画像の上方向に傾いたテクセル、ノーマル(0, 0.6, 0.8)と、平らなテクセル:
OpenGL (0, 0.6, 0.8) → (128, 204, 230) 平ら (128, 128, 255)
DirectX (0, −0.6, 0.8) → (128, 51, 230) 平ら (128, 127, 255)
緑 255 − 204 = 51 → (51 − 127.5) / 127.5 = −0.6
規約が分かっているマップでのStudioのGL/DX判定(2026-09-24):
1Kの全体マップ 128枚 判定 100 % 正解率 100 % 誤った「高」0
64²の切り出し 504枚 判定 99.6 % 正解率 99.6 % 誤った「高」0
単純なルール「緑が高めならOpenGL」、サンプル1562個:46 %平らなテクセルは(128, 127, 255)になり、変換し直すと(128, 128, 255)に戻ります。1段階の差は8ビットのエンコードの中心によるもので、損失ではありません。透明な縁のないテクスチャでは、赤が反転したマップは判定でDirectXと読まれ、Studioはその旨を表示します。
チャンネルごとに変わるもの
| チャンネル | OpenGL (Y+) | 変換 | DirectX (Y−) |
|---|---|---|---|
| R (X) | 画像の右向き | そのまま | 画像の右向き |
| G (Y) | 画像の上向き | 255 − g | 画像の下向き |
| B (Z) | 面の外向き、128以上 | そのまま | 面の外向き、128以上 |
| A | ファイルにあった値 | そのまま | そのまま |
| ambientCGのファイル名 | _NormalGL | — | _NormalDX |
Studioのテクスチャ作業画面はファイル名の_NormalGL・_NormalDXを宣言された規約として読みます。ラボはどちらも単にノーマルマップと分類し、それ以外の名前は規約について何も示しません。
できること
- 緑だけを変えるOpenGL ↔ DirectX変換(チャンネルごとに確認)。
- どのエンジンがどの規約を期待するかと根拠のドキュメント(Unrealの−Yは推定と表示)。
- ノーマルマップの検証:平均長、最大誤差、最小の青。
- Sobel・Scharr・Sobel 5×5と、タイル用テクスチャのwrapで高さ → ノーマルを生成。
使い方
- ノーマルマップをドロップ。
- 「処理」でOpenGL ↔ DirectXを選ぶ。
- 検証の数値を確認。
- 変換したマップをダウンロード。
出力されるファイル
| ファイル | 用途 |
|---|---|
rock_normal-directx.png | 変換したマップ。名前は入力がOpenGLだと仮定しています。DirectXのマップを変換した場合、中身はOpenGLなので、混乱しないよう名前を変えてください。 |
texture-report.json | 任意:点検ステージで、セット内の各ノーマルマップのnormalCheck.meanLength・unitLength・blueNonNegative。 |
変換したマップをエンジンで使う
エンジンの設定は各ドキュメントに従ったもので、Nerulioがこの読み込みを実行したわけではありません。
- Unity 6:OpenGLのマップを読み込み、Texture Typeを
Normal mapにします。DirectXのマップしかない場合は、変換せず同じインポーターのFlip Green Channelをオンにしてもかまいません。 - Godot 4:マテリアルのNormal Mapを有効にし、OpenGLのマップを割り当てます。DirectXのマップなら、インポートドックでProcess › Normal Map Invert Yをオンにして再インポートする方法もあります。
- glTF 2.0:ノーマルテクスチャは+X右・+Y上でなければならないため、DirectXのマップは書き出す前に変換します。
- DirectX方式の対象:変換した
-directx.pngを使い、そのエンジンのインポートのドキュメントに従います。 - マテリアルを上から照らし、盛り上がった部分の上側の縁が明るいか確認します。
結果の確認
- ノーマルステージに平均ベクトル長と最大誤差が出ます。正しいマップは1に近い値です。
- 保存したファイルを元とチャンネルごとに比べます(チャンネルステージ)。R・B・Aは同一で、G = 255 − gのはずです。
- 結果をもう一度変換すると、元のバイトが戻ります。
トラブルシューティング
| 症状 | 考えられる原因 | 確認方法 | 対処 |
|---|---|---|---|
| 凸が凹に見え、光が下から来るように見える | マップの規約が逆 | 上から照らしたとき、どちらの縁が明るいか | ここで変換するか、UnityのFlip Green Channel・GodotのNormal Map Invert Yを使う |
| 変換しても横方向のライティングが違う | 緑ではなく赤が反転している(NormalMap-Onlineの既定の出力がこのタイプ) | Studioのチェックパネルがスプライトで赤の反転を警告する | StudioのテクスチャでFlip red (X−)。ラボが変換するのは緑だけ |
| 変換前は正しかったのに変換後におかしい | マップがもともと目的の規約だった | もう一度変換すれば元に戻る(それ自身が逆操作) | 元のファイルを使う |
| Unityでライティングが平坦・にじんで見える | マップをDefaultのカラーテクスチャとして読み込んだ | インスペクターのTexture Type | Texture TypeをNormal mapにする |
| 検証で青が128未満、またはベクトル長が1から遠いと出る | 接空間ノーマルマップではない(ハイトマップ、オブジェクト空間のマップ、リサイズした色のコピー) | ノーマルステージの平均長と最小の青 | 緑の反転では直らない。ノーマルマップ生成でハイトから作る |
ほかの方法
| 方法 | こんなときに向いています |
|---|---|
UnityのFlip Green Channel・GodotのNormal Map Invert Y | 1つのエンジンだけで使うとき。インポート設定が2つ目のファイルなしで反転します。 |
| もう一方の版をダウンロード | 素材サイトが両方を配布しているとき(ambientCGは_NormalGLと_NormalDX)。エンジンが期待する方を取ってください。 |
| Studioの規約チェック | マップがどちらの規約か分からない、または赤の反転が疑われるとき。 |
出力ファイルと検証方法
「測定確認」は、ダウンロードしたファイルをページ外(Pillow・numpy・独立したパーサー)で開き直して性質を測ったという意味です。ゲームエンジンでの読み込みは試していません。
| 書き出し先 | ファイル | 検証 |
|---|---|---|
| Channel PNGs | <name>-r/g/b/a.png (8-bit grey) | 測定確認 numpyチャンネルごとにバイト一致。完全に透明なテクセル下のRGBも保持。 |
| Normal map | PNG · OpenGL (+Y) or DirectX (−Y) | 測定確認 numpyデコードしたベクトルは単位長(最大誤差0.0002)。OpenGLとDirectXは緑だけが異なります。 |
| Packed mask / ORM | Unity HDRP mask · URP metallic · ORM (glTF, Godot) | 測定確認 Pillowパック後のバイトを確認:ORMプリセット、AO 10 / ラフネス 80 / メタリック 220 → (10, 80, 220, 255)。 |
| Engine import | Unity · Unreal · Godot | 未検証どのエンジンでも読み込んでいません。 |
検証の根拠
チャンネル分解は4チャンネルすべてでバイト一致(最大差0)、透明テクセル2048個のRGBを保持。エッジブリードはアルファ0のテクセル448個だけを変更し、アルファは一切変更なし。OpenGL ↔ DirectXは緑(255 − g)だけが異なり、変換後の法線は単位ベクトル。
制限
- チャンネルのバイトを正確に扱えるのはPNGだけです。JPEG・WebPはブラウザがデコードし「バイト一致ではない」と表示します。
- インターレースPNGと16ビット出力は非対応。正確な経路は3,355万ピクセルまで。
- ノーマルマップの規約はピクセルから判定できず、変換のみ可能です。
- Unity・Unreal・Godotへの読み込みは行っていません。チャンネル順は各ドキュメントとエンジンのソースに基づきます。
バージョンと出典
テクスチャラボで測定(docs/TEXTURE-LAB.md):2つのダウンロードをチャンネルごとに比べると正確にdx.g == 255 − gl.gで、R・B・Aは同一。変換したマップは平均長1.0001、最大誤差0.0002でデコードされました。判定の数値は、規約が分かっている実在のマップでのStudioの評価によるものです(docs/STUDIO-TEXTURE.md、2026-09-24)。エンジンの規約は下記のドキュメントから引用し、変換したファイルのエンジンへの読み込みは実行していません。
参照した公式ドキュメント
よくある質問
自分のマップがどちらの規約か判定できますか?
いいえ。どちらも正しいノーマルマップです。ラボはどのエンジンが何を期待するかを示して変換しますが、判定できるとは言いません。
変換で劣化しますか?
しません。緑が255 − gになり、2回変換すると元のバイトに戻ります。
無料ですか?アカウントは必要ですか?
このページの機能はすべて無料で、アカウントなしで使えます。透かしや制限された書き出しはありません。
スプライトはアップロードされますか?
いいえ。Studioはこのブラウザのタブ内でデコード・パック・書き出しを行います。プロジェクトはブラウザ(IndexedDB)に自動保存され、.nerulioファイルとして保存できます。
Asepriteやエンジンのプラグインは必要ですか?
いいえ。.asepriteファイルはブラウザで読み書きします。エンジン別のバンドルには必要なものが入っています(Godotはインポート設定とシーン、UnityとGodotのタイルセットは小さなエディタースクリプト)。