世界を数式で描く 04 / 05 もくもく会トップ
SERIES 世界を数式で描く ― 04

画素を3Dの点に戻す部屋を内側から撮って点群にする

Blender で作った地下通路を、内側の264か所から撮影し、写った画素をすべて3Dの点に戻して、ブラウザで粒子として動かしました。この記事では、その仕組みを分解します。後半のラボでは、わざと計算を取り違えて点群を壊してみます。

この記事で持ち帰る3行
  1. 点群化は、レンダリング(3Dから2Dへの射影)を逆向きにたどる計算
  2. 重い光の計算は撮影時に1回だけ。表示側は色のついた点を描くだけ
  3. Blender では深度とカメラ姿勢の「正解」が分かるので、写真測量でいちばん難しい推定を飛ばせる

実例:部屋を内側から撮って点群にする

きっかけは、X に投稿された作品でした。作者の説明を要約すると、Blender の場面を Claude に264の角度からレンダリングさせ、すべての画素を3Dの点にし、three.js で粒子として動かした、というものです(@brice_deg、2026年10月20日の投稿)。この記事では、これを原作Cと呼びます。

原作Cの作り方は投稿の一文しか書かれていません。そこで、同じ手順を手元で組み直しました。これをC'(シー・プライム)と呼びます。C' のコードは Blender 用の Python(bake_room.py)と、表示用の HTML(room.html)の2本です。

V04-1実例

C' の点群:地下通路を内側から撮って戻したもの

何を見るか同じ場面の、普通のレンダー(左)と、画素を点に戻したもの(右)。右は面ではなく、色のついた点の集まりです。 触ってみよう右の画面をドラッグして向きを変え、スライダーで通路を進む。柱の裏に回ると、どの視点からも見えなかった所が穴になっています。
Blender の Cycles でレンダーした地下通路。濡れた床に蛍光灯が映り、奥の戸口が明るい
Cycles で普通にレンダーした1枚(128サンプル)
読み込むと、間引いた点群(約29万点)を表示します
点群は C' の出力(171万点)を、6cm のマスに1点まで間引き、位置を16ビットに丸めた記事用の版です。点は丸めず四角のまま描いています。
C' の工程中身(bake_room.py の設定)
場面Python で組み立てた地下通路(長さ30m・幅8m・高さ3.2m)。濡れた床、コンクリート、柱と梁、蛍光灯、白線、配管
視点通路の内側に264か所。高さ0.4〜2.6m、向きはランダム(上下は±40°まで)、柱の中は避ける
撮影Blender 5.2.2 の Cycles。透視カメラ・画角80°・128×128画素・24サンプル
画素→点各画素の中心を通る光線を場面に当てて位置を求め、色はレンダー結果から取る
間引き2.5cm の立方体ごとに束ね、位置と色を平均する
結果264 × 128 × 128 = 約433万画素 → 約171万点(25.6MB)。手元の Mac で約3分

射影の往復

レンダリングは、3Dの場面を2Dの画像に写す計算(射影)です。C' はこれを一度逆にたどって画素を3Dに戻し、ブラウザでもう一度、順向きに射影しています。

射影の往復 3Dの部屋をCyclesで射影し光を計算して画像と深度にする。それを逆射影して点群にし、three.jsで射影だけして画面に出す。 3Dの部屋形・材質・照明 画像+深度264枚 点群位置と色だけ 画面毎フレーム Cycles射影+光の計算(重い・1回だけ) 逆射影画素を視線に戻し深度の分だけ進める three.js:射影だけ(光は計算しない)

重い光の計算(どこが明るく、どこに何が映り込むか)は、撮影のときに1回だけ行い、結果を点の色として凍らせています。ブラウザの three.js は光を一切計算せず、色つきの点を画面に写すだけです。ゲームの「ライトマップ」と同じで、計算を実行時から前計算へ移す最適化です。この手法をベイクと呼びます。

画素を視線に戻す行列 K、部屋に戻す行列 T

1つの画素を3Dの点に戻すには、3つの材料がいります。

  1. その画素が、カメラから見てどの方向か:カメラの焦点距離と画像の中心から決まります。これをまとめた行列を K(内部パラメータ)と呼びます。
  2. その方向に、どれだけ進んだ所に面があったか:深度です。
  3. カメラが部屋のどこにあり、どちらを向いていたか:これをまとめた行列を T(外部パラメータ)と呼びます。

手順は「画素 → 視線の方向 → 深度の分だけ進む → 部屋の座標へ移す」の順です。下のラボの画像をクリックすると、選んだ画素でこの計算が実際の数値で表示され、3D表示にはカメラから点までの線が引かれます。

数式で見る
P = T · ( z · K⁻¹ · (u, v, 1)ᵀ )

(u, v) は画素の位置(画素の中心は u + 0.5)、z は深度、K⁻¹ は K の逆行列です。K⁻¹ · (u, v, 1) は「奥行き1のところでの視線の位置」になり、それに z を掛けるとカメラから見た点の座標になります。最後に T(カメラの回転と位置)を掛けると部屋の座標です。

K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]  fx = (画像の幅 / 2) / tan(水平画角 / 2)

ラボでは画角90°・96×96画素なので fx = 48 / tan 45° = 48 です。

V04-2 / V04-3

逆射影ラボ:撮って、戻して、壊して学ぶ

