Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】WebGPU Engine from Scratch Part 9: Shadow Maps

2025年09月16日に「Dev.to」が公開したITニュース「WebGPU Engine from Scratch Part 9: Shadow Maps」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

WebGPUで3Dグラフィックスに影を付ける一般的な手法「シャドウマップ」の実装を解説。ライトをカメラに見立て深度マップを作成し、シーンのオブジェクトとの距離を比較して影を判定する。方向性ライトでの具体的な実装手順や、影の表示改善策も紹介している。

ITニュース解説

3Dグラフィックスでリアルな世界を表現するためには、影の描画が非常に重要になる。影の表現方法にはいくつかあるが、最も正確で直感的なのは「レイトレーシング」と呼ばれる手法だ。しかし、これは計算コストが高く、リアルタイムのゲームなどでは難しい場合が多い。そこで、計算が軽く、十分に良い結果が得られる「シャドウマップ」という手法がよく利用される。多くのゲームで見る影はこのシャドウマップで描かれており、ときどき影の輪郭がギザギザに見えることがあるが、それがシャドウマップ特有のアーティファクト(視覚的なノイズ)である。この記事では、このシャドウマップの基本的な仕組みと実装方法を解説する。

シャドウマップの基本的な考え方は非常にシンプルだ。まず、各光源を特別な「カメラ」だと見立てて、その光の視点からシーン全体の「写真」を撮る。しかし、この写真は一般的な色情報ではなく、各オブジェクトが光からどれくらいの距離にあるか、という奥行き(デプス)の情報だけを記録した「デプスマップ」という特殊な画像だ。次に、実際に画面に表示するシーンをレンダリングする際、画面上の各点(これをフラグメントと呼ぶ)について、その点が光からどれくらいの距離にあるかを計算する。そして、この距離を、先ほど作ったデプスマップに記録されている同じ場所の距離と比較する。もし、デプスマップに記録された距離の方が短ければ、それは「その点の前に何か別の物体があって、光を遮っている」ということになり、その点は影だと判断される。逆に、デプスマップの距離と実際の距離がほぼ同じか、デプスマップの距離の方が長ければ、その点は光が当たっていると判断され、影ではない。

今回は、まず扱いやすい「ディレクショナルライト(平行光源)」から始める。太陽のような遠くにある光源をシミュレートするもので、光が平行に進むため、光源をカメラとして扱う場合はパースペクティブ(遠近感)のない「オーソグラフィックカメラ(平行投影カメラ)」として表現する。ただし、デプスマップを生成する範囲は、シーン全体をカバーするように有限の領域を設定する必要がある。

シャドウマップを実際に構築するには、新しいレンダリングパイプラインを組む必要がある。このパイプラインでは、基本的にはオブジェクトの形状(ジオメトリ)だけをレンダリングし、特に色を塗るシェーダーは使わない。デプスマッピングが有効になっていれば、この段階で自動的に各オブジェクトの奥行き情報がデプスマップとして出力される。光の視点からのビュー行列(どの方向を見ているか)と投影行列(どういう範囲を写すか)を正確に設定することが重要になる。ディレクショナルライトのビュー行列を作成する際、光の位置は重要ではないが、シーン全体を包含できる十分に遠い場所を選ぶ必要がある。投影行列は、シーンの範囲に合わせて左右上下の広がりと奥行きを設定する。ここでは、ShadowMappedLightという新しいクラスを導入し、ライト固有のビュー・投影行列を管理するようにした。また、光がY軸方向(真上や真下)を向いていると、数学的な計算でエラーが発生する可能性があるため、getLookAtMatrix関数を修正し、この特殊なケースでも正しく動作するように改善している。シャドウマップの画像が歪まないように、アスペクト比(縦横比)を考慮することも大切だ。

デプスマップは通常の画像と異なり、直接見ても分かりにくい場合が多い。デバッグ時には、実際にシーンに影を適用して確認するか、デプスマップを視覚化するデバッグツールを使うのが良い。デバッグツールでは、デプスマップのガンマ補正(色の明るさの調整)を無効にするオプションを導入した。また、デプスマップの表示が上下逆になっているという問題が発生したため、シェーダーでY座標を反転させる修正が必要だった。この上下反転は、デバッグ表示だけでなく、実際にシャドウマップを使う際にも必要な重要な修正点だった。

