Computer Vision · 3D Reconstruction

Reconstructing an Entire Movie Song as a 3D World

How we turned the Pinga song courtyard from Bajirao Mastani into a walkable 3D set — and all the things that broke along the way.

Sai Sri Teja Kuppa September 2026 ~18 min read

Making video — the reconstructed set

Before diving into the pipeline, here's the end result: a walkthrough of the 3D courtyard we rebuilt from the song footage.

Watch on YouTube — making-of video showing the navigable 3D set.

This project starts from one music video. The courtyard set in Pinga from Bajirao Mastani — stone arches, lanterns, a big tree, performers dancing — is beautiful on screen. We wanted something ambitious: rebuild that entire set in 3D so you could move a virtual camera through it, not just stare at one photo.

Watch on YouTube — the original song; the courtyard set we reconstructed appears throughout.

Watch for the hard cuts, close-ups on the performers, and how the camera never sits still long enough to give you a clean multi-view scan of the environment. That's exactly the problem we're solving.

Key Idea

Everything shown here comes from our pinga_set/ output folder, built with HY-World 2.0 (HunyuanWorld). Pipeline code lives in blog_informtion/code/ — HY-World stages in code/stages/, panorama generator in code/panogen/.

01

The Goal

Goal: One coherent 3D environment reconstructed from an entire song.

What we actually have as input: Hundreds of unrelated 2D frames, most of which are useless for geometry.

What we needWhat a song gives us
Overlapping views of the same wallsHard cuts to a completely different angle
Static scene geometryActors moving through every shot
Smooth camera motionJump cuts, zooms, shake
Full set visibleClose-ups where you can't even see the floor

So the real question became:

Forget reconstructing every frame. Can we find a handful of frames that, together, show us the whole set?

That shift in thinking is basically the whole project.

02

Starting From Movie Frames

A movie song isn't a clean 360° capture. It's a heavily edited music video — hard cuts, close-ups on actors, camera shake, zooms, and shots where 80% of the frame is a person, not the set. The original frames are not sufficient for reconstruction because most of the scene is observed from disjoint viewpoints with incomplete geometry.

The standard reconstruction pipeline

The generic, traditional way to build a 3D set from video looks like this:

Video frames
↓
COLMAP — structure from motion
↓
Camera poses + sparse point cloud
↓
NeRF or 3D Gaussian Splatting
↓
Navigable 3D scene

COLMAP finds correspondences across frames, estimates where the camera was for each shot, and builds a sparse 3D point cloud. Those poses then feed into NeRF or 3DGS, which densify the representation into something you can render from arbitrary viewpoints. On a smooth walk-through video — a real-estate tour, a drone orbit, a handheld scan — this works well.

Why COLMAP breaks on song footage

COLMAP assumes consecutive frames share overlapping views of the same geometry. A Bollywood song violates every assumption in that chain:

  • Hard cuts jump to a completely different angle with zero overlap
  • Close-ups fill the frame with a face, not the set
  • Moving actors break static-scene correspondence
  • Zooms and shake add motion that isn't smooth camera travel

We never got a usable sparse reconstruction from raw Pinga footage — there's no COLMAP output in pinga_set. Feature matching fails when shots don't overlap, and even the frames that do match often disagree because the scene content changed between cuts.

Limitation

COLMAP isn't broken. It's built for continuous capture. Feeding it an edited music video without preprocessing is asking it to solve a problem it was never designed for.

Learning-based alternatives — VGGT and Depth Anything V3

Knowing COLMAP wouldn't work out of the box, we went deeper into learning-based geometry methods that don't rely on explicit feature matching.

VGGT (Visual Geometry Grounded Transformer) can predict camera poses and depth directly from images, skipping traditional SfM. We ran it on the raw Pinga song. Results were bad — shots don't overlap, cuts break temporal correspondence, and half the frames are face close-ups. No coherent camera trajectory emerged.

Depth Anything V3 was the next attempt — dense monocular depth without needing multi-view overlap. It produces reasonable per-frame depth maps, but depth alone doesn't give you a unified 3D set. Without consistent camera poses tying the frames together, each shot lives in its own coordinate frame. The fundamental problem remains: the views are too sparse and too non-overlapping to fuse into one scene.

