本文へスキップ

スプライトシートのノーマルマップ · フレームごとに生成

ノーマルマップ生成ツールをスプライトシート全体にかけると、アニメーションではなく1枚の絵として扱われます。近くに並んだフレーム同士でぼかしや縁からの距離が混ざり、同じポーズでも隣のフレーム次第で立体感が変わります。ここではまずスプライト作業画面でフレームを切り、テクスチャ作業画面が各フレームを同じpx設定で個別に処理するので、再生中も立体感がぶれません。

アニメーションするスプライトシートのノーマルマップは、フレームごとに作ります。各フレームの立体を自分の矩形の中でピクセル単位の同じ設定で計算すれば、色のシートとまったく同じ配置のノーマルマップのシートが1枚できます。Nerulioではスプライト作業画面でシートを切り(グリッドか塊、そしてApply)、テクスチャタブを開き、_n.png と一緒に、AnimationPlayerがフレームを送るGodot 4のシーンか、同じスプライトの範囲を設定して _NormalMap を付けるUnity 6のインポーターを書き出します。

スプライト、スプライトシート、繰り返しテクスチャ(PNG)をドロップ
空のStudioを開く
ブラウザ内で処理し、ファイルはアップロードされません。
Studio のテクスチャ作業画面:6フレームに切ったドット絵のたいまつを、生成したノーマルマップと点光源で照らした画面
CC0アセット(OpenGameArt)を開いた実際のStudio画面です。
書き出し先
  • Godot 4 2D lights検証済み· Godot 4.7.2
  • Unity 6 URP 2D検証済み· Unity 6000.5.3f1 (URP 17.5)
  • PNG set + JSON通常ファイル

フレームの境目で起きることと、フレームごとの生成で避ける方法

ノーマルマップの生成ツールは隣のピクセルを見ます。傾きのカーネルは各ピクセルの周り1px(Pixel・Sobel・Scharr)か2px(Sobel 5×5)を読み、面取りはいちばん近い透明ピクセルまでの距離を測り、ぼかしはさらに遠くまで広がります。シート全体にかけると3つとも境目を越えます。2つのフレームが接する所では、面取りが隣のフレームのピクセルを体の続きとみなし、カーネルは境目の列で2つの絵を混ぜます。

Nerulioはフレームの領域ごとに別々に処理します。カーネルは領域の境目で止まり(フレームの最後のピクセルを繰り返す)、距離変換はフレームの縁を透明とみなし、ぼかしは境目を越えて読みません。幅・深さ・強さはフレームごとに合わせ直さない絶対ピクセル値なので、同じポーズはシートのどこにあっても同じ立体になります。tests/normals.test.mjs が、フレームのマップが隣に左右されず、スプライトと一緒に動くことを確かめています。

絵の外側にはアルファ最大の平らな法線 (128, 128, 255) が入り、シルエットの法線は透明な所へ2px複製されます。そのため、エンジンがシートをフィルタリングしたりミップマップを作ったりしても、スプライトの縁の隣のテクセルには空ではなくもっともらしい法線があります。

ライトはフレームのピクセル基準で保存されるので、プレビューでも書き出したシーンでも、すべてのフレームを同じ相対位置から照らします。領域がシートの上を移っても、アニメーションが光の中を流れていくことはありません。

フレームの領域
シート上のフレーム1つの矩形。ノーマルマップの計算はすべてその中だけで行います。
Clamp
境目の先を読まず、最後のピクセルを繰り返す縁の処理。
エッジブリード
縁の法線を透明ピクセルへ複製し、シルエットでフィルタリングしても正しく保つ処理。

具体例:80 × 110のフレームが並ぶ720 × 330のシート

GodotとUnityの検証に使ったCC0の冒険者のシート(720 × 330は2の累乗ではない):

720 / 80 = 9列    330 / 110 = 3行    → 27マス
領域ごとに:面取り・カーネル・ぼかしは80 × 110の中だけで動く
ノーマルのシート:720 × 330、同じ配置、_n.pngとして保存

Godot  フレーム10 = 1行1列 → region_rect Rect2(80, 110, 80, 110)
Unity  範囲は下から数える:y = 330 − (110 + 110) = 110
       0行 → y = 220      2行 → y = 0

Godotのシーンでは、各フレームは region_rect のキー1つで、それより前のフレームの長さの合計の時刻に置かれ、アニメーションの長さは全フレームの長さの合計です。1つのCanvasTextureが全フレームのノーマルマップを持ちます。

