gsplatはWindowsでコンパイラの壁に阻まれたので、コンパイル不要の純PyTorch製3DGSレンダラを書いて、RTX 5090上でend-to-endに学習・新規視点合成まで通した。go2を新規視点(hold-out)PSNR 26.5dB / 約60秒で再構成。最初は過学習で失敗した経緯もそのまま載せる(本記事の主役はここ)。この記事は「うまくいった自慢」ではなく、素朴な発想 → 実装 → 失敗 → 原因究明 → 修正の一連を、追試できる形で残すものです。
COLMAPという道具で推定する。ここが遅くて壊れやすい。| 用語 | かみくだき |
|---|---|
| 3DGS(3D Gaussian Splatting) | 色付きの「ぼんやりした粒」を大量に空間配置し、重ねて画像化する3D表現。学習で粒の位置・形・色・透明度を最適化する。 |
| スプラッティング(splatting) | 3Dの粒を画面に「ぺちゃっと」投影して塗り重ねる描画方式。粒は画面上では2Dの楕円になる。 |
| 姿勢(extrinsics、外部パラメータ) | カメラが世界のどこに、どちらを向いて置かれているか(回転+位置)。4×4行列で表す。 |
| 内部パラメータ(intrinsics、K) | 焦点距離や画像中心など、レンズの中の性質。ここでは画角fovyと解像度から計算する。 |
| COLMAP | 複数枚の写真だけからカメラ姿勢と粗い3D点群を復元する定番ツール。3DGSの前工程だが遅く、失敗もする。 |
| PSNR(ピーク信号対雑音比) | 画像がどれだけ正解に近いかの指標(dB、大きいほど良い)。今回は「学習に使っていない角度」で測る。 |
| hold-out(ホールドアウト、新規視点) | 学習に使わず評価だけに取っておくカメラ。ここが良くないと「丸暗記」しただけになる。 |
| densify(デンシファイ、密度化) | 足りない所に粒を増やす操作。増やしすぎると過学習の原因にもなる(後述)。 |
第1段(イメージ):霧の粒を空間にたくさん浮かべる。手前の粒から順に色を重ねると、粒の集まりが物体の形に見える。粒一つひとつが「位置・大きさ・向き・色・透明度」を持つ。
第2段(何を学習するのか):正解写真(複数角度)と、粒から描いた画像の差を小さくするように、粒のパラメータを少しずつ動かす。これはニューラルネットの学習と同じ「誤差を減らす」作業。ただし学習対象は重みではなく粒そのもの。
第3段(なぜ姿勢が要るのか):粒を「画面のどこに」塗るかは、カメラの位置・向き(姿勢)が分からないと決まらない。実写ではこの姿勢が未知なのでCOLMAPで推定する。シミュレータなら姿勢は真値で既知 ―― ここが今回の出発点。
やることは2つだけ。
MuJoCoのカメラは、実は3DGS/NeRF系が使うOpenGL規約(-Zが前方・+Yが上)と同じ座標系。だからcam_xmat(回転)とcam_xpos(位置)を並べるだけで、そのままtransforms.jsonのtransform_matrixになる。COLMAPは一切登場しない。
# 概念コード(実装はsim_source側)
def camera_to_world(cam):
R = data.cam_xmat[cam].reshape(3, 3) # カメラ軸の世界表現 = c2w回転
t = data.cam_xpos[cam] # カメラ位置
c2w = eye(4); c2w[:3,:3] = R; c2w[:3,3] = t
return c2w # そのままnerfstudio/3DGS形式
# シーンをオービット撮影して 3DGS データセット化
capture_orbit(scene_xml, out_dir, n_views=36, radius=1.3, lookat=(0,0,0.2))
# → out_dir/transforms.json + images/ が出る
姿勢は自己申告では信じない。再投影(reprojection)で機械的に確かめた:
ここが合っていないと以降すべてが崩れるので、最初にここを固めた。
gsplatではなく、純PyTorchで書いたのか(正直な事情)本当は公式の高速実装gsplatを使いたかった。環境を実測した結果:
| 項目 | 結果 |
|---|---|
| GPU | RTX 5090(Blackwell, compute capability 12.0)を認識 |
| PyTorch | torch 2.11 + cu128 でGPU演算OK |
gsplat Pythonパッケージ |
インストールOK |
gsplat のCUDAカーネル |
“No CUDA toolkit found. gsplat will be disabled.” |
gsplatはWindowsでは初回実行時にCUDAカーネルをその場でコンパイルする。そのために CUDAコンパイラnvcc と MSVCコンパイラcl.exe の両方が要る。nvccは入れられたが、cl.exe(C++コンパイラ)の導入は対話的な管理者昇格(UAC)が必要で、無人では通せなかった。加えてWindows向けのビルド済みwheelも存在しない。
そこで方針転換:「まずGPUで3DGSが動く事実」を作るのが先。コンパイル不要の純PyTorch製レンダラなら、いま手元のGPUで即座に検証できる。速度で劣るぶんは小さめのシーンで補い、gsplatは将来の高速バックエンドとして差し替え可能にしておく。
教訓:「速い正解」より「動く事実」を先に置く。土台が動けば、最適化はいつでも足せる。
粒(3Dガウシアン)を画面に塗る手順は、ちゃんと式で書ける。
F = diag(1, -1, -1) で揃える。(x, y, z) を画素 u = f_x·x/z + c_x, v = f_y·y/z + c_y に。Σ = R·S·Sᵀ·Rᵀ(Rは向き、Sは大きさ)。これを投影のヤコビアン J で2Dに落とす:Σ' = J·Σ_cam·Jᵀ(2×2)。画面上の楕円が決まる。α = 不透明度 · exp(-½ dᵀ Σ'⁻¹ d)(dは画素と粒中心の差)。粒を手前から奥へ深度ソートし、重み_i = α_i · Π_{j<i}(1-α_j) で前から重ねる。この「重み」をcumprod(累積積)でベクトル化すれば、粒ごとのループなしにGPUで一気に計算できる。タイル分割は省いた簡略版(=小さめシーン向けのPoC実装)。
単体テスト:世界原点に赤い粒を1個置き、正面カメラで描くと、画像中心±0pxに赤が出て、隅は背景色。投影と合成が正しいことを最小構成で固定した。
go2(MuJoCo Menagerie)図:go2のターンテーブルGIF(学習済みガウシアンを全周描画) / 左=正解・右=新規視点レンダの比較(※Qiita公開時に画像アップロード)
最初のgo2は18視点で回した。ログを見て青ざめた:
[iter 200] train=23.6 test=23.6
[iter 360] train=24.6 test=20.7 ← test だけ下がり始める
[iter 600] train=25.6 test=19.5 ← 学習は上がるのに新規視点は悪化
学習視点は良くなるのに、hold-out(新規視点)はどんどん悪くなる ―― 典型的な過学習。原因は、densifyで増やした粒が「学習した角度でだけ辻褄が合う floater(浮遊ゴミ)」になり、見ていない角度で破綻していた。
honestに直した(3点):
結果、testはtrainに追随して26.5dBまで単調に改善。過学習は消えた。
図:hold-out PSNRの推移(赤破線=18視点で過学習して19.5dBまで低下 / 緑=36視点+early-stopで26.5dBまで改善)。失敗の線を消さずに残すのが本図の主旨。
この「testが下がったら、まずデータ(視点)と正則化(prune/early-stop)を疑う」は、3DGSに限らずあらゆる当てはめで効く教訓。
torch 2.11+cu128 / mujoco / pillow)。既存プロジェクトのCPU版torchはそのまま。gsplatネイティブを動かすには CUDA Toolkit(できれば12.8系でtorchのcu128に合わせる)+ VS Build Toolsの「C++によるデスクトップ開発」が要る。nvccだけでは不足でcl.exeが必須。ここはGUIインストーラ(管理者権限)が現実的。nvccは導入できたがcl.exeが無人導入できず、かつCUDA 13.3とtorch cu128(12.8)の版ズレ懸念もあり、gsplatネイティブは保留。純PyTorchで成果を出した。gsplatも通した ―― Windowsの4つの罠純PyTorch版で成果を出したあと、あきらめていた公式gsplatのネイティブビルドにも成功した(RTX 5090で1秒あたり1532回のラスタライズ)。ここに至るまで、Windows特有の罠を1枚ずつ剥がした。debuggingの記録としてそのまま残す。
| # | 症状 | 真因 | 対処 |
|---|---|---|---|
| 1 | No CUDA toolkit found |
C++コンパイラcl.exeが無い |
VS Build Toolsの「C++によるデスクトップ開発」を導入(管理者権限が要る唯一の手作業) |
| 2 | torchヘッダで型エラー | システムのnvcc 13.3が、torch(CUDA 12.8ビルド)のヘッダと非互換 | nvcc 12.8をconda(micromamba)でユーザー空間に導入(管理者権限も不要、バージョンぴったり一致) |
| 3 | cudaStream_t s, bool small が bool char に化ける |
Windowsのrpcndr.hが#define small char。torch 2.11がパラメータ名にsmallを使っていた(Windowsバグ) |
該当ヘッダに#undef smallを1行 |
| 4 | cl : error D8021 : '/Wno-attributes' |
gsplatの既定にGCC用フラグ-Wno-attributesが入っていて、MSVCが拒否 |
Windowsではそのフラグを外す |
教訓:No CUDA toolkit foundのような大雑把なエラーの奥には、こういう具体的で小さな原因が積み重なっている。エラーを1枚ずつ剥がす――推測で全部を一度に直そうとせず、いま出ている1つの原因だけを確実に潰して再ビルド、を繰り返すのが結局いちばん速い。特に#3は、コンパイラが吐いたbool charという“ありえない字面”をそのまま読んだのが決め手だった(推測せず、出力の生の姿を見る)。
ネイティブ版はタイル分割CUDAなので、純PyTorchの非タイル実装より桁違いに速く、高解像度・多ガウシアンに耐える。go2を256px・2万ガウシアンで学習しても数十秒で回る。
ネイティブビルドの完全な再現手順は付録(リポジトリの
docs/GSPLAT_NATIVE_WINDOWS.md)に置いた。
静止した3DGSの全周プレビューは綺麗だが、ロボットの本当の価値は動きにある。そこで、動く3DGSまで作った。フルの4D Gaussian Splatting(時間条件つきガウシアン)は重いので、別の道を取った:
ロボットは剛体リンクの集合である、という事実を使う。
これは骨格アニメの「スキニング」を、学習した3DGSに対してやるようなもの。姿勢が真値なので破綻しない(実写の動的再構成が難しいのと対照的)。go2 の脚を動かすと、再構成したガウシアンの犬がちゃんと脚を運ぶ。正準学習はわずか4秒、あとは各フレームを剛体変換して描くだけなので軽い。
--motion-file に実際の歩行方策の qpos 軌道を渡せば、本物の歩行を3DGSで再生できる。
ここが「可視化=差別化」の勘所だと思う。実写3DGSは静的シーンが主戦場で、動く被写体は難物。sim は姿勢・深度・材質・接触・関節角のすべてを真値で持っているから、その真値を素直に使うだけで、実写では面倒な動的再構成が自然に手に入る。「ズルできる」ことこそが差別化になる。
gsplatネイティブ化(タイル分割CUDAで桁違いに高速&大規模に)。C++ツールチェーンを入れて版を揃える。物理シミュレータは「真値の宝庫」。姿勢だけでなく、深度・材質・接触・関節角まで全部分かっている。その真値を素直に使うと、実写で難しい前処理が丸ごと消える ―― 今回はその一例でした。
本記事の実装・数式・失敗の記録はすべて実測に基づきます。数値は手元のRTX 5090での再現値で、環境により変わります。
良かったら、いいね♡ をクリックして下さい。次の一本を書く励みになります。