Observation

Both COLMAP-style SfM and modern learned geometry models fail for the same reason — not enough shared visual evidence across shots. The bottleneck isn't the reconstruction algorithm. It's the input footage.

Native video models — single frame + trajectory control

Another class of approach skips multi-view reconstruction entirely: native video generation models that take a single input frame and a camera trajectory, then synthesize a video along that path. Trajectory control is specified either through text prompts or keyboard commands (pan left/right, orbit, etc.).

We ran the same Pinga courtyard frame through two such systems to see what a one-image baseline looks like before any panorama stitching:

LingBot
360° sweep from one frame
HY-World 1.5
360° orbit
HY-World 1.5
180° pan left

These outputs look plausible for a short camera move, but everything beyond the original viewing angle is generated — same fundamental limitation as single-image panorama methods. They are useful baselines; they are not a substitute for building reference views with real coverage of the set.

Why a single generated view isn't enough either

Even world-generation models that skip multi-view input entirely — like HY-World 2.0 with a single image — hit the same wall from a different angle. One frame from the song might show you the front arch and nothing else. The back of the courtyard? Never seen. The left wall? Blocked by a dancer. Run world generation on that and the model just makes stuff up — and it'll contradict the next shot in the song.

We need four major regions of the set (front, sides, back) to have real visual evidence before any 3D model touches it. We ran both a single-image and a multi-image approach on the same Pinga courtyard to prove this:

1 image (songs_set/)~4 images (pinga_set/)
InputOne song frame → HY-Pano 2.0~4 frames → manual stitch
PanoramaAI hallucinates unseen sidesReal coverage from multiple angles
Downstream 3DFull pipeline, weak geometryFull pipeline, real evidence
Single movie frame — one angle of the courtyard
1 image — songs_set/side_view.png
View 0 from 4-side stitch View 1 from 4-side stitch View 2 from 4-side stitch
~4 images — cardinal views from pinga_set stitch
Key Idea

Every approach we tried — COLMAP, VGGT, Depth Anything V3, single-image generation — fails unless someone first curates the input. The next section is about how we did that.

03

Creating Better Reference Views

We stopped asking "can AI reconstruct the whole song?" and started asking "what's the minimum set of good observations we need?" The honest part: humans pick the frames and verify the geometry. Models handle the scale work after that.

Step 1 — Picking the right frames

Automatic frame selection failed — too many cuts, too many actor shots. So we manually picked about four complementary views that together cover the courtyard from different angles.

View bin 0 — front region of the courtyard
View 0
View bin 1 — side region of the courtyard
View 1
View bin 2 — rear/alternate region
View 2

Each bin gets its own camera trajectories later. Unrelated song shots never get mixed together — that's the whole point of binning.

Step 2 — Building the panorama

The original framing was often too tight or actor-centric. We therefore introduced an intermediate step to obtain wider, more spatially useful references before stitching.

~4 song frames
↓
Wider horizontal views
↓
MoGe v2 depth
↓
Manual point-cloud stitch
↓
Inpaint gaps
↓
panorama.png
  1. Pick ~4 frames from the song — each showing a different part of the courtyard
  2. Generate wider horizontal views where the original framing was too tight or actor-centric
  3. Run MoGe v2 for dense metric depth on each view
  4. Stitch point clouds by hand — we verified alignment ourselves instead of trusting automatic fusion
  5. Inpaint remaining gaps in the stitched equirectangular panorama
MoGe v2 depth maps for the four cardinal views used in panorama stitching
MoGe v2 depth for each of the four cardinal views — the metric depth maps we used to align and stitch the point clouds before building the equirectangular panorama.

Panorama outputs — same next step, very different starting point

Both panoramas went through the same HY-World 2.0 pipeline. The difference is what went in.

HY-Pano panorama from single image
❌ 1 image → HY-Pano 2.0
songs_set/panorama.png
Custom 4-side stitched panorama
✅ 4 sides → manual stitch
pinga_set/panorama.png

