リアルタイムレンダリングの基礎数学|PBRとシェーダーが読める最小限

リアルタイムレンダリングの解説記事を読んでいて、「内積」「正規化」「ワールド空間」「ビュー空間」といった言葉が出てくると、つい流し読みしてしまった経験はないでしょうか。本記事は、それらの用語を「数式の証明」ではなく 「グラフィックスで何のために使われているか」 という軸だけで整理する、語彙集です。

PBR(CR-01) を読み終えた次の一冊として、Material Editor(Unreal のマテリアル編集UI)や Shader Graph(Unity のシェーダ編集UI)、各種ドキュメントを読み解くための最低限の数学を、一気に押さえます。導出や証明には踏み込みません。

夕宮たいだ

ふぁ……みんな〜、今日は数学のおはなしだよぉ。といっても、計算問題はやらないからぁ。「なんのために使われてるか」だけ、いっしょに眺めていこ〜。

目次

1. なぜグラフィックスに数学が必要なのか

ひとことで:CG は「光と物体を数値で扱う仕組み」だからです。

3DCG は、突き詰めればメモリ上にある「点・面・色・光」の数値を操作する作業です。点の位置や面の向きはベクトルで、それらを動かす操作は行列で表現されます。シェーダーや Material Editor のノードも、内部はベクトルと行列の組み合わせでしかありません。

この共通言語を最低限知らないと、公式ドキュメントやチュートリアルの解説で頻繁に止まることになります。逆に「内積・外積・正規化・行列・座標空間」の5語の意味を直感で押さえれば、ほとんどの記事は読み進められます。PBR(CR-01)で登場した「拡散反射」「鏡面反射」も、シェーダーが実際に行うのはベクトル同士の掛け算と足し算です。

2. ベクトルの基本:内積・外積・正規化

ひとことで:方向と長さをまとめて運ぶ「矢印」です。

ベクトルは「向き+長さ」を持つ矢印として理解します。

  • 位置ベクトル:原点からその点までを指す矢印(点として扱う)
  • 方向ベクトル:「どちらを向いているか」だけが本質の矢印

法線(Normal)、ライト方向(Light Direction)、視線方向(View Direction)など、シェーダーで頻出するベクトルはほぼすべて方向ベクトルです。

内積(Dot Product)

ひとことで:ふたつの矢印が「どれくらい同じ方向を向いているか」を1つの数値にしたものです。

内積は次の式で表されます。

A · B = |A| × |B| × cosθ

ここで θ は2本のベクトルがなす角度です。両者を「正規化済み(長さ=1)」にしておけば、結果は cosθ そのものになります。

  • θ = 0°(同じ方向)→ +1
  • θ = 90°(直角)→ 0
  • θ = 180°(逆方向)→ -1

シェーダーで dot(N, L) という記述が出てきたら、「面の法線 N と光の向き L、どれくらい揃ってる?」という意味だと読み替えてください。揃っているほど面は明るく、ズレるほど暗くなる──これが後述するランバート反射の根っこになります。次の図で、2本のベクトルがなす角 θ と内積の関係を確認できます。

内積と角度の関係。2本のベクトルが揃うほどdot値が大きくなり面が明るくなる
図1:内積は2本のベクトルの揃い具合を表す
夕宮たいだ

ほよ? PBRの解説で見た cosθ って、これのことだったんだぁ。たんなる角度じゃなくて「揃い具合スコア」って思うとわかりやすいねぇ。

外積(Cross Product)

ひとことで:ふたつのベクトルの両方に垂直な、3本目のベクトルを作る計算です。

外積 A × B は、A と B の両方に対して直角なベクトルを返します。長さは「A と B が作る平行四辺形の面積」に等しいというおまけ付きですが、グラフィックスで覚えておくべきは「両方に垂直なベクトルが手に入る」という効果のほうです。

代表的な使い道は次のとおりです。

  • タンジェント空間の構築CR-04 ノーマルマップとタンジェント空間 で詳説):法線(Normal、面の真上方向)と接線(Tangent、面に沿うU方向)から従法線(バイノーマル:上記2つに垂直なV方向のベクトル)を外積で求める
  • 三角形の面の法線計算:3頂点のうち2辺ベクトルの外積で得られる
  • UV平面の方向計算

