Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions docs/development/PROCEDURAL_3D_SCENE_SPEC.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Procedural 3D scene spec (G3)

Status: design only. No engine, UI, or `_launch_runtime.py` change ships with
this document. Playback and HTTP inspection remain blocked on the catalog
resolver contract (GROK-API-001, mailbox) and on explicit ownership of
compositor hotspots.
Status: the 2.5D compositor stays the **Video 2.5D** studio tab (`scene3d`).
Real 3D is a **separate Video 3D** tab (`world3d`) in `ui/src/features/scene3d/`.
They share asset pickers and GLB clip identity, not one overloaded panel.
Wizard hook: `hocuspocus:world3d-workflow-request` mounts the same template ids
later; 2.5D `video_3d` actions stay on the compositor. Private Meshy GLBs stay
off git. `_launch_runtime.py` is still not mounted for G2.

Related: [GLB animation import contract](GLB_ANIMATION_IMPORT_CONTRACT.md)
(`inspect_glb`, schema `glb-inspection-v1`).
Expand Down Expand Up @@ -44,11 +46,13 @@ selector must use that pair. Human labels must not rename clips.
| B | Add a **separate scene mode** with a real graph (nodes, camera, light, clips by id) beside the compositor | Yes | Medium | Proposed minimum. |
| C | Replace the compositor with a general 3D engine | No | High | Rejected. Breaks Character Kits, atmospheres, 2.5D recipes, browser capture. |

### Decision (proposed, not implemented)
### Decision

Ship **B** only after G2’s resolver contract and hotspot ownership are
agreed. Mode A remains the default for existing templates and the Omarchy
compositor work owned by the principal.
Ship **B** as a sibling, not a replacement. The 2.5D compositor stays the
**Video 2.5D** tab. The 3D stage lives in `ui/src/features/scene3d/` as the
**Video 3D** tab (`world3d`), not a toggle inside the compositor panel. G2’s
resolver remains unmounted; local GLB files are read in the browser only.
Mode A remains the path for existing templates and the Omarchy compositor work.

Criteria for choosing B for a shot: more than one mesh must share space,
occlusion, a camera move in world units, or a light. Otherwise stay on A.
Expand Down Expand Up @@ -119,6 +123,19 @@ the G1 report or the resolver, for example:
No magic aliases (`idle`, `run`, `dance`) unless that exact string is the
clip name.

The 2.5D compositor’s `seamlessHorizontal` / cylinder panorama has a 3D
sibling: an image slot with `loop: { cylinder: true, speed }` maps onto the
inside of an open Y-up cylinder (`CylinderGeometry`, BackSide,
RepeatWrapping). UV `offset.x = (t * speed) mod 1` scrolls the world. The
`run-loop` template is the treadmill shot: `subject_1` stays at the origin,
camera family `pursuit`, and the background cylinder moves. Clip selection
stays `(index, exact glTF name)`; do not infer Running. Any image background
can enable the same infinite cylinder without switching template.

Video 3D export is independent of the 2.5D compositor. The world editor paints
three.js frames (`paintWorld`) and encodes H.264 in the browser. It does not
call `SceneAnimatorPanel` recording. Published files are named `world3d-*`.

## 6. Time, persistence, languages

- Scene time is deterministic. Reuse offset/speed/trim/loop/reverse from
Expand Down
69 changes: 64 additions & 5 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^17.0.13",
"three": "^0.183.0",
"zustand": "^5.0.11"
},
"devDependencies": {
Expand All @@ -44,6 +45,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.183.0",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
Expand Down
8 changes: 8 additions & 0 deletions ui/src/components/MainContent/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {

const SceneAnimatorPanel = lazy(() => import('../Sidebar/SceneAnimatorPanel')
.then(module => ({ default: module.SceneAnimatorPanel })))
const Scene3DEditorPanel = lazy(() => import('../../features/scene3d/Scene3DEditorPanel')
.then(module => ({ default: module.Scene3DEditorPanel })))
const RigAnimatePanel = lazy(() => import('../Sidebar/RigAnimatePanel')
.then(module => ({ default: module.RigAnimatePanel })))
const ComicEditorPanel = lazy(() => import('../../features/comics/ComicEditorPanel')
Expand Down Expand Up @@ -609,6 +611,12 @@ export function MainContent() {
<SceneAnimatorPanel />
</div>
</div>
) : mediaFilter === 'world3d' ? (
<div className="flex-1 overflow-y-auto p-4 md:p-8">
<div className="max-w-[1600px] mx-auto">
<Scene3DEditorPanel />
</div>
</div>
) : mediaFilter === 'animate3d' ? (
<div className="flex-1 overflow-y-auto p-4 md:p-8">
<div className="max-w-2xl mx-auto">
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/MainContent/TabFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export function TabFilter() {
{ value: 'comics', label: t('tabs.comics'), description: t('descriptions.comics'), icon: <BookOpen size={15} />, action: () => openFilter('comics') },
{ value: 'characters', label: t('tabs.characters'), description: t('descriptions.characters'), icon: <WandSparkles size={15} />, action: () => openFilter('characters') },
{ value: 'scene3d', label: t('tabs.scene3d'), description: t('descriptions.video3d'), icon: <MonitorPlay size={15} />, action: () => openFilter('scene3d') },
{ value: 'world3d', label: t('tabs.world3d'), description: t('descriptions.world3d'), icon: <Boxes size={15} />, action: () => openFilter('world3d') },
{ value: 'animate3d', label: t('tabs.animate3d'), description: t('descriptions.animate3d'), icon: <MonitorPlay size={15} />, action: () => openFilter('animate3d') },
]
const productionItems: MenuItem[] = [
Expand Down
Loading
Loading