Look at the single-image panorama — it looks like a courtyard at first glance. But large sections were never in the original frame. HY-Pano invented the back wall, the opposite side, details that may not match other shots in the song. The 4-side version has photographic evidence behind each direction.

1 image (HY-Pano)~4 images (custom stitch)
What's realOne viewing angleFront, sides, back observed
What's generated~75% of the panoramaOnly gaps between stitched views
GeometryDepth guessed from hallucinationsDepth from MoGe on real frames
Matches the song?Often contradicts other shotsBuilt from frames picked from the song
Good for a quick test?Yes—
Good for set reconstruction?No — we abandoned thisYes — this is what we used
Final 4-side stitched panorama — HY-World pipeline input
Final pipeline input — pinga_set/panorama.png

The quick-test alternative — single frame through HY-Pano 2.0 (code/panogen/) — lives in songs_set/. Fine for prototyping; not what we used for the final reconstruction.

04

From 2D Images to a 3D World

Once we have a trustworthy panorama, HY-World 2.0 takes over. The transition follows a clear chain:

2D reference panorama
↓
Depth / geometry estimation (MoGe)
↓
Global point cloud + mesh
↓
View sampling + camera planning
↓
3D representation

HY-World samples views from the panorama

Stage 1 (traj_generate.py) samples orbit and polar views to build depth banks and plan camera paths.

Cardinal views (used as reference frames for trajectories):

Cardinal view 0
Cardinal 0
Cardinal view 1
Cardinal 1
Cardinal view 2
Cardinal 2

Orbit + polar samples (27 horizontal + 16 vertical views for depth coverage):

Pano orbit view 0
Orbit 0°
Pano orbit view 180
Orbit ~180°
Polar view looking up
Polar up
Polar view looking down
Polar down

From here HY-World also builds a mesh, detects objects, and plans 33 camera trajectories across the scene.

05

Scene Reconstruction

Segmentation + masks

HY-World runs a VLM to figure out what's in the scene — tree, pillars, benches, arches, etc. That drives object-targeted camera paths ("fly toward the tree").

Object segmentation overlay on the panorama
Segmentation (11 objects)
Sky mask — white region is sky
Sky mask
Valid geometry mask for view0
Valid geometry mask

Detected objects: tree, building, door, statue, bench, fence, lamp, sheds, staircase, pillar, trash can.

It gets worse downstream

Same pipeline, same camera path (view0/traj0) — only the panorama input changed. Garbage in, garbage out — but it compounds. A weak panorama poisons depth, mesh, trajectories, and every inpainted frame.

Segmentation from single-image panorama
1 image → segmentation
Segmentation from 4-side panorama
4 images → segmentation

1 image → WorldStereo

songs_set/view0/traj0

4 images → WorldStereo

pinga_set/view0/traj0

Camera trajectories

At this point we have a 3D mesh of the courtyard. What we actually need is video: a sequence of frames shot from many different camera positions, as if someone walked through the set with a camera. For Pinga, HY-World planned 33 trajectories total — 693 camera poses (33 × 21 frames).

Top-down navmesh view showing all planned camera trajectories
All planned paths visualized on the navmesh

Phase A — Simple pans from the panorama

HY-World splits the input panorama into three cardinal views and generates simple rotation paths from each one. Each view bin gets up to 3 trajectories:

TrajectoryMotionWhat you'd see
traj0Pan right (~120°)Camera rotates right from the reference frame
traj1Pan left (~120°)Camera rotates left
traj2Tilt up / aerialCamera lifts and looks down slightly

That's 9 trajectories (3 views × 3 moves). Paths are checked against the mesh so the camera doesn't clip through geometry.

Phase B — Finding objects to look at (VLM + SAM3)

Panorama image
    ↓
Qwen3-VL (VLM) — "list the objects you'd navigate toward"
    ↓  →  objects.json (tree, pillar, bench, building, door…)
SAM3 segmentation — draw a mask around each object
    ↓  →  target_camera.json (2D/3D position, direction, size per object)