何を見るか数式で定義した部屋(壁に50cm の目地、床は市松模様、立方体と球)を JavaScript で光線追跡して撮影し、その画像を逆射影した点群です。黄色の四角錐がカメラです。 触ってみよう①「深度の種類を取り違える」を入れて、平らな壁が曲がるのを見る。② 視点を1→6→26と増やし、立方体や球の裏の穴が埋まるのを見る。③ 下の画像をクリックして、1つの画素が3Dの点に戻る計算を追う。
ラボは画面に入ると読み込みます。
撮影はラボの中で毎回やり直しています(26視点で約24万画素)。カメラは計算しやすい OpenCV 流の向き(+Z が前・Y が下)で書いています。Blender のカメラは −Z が前・Y が上なので、実物を扱うときは向きの約束を必ず確かめてください。

壊すと何が起きるか

わざと壊す項目起きること理由
深度の種類を取り違える平らな壁が、カメラに向かってふくらむ深度には「視線軸方向の z」と「光線に沿った距離」の2種類がある。z を距離だと思って単位ベクトルに掛けると、画面の端ほど手前に引き寄せられる
輪郭の深度を平均する立方体や球の縁から、点が空中に尾を引く輪郭の画素では前景と背景の深度が混ざり、どちらの面にもない距離になる(flying pixels)
画角を取り違える視点ごとの点群がずれて、二重に見えるK が違うと、視線の方向が視点ごとにずれる。1視点だけだと気づきにくい
色空間を二重に変換する全体が白っぽくなる撮影時に表示用の sRGB にした色を、表示側がもう一度 sRGB に変換している
Z上→Y上の変換を外す部屋が横倒しになるBlender は Z が上、three.js は Y が上。(x, y, z) を (x, z, −y) に写す必要がある

「鏡面の床」を入れると、床の色が視点によって変わります。視点を増やすと、同じ床の点に違う色が重なり、まだらに見えます。「10cm のマスで間引く」を入れると、同じマスの点が1点にまとめられ、色も平均されてまだらがならされます。「点密度のヒートマップ」では、カメラに近い面ほど点が混んでいることが分かります。

深度の定義について:Blender の Cycles は、2020年5月の変更で、透視カメラの深度パスを視線軸方向の z(EEVEE や OpenGL と同じ)にしました。パノラマカメラは変更されず、カメラ中心からの距離のままです(コミット 18cda8be)。使うバージョンとカメラの種類で確かめてください。

何を捨てたか

点群は、場面そのものではなく「特定の視点から見えた光の記録」です。だから、次のものが抜け落ちます。

捨てたもの現れる症状背景にある原理
視点による光の変化反射やハイライトが面に貼りつき、視点を動かすと不自然。複数の角度の色がずれてちらつく光沢の見え方は視点によって変わる。3D Gaussian Splatting が点ごとの色を視点の関数(球面調和関数)にしているのは、この問題を解くため
見えなかった面柱の裏などに穴があく点になるのは見えた所だけ。多くの角度から撮るのは、隠れた所を埋めるため
均一な密度近い面は密、遠い面は疎。重なった視野では点が重複する遠くの1画素ほど広い面積を覆う(透視投影)
面のつながり近づくと点の隙間が見える点は面ではなく、面から取った標本にすぎない

C' では、2.5cm のマスで位置と色を平均しています。濡れた床の映り込みのように視点で変わる色は、ここでならされます。ちらつきは減りますが、映り込みは「床に描かれた模様」になります。

内側から撮る意味:答えが分かっている写真測量

現実のカメラでは、その場で向きを変えるだけでは奥行きが分かりません。視差が生じないからです。奥行きを測るには、カメラを動かして三角測量をする必要があります。

Blender では、深度とカメラ姿勢を正解として取り出せます。だから部屋の中心の一点から撮っても3Dに戻せます。つまり C' は「答え(深度とカメラ姿勢)が最初から分かっている写真測量」です。現実の写真測量や NeRF、3D Gaussian Splatting では、この答えを画像から推定する部分(SfM など)がいちばん難しい所です。C' はその難所を外したので、幾何だけを純粋に体験できます。

部屋の内側から撮ると、すべての画素がどこかの面に当たります。無限遠の背景がないので、点にできない画素がありません。これも、点群化しやすい理由です。

C' の実装で分かったこと

原作Cとの違い

原作C(@brice_deg)C'
作り方の情報作者の説明は投稿の一文のみコード全体が手元にある
視点の数264(作者の説明)264(同じ数にそろえた)
見せ方元のレンダー映像と点群の映像を交互に切り替える編集がある点群だけ

原作Cの映像そのものは、作者の投稿で見てください。この記事では、原作の画像や映像は使っていません。

用語集

射影
3Dの点を2Dの画像の上に写す計算。カメラで撮るのと同じこと
逆射影
画素と深度から、3Dの点を求める計算。射影の逆向き
深度
カメラから面までの奥行き。視線軸方向の z と、光線に沿った距離の2種類がある
内部パラメータ K
焦点距離と画像の中心をまとめた行列。画素を視線の方向に変換する
外部パラメータ T
カメラの位置と向きをまとめた行列。カメラから見た座標を部屋の座標へ移す
点群
位置と色を持つ点の集まり。面やつながりの情報は持たない
ベイク
重い計算を前もって済ませ、結果を固定して保存すること
写真測量
複数の写真から、カメラの位置と物の形を推定する技術
ボクセル
空間を区切った小さな立方体のマス。点を間引くのに使う

この記事の問い

シリーズの問いの地図(00 目次)のうち、この記事が答える問いです。