次に、生成したシャドウマップを実際に利用するためのバインディング設定を行う。WebGPUの仕様上、デプステクスチャはレイヤーを持つことができないという制限があるため、複数の光源の影を扱いたい場合は、個々のシャドウマップを別々にバインドする必要がある。そのため、最大数(今回は4つ)のシャドウマップをバインドできるように準備し、シャドウマップが存在しない場合は「ダミー」の小さなデプステクスチャで埋める工夫が必要になる。影の比較処理には、特別な「コンパレーゾンサンプラー」を使用する。これは、デプス値を比較する機能を持ったサンプラーで、compare: "less"という設定により、デプスマップの値が計算されたデプス値より小さい場合に1.0(影ではない)、それ以外を0.0(影である)として返す。シェーダーには、影を計算するために光の視点からの変換行列(ビュー行列と投影行列)を渡す必要がある。

シェーダー側では、各フラグメントのワールド座標を、光の変換行列を使って光空間の座標に変換する。この座標をさらに正規化し、デプスマップのUV座標(画像のどの位置にアクセスするか)とデプス値(光からの距離)に変換する。そして、textureSampleCompare関数を使って、デプスマップに記録されたデプス値と、現在レンダリングしているフラグメントのデプス値を比較する。この比較結果(0.0または1.0)が、そのフラグメントが影になっているかどうかを示す値となる。この影の値を、各ライトの色に掛け合わせることで、最終的なピクセルの色に影を適用する。最初の実装では、影の境界線にギザギザとしたアーティファクト(シャドウアクネ)や、オブジェクトの表面に自己遮蔽による模様(バンディング)が発生した。これは、デプスマップのサンプリングと実際のレンダリングでのデプス値のわずかなずれが原因で、これを軽減するために、デプス比較時に微小な「バイアス値」(オフセット)を加えることで対応した。また、シャドウマップの解像度を上げることで、影のギザギザを減らすこともできる。

複数の光源に対応するために、影の計算ロジックを修正する必要があった。各ライトがそれぞれ独自の影を落とすため、影の計算はライトごとに独立して行われ、その結果をライトごとに適用してから最終的な色に加算する必要がある。そこで、ShadowMappedLightクラスにhasShadowというプロパティを追加し、影を落とすライトと落とさないライトを区別できるようにした。また、WebGPUのバインドグループの最大数の制約を考慮し、シャドウマップのバインディングをライトのバインドグループに統合した。これにより、各ライトの構造体に、そのライト固有の変換行列や、関連するシャドウマップがどのスロットにバインドされているかを示すインデックス情報を含めるように変更した。シェーダーでは、ループを使って各ライトについて影の有無を確認し、影を落とすライトの場合のみ、そのライト固有の変換行列とシャドウマップを使って影の計算を行い、その結果をライトの色に適用するようにした。

影の品質改善に関しては、シャドウアクネやバンディングをさらに軽減するため、バイアス値をライトの方向と法線の内積に基づいて動的に調整する試みを行った。これにより、光が斜めから当たる面での自己遮蔽アーティファクトを抑制できる。最終的には、GPUのレンダリングパイプライン設定でdepthBiasdepthBiasSlopeScaleという値を設定することで、同等の効果をより効率的に得られることが分かった。これらの設定は、デプスマップを生成する際に、GPUが深度値に自動的にオフセットを加える機能である。ただし、これらのバイアス値の適切なチューニングは試行錯誤が必要な場合が多い。

ここまでで、ディレクショナルライトに対するシャドウマップの実装が完了した。これは最も一般的で有用な影のタイプだ。今後は、スポットライト(パースペクティブ変換を利用する)や、ポイントライト(キューブマップを生成するために6方向からのデプスマップが必要)の影の実装、さらにシャドウマップの視覚的な品質を向上させつつパフォーマンスを最適化する「カスケードシャドウマップ」などの発展的な技術を学ぶ余地がある。

関連コンテンツ

関連IT用語