Navmesh path planning — compute routes on the walkable surface

For Pinga, the top-ranked object was the tree (score 0.92, direction: Back). Pillars and benches ranked next.

Phase C — Navmesh path planning (four modes)

ModeQuestion it asksOutput foldersCount (Pinga)
Surround"Can we orbit around this object?"target_tree_0, target_pillar_1…8
Exploration"What if we wander in 8 directions?"wonder_0, wonder_1, wonder_76
Reconstruct"Can we revisit under-covered areas?"reconstruct_1, reconstruct_3…10
Target"Walk straight toward each object"Planned & saved, not rendered—
Exploration navmesh map
Explore
8 sectors → wonder_*
Reconstruct navmesh map
Reconstruct
reconstruct_*
Surround navmesh map
Surround
target_* folders
Target navmesh map
Target
planned, not rendered

What a trajectory folder contains

render_results/view0/traj0/
├── start_frame.png       ← first frame (conditioning image)
├── camera.json           ← 21 w2c poses + intrinsics
├── render.mp4            ← point-cloud splat video (Stage 2)
├── render_mask.mp4       ← white = holes to fill
├── worldstereo-memory-dmd_result.mp4  ← inpainted result (Stage 3)
├── traj_caption.json     ← VLM-written scene description
└── memory_inputs/        ← panorama references WorldStereo used

Mechanical pipeline — how 33 trajectories are produced

All of this is produced inside traj_generate.py in two major stages. Inventory: 9 cardinal pans + 8 object orbits + 10 reconstruct paths + 6 exploration paths = 33, across 16 folder groups under pinga_set/render_results/.

Step 0 — Shared setup (before any trajectory exists):

panorama.png
  → MoGe depth prediction        → full_depth_prediction.pt
  → global point cloud + mesh    → global_pcd.ply, global_mesh.ply
  → pano_bank (27 orbit samples) → render_results/pano_bank/
  → polar_bank (16 elevation)    → render_results/polar_bank/

The navmesh is built from global_mesh.ply using Recast: mesh surface → walkable triangles → dense graph (NavMeshGraph, sample spacing 0.05 m). Every walking trajectory is constrained to this graph. The camera origin is (0, 0, 0).

Stage A — Cardinal rotation trajectories (9 paths): The equirectangular panorama is split into 3 horizontal look directions at 120° spacing (split_view_num=3). For each split, HY-World renders a pinhole start_frame.png (832×480), projects the global PCD, and applies three motion templates: right-rotation, left-rotation, up-right-aerial. get_c2w() interpolates 20 intermediate poses and queries a KD-tree on the global mesh — trajectories that would clip through geometry are discarded.

Stage B — Navmesh trajectories (24 paths): Qwen3-VL lists objects → SAM3 masks → depth lift to 3D anchor points. Four path generators run from the origin via Dijkstra. Every navmesh polyline is converted via B-spline fit (smoothing 0.2–0.5), arc-length resample to nframe=21, and camera frames built with look-at toward object center (surround/reconstruct) or path tangent (exploration).

ModeFilterPinga result
ExplorationKeep top-3 most diverse paths (wonder_topk=3)8 candidates → 3 groups (wonder_0, wonder_1, wonder_7)
SurroundDrop trajectories too similar to existing ones (traj_sim_threshold)5 object groups, 8 ground paths
ReconstructFPS diversity selection (recon_topk=5)10 pairs → 5 groups, 10 paths
TargetPlanned and saved to navmesh mapsNot rendered in this run

Unlike view* (fixed cardinal slice), navmesh groups get a fresh panorama slice at the trajectory's starting orientation. Some groups also get a second variant (traj1): aerial lift for target_* and wonder_*, e-loop orbit at endpoint for reconstruct_*.

Complete inventory — all 33 output trajectories

Cardinal rotations (9) — camera fixed, rotation only:

GroupTrajcamera.json type
view0/, view1/, view2/traj0, traj1, traj2right-rotation, left-rotation, up-right-aerial

Surround / object orbit (8) — navmesh walk around VLM-detected object:

