Ray Tracing from Scratch in Rust ~ 簡易CPUレンダラの自作と仕組みの勉強 ~
はじめに
皆さんこんにちは,一関高専 村上研究室所属 小山田です。
今回は,Ray Tracingの基礎的な仕組みを学ぶために,自作した簡易CPUレンダラについて紹介します.
筆者は,CGの分野とはあまり縁がなかったのですが,最近は3Dシミュレーションを行うことが多いため,実は結構気になっていた分野ではありました.
制作を進める中で,どこがそんなに時間のかかる処理なのか,RTコアの無いGPUはなんでシミュレーションが若干重いのかなど,気になっていた疑問を少しだけ解消できたように思います.

この制作を行う上で,GitHub CopilotとAIをゴリゴリに使っていますし,論文も何も追っていません.
しかし,基礎的な仕組みを理解した気になれる記事になっていると思いますので,興味のある方は是非読んでみて下さい.
なお,これで作成した制作物は,現在筆者が受講しているCGの授業の課題制作として提出予定です😎
制作物 ↓↓↓
ここからは宣伝です.
今年も,記事の文頭にこれが沢山載る季節になりましたね.そうです,アドカレです.
この記事は,一関高専の学生で書いているAdvent Calender 2025の11日目の記事となります.
是非,他の記事もご覧ください !
追記: Rust Advent Calendar 2025 のシリーズ2 13日目にも登録しました.
Rust信者が増えますように🙏
Ray Tracingとは
n番煎じ感が否めませんが,解説します.
Ray Tracingとは,光線(ray)の追跡(tracing)を行うことで,3Dシーンのレンダリングを行う手法です.
従来のレンダリング手法であるラスタライズ法では,3Dシーンを2D画像に変換する際に,ポリゴンをピクセルに変換する過程で,光の反射や屈折,拡散などの複雑な現象を正確に表現することが難しいという問題がありました.
一方,Ray Tracingでは,光の経路をシミュレートすることで,これらの現象をよりリアルに表現することが可能となります.

ここからの処理は,基本的に以下の流れで行われます.
- Rayの生成 (カメラからシーンに向けてRayを発射)
- Rayの追跡
- 面との交差判定
- 反射・屈折・拡散したRayを再度追跡
- これを一定の深さまで繰り返す
- Rayの色の計算 (光を初期のRayへ逆伝搬)
なお想像がつくと思いますが,Rayは非常に大量に発射され,途中で分岐もするため,計算量が膨大になります.
この問題を解消するため,BVHなどの空間分割アルゴリズムが用いられ,木構造を辿ることで交差判定の計算量を削減します.
因みに,BVHで用いられるBounding BoxとRayの交差判定を高速に並列で行うのがRTコアの主な役割です.
参考: Bounding Volume Hierarchy (BVH) の実装 - 構築編
しかし,今回の制作では最適化を一切行わず,単純な全探索(再帰関数)で交差判定を行っていますので,結構重いです.
一つ,筆者の疑問としてあるのは,再帰関数的な処理をGPUで高速化するのはどうやっているのか,という点です.
ループを展開するのは可能だと思いますが,それでもかなり複雑な処理になると思います.
やはり,ゲームエンジンを作ってくれている先人に感謝ですね...🙏
Rayの実装
Rayは,始点(origin)と方向(direction)を持つベクトルとして実装します.
なお光の色はRayの逆伝搬の際に計算するため,Ray自体は色を持ちません.
Rayの生成
Rayの生成は,カメラからシーンに向けて行います.
カメラは,位置(position),方向(direction),上方向(up),画角(fov)を持ち,縦横の幅(width, height)からRayを生成します.
計算としては,以下の順番で行います
- カメラの方向ベクトルと垂直で距離が1離れた仮想画面を仮定し,その画面の右方向(right)と上方向(up)を計算
外積を取ることで,
再度外積を取って,
※入力のupベクトルは画面の上方向と一致しない場合があるため再計算
- 仮想画面の幅と高さを計算
縦画角(fov)から,仮想画面の高さ
仮想画面の幅
※
- 各ピクセルに対応するRayを生成
各ピクセル
画面の中心を原点とした座標系で,[-0.5, 0.5]の範囲に正規化
仮想画面上の位置を計算 (左上が原点,右方向が正,下方向が正の画面座標系)
Anti-Aliasing
各ピクセルに対して一つのRayを生成するだけでは,画像がギザギザになってしまうためAnti-Aliasingを行います.
各ピクセルを更に細かいサブピクセルに分割し,各サブピクセルに対してRayを生成します.
処理としては,
Rayと面の交差判定
今回の実装では,基本的な球体(sphere)と三角形(triangle)の2種類の面との交差判定を実装しています.
球体との交差判定
まず,Rayの始点を

