Turn Shot Designer (Hollywood Camera Work .hcw) scene files into Unreal
Engine 5.8 scenes (compatible with 5.6+) — cameras (with moves), actors,
props, walls, and lights — via a Python script you run inside the editor.
Shot Designer .hcw ──parse──▶ IR (JSON) ──emit──▶ Unreal 5.8 Python script
(plain XML) neutral scene run in the editor
No third-party dependencies for the core pipeline (stdlib only).
pip install -e . # provides the `vsm` command
# or run without installing:
PYTHONPATH=src python -m virtualsetmaker.cli --help# Parse a Shot Designer scene and emit an Unreal script
# (default output: Sceneforclaude_unreal.py beside the input; -o to override):
vsm build samples/Sceneforclaude.hcw
# Tunables via flags (defaults are 35mm, 1.5 m camera height, 2.5 m walls):
vsm build scene.hcw --focal-length 50 --camera-height 1.7 --wall-height 300 \
--frame-rate 30 --content-path /Game/Blocking --units-per-meter 100
# Inspect the intermediate representation as JSON:
vsm parse samples/Sceneforclaude.hcw -o scene.json
# Emit straight from a hand-authored IR (no .hcw needed) — good for demos:
vsm emit examples/example_scene.json
# Just confirm a file is a Shot Designer scene:
vsm probe samples/Sceneforclaude.hcw
# List the prop blockout library:
vsm props
# Open the graphical exporter:
vsm gui # (or run `vsm-gui`)Every tunable can also be set once in ~/.virtualsetmaker.json (created by the
GUI; hand-editable). Precedence: CLI flag > config defaults > built-in.
Both vsm and the GUI honor the defaults section:
{
"input_dir": "C:/scenes",
"output_dir": "C:/exports",
"units_per_meter": 100,
"defaults": {
"focal_length_mm": 35,
"camera_height_m": 1.5,
"wall_height_cm": 250,
"wall_thickness_cm": 10,
"frame_rate": 24,
"ue_content_path": "/Game/VSM",
"manny_paths": ["/Game/MyChars/Hero.Hero"],
"quinn_paths": ["/Game/MyChars/Heroine.Heroine"]
}
}| key | meaning | built-in |
|---|---|---|
focal_length_mm |
lens for every camera (Shot Designer stores none) | 35 |
camera_height_m |
camera height (Shot Designer is 2D top-down) | 1.5 |
wall_height_cm / wall_thickness_cm |
wall blockout dimensions | 250 / 10 |
frame_rate |
level-sequence display rate | 24 |
ue_content_path |
where the sequence asset is created | /Game/VSM |
manny_paths / quinn_paths |
skeletal-mesh asset paths tried for actors | UE mannequins |
units_per_meter |
Shot Designer units per meter | 100 |
input_dir/output_dir/top-level units_per_meter are the GUI's remembered
state; unknown keys and bad values are ignored, never fatal.
vsm gui (or the vsm-gui command / vsm-gui.exe) opens a small exporter:
add one or more .hcw files, choose an export folder, hit Export — each
scene becomes <name>_unreal.py in that folder, with a log of object counts
and any unmatched-prop notes. Last-used folders are remembered in
~/.virtualsetmaker.json.
Standalone exe (no Python needed to run it):
- Get this repo onto the PC (clone or download ZIP).
- Double-click
packaging\build_windows.bat— one-time build; needs Python 3 from python.org with the default tcl/tk option. It installs PyInstaller and producesdist\vsm-gui.exe. - Double-click
dist\vsm-gui.exeto use the exporter from then on.
Or via pip: install Python 3.9+, then pip install . in the repo — this
gives vsm (terminal) and vsm-gui (windowed, no console) commands.
- Enable Edit → Plugins → "Python Editor Script Plugin" (restart once).
- Open the Output Log, switch the dropdown from
CmdtoPython. - Run
py "C:/path/to/scene.py"(or paste the file path).
It spawns the actors/props/walls/lights into the current level and creates a
Level Sequence (/Game/VSM/SEQ_<name>) with the CineCamera(s), any camera
move keyframes, focal-length animation, and a camera-cut track. The sequence
is opened in Sequencer as part of the build (required by the UE 5.6+
LevelSequenceEditorSubsystem spawnable API) and left open for you.
- 1 Shot Designer unit = 1 cm (override with
--units-per-meter). - Shot Designer positions are screen-space (
+ydown) with angles in radians. Both that frame and Unreal's are left-handed, so coordinates and yaw carry over unchanged (chirality preserved — scenes and shots are never mirrored); the whole conversion is pinned down in one place,coords.py. - Actors render as the UE Mannequin —
SKM_Manny, orSKM_Quinnfor Shot Designer Type B (female) characters — facing where the character faces, with a capsule fallback (plus instructions in the Output Log for adding the Third Person content pack) if the meshes aren't in the project.
Props aren't anonymous cubes: each Shot Designer objectKey maps to a
parametric blockout recipe in
emit/blockouts.py — a chair is a seat
- backrest + four legs, a table is a top + legs, an open door is a frame + a
swung panel, all at real-world dimensions. Run
vsm propsto see the recipes.
The library covers the entire Shot Designer object palette (62 recipes, mapped from the app's palette screenshots): all furniture and tables, desk/hand props (laptop, monitor, phone, plate, cup, paper, guns), greenery (bush, tree), every vehicle (car, mini bus, semi truck, trailer, motorcycle, tank, small plane, fighter jet, commercial jet), production gear (camera crane, boom mic, microphone, monitor village, equipment), all door/window types (open, closed, double open, double closed, medium opening, prison bars, window), and floor arrows.
Lights spawn as placeholder rig geometry + a real Unreal light attached at the fixture's emit point, grouped so they move together: fresnels/ellipsoidal/ PAR/scoop/cyc/open-face get a stand + head with a pitched SpotLight at the lens; panels/FLO/softbox/silk/bounce get their slab/frame with a RectLight on the face; china ball/balloon/practical/light-on-a-stick get hanging or lamp geo with a PointLight inside; Sun spawns a DirectionalLight only (position is meaningless for it, so no rig); the Virtual Speed Rail spawns rigging geo with no light.
Matching is exact key first, then ordered substring aliases (COUCHMODERN →
SOFA; BOOMMICROPHONE → boom rig, not phone), so unseen key-name variants land
on the right shape. Unmatched kinds fall back to a labeled 1 m cube and
vsm build prints a note — send a .hcw using that prop and the recipe can be
added in a few lines. Parts spawn grouped (attached to the first part) in the
VSM/Props outliner folder, so each prop moves as one unit.
Every recipe (and every light-rig type) has a real low-poly 3D model,
authored procedurally by virtualSetmaker itself (geo/ package) — no
Starter Content, no downloads, no plugins. vsm build writes the models a
scene needs as OBJ files into a vsm_props/ folder next to the generated
script (plus one shared .mtl). Keep that folder beside the script when
you copy it to the UE machine.
On first run the script imports each model into
/Game/VSM/Meshes/SM_VSM_<NAME> (synchronous AssetImportTask, which
routes OBJ through Interchange in UE 5.1+), assigns flat previz colors as
MI_VSM_<material> instances under /Game/VSM/Materials (wood, porcelain,
foliage, metal... parented to the engine's BasicShapeMaterial, so colors are
deterministic even where the importer ignores .mtl), and spawns it scaled
so its bounding box matches exactly the box the blockout would occupy —
layout never changes, only looks. Later runs reuse the imported assets
(delete /Game/VSM/Meshes to force a re-import after upgrading).
The imported meshes are deliberately left unsaved (dirty) — save them
with a normal File → Save All whenever you like. Saving from inside the
import frame races Interchange's deferred source-data commit and writes a
corrupt package (StaticMeshDescriptionBulkData != nullptr assert that
crashes the editor on every later load). If a project ever gets into that
crash loop from packages saved by an older script version: close the
editor, delete <project>/Content/VSM/Meshes/ in the file explorer, reopen
and re-run — the meshes re-import cleanly.
Fallbacks at every step: vsm_props/ missing, import fails, or the importer
rotates the up-axis (a guard compares imported bounds against authored
extents) → that prop spawns as its blockout parts, with a log line saying
why. Set use_prop_meshes to false in the config's defaults section to
stay all-blockout.
Wall inserts (doors/windows/openings) deliberately stay blockouts: their recipe spans are the verified wall-carve widths.
Recipes live in emit/blockouts.py.
Each is a list of primitive parts built with _p(shape, offset, size, rot):
- Frame (before the prop's own rotation):
+X= width (right),+Y= toward the prop's front,+Z= up. offset— the part's center in cm, relative to the prop origin on the floor.size—[x, y, z]extent in cm (basic-shape meshes are 100 cm, so actor scale = size/100).rot— optional[pitch, yaw, roll]degrees (pitched cylinders make car wheels, a rolled one makes a tank barrel).- Add the recipe under its Shot Designer
objectKey, plus anALIASESrow for name variants — order matters: more specific substrings must come first (ARMCHAIRbeforeCHAIR,CARPETbeforeCAR).
Shot Designer's objectScaleX/Y is relative to each icon's native art size
— not to our real-world recipes. The SD_NATIVE table in blockouts.py now
carries values measured from the app's own vector art (the 1.80.8
installer's FXG assets; the app places art at 1 unit = 1 cm, confirmed in its
decompiled source), so the emitted footprint is objectScale x native —
exactly what the Shot Designer canvas shows. Some icons differ from their
names: the "Square Table" icon is really a 120x220 cm rectangular table, and a
fresh door is placed at objectScale 0.7 by default. Doors/windows/openings are
wall-snapped (their art includes wall stubs) and keep the verified
opening-based sizing without a native entry. For any future icon, the same
measurement can be redone from the installer's FXG/ assets.
vsm build/parse (and the GUI log) can print:
warning: skipped N <Tag> object(s)— the.hcwcontains a Shot Designer object type the parser doesn't handle yet; nothing was generated for it.warning: the document has N snapshot(s) beyond the current one— only the current snapshot converts; objects existing only in other snapshots won't appear.warning: file version '…' is not the tested ['1']— a newer Shot Designer file format; parsing is attempted but new features may be missing.note: no blockout recipe for prop kind '…'— that prop spawned as a generic 1 m cube (see Adding a prop recipe).
Inside Unreal, the generated script logs VSM: lines for every spawn failure
plus a final spawned-vs-expected summary per category.
- Static scenes (placement of camera, actors, props, walls, lights): done,
verified against a real
.hcw. - Camera moves: done. Shot Designer stores a move as numbered camera stop
marks (
<stopMarks>) linked by a<Track>dolly path; the parser merges each chain into one camera keyed at the start and end stops (position and look direction from the stops themselves) plus a key per intermediate track point so curved paths keep their bow (Unreal's AUTO tangents smooth through them). Timing comes from path length at the scene's camera-speed index (speed 3 ≈ 0.75 m/s dolly;_DOLLY_MPS_PER_SPEEDinparse/shotdesigner.py). Verified against straight-line and curved-move samples insamples/.
- virtualSetmaker now ships its own 3D models. UE 5.7+/5.8 prebuilt
installs carry no Starter Content anywhere, so v0.3.0's mesh path was dead
on arrival there. Replaced outright: a pure-stdlib procedural mesh kit
(
geo/: chamfered boxes, lathes, convex prisms, spheres, tori, tubes) authors a low-poly model for all 62 recipes and all 10 light-rig types — lathed toilet bowl and pedestal sink, car with a real side-profile body and rubber wheels, lobed trees/bushes, tapered aircraft fuselages, book rows in the bookcase, burners on the stove, pillows on the beds — each bbox-contracted to its blockout within a twentieth of a millimeter so every calibration (SD_NATIVE, frozen carve spans) is untouched. vsm buildwrites the scene's models tovsm_props/beside the script; the script imports them once (synchronous AssetImportTask → Interchange), colors them viaMI_VSM_*instances, fits them to the blockout's exact box, and falls back to blockout parts per-prop if anything is missing — including an axis-swap guard against importer up-axis surprises.use_starter_meshesconfig knob renameduse_prop_meshes(old key is ignored, per the settings file's unknown-key policy). Starter Content install code deleted; the mannequin auto-install (which genuinely works — Templates/ still ships) is untouched. 277 tests.
- Every recipe audited against real-world dimensions. Crude single-cube props became recognizable multi-part blockouts: bookcase (sides/back/5 shelves), dresser (drawer faces), wardrobe (plinth/doors/crown), kitchen counter (toe kick + 92 cm countertop), french-door fridge (91 cm wide — was a 70 cm euro larder), 30" range with backguard, two-piece toilet (tank/bowl/seat), bathtub rim, bench legs, dog with legs and snout, road-case stack, Magliner-style video village. Vehicles got true heights (sedan roof 145, Sprinter-class van 245 with hood + windshield, semi cab 380, trailer at the 13'6" legal max) and aircraft got the parts that sell the silhouette (Cessna high wing + tailplane, fighter canopy, 737-class fin/engines/tailplane). Prison bars doubled to 13 (≈5" real centers). Light rigs: C-stand base spread 85, Kino 4ft 4Bank panel, 170 cm balloon light. Wall-insert spans (the carve widths) are frozen and pinned by tests.
- Real meshes where the engine has them — see Real prop meshes above. Payloads carry both the mesh spec and the blockout parts; the runtime prefers the mesh and falls back cleanly, so a bare project still builds.
- New
use_starter_meshesconfig knob; end-of-run summary now counts real meshes vs blockout parts; fake-unrealend-to-end tests (118 total).
- Doors/windows carve real wall openings — wall inserts (
DOOR*,OPENING,WINDOWrecipes) now snap flush onto the nearest parallel wall segment (origin projected onto the centerline, yaw aligned to the wall while keeping the swing side) and the segment's cube is split around the opening: side pieces, a lintel above, and a sill below windows. Frames sit in a real hole instead of clipping a solid wall. Matching is geometric (within 20 cm and 15°), so hand-placed pieces work —<snapPath>is no longer trusted. - Set-piece rotation fixed — the angle convention now keys on the element
type (
<GenericSet>art is authored width-along-X → angle-direct;<GenericProp>furniture → angle+90), not on<snapPath>. Windows and prison bars placed without a snap were previously twisted 90°. - Prison bars calibrated —
SD_NATIVEgains a width-only entry (155.9 SD units, from adjacent icons tiling edge-to-edge in a real scene), so bar runs match their on-canvas span; rail depth stays true-to-life (Noneaxes inSD_NATIVEnow mean "no correction"). - Mannequin lookup made robust — when none of the candidate paths load,
the emitted script sweeps the Asset Registry for any
/Gameskeletal mesh namedSKM_Manny/SKM_Quinn(and variants), wherever the project keeps them; if none exist it logs an error with the exact content-pack steps instead of a quiet capsule fallback.
- Prop size calibration (measured) — new
SD_NATIVEtable maps Shot Designer icons' native art sizes so scaled props emit at their true on-canvas span (objectScale × native). Values are measured from the app's own vector art (installer FXG assets, 30 freestanding icons), anchored against real-scene measurements. Fixes from the same decompiled source: the Cup icon's real keyBOTTLE(was a generic cube),DOUBLEDOOROPENmapping to the closed-door recipe,HOLLYWOODLIGHT(Light On A Stick) getting the stand fixture instead of its stick + point light, the exact 8-color character palette, and confirmation that<cameraStyle>carries no lens data (display-only). - Config file + CLI flags — every hardcoded default is now tunable: focal
length, camera height, wall height/thickness, frame rate, UE content path,
and mannequin asset paths, via
vsm buildflags or adefaultssection in~/.virtualsetmaker.jsonshared by the CLI and GUI (flag > config > built-in). - Camera filmback — the sensor size carried in the IR (36×24 default) is now applied to the CineCamera filmback, so focal lengths frame correctly.
- Actor colors — Shot Designer character colors (the packed
<color>value) now tint the capsule-fallback actors; mannequin spawns keep their own materials. - CLI/GUI fixes —
vsm build/parse/emitprint friendlyerror:lines instead of tracebacks;vsm builddefault output is now<name>_unreal.pybeside the input (same convention as the GUI); the GUI no longer freezes during a batch export (worker thread) and failures show their traceback in the log pane (the windowed exe has no console to hide them in); an untested.hcwfile version now produces a warning instead of silence. - Light table unified — each light fixture now carries its Unreal light
class in one table;
SOFT/FRAME/LANTERN/BULBkinds get matching rig geometry instead of the generic stand.
- Actors spawn as UE Mannequins —
SKM_Mannyfor Type A characters,SKM_Quinnfor Type B (the.hcw<female>flag), with the correct facing (mannequin meshes are authored facing +Y, so spawns apply a −90° yaw offset). If the project lacks the meshes, the Output Log explains how to add the Third Person content pack instead of silently dropping capsules. - Fixed mirrored scenes — the screen→Unreal conversion previously negated Y, which reflected the entire scene: props appeared on the wrong side of the room and every through-camera shot was flipped left/right. Coordinates and yaw now carry over unchanged, so Unreal matches the Shot Designer plan exactly.
- Calibrated prop orientation — freestanding props store the direction of
their back in the
.hcw(a sofa placed against a wall now sits flush, facing the room); doors/windows snapped into a wall follow the wall's direction, with door panels swinging into the room. - Export diagnostics — unsupported Shot Designer object types and multi-snapshot documents are reported at export instead of dropped silently; the generated script survives individual spawn failures, names them in the Output Log, and reports spawned-vs-expected counts per category.
- Known issue — prop footprints are real-world recipe dimensions ×
objectScale, but some Shot Designer icons (notably tables) have native sizes larger than real-world furniture, so those props come out small. (Fixed 2026-07-20 via theSD_NATIVEcalibration table.)
- Camera moves — stop marks + dolly
<Track>chains become one keyframed CineCamera per move, with curve bow preserved and timing from path length at the scene's camera-speed index. - Unreal 5.8 target (compatible 5.6+) — spawnables go through
LevelSequenceEditorSubsystem. - Fixes — generated-script rotations (the UE Python
Rotatorconstructor is(roll, pitch, yaw), not the C++ order), scene data embedded as JSON (not pasted as a Python literal), and the Windows standalone-exe build. - Initial release —
.hcw→ IR → Unreal editor Python script; full prop palette blockout library; lights with real Unreal light types; tkinter GUI and Windows packaging.
src/virtualsetmaker/
ir.py neutral scene dataclasses (+ JSON, validation)
coords.py the single Shot Designer→Unreal coordinate transform
parse/shotdesigner.py .hcw XML → IR
parse/probe.py format guard
emit/unreal_python.py IR → Unreal 5.8 Python script
emit/blockouts.py prop + light-fixture blockout recipes
build.py shared build core (used by CLI and GUI)
settings.py shared config (~/.virtualsetmaker.json) + Defaults
gui.py tkinter exporter (vsm gui / vsm-gui)
cli.py the `vsm` command
packaging/ PyInstaller spec + Windows one-click build script
examples/example_scene.json hand-authored IR with an animated push-in
samples/Sceneforclaude.hcw real Shot Designer sample
tests/ pytest suite (IR, coords, parser, emitter)
pip install -e ".[dev]"
pytest