postprocessing について
postprocessingは、Three.jsの描画ワークフローでpostprocessingライブラリを使うためのスキルガイドです。HDR設定やエフェクトのパス構成に加え、透明オブジェクトのハローやシェーダー値によるブルームのフラッシュの調査方法を説明します。
主な機能
- 01EffectComposerのパス設定
- 02アンビエントオクルージョンとブルームの設定
- 03HDRバッファとトーンマッピング
- 04透明オブジェクトのAOアーティファクト対策
- 05NaNやInfによるブルームのフラッシュ調査
- 06その他のフルスクリーンエフェクトの紹介
活用シーン
- 01シーンへの接触影と控えめな発光の追加
- 02HDR描画とトーンマッピングの設定
- 03白いフラッシュやVFXの暗いハローの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill postprocessingインストール後、Claude Code で/postprocessingを入力して読み込まれたか確認します。
詳細
postprocessing によるポストプロセッシング
適切なジオメトリを正しくレンダリングしても、仕上げの処理がなければ、平坦で生のままに見え、完成した印象になりません。軽く上品な仕上げは、洗練されたゲームの見た目に欠かせないもので、任意の追加要素ではありません。仕上げは 3 つの要素で構成されます。接地影でオブジェクトを際立たせるアンビエントオクルージョン、発光するものや明るいものを光らせる控えめなブルーム、そして雰囲気を整えるトーンマッピングと穏やかなカラーグレーディングです。シーンが正しく見えるようになってから追加し、控えめに保ちましょう。強すぎるブルームや極端なグレーディングは、何もしないよりも見栄えが悪くなります。
ポストプロセッシングでは、パスとエフェクトという概念を導入し、全画面画像操作ツールを使って一般的なレンダリングワークフローを拡張します。以下のセクションでは、上品な仕上げそのもの、それに必要な HDR と色空間の設定、それを損なう 2 つの問題(透過 VFX の周囲にできる暗いハローと、たった 1 つの不正なフラグメントが画面全体に広がる白いフラッシュ)、そしてライブラリのその他のエフェクトの使い方を説明します。
上品な仕上げ
仕上げは、適切に設定したレンダラーとコンポーザーに、控えめな EffectPass を 1 つ追加するだけです。まずレンダラーの属性を設定し、次にコンポーザーを接続し、必要に応じて穏やかなカラーグレーディングを追加します。
ポストプロセッシングワークフローで使う WebGLRenderer には、画面上の深度バッファもステンシルバッファも必要ありません。また、ディスクリート GPU を優先すると効果的です。アンチエイリアスはパイプライン内で処理するため、ここでは無効にします。
import { WebGLRenderer } from 'three'
const renderer = new WebGLRenderer({
powerPreference: 'high-performance',
antialias: false,
stencil: false,
depth: false,
})
EffectComposer はパスを管理し、実行します。さらに処理するために、バッファーをクリアしてシーンをレンダリングする 最初の パスとして RenderPass を使います。その後、全画面画像エフェクトを EffectPass でレンダリングします。1 つの EffectPass で任意の数のエフェクトを 1 つのシェーダーにまとめられるため、エフェクトごとにパスを連結するのではなく、仕上げのエフェクトを 1 つのパスにまとめます。
アンビエントオクルージョンにはシーンの法線が必要です。NormalPass を追加し、その .texture を SSAOEffect に渡します。法線バッファーなしで new SSAOEffect(camera) だけを作成しても、何もレンダリングされません。これはよくある AO の間違いです。トーンマッピングは最後に適用します。1 つの EffectPass 内のエフェクトは配列の順番に適用されるため、トーンマッピングを最後の引数にします。
import {
EffectComposer, EffectPass, RenderPass, NormalPass,
SSAOEffect, BloomEffect, ToneMappingEffect, ToneMappingMode,
} from 'postprocessing'
import { HalfFloatType } from 'three'
const composer = new EffectComposer(renderer, { frameBufferType: HalfFloatType })
composer.addPass(new RenderPass(scene, camera))
// Ambient occlusion needs scene normals: add a NormalPass and feed its texture to SSAOEffect.
// new SSAOEffect(camera) alone (no normal buffer) renders nothing — this is the usual AO mistake.
const normalPass = new NormalPass(scene, camera)
composer.addPass(normalPass) // but hide transparent VFX for this pass — see "SSAO and transparent objects" below
const ssao = new SSAOEffect(camera, normalPass.texture, { worldDistanceThreshold: 20, worldDistanceFalloff: 5, radius: 0.1, intensity: 2 })
const bloom = new BloomEffect({ intensity: 0.4, luminanceThreshold: 0.85 }) // subtle — see below
const toneMapping = new ToneMappingEffect({ mode: ToneMappingMode.ACES_FILMIC })
// Tone mapping goes LAST. Effects in one EffectPass apply in array order.
composer.addPass(new EffectPass(camera, ssao, bloom, toneMapping))
renderer.setAnimationLoop(() => composer.render())
穏やかなカラーグレーディングを行うには、トーンマッピングより 前に、同じ EffectPass にグレーディングエフェクトを追加します。BrightnessContrastEffect、HueSaturationEffect、またはフィルム LUT 用の LUT3DEffect が使えます。グレーディングは控えめに保ちましょう。強い色かぶりを加えるのではなく、コントラストや彩度を数ポイント調整する程度にします。
HDR と必要な色空間設定
この自然な仕上がりを実現するには、HDR のリニアワークフローが前提となります。高精度バッファー、sRGB 出力色空間、そしてレンダラーではなくパイプライン内でのトーンマッピングが必要です。
新しいアプリケーションでは、カラー管理にリニアワークフローを採用してください。ポストプロセッシングはこれを自動的にサポートします。WebGLRenderer.outputColorSpace を SRGBColorSpace に設定すれば、ポストプロセッシングにも反映されます。
デフォルトでは、ポストプロセッシングは中間結果の保存に UnsignedByteType の sRGB フレームバッファーを使用します。これはハードウェアサポート、効率、品質のバランスを取るための選択です。リニアな結果では、通常、劣化を防ぐためにカラーチャネルあたり少なくとも 12 ビットが必要です。精度の低い sRGB バッファーでは、色が [0.0, 1.0] にクランプされ、情報の損失が暗い色域に偏るため、暗いシーンで目立つバンディングが発生します。リニアな高精度の HalfFloatType バッファーではこうした問題が発生せず、デスクトップデバイスで HDR のようなワークフローを実現するための推奨オプションです。
コンポーザーの構築時に高精度フレームバッファーを有効にします。
import { HalfFloatType } from 'three'
const composer = new EffectComposer(renderer, {
frameBufferType: HalfFloatType,
})
トーンマッピングとは、HDR カラーを LDR 出力カラーに変換する処理です。ポストプロセッシングを使用する場合は、レンダラーの toneMapping を NoToneMapping(デフォルト)に設定し、さらに高精度フレームバッファーを有効にする必要があります。そうしないと、トーンマッピングが処理を行う前に、パイプラインの最初の段階で色が [0.0, 1.0] にマッピングされてしまいます。トーンマッピングを有効にするには、パイプラインの最後に ToneMappingEffect を使用します。
クリアカラーに関する注意点です。レンダラー単体では、クリアカラーにトーンマッピングは適用されません。クリア処理ではシェーダーが関与しないためです。ポストプロセッシングは入力画像全体に適用されるため、トーンマッピングも画像全体に均一に適用されます。そのため、ポストプロセッシングの有無によって、クリアカラーの背景に対するトーンマッピングの結果は異なります。ポストプロセッシングを使う方法が正しい方法です。
失敗しやすいケース
自然な仕上がりを損なう原因は 2 つあります。SSAO が透明な VFX の周囲に暗いハローを作ることと、単一の NaN または Inf フラグメントがブルームによって全画面の白いフラッシュを引き起こすことです。
SSAO と透明オブジェクト(VFX の周囲の暗いハロー)
NormalPass は、オーバーライドマテリアルを使ってシーン全体を法線バッファーにレンダリングします。透明オブジェクトも対象です。しかし、加算型のマズルフラッシュ、パーティクル、トレーサー、ビーム、UI プレーンなどの透明な VFX は、depthWrite: false でレンダリングされるため、SSAO がサンプリングする深度バッファーには入りません。その結果、SSAO は対応する深度のない法線を検出し、それぞれの周囲に遮蔽のハローを作ります。パーティクルに追従する薄暗い矩形が現れ、射撃時に最も目立ちます。
これは、CSM がシャドウコードをシェーダーに挿入して VFX を不透明な暗い矩形としてレンダリングする問題とは別のバグです。lights スキルを参照してください。
修正するには、NormalPass から透明オブジェクトを除外し、法線を深度バッファーと一致させます。確実で汎用的な修正方法は、このパスの間だけすべての透明オブジェクトを非表示にし、終了後に元に戻すことです。NormalPass の前後に何もしないパスを 2 つ配置します。TransparentToggle は Pass を拡張し、this.needsSwap = false を設定します。非表示にする分岐ではシーンを走査し、表示中の透明オブジェクトを hidden に追加して非表示にします。復元する分岐では再び表示状態にし、hidden をクリアします。
import { Pass } from 'postprocessing'
// Hides (or restores) every transparent object so the NormalPass sees only opaque geometry.
class TransparentToggle extends Pass {
constructor(scene, hidden, hide) { super('TransparentToggle'); this.scene = scene; this.hidden = hidden; this.hide = hide; this.needsSwap = false }
render() {
if (this.hide) this.scene.traverse((o) => { if (o.visible && o.material?.transparent) { this.hidden.push(o); o.visible = false } })
else { for (const o of this.hidden) o.visible = true; this.hidden.length = 0 }
}
}
const hidden = []
composer.addPass(new TransparentToggle(scene, hidden, true)) // hide for the normal pass
composer.addPass(normalPass)
composer.addPass(new TransparentToggle(null, hidden, false)) // restore before the effect pass draws the lit color buffer
不透明な AO サーフェスには影響しません。接触シャドウによって、引き続きオブジェクトに接地感を与えます。
ブルームによるシェーダーの NaN(全画面が白く点滅)
HDR バッファーに NaN(または Inf)を書き込むフラグメントが1つあるだけで、ブルーム処理を行うと全画面が白く点滅します。その仕組みは次のとおりです。ブルームのミップマップのダウンサンプリングで、そのフラグメントがより粗いミップすべてに平均化されるため、汚染されたピクセル1つがブルームテクスチャ全体に広がり、さらにフレーム全体に加算されます。
特徴ははっきりしています。フラッシュはポストプロセスなしでは発生しません。ブルームがなければ、不正な値は少数の元ピクセルにとどまり、通常は見えません。また、背景が透けて見えない、輪郭のはっきりした完全不透明な白で、元の値がビューに依存するため、特定のカメラ角度や位置でのみ発生します。
よくある原因は、数学的には非負の底が浮動小数点の丸めでわずかに負になる GLSL の pow() です。x < 0 に対する pow(x, y) の動作は未定義です。exp(y * log(x)) として実装するドライバーでは NaN を返し、それ以外では 0 を返します。そのため、このフラッシュはGPU/ドライバーに依存し、お使いの環境では再現しない可能性があります。典型例はフレネル/リム項です。正面から表面を見ると、正規化された2つのベクトルの abs(dot(...)) が ~1.0000001 に丸められ、底が負になります。
float fres = pow(1.0 - abs(dot(normalize(n), normalize(v))), 1.4); // NaN risk: base can be < 0
float ndv = clamp(abs(dot(normalize(n), normalize(v))), 0.0, 1.0); // clamp the base ≥ 0 first
float fres = pow(1.0 - ndv, 1.4); // safe
すべての pow の底を max(0.0, base)(または clamp)でガードし、入力がゼロベクトルになり得る normalize() もガードしてください。NaN を伴わないオーバーフローも同様に広がります。HalfFloat の最大値(約65504)を超える値はコンポーザーのバッファー内で Inf となり、同じように広がるため、発光/加算出力が上限を超えないようにしてください。
ローカル環境で再現しない場合に原因を見つけるには、シーンを FloatType のレンダーターゲットに描画し、readRenderTargetPixels を使って NaN/Inf/極端に大きな値をスキャンしてください。疑わしいマテリアルを確認するには、そのカラー uniform の1つに NaN を書き込み、画面全体が白くなることを確かめます。
ほかのエフェクトを使う
このライブラリには、センスよく使うエフェクトのセット以外にも多くのエフェクトが用意されており、どれも同じ方法で追加できます。ただし、ゲームの仕上がりを決めるのは、使えるエフェクトをすべて使うことではなく、よく考えて選んだ少数のエフェクトです。
カタログには、被写界深度、ビネット、アウトライン(選択/ハイライト用)、ゴッドレイ、色収差、グリッチ、ピクセル化、衝撃波などが含まれます。どれも同じように追加できます。エフェクトを構築し、EffectPass に渡してください。名前を推測するのではなく、パッケージのエクスポートを確認して、現在利用できるエフェクトとそれぞれのオプションを調べてください。
この方法はパフォーマンス面でも有利です。EffectPass は任意のエフェクトの組み合わせを自動で整理して1つのシェーダーに統合するため、レンダー操作を最小限に抑え、従来のパスチェーンによるパフォーマンス上のペナルティなしで多くのエフェクトを組み合わせられます。さらに、各エフェクトはそれぞれ独自のブレンド関数を選べます。全画面のレンダー操作では、画面を覆う単一の三角形も使用します。四角形と比べて、これは最新の GPU のラスタライズパターンになじみ、画面の対角線に沿った不要なフラグメント計算をなくします。GPGPU パスや複雑なフラグメントシェーダーを使うエフェクトでは、特に効果があります。