FolderObjectTrajs
target_tree_0/tree, Backtraj0
target_tree_4/tree, Righttraj0, traj1 (aerial)
target_pillar_1/pillar, Lefttraj0, traj1 (aerial)
target_pillar_2/pillar, Backtraj0, traj1 (aerial)
target_bench_3/bench, Fronttraj0

Reconstruct (10) — navmesh walk to under-covered region:

FolderTargetTrajs
reconstruct_1/tree, Righttraj0 (approach), traj1 (e-loop)
reconstruct_3/structural voidtraj0, traj1
reconstruct_4/tree, Fronttraj0, traj1
reconstruct_8/pillar, Backtraj0, traj1
reconstruct_9/pillar, Lefttraj0, traj1

Exploration (6) — outward wander per compass sector:

FolderTrajs
wonder_0/, wonder_1/, wonder_7/traj0 (ground), traj1 (aerial)

All poses are aggregated into render_results/cameras.glb and cameras_navi.glb for 3D inspection. Script: code/stages/01_traj_generate.py · Navmesh: hyworld2/worldgen/src/navi_utils.py

Filling the holes (WorldStereo inpainting)

HY-World renders each camera path by splatting the point cloud into video frames. Problem: the point cloud is sparse. Large chunks of every frame are empty gray holes. WorldStereo tries to fill those holes using the panorama as memory.

The good case — view0 (cardinal view, real reference frame):

① Raw render (holes expected)

② Hole mask (white = missing)

③ WorldStereo result

The bad case — wonder_7 (exploration, PCD-only start frame):

① Raw render

② Hole mask (much bigger)

③ WorldStereo result (weaker)

Limitation

The mask file for wonder_7 is ~2.7× larger than view0's — that's 50–68% of pixels missing in many trajectories. WorldStereo treats the mask as soft guidance (ControlNet-style), not hard inpainting. Combined with sparse geometry and only four panorama memory views, some gray patches survive.

Training the final 3D scene (3DGS)

WorldStereo outputs get fed into a 3D Gaussian Splatting trainer — 787 frames, depth maps, normals, 8000 training steps. Output: a navigable radiance field you can render from any angle.

OutputSizeWhat it is
global_mesh.ply82 MBStage-1 mesh (primary mesh for now)
point_cloud_7999.ply123 MBTrained 3DGS point cloud
ckpt_7999_rank0.pt171 MBTraining checkpoint

3DGS frame count: 33 trajectories × 21 frames = 693, plus 54 pano_bank + 40 polar_bank = 787 images in gs_data/images/.

06

Building the Movie Set

Individual reconstructed elements become a coherent movie environment through a staged assembly. Here's the progression:

Reference Images → Intermediate Representations → 3D Components → Complete Scene
Movie Song
↓
~4 keyframe views (manual)
↓
panorama.png
↓
HY-World Stage 1 → mesh + 33 trajectories
↓
Stage 2 → render.mp4 + render_mask.mp4
↓
Stage 3 → WorldStereo fills holes
↓
Stage 4 → 787 training frames
↓
Stage 5 → 3DGS checkpoint
↓
Walkable 3D set
StageScriptWhat happens
1traj_generate.pyDepth, mesh, navmesh, trajectories
2traj_render.pyPoint-splat conditioning videos
3video_gen.pyWorldStereo inpainting
4gen_gs_data.pyPrepare 3DGS training data
5world_gs_trainerTrain for 8000 steps
6extract_mesh.py(not run yet)
Input panorama

Reference Panorama

4-side stitched equirectangular input from real song frames

Segmentation overlay

Scene Understanding

11 detected objects drive camera path planning

camera.json (21 poses)
    ↓  traj_render.py
render.mp4 + render_mask.mp4   ← gray holes where PCD is sparse
    ↓  video_gen.py (WorldStereo)
worldstereo-memory-dmd_result.mp4   ← inpainted video
    ↓  gen_gs_data.py
787 training frames for 3DGS

Run it: pinga_set/run_mesh_pipeline.sh — see code/ for all scripts.

07

Challenges and What Didn't Work