外積 A×B が平面に対して垂直に立つ様子を、下図で示します。

外積A×Bが2本のベクトルに垂直な3本目のベクトルを作る概念図
図2:外積は面に垂直な方向を作る
夕宮たいだ

ふふ、外積って一見地味なんだけどぉ……ノーマルマップの仕組みを支えてるの、これなんだよぉ。実は便利でしょ?

正規化(Normalize)

ひとことで:長さを 1 に揃えて、方向だけ取り出す操作です。

ベクトルは「向き × 長さ」の情報を持ちますが、シェーダーで欲しいのは「方向」だけというケースが大半です。光の向きは「どちらから来るか」さえわかれば足り、法線も「面がどっちを向いているか」が本質です。

そこで normalize() を通して長さを 1 にしてから内積などに使います。これを忘れると、内積の結果が綺麗な cosθ ではなく |A||B|cosθ のままになり、明るさが暴れます。正規化忘れは、シェーダーが「なぜか想定どおりに光らない」ときの初手の容疑者です。

3. 行列とトランスフォーム:4×4行列の正体

ひとことで:「移動・回転・スケール・投影」を1つの箱で持ち運ぶ仕組みです。

オブジェクトを動かしたい・回したい・拡大したいときに使うのが、4×4 の行列です。なぜ 4×4 なのかを軽く触れておくと、3D の「平行移動」を行列で扱うために1次元ぶん多めにとってある(同次座標系)ためですが、要素の中身までは本記事では踏み込みません。

「行列は箱で、ベクトルを通すと、動かしたり回したりした結果が出てくる」という理解で十分です。

1つの行列で複数の変換をまとめられる

ワールド・ビュー・プロジェクションの変換行列のように、行列は「移動と回転とスケールをまとめてかける」操作を一発で書けます。

頂点シェーダーでよく見る mvp * positionmvp は、Model(ワールド)・View(ビュー)・Projection(プロジェクション)の3つの行列を合成したものです。1つのキューブが行列によって平行移動・回転・スケール・複合変化していく様子を、下図で並べて示します。

4×4行列によってキューブが移動・回転・スケールされる流れ
図3:行列は複数のトランスフォームをまとめて扱える

順序を間違えると別物になる

行列の掛け算は、順序が大事です。A × BB × A は基本的に違う結果になります。

具体例として、

  • ① 回転 → ② 平行移動 の順だと、オブジェクトはその場で回ってから移動する
  • ① 平行移動 → ② 回転 の順だと、移動した先を中心にもう一度回ってしまい、ぐるっと弧を描いた位置に飛ぶ

ボーンのトランスフォーム、親子関係の更新、カメラの動きなど、合成順序のミスは「想定外の場所にモデルが飛ぶ」事故の常連です。

回転してから移動する場合と移動してから回転する場合で結果が変わる比較図
図4:行列の掛け算は順序で結果が変わる
夕宮たいだ

順番、絶対まちがえちゃダメだよ! 「回ってから動かす」と「動かしてから回す」、まったく別物になっちゃうからぁ……ほんと、よく事故るんだぁ。

投影行列のざっくり理解

最終的に画面に表示するには、3D の座標を2D に「潰す」必要があります。これを行うのが投影行列です。

「奥のものは小さく、手前のものは大きく」という遠近感もこの行列が作ります。直交投影と透視投影で別の行列を使い分けます。

4. 座標空間の階段:オブジェクト→ワールド→ビュー→クリップ

ひとことで:「同じ点でも、どの空間にいるかで数値が違う」ことを認める仕組みです。

頂点シェーダーで一番つまずきやすいのが、この空間の話です。3D の頂点は、おおむね次の階段を上っていきます。

1. オブジェクト空間(ローカル空間):モデル単体の中での座標。原点はモデル内部 2. ワールド空間:シーン全体の中での座標。複数のモデルを同じ尺度で配置 3. ビュー空間(カメラ空間):カメラを原点としたときの座標 4. クリップ空間:投影行列を通した後。画面の縦横に正規化された箱の中 5. (その後スクリーン空間に展開され、ピクセルになる)

オブジェクト空間からスクリーン空間まで頂点座標が変換される階段図
図5:頂点は複数の座標空間を順番に通る