できること

  • フレームはスプライト作業画面(「適用」)から。テクスチャ作業画面は自分ではフレームを切らず、フレームのないシートは1枚の絵として照らします。
  • 距離変換・ぼかし・カーネルはフレームの境界を越えて読みません。tests/normals.test.mjsで、フレームのマップが隣と無関係で、スプライトと一緒に動くことを確認。
  • ベベルの幅・深さ・ディテールは絶対px値で、フレームごとに調整し直しません。128px以下のドット絵フレームには1.5pxの縁を提案。
  • ライトの位置はフレームのpx基準なので、全フレームが同じように照らされます。Enterでライト付き再生、`でシート全体と1フレーム表示を切り替え。
  • 高さブラシ(B)の1ストロークは始めたフレームの中にとどまり、取り消しは1回単位。

使い方

  1. スプライトシートをドロップすると、スプライト作業画面で開きます。
  2. グリッドとアニメーションを確認して「適用」。
  3. テクスチャタブを開くと、全フレームのノーマルマップがすぐに生成されます。
  4. ベベルとディテールを調整し、ライトツール(L)で光を置いてEnterでライト付きのアニメーションを確認。
  5. 書き出し(Ctrl+E)でGodot 4、Unity 6 URP 2D、PNGセットから選ぶ。

結果の確認

  • ` キーでシート全体と1フレームを切り替えます。そのフレームの立体がどちらでも同じに見えるはずです。
  • Lit表示でEnterを押します。よく似た2つのフレームの間で立体がちらつくなら、高さ表示で比べてください。違いは隣ではなく絵そのものにあります。
  • _n.png を色のシートの隣で開くと、シルエットがピクセル単位で重なるはずです。
  • GodotでAnimationPlayerを再生すると、キーごとに region_rect が1フレームずつ進み、法線もスプライトと一緒に動きます。

シート一括処理とフレームごとの処理

シート全体を一度にフレームごと(この作業画面)
フレーム境界付近のベベル縁からの距離が隣のフレームに届くことがある各フレームの中だけで測る
ぼかしとディテール隣のフレームへにじむことがあるフレーム境界で止まる
2つのフレームにある同じ絵立体感が変わることがある同じ立体感、スプライトと一緒に移動
ライトの位置シートのpx基準:位置の違うフレームは照らされ方も違うフレームのpx基準:全フレームが同じように照らされる
書き出しノーマルマップ画像1枚、エンジン設定は手作業AnimationPlayer付きのGodotシーン、Unityのスプライト範囲+_NormalMap

トラブルシューティング

症状考えられる原因確認方法対処
フレームの一辺に沿ってまっすぐな明るい尾根が出る絵がフレームの縁に触れており、フレームの境目は透明とみなされるのでそこに縁ができた尾根がフレームの境目とぴったり重なる絵の周りに余白を取って切り直すか、高さブラシでその帯を平らにする
あるポーズの立体が次のポーズへ漏れるノーマルマップをシート全体から作った(別のツール、またはフレームを切っていないシート)テクスチャ作業画面の領域が1つだけで、フレームパネルが空スプライト作業画面でフレームを切り(シート分割を参照)、生成し直す
エンジンでスプライトが動くと縁がちらつくバイリニアフィルターがノーマルのシートのフレームの境目の先をサンプリングしているNearest・Pointフィルターでは消えるドット絵はNearest・Point、フィルターを使う絵はフレーム間に間隔を空ける(アトラスの余白)
Unityでシートの違う部分が表示される書き出し後にシートを編集・リサイズしたため範囲が合わないSprite Editorの範囲とPNGのサイズを比べる書き出し直してApply Texture JSONを再実行

ほかの方法

方法こんなときに向いています
任意のツールでフレームを1枚ずつ作って組み直すフレームが少なく、使い慣れたツールがあるとき。配置を手作業で同じに保つ必要があります。
Laigterのシート分割アプリでフレーム数かグリッドサイズでシートを分割できます(1.12.0リリース)。面取りがフレームの境目をどう扱うかは測定していません。
色とノーマルのアトラスを一緒にパック(TexturePacker --pack-normalmaps)多数のスプライトを1つのアトラスにパックするとき。Studioのテクスチャ作業画面はシート1枚単位で、複数スプライトをパックしたアトラスは扱いません。

