本文へスキップ

2Dノーマルマップ生成 · 高さやスプライトの明るさから

高さやグレーの画像を接空間ノーマルマップに。同じ強さはどのカーネルでも同じ傾き、平らな入力は正確に(128, 128, 255)になり、規約は画面に表示されます。

ノーマルマップ生成ツールは、高さ画像やグレー画像を接空間ノーマルマップに変換します。ピクセルごとに高さが左右・上下へどれだけ急に上がるかを測り、その傾きをRGBで書き込み、平らな所はちょうど (128, 128, 255) になります。このページはテクスチャラボを開きます。高さマップ・グレーのテクスチャ・スプライトをドロップし、高さにする明るさかアルファ、Sobel・Scharr・Sobel 5×5のカーネル、強さ、OpenGL(Godot・Unity)かDirectXの緑を選んでPNGをダウンロード。処理はブラウザ内で、アップロードはしません。

テクスチャマップをドロップ(PNGはチャンネルのバイトをそのまま保持)
テクスチャラボを開く
ブラウザ内で処理し、ファイルはアップロードされません。
テクスチャラボ:CC0テクスチャから生成したノーマルマップとOpenGL/DirectXの規約表示
CC0アセット(ambientCG)を開いた実際のテクスチャラボ画面です。
出力
  • Channel PNGs測定確認· numpy
  • Normal map測定確認· numpy
  • Packed mask / ORM測定確認· Pillow
  • Engine import未検証

高さからノーマルへ:勾配・カーネル・強さ

ラボはピクセルごとに高さの値を1つ読みます。画像の明るさ(Rec. 709の輝度、0.2126 R + 0.7152 G + 0.0722 B)かアルファチャンネルを0〜255で読み、0〜1として使います。白が高く、黒が低い扱いです。

微分カーネルが隣のピクセルからx方向とy方向の傾きを推定します。既定はSobel 3×3。Scharr 3×3は中央の行を重く見るので斜めの傾きがそろいやすく、Sobel 5×5は広く平均するためノイズは減りますが細部も消えます。どのカーネルも1ピクセルで高さが1単位上がる坂を傾き1と読むよう正規化されているので、カーネルを変えても同じ強さなら同じ傾きになります。

ノーマルは (−強さ·∂h/∂x, 強さ·∂h/∂y, 1) を長さ1に正規化し、(n + 1) × 127.5 で書き込みます。DirectXの出力は緑の符号が逆になるだけで、2つのファイルの違いはそれだけです。Wrapをオンにするとカーネルが反対側の縁をつないで読み、繰り返しテクスチャにはこれが必要です。オフだと縁のピクセルを引き延ばすので、並べたときに継ぎ目が出ます。

高さマップ
白が高く黒が低いグレー画像。
カーネル
隣のピクセルから傾きを推定する小さな重みの格子。
強さ
傾きに掛ける値。スライダーは0〜10、既定は2。
Wrap
反対側の縁をつないで読み、繰り返しマップに継ぎ目を作らない方式。

具体例:1ピクセルごとに32段階明るくなる坂

右へ行くほど1ピクセルにつき32段階明るくなるグレー画像、Sobel 3×3、OpenGL:

傾き = 32 / 255 = 1ピクセルあたり0.1255
強さ1:  n ∝ (−0.1255, 0, 1) → (−0.124, 0, 0.992) → RGB (112, 128, 254)   傾き  7.2°
強さ2:  n ∝ (−0.2510, 0, 1) → (−0.243, 0, 0.970) → RGB ( 96, 128, 251)   傾き 14.1°
平らな所: n = (0, 0, 1)                           → RGB (128, 128, 255)

右へ上がる面は左を向くので、赤が128より小さくなります。ベクトルを正規化し直すため、強さを2倍にしても傾きは2倍になりません。傾き = atan(強さ × 勾配) です。

できること

  • 明るさかアルファチャンネルから高さを取り、正規化したSobel 3×3・Scharr・Sobel 5×5カーネルを使用。
  • OpenGL(+Y、既定)かDirectX(−Y)、XやYの反転。
  • タイル用テクスチャのwrapサンプリング:縁のテクセルが実際の2倍リピート内の同じテクセルと一致。
  • ディテールノーマルをReoriented Normal Mappingで合成(平らになるRGB合成なし)。

使い方

  1. 高さマップかスプライトをドロップ。
  2. ソース(明るさ・アルファ)、カーネル、強さを選ぶ。
  3. エンジンが期待する規約を選ぶ。
  4. ノーマルマップをダウンロード。

結果の確認

  • 高さ画像の平らな部分は正確に (128, 128, 255) になるはずです。ラボは3つのカーネルと2つの規約すべてでこれを試験しています。
  • 結果の下にある検証値を見ます。ベクトルの平均長が1.000に近く、ノーマルは面の内側を向けないので青が128を下回らないこと。
  • 繰り返しテクスチャなら、Wrapをオンにした状態で継ぎ目を確認します。たとえばタイル状ノーマルマップの継ぎ目チェックで。
  • 使う前にライトで照らしてみます。エンジンで、または近似(ライト1灯、GGX)と明記されたラボの材質プレビューで。

トラブルシューティング