たとえばキャラクターの「頭の頂点」は、オブジェクト空間では原点近くの (0, 1.6, 0) かもしれませんが、ワールド空間ではシーンに置かれた位置 (12.4, 4.5, -7.8)、ビュー空間ではカメラ正面の手前 5m などに変わります。同じ頂点なのに、空間ごとに数値はまったく異なるのがポイントです。

なぜ階段にするのか

各空間には「これをやるのに都合がいい」理由があります。

  • 別々のモデルが別々の DCC で作られても、ワールドに置けば同じ尺度で扱える
  • カメラがどこを向いていても、ビュー空間に持ち込めば「カメラからの距離・向き」がわかる
  • 投影行列を通せば、画面に出していい範囲か(クリップ判定)が単純な比較で済む

シェーダーは、その間を行列で行き来しているだけです。

夕宮たいだ

うぐぅ……空間が4つもあるの、むずかしいねぇ。でもね「モデル作る人」「シーンに置く人」「カメラを構える人」「画面に映す人」がそれぞれ別の都合を持ってる、って思うと案外すっきりしてくるんだぁ。

DCC とエンジンで規約が違う

DCC ソフトとゲームエンジンでは、座標系の規約に差があります。

ソフト手系上方向
Maya右手系Y-up
Houdini右手系Y-up
Unity左手系Y-up
Unreal Engine左手系Z-up

この差は「インポートしたら向きがおかしい」「軸が反転する」といった事故の原因です。詳細は CR-04 ノーマルマップとタンジェント空間CR-13 ライティング基礎 で扱います。

5. 光の式の最低限:ランバートとフレネル

ひとことで:シェーダーが面の明るさを決める、いちばん基本のレシピです。

シェーダーが書いている計算は、突き詰めれば「この面はどれくらい明るいか?」を決める式です。最初に押さえるのは2本だけで十分です。

ランバート反射

ひとことで:「光と面が90°より傾いていくほど暗くなる」という直感を式にしたものです。

ランバート反射は、拡散反射の最も単純なモデルです。

diffuse = max(0, dot(N, L))

  • N:面の法線
  • L:面から光源へ向かうベクトル

光と法線が真正面(0°)で揃えば +1 で最も明るく、直角(90°)で 0、後ろ側(90°超)は max(0, …) で 0 にクランプします。光が真上・斜め45°・浅い角度から面に当たる場合の明るさを、下図に並べて示します。

ランバート反射とフレネル効果の概念図。光と法線の角度で明るさと反射が変わる
図6:角度が変わると明るさと反射の見え方が変わる

これが PBR の Diffuse 項(拡散反射の項)の根っこです。Disney BRDF(Disney 社が論文発表した汎用 BRDF モデル。Unreal の標準ベース)や Burley(Disney BRDF の提案者 Brent Burley の名前を冠した式群)といった式も、ランバートを基準に「もう少し凝った表現」を足し引きしているだけです。

反射ベクトル

ひとことで:鏡のように光が跳ね返る方向を求める式です。

ハイライトの計算には、光が面で反射した先の方向が必要です。

R = L - 2 × dot(N, L) × N

この R と視線ベクトルの内積を取ると、「その方向にどれくらいハイライトが乗るか」が出ます。Phong(フォン:1975年に発表された古典的なハイライト計算式)から GGX(現代の PBR で標準的に使われている、より物理的に正確な分布関数)系まで、ハイライト計算の起点はこの反射ベクトルです。

フレネル

ひとことで:「浅い角度から見るほど、表面の反射率が上がる」現象の名前です。

水面・床・ガラスなどを観察すると、真上から見るより視線を寝かせて見るほうが反射が強く感じられます。これがフレネル現象です。

PBR の鏡面反射計算にはほぼ必ずフレネル項が含まれますが、式(Schlick の近似:シュリック近似と読む。本物のフレネル式を簡略化した実用版)の暗記は不要です。「浅い角度ほど反射が強い」という現象名だけ覚えておけば、シェーダーグラフで Fresnel ノードを見たときに迷いません。

夕宮たいだ

ここまで来ると、Material Editor の Dot とか Fresnel とかのノード、もう怖くないねぇ。みんな、上で出てきた話の名前がそのまま貼ってあるだけなんだぁ。

6. シェーダーグラフで「読める」ようになる練習

ひとことで:実機シェーダーは、ここまでの数学の組み合わせでできています。