エンジン別の書き出しと検証方法

「検証済み」は、実際のCC0アセットから書き出したファイルを本物のエンジン・ツールで読み込み、元データと比較したという意味です。読み込んでいない形式はそのように表示します。

書き出し先ファイル検証
Godot 4 2D lights_lit.tscn · CanvasTexture .tres · normal / specular PNG検証済み Godot 4.7.2実在のCC0ケース6件をGodot(gl_compatibility)で描画し、確認した全フレームがStudioのライティングプレビューと1/255以内で一致。緑を反転した同じ描画は3.8〜37段階ずれるため、規約の誤りを検出できます。
Unity 6 URP 2DPNG · nerulio-texture.json · Editor/NerulioNormalMapImporter.cs検証済み Unity 6000.5.3f1 (URP 17.5)12回中12回合格(6ケース×ガンマ・リニア色空間):URPが適用したライティングが書き出したノーマルマップと一致(平均誤差0.0037〜0.0085)。光の減衰はURP独自なので、明るさはプレビューではなくUnityの基準です。
PNG set + JSONalbedo · _n (OpenGL) · _n_dx (DirectX) · 16-bit height · AO · specular · manifest通常ファイルどのエンジン・ツールでも使える通常ファイルで、エンジンでの実行検証はうたっていません。

検証の根拠

Godot 4.7.2で実在のCC0ケース6件(ドット絵、HDスプライト、繰り返しテクスチャ、読み込んだDirectXマップ)を描画し、確認した全フレームがStudioのプレビューと1/255以内。Unity 6000.5 URP 2D:12回中12回合格(6ケース×ガンマ・リニア)。規約が分かっている実在のマップでOpenGL/DirectX判定:1Kのマップ128枚で100%正解、1,562サンプル中で誤った「高」判定は0件(「緑が明るければOpenGL」という単純な規則は46%)。

制限

  • テクスチャ作業画面はフレームを切りません。フレームのないシートは1枚の絵として照らします。
  • Wrap・Mirrorモードでは、領域の境目付近のブラシのライブプレビューが、描いた後の正確な再計算まで少し違うことがあります。
  • ノーマルマップを複数スプライトのアトラスと一緒にパックすることはしません。
  • 透明な縁がない絵(繰り返しテクスチャ)では、赤の反転とDirectXの緑を区別できません。チェックパネルがそれを伝え、「赤を反転」を用意しています。
  • シートはスプライト作業画面でフレームを切るまで1枚の絵としてライティングされます。テクスチャ作業画面ではフレームを切りません。
  • リムライトはプレビュー専用で、Unityにはスペキュラーマップを書き出しません(Godotには書き出し)。
  • Unityは独自の光の減衰を使います。書き出したノーマルマップはUnityで検証済みですが、明るさの曲線はUnityのものです。

バージョンと出典

tests/normals.test.mjs は、フレームのマップが隣に左右されず、スプライトと一緒に動くことを示しています。Godot 4.7.2は60フレームの侍と720 × 330の冒険者を含め、書き出したAnimationPlayerのフレームを1/255以内で描画し、同じ6ケースがUnity 6000.5.3f1で12回中12回合格しました。ノーマルマップがスプライトとUVを共有する必要があることはUnityのマニュアルに書かれています。

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

よくある質問

シート全体で1枚のノーマルマップを作るのではだめ?

フレームが接していたり近かったりすると、シート全体を一度に処理したときにぼかしや縁からの距離が隣のフレームへはみ出し、同じ絵でも隣次第で立体感が変わってアニメーションがちらつきます。フレームごとに処理すれば、シートのどこにあっても同じ結果になります。

Godotへの書き出しでアニメーションも再生されますか?

はい。<名前>_lit.tscnにはCanvasTextureを使うSprite2D、環境光のCanvasModulate、ライトごとのPointLight2D、フレームを切り替えるAnimationPlayerが入っています。Godot 4.7.2の描画は、そのAnimationPlayer経由も含めて確認した全フレームでStudioの基準と1/255以内で一致しました。

Unityでは?

Unity 6 URP 2DのバンドルはJSONからスプライトの範囲を設定し、ノーマルマップを_NormalMapのセカンダリテクスチャ(リニア、非圧縮、リサンプルなし)として付けます。Unity 6000.5.3f1で12回すべて合格し、明るさのカーブはURP自身のものです。

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

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

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

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

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

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