このとき,Ray上の点
この式を変形すると,以下の2次方程式が得られます.
ノルムの2乗を展開すると内積で計算できるため,スカラを係数にもつ
あとは,係数を以下のように設定し,解の公式と判別式を用いて,
最後に,
三角形との交差判定
交差判定にはMöller–Trumboreアルゴリズムを用います.
まず,Rayの始点を
また,三角形の頂点を
そして,交差判定を行う前に,三角形とRayが平行でないかを確認します.
式としては,以下のように表されます.
ここで
そのため,
因みに,スカラー三重積の循環性から,以下のように書き換えることもできます.
筆者的には,面の法線ベクトルとRayの方向ベクトルの内積を求めている,こちらの式の方がイメージしやすかったです.
そして次に,Rayと三角形を含む面との交点
この式には,
最初に,
以下の式には,
-
を求めるu
両辺を
そうすると,
-
を求めるv
両辺を
そうすると,
-
を求めるt
両辺を
そうすると,
最後に,
-
が0以上であること (交点がRayの始点より前にない)t -
(交点が三角形の内部にある)0 \leq u \leq 1 \land 0 \leq v \leq 1 \land u + v \leq 1
Rayの分岐
Rayが面と交差したタイミングで,反射・屈折・拡散などの処理を行い,新たなRayを生成します.
Rayの追跡による色の計算は再帰的に行なわれるため,初めに終了条件を設定します.
なお,終了条件に合致した場合,背景色を返すようにしています.
- 再帰の深さが一定値に達した場合
- カメラから出たRayへの寄与が非常に小さくなった場合
- Rayが面や光源と交差しなかった場合
分岐処理には,Rayと面の法線ベクトルから,次に生成するRayの方向を決定します.
反射
反射は,入射角と等しい角度でRayが跳ね返る現象です.
反射ベクトル
入射ベクトルの
屈折
屈折は,Rayが異なる媒質に入る際に,進行方向が変化する現象です.
屈折の方向は,スネルの法則に基づいて計算されます.
まず,入射角
参考: https://www.optics-words.com/kogaku_kiso/snells-law.html
次に,屈折角が90度を超える場合,全反射が発生するため,それについても考慮します.
以下の式が成り立つ場合,全反射は発生しません.
このとき,
そして,屈折ベクトルが存在することが確認できたら,以下の手順で導出することができます.
反射と同じく,入射ベクトル
次に,接線方向の成分を屈折率に基づいてスケーリングして,屈折後の接線方向の成分
そして,法線方向の成分
最後に,2つのベクトルを加算して屈折ベクトル
拡散
拡散には,一般的にはランダムな方向にRayを散らすことで,面が光を均一に反射する様子をシミュレートします.
しかし,今回は簡易的に,法線ベクトルを方向とするRayを生成しています.
再衝突の防止
Rayが面と交差した直後に,その面と再度交差してしまう問題を防止するために,交差点から法線方向に微小なオフセットを加えた位置から新たなRayを発射します.
色の計算
色の話をする前に,光の2分類について説明します.
光には,大きく分けて直接光(direct light)と間接光(indirect light)の2種類があります.
直接光とは,光源から直接物体に届く光のことを指します.
一方,間接光とは,物体から反射・屈折した後に他の物体に届く光のことを指します.
直接光の計算
直接光の計算は,Rayが面と交差した際にその交差点から光源に向けてShadow Rayを発射することで行います.
Shadow Rayが他の物体と交差しなかった場合,その光源からの光が直接交差点に届いていると判定します.
このとき,その光源からの光
この実装では,素材の色をalbedoで表しています.
これをすべての光源に対して計算し,合計したものが直接光の寄与となります.
間接光の計算
間接光の計算は,Rayが面と交差した際に生成された新たなRayを再帰的に追跡することで行います.
最終的には,各Rayに対して,係数をかけて合計することで,間接光の寄与を計算します.
最終的な色の計算
最終的な色
ただし,ここで注意なのが,ここで出てくる色の合計値はあくまでRayと面の交点(Rayの終点)における色であり,Rayの始点にその色が届くまでには距離に応じた減衰が発生するという点です.
(e.g. 夕日が赤いのは,長距離を通過する際に青い成分が散乱されてしまうため)

そのため,Rayが通過するマテリアルの吸収係数
ここで,
最終的な色
簡略化した実装
因みに,普通は物質を金属と非金属に分けるようです.
ざっくり説明すると,金属は光を一度吸収してから反射するため,反射光の色が物質の色に強く影響されます.
一方,非金属は光を吸収せずに反射するため,反射光の色が物質の色にあまり影響されません.
画像への出力
最終的に,各ピクセルに対して計算された色を画像として出力します.
しかしこの実装では,実際の光に基づいて色を計算しているため,色の値が0から1の範囲を超えることがあります.
これを,ハイダイナミックレンジ(HDR)と呼びます.
そのため,最終的な画像に出力する前に,トーンマッピングを行い,色の値を0から1の範囲に収めます.今回は,以下の3つの手法を実装しています.
- Reinhard
- Exposure
- ACES Filmic
Reinhard
Reinhardトーンマッピングは,以下の式で表されます.
パラメタが無いため,簡単に実装・利用できます.
Exposure
Exposureトーンマッピングは,以下の式で表されます.
ここで,
係数を調整することで,白飛びや黒つぶれを防ぐことができます.
ACES Filmic
ACES Filmicトーンマッピングは,以下の式で表されます.
e.g.
参考: https://hikita12312.hatenablog.com/entry/2017/08/27/002859
おわりに
今回は,自作した簡易CPUレンダラについて紹介しました.
本当にAI様様で,かなり短時間でここまでRay Tracingの基礎的な仕組みを理解できたことに驚いています.
よく考えてみれば,ゲームエンジンを作っている方たちは,こういった複雑な処理を高速に行えるようにしてくれているだけでなく,更に物理演算までやってくれているわけですから,本当に感謝しかないですね🙏
とりあえず,日々感謝しなががら,シミュレータを使わせていただきます🙇♂
今後の展望としては,GPUでの実装や,BVHなどの最適化手法も少しは勉強してみたいと思っています.(いつになるやら...)
以上!!
Discussion