Every failure pushed us toward better input preparation rather than a bigger model. The pattern is consistent across the project.

Thing we triedWhat happened
COLMAP on raw footageNo usable sparse reconstruction — shots don't overlap
VGGT on raw songBad reconstruction — cuts kill correspondence
Depth Anything V3 on raw songPer-frame depth OK, but views too sparse and non-overlapping to fuse
Single-frame HY-WorldWould hallucinate most of the set
Raw PCD without panoramaTrajectories worked, inpainting quality was poor
WorldStereo on sparse trajectoriesHoles remain — mask is soft guidance, not hard fill
DMD fast mode (4 denoising steps)Fast but lower quality
Mesh extraction (Stage 6)Not executed yet

Single-image vs. multi-view: a concrete example

What we tried

Run the full HY-World pipeline on a single song frame expanded via HY-Pano 2.0.

What happened

The panorama looked plausible but ~75% was hallucinated. Downstream segmentation and WorldStereo outputs contradicted other shots in the song.

Why it was insufficient

Depth was guessed from invented geometry. A weak panorama poisons every downstream stage.

What we changed

Manual frame picking, wider view generation, MoGe depth, and hand-verified point-cloud stitching to build a panorama with photographic evidence in each direction.

Observation

The hardest part wasn't picking a model. It was designing a pipeline where each tool does one thing it's actually good at — humans pick frames and verify geometry, MoGe gives dense depth, HY-World builds mesh and plans paths, WorldStereo fills holes, 3DGS turns it all into a renderable scene.

08

Final Result

Everything below is real output from pinga_set/.

Final input panorama of the Pinga courtyard
Input panorama — the foundation for the entire reconstruction
View bin 0
View bin 0
View bin 1
View bin 1
View bin 2
View bin 2

View1 — second cardinal bin

Target tree — VLM picked the tree, camera flies toward it

Good vs. bad inpainting

✅ view0 — real reference frame, fewer holes

⚠️ wonder_7 — PCD-only start, many holes remain

3D assets

Open in MeshLab or CloudCompare:

09

What Comes Next

Stage 6 (extract_mesh.py) — extracting a mesh from the 3DGS checkpoint — wasn't run yet. The gs_output/mesh/ directory doesn't exist. The primary mesh for now remains the Stage-1 global_mesh.ply (82 MB).

No single model eats a Bollywood song and spits out a 3D set. A chain of imperfect tools, with humans at the critical decision points, gets you most of the way there — but mesh extraction from the trained radiance field is the obvious next step.

Checkpoints & models

Weights live in pinga_set/ — symlinked under media/checkpoints/ (not duplicated).

ArtifactPathSizeUsed for
Panorama depthrender_results/full_depth_prediction.pt100 MBStage 1 init
3DGS checkpointgs_output/ckpts/ckpt_7999_rank0.pt171 MBFinal radiance field
Sky depth cachegs_output/sky_depth_cache.pt522 MBTraining
Global meshrender_results/global_mesh.ply82 MBNavigation / rendering
3DGS point cloudgs_output/ply/point_cloud_7999.ply123 MBVisualization

Other external models (downloaded at runtime): Qwen3-VL-8B (trajectory planning), WorldStereo (inpainting), HunyuanWorld-Mirror (depth bank), MoGe v2 (upstream depth).

Training config: pinga_set/gs_output/cfg.yml · Pipeline runner: code/run_mesh_pipeline.sh · Panorama generator: code/panogen/

Important

All media from pinga_set/. Code in code/. Raw movie frames and upstream keyframe picks aren't in this repo — only the HY-World pipeline outputs.

References

  1. HY-World 2.0 World generation, mesh, trajectories, and inpainting pipeline github.com/Tencent-Hunyuan/HY-World-2.0
  2. MoGe Monocular metric depth estimation for panorama stitching github.com/microsoft/MoGe
  3. VGGT Visual Geometry Grounded Transformer — learned geometry baseline github.com/facebookresearch/vggt
  4. 3D Gaussian Splatting Real-time radiance field rendering and final scene training github.com/graphdeco-inria/gaussian-splatting