テクスチャのエッジブリード · 透明スプライトの黒いふちを解消
バイリニアフィルターとミップマップは、透明テクセルの色を縁に混ぜます。エッジブリードはそのテクセルをスプライト自身の色で埋め、アルファは一切書き換えません。
透明なスプライト・葉・デカールの周りの黒い(または白い)ふちは、テクスチャの完全に透明なピクセルに黒や白のRGBが残っていて、バイリニアフィルターや小さなミップマップレベルがその隠れた色を見える縁に混ぜるために出ます。直し方は、そのピクセルを縁自身の色で埋める(エッジブリード、ダイレーション、アルファパディング)か、エンジンに読み込み時にやらせる(GodotのFix Alpha Border、UnityのAlpha is Transparency)かです。Nerulioのエッジブリードはアルファ0のテクセルへRGBを2・4・8・16px押し広げ、アルファは一切書き換えません。

- Channel PNGs測定確認· numpy
- Normal map測定確認· numpy
- Packed mask / ORM測定確認· Pillow
- Engine import未検証
ふちはどこから来るか
PNGは色とアルファを別々に保存するので(ストレートアルファ)、アルファ0のテクセルにもRGBの値があります。多くのソフトはそこに(0, 0, 0)か(255, 255, 255)を書きます。等倍ではアルファ0が隠すので見えません。
フィルタリングとミップマップは隣同士を混ぜます。不透明な縁のテクセルと透明なテクセルのちょうど中間のバイリニアサンプルは、2つの色と2つのアルファを平均し、ミップマップの各レベルは上のレベルの2 × 2ブロックを平均します。結果のアルファは正しく半分ですが、色は隠れた黒や白に引っぱられ、縁が暗くなったり光ったりします。レベルが小さくなるほど強まるため、物体が遠いときや小さく描かれたときだけふちが見えることがよくあります。
エッジブリードは隠れた色を置き換えます。Nerulioは1周ずつ色を広げます。すでに色のあるテクセルに隣接する透明テクセルは、その隣(8方向)のRGBの平均を受け取るので、N周の後にはスプライトからN px以内のすべてが縁の色を持ちます。アルファが0より大きいテクセルには触れず、アルファのバイトは1つも書きません。乗算済みアルファのテクスチャは別の方法でこの問題を避けます。色にアルファが掛かっているので、透明なテクセルは何も加えないからです。
どこまでにじませるか。ボックスフィルターのミップマップでは、レベルkの1テクセルが元の2ᵏ × 2ᵏテクセルを覆うため、縁のブロックに混ざる透明テクセルはスプライトから最大2ᵏ − 1 px離れています。したがって4pxのブリードで1/4まで、8pxで1/8まで、16pxで1/16までのレベルがきれいに保たれます。ラボのミップマッププレビューが表示する3つのレベルです。
- ストレートアルファ
- 色とアルファを独立に保存する方式。アルファ0の場所にもRGBがあります。
- エッジブリード(ダイレーション)
- 透明テクセルのアルファは変えずに、縁の色を外側へコピーすること。
- ミップマップレベル
- テクスチャが小さく描かれるときにGPUが使う縮小済みのコピー(1/2、1/4、1/8 …)。
- 乗算済みアルファ
- 色にアルファを掛けておく方式。隠れた色が漏れ出すことはありません。
例:縁のブロック1つの前後
赤いスプライトのテクセル(210, 40, 30, 255)の隣に、黒を持つ透明なテクセルがあります。ミップマップのレベル1は2 × 2ブロック(不透明2つ、透明2つ)を平均します。
ブリードなし (210+210+0+0)/4, (40+40+0+0)/4, (30+30+0+0)/4, アルファ (255+255+0+0)/4
= (105, 20, 15, 128) 半透明で明るさも半分:暗いふち
ブリードあり 透明テクセルが (210, 40, 30, 0) を持つ
= (210, 40, 30, 128) 半透明、赤はそのまま
24 × 24の不透明な正方形の周りで埋まるテクセル数(N pxのブリード:(24 + 2N)² − 24²):
2 px → 208 4 px → 448 8 px → 1024 16 px → 25604pxの値は、ラボがテスト用スプライトで測定した数値と同じです。448テクセルが変わり、すべてアルファ0で、アルファのバイトは1つも変わりませんでした。
できること
- アルファを持つテクセルから透明テクセルへRGBを2/4/8/16px押し広げます。アルファは不変。
- GPUと同じボックスフィルターでミップマップをプレビュー(1/4〜1/16)。ブリードしていない縁は目に見えて暗くなります。
- 高品質リサンプラーで2のべき乗にリサイズ(ニアレスト・縮小・拡大・フィット)。
- 一括書き出し:リサイズ → ブリード → エンコード。ブリードを最後にするので、リサイズで元に戻りません。
使い方
- スプライトかテクスチャ(PNG)をドロップ。
- にじませるピクセル数を選ぶ。
- ミップマッププレビューを前後で比較。
- ダウンロード。
結果の確認
- 補正 › ミップマップで、ブリード後は1/4〜1/16の縁が暗くならずスプライトの色を保つはずです。
- 結果の注記に埋めた透明テクセルの数が出ます。保存したPNGをチャンネルステージで開くと、Aチャンネルは変わらず、アルファ0の下のRGBに縁の色が入っています。
- エンジンでミップマップを有効にしたまま物体を小さく、または遠くに表示します。ふちが消えているはずです。
トラブルシューティング
| 症状 | 考えられる原因 | 確認方法 | 対処 |
|---|---|---|---|
| 物体が小さいときや遠いときだけ黒・白のふちが出る | ミップマップのレベルが隠れたRGBを縁に混ぜている | 補正 › ミップマップでブリード前後の1/4・1/8・1/16を比べる | 見える最小のレベルまでにじませる(4px → 1/4、8px → 1/8、16px → 1/16)か、エンジンのインポート設定を使う |
| スプライトが動く・回る・拡大されると等倍でも細いふちが出る | バイリニアフィルターが縁と透明な隣の間をサンプリングしている | エンジンの画面を拡大するとふちがシルエットに沿っている | 2px以上にじませる。ドット絵なら代わりにNearestフィルター(Godotでぼやけるドット絵) |
| テクスチャをリサイズしたらブリードが消えた | リサイズがCanvasを通り、アルファ0の下の色を捨てた | リサイズ後のファイルをチャンネルステージで開くと、アルファ0の下のRGBが0に戻っている | リサイズを先に、ブリードは最後に。ラボの書き出しはリサイズ → ブリード → エンコードの順で動く |
| WebPやJPEGで保存したらブリードが消えた | これらの形式はブラウザのCanvasを通してエンコードされ、JPEGにはアルファ自体がない | PNGとWebPをチャンネルステージで比べる | ブリードしたテクスチャはPNGで保存する |
| アトラスで隣のスプライトの色が縁に出る | スプライトが隙間なくパックされ、フィルターが隣のスプライトまで届く | アトラスでスプライト同士が接しているか見る | 必要なのはブリードではなくスプライト間の余白・押し出し:アトラスの余白、スプライトシートのパッカー |
| 半透明の縁のピクセル自体が暗い | 黒いマット上で書き出され、アルファ1〜254のピクセルに色が焼き込まれている | 暗いピクセルのアルファが0より大きい | Nerulioでは直せない(ブリードはアルファが0より大きいテクセルに触れない)。元データからマットなしで書き出し直す |
ほかの方法
| 方法 | こんなときに向いています |
|---|---|
| Godot 4:Process › Fix Alpha Border(インポート設定) | Godotに読み込むテクスチャ。読み込み時に境目のピクセルを周囲の色で埋め、既定でオンなので、通常はブリード済みPNGは不要です。 |
| Unity:Alpha is Transparency(インポート設定) | Unityに読み込むテクスチャ。アルファの縁のフィルタリングの乱れを避けるため、色チャンネルをダイレーションします。 |
| 乗算済みアルファ | レンダラーが乗算済みテクスチャでブレンドする場合。隠れた色が漏れることはありません。 |
出力ファイルと検証方法
「測定確認」は、ダウンロードしたファイルをページ外(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):24 × 24の正方形の周りで448テクセル(ちょうど4周)が変わり、アルファが0より大きいものはなし。アルファ平面の差は0バイト、最初の周は(210, 40, 30, 0)。ミップマッププレビューはsrc/game/texture-fix.jsのmipChainと同じチャンネルごとのボックス平均です。エンジンのインポート設定はGodot 4.7とUnity 6のドキュメントから引用しました。
参照した公式ドキュメント
よくある質問
透明な部分が見えるようになりますか?
いいえ。アルファ0の下の色だけが変わります。テストでは変わった448テクセルはすべてアルファ0で、アルファのバイトは1つも変わりませんでした。
アトラスの余白と同じですか?
違います。余白はアトラス内のスプライト間にすき間を作るもので、ブリードは画像内の透明ピクセルを埋めるものです。パックしたアトラスには両方を使ってください。
無料ですか?アカウントは必要ですか?
このページの機能はすべて無料で、アカウントなしで使えます。透かしや制限された書き出しはありません。
スプライトはアップロードされますか?
いいえ。Studioはこのブラウザのタブ内でデコード・パック・書き出しを行います。プロジェクトはブラウザ(IndexedDB)に自動保存され、.nerulioファイルとして保存できます。
Asepriteやエンジンのプラグインは必要ですか?
いいえ。.asepriteファイルはブラウザで読み書きします。エンジン別のバンドルには必要なものが入っています(Godotはインポート設定とシーン、UnityとGodotのタイルセットは小さなエディタースクリプト)。