症状考えられる原因確認方法対処
凸が凹に見えるこの絵は暗い所が盛り上がっているのに、ラボは暗い所を低いと読む確実な凸部の明るい面がライトと反対を向いている「Xを反転(赤)」と「Yを反転(緑)」を両方オン(高さの反転と同じ)。上下だけおかしいなら規約だけを切り替える
繰り返す所に線が出るWrapがオフで、縁のピクセルが引き延ばされた結果を2 × 2に並べて境目を見るWrapをオンにして書き出し直す
凹凸がざらざらしてノイズが多いJPEGの圧縮ノイズや塗りの質感を高さとして読んだなめらかなはずの所を拡大PNGの元画像、Sobel 5×5、または低い強さを使う
スプライトの輪郭は平らで内側だけでこぼこ明るさには面取りがなく、アルファをソースにしたときだけシルエットが高さになるソースをアルファに切り替えて比べるアルファを高さに使うか、スプライト用ノーマルマップ生成の面取りを使う

ほかの方法

方法こんなときに向いています
Studioのテクスチャ作業画面(スプライトのノーマルマップ)スプライトなら、シルエットからの面取り、Godotと同じ計算のライト、フレームごとのシート、検証済みのGodot 4・Unity 6書き出し。
3Dソフトでハイポリモデルからベイク面が実際に3Dで存在するなら、明るさからの推定ではなく本物のノーマルが得られます。
OpenGL ↔ DirectX変換ノーマルマップはもうあり、別の緑の規約だけが必要なとき。

出力ファイルと検証方法

「測定確認」は、ダウンロードしたファイルをページ外(Pillow・numpy・独立したパーサー)で開き直して性質を測ったという意味です。ゲームエンジンでの読み込みは試していません。

書き出し先ファイル検証
Channel PNGs<name>-r/g/b/a.png (8-bit grey)測定確認 numpyチャンネルごとにバイト一致。完全に透明なテクセル下のRGBも保持。
Normal mapPNG · OpenGL (+Y) or DirectX (−Y)測定確認 numpyデコードしたベクトルは単位長(最大誤差0.0002)。OpenGLとDirectXは緑だけが異なります。
Packed mask / ORMUnity HDRP mask · URP metallic · ORM (glTF, Godot)測定確認 Pillowパック後のバイトを確認:ORMプリセット、AO 10 / ラフネス 80 / メタリック 220 → (10, 80, 220, 255)。
Engine importUnity · Unreal · Godot未検証どのエンジンでも読み込んでいません。

検証の根拠

チャンネル分解は4チャンネルすべてでバイト一致(最大差0)、透明テクセル2048個のRGBを保持。エッジブリードはアルファ0のテクセル448個だけを変更し、アルファは一切変更なし。OpenGL ↔ DirectXは緑(255 − g)だけが異なり、変換後の法線は単位ベクトル。

制限

  • 明るさは形ではありません。塗った影はくぼみに、塗ったハイライトはふくらみになります。
  • ここには面取り、2Dライトのプレビュー、エンジン用バンドルはありません。これらのマップは数値で確認しただけで、エンジンでは描画していません。
  • 出力は8ビットPNGで、16ビットの高さ入力は上位バイトだけを使います。
  • チャンネルのバイトを正確に扱えるのはPNGだけです。JPEG・WebPはブラウザがデコードし「バイト一致ではない」と表示します。
  • インターレースPNGと16ビット出力は非対応。正確な経路は3,355万ピクセルまで。
  • ノーマルマップの規約はピクセルから判定できず、変換のみ可能です。
  • Unity・Unreal・Godotへの読み込みは行っていません。チャンネル順は各ドキュメントとエンジンのソースに基づきます。

バージョンと出典

テクスチャラボ、ダウンロードしたファイルで測定:生成・変換したマップは単位ベクトルにデコードされ(平均長1.0001、最大誤差0.0002)、平らな高さは3つのカーネルと2つの規約すべてで (128, 128, 255, 255)、Wrapの縁は実際に2倍に並べた画像内の同じピクセルと一致し、OpenGLとDirectXの出力は緑(255 − g)だけが違います。UnityとGodotのY+の規約は各ドキュメントに従います。

参照した公式ドキュメント

よくある質問

LaigterやSpriteIlluminatorと同じ?

違います。高さや明るさの勾配を計算するもので、形を塗ったり面取りしたりはしません。ドット絵では高さはあなたのグレー値から来ます。

どちらの規約を使えばいい?

Unity・Godot・BlenderはOpenGL(+Y)、UnrealはDirectX(−Y)を期待します。Unreal側はEpicのglTFエクスポーターの文書から推定したものとしてラボに表示します。

無料ですか?アカウントは必要ですか?

このページの機能はすべて無料で、アカウントなしで使えます。透かしや制限された書き出しはありません。

スプライトはアップロードされますか?

いいえ。Studioはこのブラウザのタブ内でデコード・パック・書き出しを行います。プロジェクトはブラウザ(IndexedDB)に自動保存され、.nerulioファイルとして保存できます。

Asepriteやエンジンのプラグインは必要ですか?

いいえ。.asepriteファイルはブラウザで読み書きします。エンジン別のバンドルには必要なものが入っています(Godotはインポート設定とシーン、UnityとGodotのタイルセットは小さなエディタースクリプト)。