シェーダーエディタ(UE Material Editor / Unity Shader Graph)の頻出ノードを、本記事の語彙で読み替えます。

Dot Product ノード

2つのベクトル入力を取り、スカラー1つを返します。本記事で言う内積です。「面の法線」と「光の方向」を入れれば、それがランバート反射そのものです。

Normalize ノード

ベクトル入力を受けて、長さを1に整えて返します。後段で Dot を取るための前処理として頻繁に置きます。

Transform ノード

座標空間を切り替えるノードです(例:Tangent Space → World Space)。本記事の「座標空間の階段」が、UI のドロップダウンに直接出てきている場面です。

最小例

「ライトの当たり加減で明るさが変わる最小のマテリアル」は、次のように作れます。

1. World Normal を取り出す 2. 光源方向(Directional Light の Vector)を Normalize する 3. 1 と 2 を Dot Product に通す 4. 結果を BaseColor に乗算してエミッシブに出力

これで、面が光に向いているほど明るくなるシェーダーの完成です。中身は完全に「内積+正規化」だけです。

7. ハンズオン演習

ひとことで:紙とペンと、実機の両方で「同じこと」を確認しましょう。

演習1:紙とペンで2Dの内積

ベクトル A = (1, 0)B = (1, 1) のとき、内積を計算してみてください。

A · B = 1×1 + 0×1 = 1

|A| = 1|B| = √2 なので cosθ = 1/√2、つまり θ = 45° です。「(1,1) は (1,0) から 45° 傾いている」という直感と一致することを、自分の手で確かめておきます。

演習2:Material Editor で Dot(N, L) を可視化

UE / Unity のいずれかで、World Normal と Light Vector の Dot Product だけを Emissive に出力するマテリアルを組みます。

  • ライトを真上から当てる → 上面が白く、側面が黒くなる
  • ライトを横から当てる → 当たっている側だけ白くなる

数式が「画面の見え方」と直結することを、目で確認しておきます。

8. チェックリスト

ひとことで:本記事を理解できたかをセルフチェックする項目です。

  • [ ] 内積を「ふたつのベクトルの揃い具合」と説明できる
  • [ ] 外積で「両方に垂直なベクトル」が手に入ることを説明できる
  • [ ] 正規化が「長さを1にすること」と即答できる
  • [ ] 4×4行列が「移動・回転・スケール・投影をまとめる箱」だと説明できる
  • [ ] 行列の掛け算は順序が大事だと説明できる
  • [ ] オブジェクト・ワールド・ビュー・クリップの4空間の役割を、ざっくり言える
  • [ ] ランバート反射が max(0, dot(N, L)) であると思い出せる
  • [ ] フレネルが「浅い角度から見るほど反射が強くなる現象」だと言える

9. よくある間違い・トラブルシュート

ひとことで:正規化忘れ・順序ミス・空間取り違えの3大事故が定番です。

正規化を忘れる

長さがバラバラのベクトル同士で内積を取ると、結果がスケール込みの値になり、明るさやハイライトが暴れます。Dot の前には常に Normalize を置く、と覚えておくと事故が減ります。

行列の合成順序を間違える

「移動してから回転」と「回転してから移動」を取り違えると、モデルが想定外の場所に飛びます。親子関係(ボーン、リグ)の組み立てで特に頻発します。

空間を取り違えてベクトル演算する

オブジェクト空間の法線とワールド空間のライト方向で内積を取ると、結果は意味を失います。Material Editor で World NormalWorld Light Vector のように、両方を同じ空間に揃えてから計算するのが鉄則です。

DCC とエンジンの規約差を見落とす

「Maya は Y-up・右手系」「UE(Unreal Engine)は Z-up・左手系」のような差を踏まえずに FBX(3Dデータ交換用ファイル形式の業界標準)をエクスポートすると、軸が入れ替わったり、メッシュが裏返ったりします。詳しくは CR-04 ノーマルマップとタンジェント空間 で扱います。

夕宮たいだ

ふぁ……これで、リアルタイムレンダリングを読み解くための「語彙集」はそろったかなぁ。次の記事で、いよいよノーマルマップとかタンジェント空間に入っていくよぉ。

10. 次に読む記事

ひとことで:本記事の語彙を使って、より具体的なテーマに進みましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次