cozyclay 2.1.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +43 -116
- package/bin/agent/agent-runner.mjs +14 -15
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +3 -3
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-D78eiKTa.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +151 -2625
- package/src/agent-progress.js +80 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +12 -2
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +10 -8
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +214 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +55 -13
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +53 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +47 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +60 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +42 -413
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +79 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +113 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +197 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +177 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +144 -0
- package/src/motion3/compiler/validate-parts.js +118 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +74 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +177 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +204 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/people.js +82 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +125 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +195 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +44 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/people-checks.js +37 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +228 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +129 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +113 -0
- package/src/motion3/schema/types.js +149 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +83 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +85 -0
- package/src/motion3/skills/info.js +80 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/qa-label.js +25 -0
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +9 -51
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +16 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +47 -77
- package/src/studio-app-binding.js +27 -47
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +15 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +109 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +9 -125
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -182
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /package/src/{ardy → motion}/range-pin.js +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The HDR environments the stage can use: the built-ins and every environment
|
|
2
|
+
// file loaded into this browser's asset store. The Light panel lists them and
|
|
3
|
+
// inspect_studio { scope: "catalogue" } answers them as `environments`, so the
|
|
4
|
+
// agent, which cannot upload a file, can pick a map the user already loaded and
|
|
5
|
+
// aim it. Loading a file goes through here too: checked, decoded once, stored
|
|
6
|
+
// under its content address and remembered decoded.
|
|
7
|
+
import { declaredHdrSize, HDR_BUILTINS, HDR_MAX_BYTES, hdrFileProblem, hdrIdForBytes, isHdrAssetId, sniffHdrType } from "./stage-hdr.js";
|
|
8
|
+
import { getAsset, listAssetIds, openAssetDb, putAsset } from "./scene-assets.js";
|
|
9
|
+
import { decodeEnvironment } from "./stage-hdr-image.js";
|
|
10
|
+
import { rememberHdrMap } from "./stage-hdr-map.js";
|
|
11
|
+
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const builtins = HDR_BUILTINS.map(({ id, name, brightest, note }) => ({ id, name, source: "builtin", brightest, note }));
|
|
14
|
+
let files = [], catalogue = Object.freeze([...builtins]);
|
|
15
|
+
|
|
16
|
+
function publish(next) {
|
|
17
|
+
files = next;
|
|
18
|
+
catalogue = Object.freeze([...builtins, ...files.map(({ id, name, width, height, megabytes, brightest }) => ({
|
|
19
|
+
id, name, source: "file", ...(width ? { size: `${width}x${height}` } : {}), megabytes, ...(brightest ? { brightest } : {}),
|
|
20
|
+
}))]);
|
|
21
|
+
for (const listener of listeners) listener();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every environment, built-ins first: { id, name, source: "builtin" | "file", brightest?, note?, size?, megabytes? }. */
|
|
25
|
+
export const hdrEnvironmentCatalogue = () => catalogue;
|
|
26
|
+
export function subscribeHdrLibrary(listener) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => listeners.delete(listener);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Re-read the stored environment files' names and sizes. Without IndexedDB the list is the built-ins. */
|
|
32
|
+
export async function refreshHdrLibrary() {
|
|
33
|
+
let db = null;
|
|
34
|
+
try {
|
|
35
|
+
db = await openAssetDb();
|
|
36
|
+
const ids = (await listAssetIds(db)).filter(isHdrAssetId);
|
|
37
|
+
const records = (await Promise.all(ids.map((id) => getAsset(db, id)))).filter(Boolean);
|
|
38
|
+
publish(records.map(({ id, name, width, height, brightest, bytes }) => ({ id, name: name || id, width, height, brightest, megabytes: Number((bytes.byteLength / 1048576).toFixed(2)) }))
|
|
39
|
+
.sort((a, b) => a.name.localeCompare(b.name)));
|
|
40
|
+
} catch {
|
|
41
|
+
publish([]);
|
|
42
|
+
} finally {
|
|
43
|
+
db?.close?.();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load one environment file the user picked: at most HDR_MAX_BYTES (32 MB) and
|
|
49
|
+
* HDR_MAX_PIXELS, decoded before it is stored so a broken file never reaches
|
|
50
|
+
* the store. Answers { id, name }; throws with a sentence for the user.
|
|
51
|
+
*/
|
|
52
|
+
export async function importHdrFile(file) {
|
|
53
|
+
const early = file.size > HDR_MAX_BYTES ? hdrFileProblem(file.size, null, null) : null;
|
|
54
|
+
if (early) throw new Error(early);
|
|
55
|
+
const buffer = await file.arrayBuffer(), bytes = new Uint8Array(buffer), type = sniffHdrType(bytes), size = declaredHdrSize(type, bytes);
|
|
56
|
+
const problem = hdrFileProblem(bytes.length, type, size);
|
|
57
|
+
if (problem) throw new Error(problem);
|
|
58
|
+
let map;
|
|
59
|
+
try { map = await decodeEnvironment({ type, bytes: buffer }); }
|
|
60
|
+
catch (error) { throw new Error(`that file could not be decoded (${error.message})`, { cause: error }); }
|
|
61
|
+
const id = await hdrIdForBytes(bytes), db = await openAssetDb();
|
|
62
|
+
try {
|
|
63
|
+
await putAsset(db, { id, type, bytes: buffer, name: file.name || id, ...(size ?? { width: map.width, height: map.height }), brightest: map.brightest });
|
|
64
|
+
} finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
rememberHdrMap(id, map);
|
|
68
|
+
await refreshHdrLibrary();
|
|
69
|
+
return { id, name: file.name || id };
|
|
70
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The stage's HDR environment maps as textures, decoded once per session and
|
|
2
|
+
// shared by every scene that names them. A built-in map is drawn on first use,
|
|
3
|
+
// synchronously, so a look that names one renders it in the same frame; a
|
|
4
|
+
// loaded file is read from the asset store and decoded off the render path,
|
|
5
|
+
// and the stage keeps its own sky light until it lands (or if it never does).
|
|
6
|
+
// stage-sky.jsx prefilters the texture with PMREMGenerator for the light and
|
|
7
|
+
// draws it as the background.
|
|
8
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
9
|
+
import * as THREE from "three";
|
|
10
|
+
import { isBuiltinHdrId, isHdrAssetId } from "./stage-hdr.js";
|
|
11
|
+
import { BUILTIN_HEIGHT, BUILTIN_WIDTH, builtinEnvironmentPixels } from "./stage-hdr-builtins.js";
|
|
12
|
+
import { analyzedMap, decodeEnvironment, halfMapFromFloats } from "./stage-hdr-image.js";
|
|
13
|
+
import { getAsset, openAssetDb } from "./scene-assets.js";
|
|
14
|
+
|
|
15
|
+
// Decoded files kept besides the built-ins; an older one is disposed (three
|
|
16
|
+
// uploads it again if a scene still draws it).
|
|
17
|
+
const KEPT_FILES = 3;
|
|
18
|
+
// A stored file that fails to load is tried again a few times (5 s apart), then left failed until a reload.
|
|
19
|
+
const RETRIES = 3;
|
|
20
|
+
const entries = new Map(), listeners = new Set(), failures = new Map();
|
|
21
|
+
const notify = () => { for (const listener of listeners) listener(); };
|
|
22
|
+
const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); };
|
|
23
|
+
|
|
24
|
+
function textureOf({ width, height, data }) {
|
|
25
|
+
const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.HalfFloatType);
|
|
26
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
27
|
+
texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
28
|
+
texture.magFilter = THREE.LinearFilter;
|
|
29
|
+
texture.minFilter = THREE.LinearFilter;
|
|
30
|
+
texture.needsUpdate = true;
|
|
31
|
+
return texture;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function store(id, map) {
|
|
35
|
+
const entry = Object.freeze({ id, width: map.width, height: map.height, horizon: map.horizon, brightest: map.brightest, texture: textureOf(map) });
|
|
36
|
+
entries.set(id, entry);
|
|
37
|
+
const files = [...entries.values()].filter((row) => row.texture && isHdrAssetId(row.id));
|
|
38
|
+
for (const stale of files.slice(0, Math.max(0, files.length - KEPT_FILES))) {
|
|
39
|
+
entries.delete(stale.id);
|
|
40
|
+
stale.texture.dispose();
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What the cache holds for an id: a ready map, a pending or failed load, or null. Draws a built-in on first ask. */
|
|
46
|
+
function snapshot(id) {
|
|
47
|
+
if (!id) return null;
|
|
48
|
+
if (!entries.has(id) && isBuiltinHdrId(id)) store(id, analyzedMap(halfMapFromFloats(BUILTIN_WIDTH, BUILTIN_HEIGHT, builtinEnvironmentPixels(id))));
|
|
49
|
+
return entries.get(id) ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Start reading and decoding a stored file once. A failed load is forgotten after a while so a later ask retries. */
|
|
53
|
+
function request(id) {
|
|
54
|
+
if (!isHdrAssetId(id) || entries.has(id)) return;
|
|
55
|
+
const pending = Object.freeze({ id, pending: true });
|
|
56
|
+
entries.set(id, pending);
|
|
57
|
+
(async () => {
|
|
58
|
+
const db = await openAssetDb();
|
|
59
|
+
try {
|
|
60
|
+
const record = await getAsset(db, id);
|
|
61
|
+
if (!record) throw new Error("it is not in this browser's asset store");
|
|
62
|
+
return await decodeEnvironment(record);
|
|
63
|
+
} finally { db.close(); }
|
|
64
|
+
})().then((map) => {
|
|
65
|
+
if (entries.get(id) === pending) store(id, map);
|
|
66
|
+
}, (error) => {
|
|
67
|
+
console.warn(`[cozyclay] could not load HDR environment ${id}: ${error.message}`);
|
|
68
|
+
if (entries.get(id) !== pending) return;
|
|
69
|
+
const failed = Object.freeze({ id, failed: error.message }), count = (failures.get(id) ?? 0) + 1;
|
|
70
|
+
entries.set(id, failed);
|
|
71
|
+
failures.set(id, count);
|
|
72
|
+
if (count < RETRIES) setTimeout(() => { if (entries.get(id) === failed) { entries.delete(id); notify(); } }, 5000);
|
|
73
|
+
}).finally(notify);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Warm the cache with a map the importer already decoded. */
|
|
77
|
+
export function rememberHdrMap(id, map) {
|
|
78
|
+
failures.delete(id);
|
|
79
|
+
store(id, map);
|
|
80
|
+
notify();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The ready map for an id ({ texture, horizon, brightest, width, height }), or null while it loads or when it cannot. */
|
|
84
|
+
export function readyHdrMap(id) {
|
|
85
|
+
const entry = snapshot(id);
|
|
86
|
+
return entry?.texture ? entry : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** React: the ready map for `id`, loading it when needed; re-renders when it lands. */
|
|
90
|
+
export function useHdrMap(id) {
|
|
91
|
+
const entry = useSyncExternalStore(subscribe, () => snapshot(id));
|
|
92
|
+
useEffect(() => { request(id); }, [id, entry]);
|
|
93
|
+
return entry?.texture ? entry : null;
|
|
94
|
+
}
|
package/src/stage-hdr.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// HDR environments for the stage look: the stage.lighting.hdr* fields, the
|
|
2
|
+
// built-in maps the Studio generates itself, and the records of the .hdr / .exr
|
|
3
|
+
// / equirectangular picture files a user loads. A loaded file lives in the
|
|
4
|
+
// shared asset store (scene-assets.js) under an `hdr-` id derived from its bytes,
|
|
5
|
+
// exactly like a picture or a model, so it travels in a saved project and
|
|
6
|
+
// survives a reload. Pure data and arithmetic, no three.js: the scene document,
|
|
7
|
+
// the tests and the agent sidecar load it.
|
|
8
|
+
|
|
9
|
+
/** Content-addressed prefix of a stored environment file. */
|
|
10
|
+
const HDR_ID_PREFIX = "hdr-";
|
|
11
|
+
const HDR_ID = /^hdr-[0-9a-f]{32}$/;
|
|
12
|
+
/** The file ceiling: the same 32 MB a project file decodes an embedded asset against (scene-assets.js ASSET_MAX_SOURCE_BYTES). */
|
|
13
|
+
export const HDR_MAX_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
/** Decoded size ceiling for float maps (half floats: 8 bytes a pixel while decoding). */
|
|
15
|
+
export const HDR_MAX_PIXELS = 4096 * 4096;
|
|
16
|
+
/** Stored MIME types: Radiance RGBE, OpenEXR, and an LDR equirectangular picture. */
|
|
17
|
+
const HDR_TYPES = Object.freeze(["image/vnd.radiance", "image/x-exr", "image/png", "image/jpeg"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The maps the Studio draws itself (stage-hdr-builtins.js), so the agent has
|
|
21
|
+
* real environments without a file. `brightest` is the direction of the map's
|
|
22
|
+
* brightest light at hdrRotation 0, as a world vector (y up, +z toward the
|
|
23
|
+
* default camera); hdrRotation turns it about +y.
|
|
24
|
+
*/
|
|
25
|
+
export const HDR_BUILTINS = Object.freeze([
|
|
26
|
+
{ id: "builtin:night-street", name: "Night street", ko: "밤거리", brightest: [0.72, 0.2, 0.66],
|
|
27
|
+
note: "dark blue night sky, a building skyline with lit windows, two warm sodium street lamps (front right and back left) pooling light on wet asphalt, a dim moon" },
|
|
28
|
+
{ id: "builtin:overcast-field", name: "Overcast field", ko: "흐린 들판", brightest: [-0.35, 0.66, -0.67],
|
|
29
|
+
note: "bright soft grey overcast sky (brighter overhead), a faint sun glow behind the clouds back left, a distant tree line and a green field" },
|
|
30
|
+
{ id: "builtin:studio-softbox", name: "Studio softbox", ko: "스튜디오 소프트박스", brightest: [-0.6, 0.4, 0.7],
|
|
31
|
+
note: "dark grey photo studio: a large white softbox front left, a tall strip light right, a soft overhead panel and a rim panel behind" },
|
|
32
|
+
].map((entry) => Object.freeze({ ...entry, brightest: Object.freeze(entry.brightest) })));
|
|
33
|
+
const BUILTIN_IDS = new Set(HDR_BUILTINS.map((entry) => entry.id));
|
|
34
|
+
|
|
35
|
+
export const isHdrAssetId = (value) => typeof value === "string" && HDR_ID.test(value);
|
|
36
|
+
export const isBuiltinHdrId = (value) => BUILTIN_IDS.has(value);
|
|
37
|
+
/** What stage.lighting.hdr may hold: a built-in name or a stored file's id. */
|
|
38
|
+
export const isHdrEnvironmentId = (value) => isBuiltinHdrId(value) || isHdrAssetId(value);
|
|
39
|
+
|
|
40
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
41
|
+
/** Degrees into [-180, 180): 180 wraps to -180, like the other yaw fields. */
|
|
42
|
+
export const wrapDegrees = (value) => ((((value + 180) % 360) + 360) % 360) - 180;
|
|
43
|
+
|
|
44
|
+
/** The four HDR fields of stage.lighting, present and in range: no map is the default. */
|
|
45
|
+
export function normalizeHdrFields(source = {}) {
|
|
46
|
+
return {
|
|
47
|
+
hdr: isHdrEnvironmentId(source.hdr) ? source.hdr : null,
|
|
48
|
+
hdrRotation: finite(source.hdrRotation) ? Number(wrapDegrees(source.hdrRotation).toFixed(3)) : 0,
|
|
49
|
+
hdrIntensity: finite(source.hdrIntensity) ? Math.max(0, Math.min(4, source.hdrIntensity)) : 1,
|
|
50
|
+
hdrBackground: typeof source.hdrBackground === "boolean" ? source.hdrBackground : true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export const HDR_FIELDS = Object.freeze(Object.keys(normalizeHdrFields()));
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------ files ---- */
|
|
56
|
+
|
|
57
|
+
const ascii = (bytes, start, length) => String.fromCharCode(...bytes.subarray(start, Math.min(bytes.length, start + length)));
|
|
58
|
+
|
|
59
|
+
/** The stored MIME type of an environment file from its first bytes, or null. */
|
|
60
|
+
export function sniffHdrType(bytes) {
|
|
61
|
+
if (ascii(bytes, 0, 10) === "#?RADIANCE" || ascii(bytes, 0, 6) === "#?RGBE") return "image/vnd.radiance";
|
|
62
|
+
if (bytes[0] === 0x76 && bytes[1] === 0x2f && bytes[2] === 0x31 && bytes[3] === 0x01) return "image/x-exr";
|
|
63
|
+
if (bytes[0] === 0x89 && ascii(bytes, 1, 3) === "PNG") return "image/png";
|
|
64
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Width and height from a Radiance header ("-Y 512 +X 1024"), or null. */
|
|
69
|
+
function radianceSize(bytes) {
|
|
70
|
+
const match = /\n\s*-Y\s+(\d+)\s+\+X\s+(\d+)\s*\n/.exec(ascii(bytes, 0, 8192));
|
|
71
|
+
return match ? { width: Number(match[2]), height: Number(match[1]) } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Width and height from an OpenEXR header's dataWindow attribute, or null. */
|
|
75
|
+
function exrSize(bytes) {
|
|
76
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
77
|
+
const text = (at) => { let end = at; while (end < bytes.length && bytes[end] !== 0) end++; return [ascii(bytes, at, end - at), end + 1]; };
|
|
78
|
+
let at = 8;
|
|
79
|
+
while (at < Math.min(bytes.length, 65536)) {
|
|
80
|
+
const [name, afterName] = text(at);
|
|
81
|
+
if (!name) return null;
|
|
82
|
+
const [, afterType] = text(afterName);
|
|
83
|
+
const size = view.getInt32(afterType, true), value = afterType + 4;
|
|
84
|
+
if (name === "dataWindow" && size === 16) {
|
|
85
|
+
const [xMin, yMin, xMax, yMax] = [0, 4, 8, 12].map((offset) => view.getInt32(value + offset, true));
|
|
86
|
+
return { width: xMax - xMin + 1, height: yMax - yMin + 1 };
|
|
87
|
+
}
|
|
88
|
+
at = value + size;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The pixel size a float map declares before it is decoded (null for a picture). */
|
|
94
|
+
export function declaredHdrSize(type, bytes) {
|
|
95
|
+
try { return type === "image/vnd.radiance" ? radianceSize(bytes) : type === "image/x-exr" ? exrSize(bytes) : null; }
|
|
96
|
+
catch { return null; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Why an environment file cannot be loaded, as a sentence for the user, or null when it can. */
|
|
100
|
+
export function hdrFileProblem(byteLength, type, size) {
|
|
101
|
+
if (byteLength > HDR_MAX_BYTES) return `that file is ${(byteLength / 1048576).toFixed(1)} MB; an HDR environment can be at most ${HDR_MAX_BYTES / 1048576} MB`;
|
|
102
|
+
if (!type) return "that file is not an HDR environment CozyClay can load (Radiance .hdr, OpenEXR .exr, or an equirectangular .png / .jpg)";
|
|
103
|
+
if ((type === "image/vnd.radiance" || type === "image/x-exr") && !size) return "that HDR file has no readable image size";
|
|
104
|
+
if (size && size.width * size.height > HDR_MAX_PIXELS) return `that HDR is ${size.width} x ${size.height}; the limit is ${HDR_MAX_PIXELS / 1048576} megapixels (4096 x 4096)`;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function sha256Hex(bytes, subtle) {
|
|
109
|
+
if (!subtle?.digest) throw new Error("SubtleCrypto is unavailable — a secure context is required to load an HDR");
|
|
110
|
+
const digest = await subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
111
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The id these bytes are stored under: `hdr-` and the first 128 bits of their SHA-256. */
|
|
115
|
+
export async function hdrIdForBytes(bytes, subtle = globalThis.crypto?.subtle) {
|
|
116
|
+
const view = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
|
|
117
|
+
return `${HDR_ID_PREFIX}${(await sha256Hex(view, subtle)).slice(0, 32)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const direction = (value) => Array.isArray(value) && value.length === 3 && value.every(finite) ? value.map((v) => Number(v.toFixed(2))) : null;
|
|
121
|
+
/** A stored environment record repaired, or null to drop it (scene-assets.js normalizeAsset). */
|
|
122
|
+
export function normalizeHdrAsset(record, bytes) {
|
|
123
|
+
if (!isHdrAssetId(record?.id) || typeof record.type !== "string" || !HDR_TYPES.includes(record.type.toLowerCase())) return null;
|
|
124
|
+
const width = Math.round(Number(record.width)), height = Math.round(Number(record.height)), brightest = direction(record.brightest);
|
|
125
|
+
return {
|
|
126
|
+
id: record.id, type: record.type.toLowerCase(), bytes, name: typeof record.name === "string" ? record.name : "",
|
|
127
|
+
...(width > 0 && height > 0 ? { width, height } : {}),
|
|
128
|
+
...(brightest ? { brightest } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The stage look: the key light the user grabs plus stage.lighting (sky, fog,
|
|
2
|
+
// ambient, fill, exposure, shadow softness, environment light, ambient
|
|
3
|
+
// occlusion) and the recipes that set them in one action. Pure data and arithmetic — no three.js or React — so the scene
|
|
4
|
+
// document, the command layer, the tests and the agent sidecar can all load it.
|
|
5
|
+
// The renderer reads it through stageLook() (room.jsx, stage-sky.jsx).
|
|
6
|
+
import { elementByPath } from "./studio-elements.js";
|
|
7
|
+
import { GRID_BACKGROUND, GRID_FOG, stageBackground } from "./grid-view.js";
|
|
8
|
+
import { environmentSpec } from "./stage-environment.js";
|
|
9
|
+
import { HDR_FIELDS, normalizeHdrFields } from "./stage-hdr.js";
|
|
10
|
+
|
|
11
|
+
export const LIGHTING_PRESETS = Object.freeze(["studio", "day", "overcast", "golden", "dusk", "night", "interior", "stage"]);
|
|
12
|
+
|
|
13
|
+
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
14
|
+
const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
const color = (value, fallback) => (typeof value === "string" && HEX.test(value) ? value : fallback);
|
|
16
|
+
const limits = (path) => elementByPath(path);
|
|
17
|
+
const finite = (value, fallback, { min, max }) => (typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback);
|
|
18
|
+
|
|
19
|
+
// The tuned rig: these values render the stage exactly as it looked before the
|
|
20
|
+
// look was authorable, so a stage saved earlier (no lighting, no key colour)
|
|
21
|
+
// renders unchanged.
|
|
22
|
+
const DEFAULT_KEY_LIGHT = Object.freeze({ x: 6, y: 9, z: 4, intensity: 1.12, warmth: 0.5, color: null });
|
|
23
|
+
// envLight and ao are 0 in the studio look: no environment map, no occlusion pass.
|
|
24
|
+
// No look names an HDR environment (stage-hdr.js); the user or the agent picks one.
|
|
25
|
+
const DEFAULT_LIGHTING = Object.freeze({ preset: "studio", sky: null, skyTop: null, ambient: 1, hemiSky: "#ffffff", hemiGround: "#5a5d64", fill: 1, fog: 0, exposure: 1, shadowSoftness: 0, envLight: 0, ao: 0, ...normalizeHdrFields() });
|
|
26
|
+
const LIGHTING_NUMBERS = ["fill", "fog", "exposure", "shadowSoftness", "envLight", "ao"];
|
|
27
|
+
|
|
28
|
+
/** Clamp a stored key light back into the stage's usable envelope. `color`
|
|
29
|
+
* (a #rrggbb or null) overrides the warmth slider when set. */
|
|
30
|
+
export function createKeyLight(value) {
|
|
31
|
+
const source = plain(value) ? value : {};
|
|
32
|
+
const key = Object.fromEntries(["x", "y", "z", "intensity", "warmth"].map((axis) => [axis, finite(source[axis], DEFAULT_KEY_LIGHT[axis], limits(`stage.keyLight.${axis}`))]));
|
|
33
|
+
return { ...key, color: color(source.color, null) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** stage.lighting with every field present and in range; anything missing or
|
|
37
|
+
* malformed falls back to the studio look. */
|
|
38
|
+
export function normalizeLighting(value) {
|
|
39
|
+
const source = plain(value) ? value : {};
|
|
40
|
+
const number = (key) => finite(source[key], DEFAULT_LIGHTING[key], limits(`stage.lighting.${key}`));
|
|
41
|
+
return {
|
|
42
|
+
preset: LIGHTING_PRESETS.includes(source.preset) ? source.preset : DEFAULT_LIGHTING.preset,
|
|
43
|
+
sky: color(source.sky, null),
|
|
44
|
+
skyTop: color(source.skyTop, null),
|
|
45
|
+
ambient: number("ambient"),
|
|
46
|
+
hemiSky: color(source.hemiSky, DEFAULT_LIGHTING.hemiSky),
|
|
47
|
+
hemiGround: color(source.hemiGround, DEFAULT_LIGHTING.hemiGround),
|
|
48
|
+
...Object.fromEntries(LIGHTING_NUMBERS.map((key) => [key, number(key)])),
|
|
49
|
+
...normalizeHdrFields(source),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Each recipe is a complete look: every lighting field plus the key's colour,
|
|
54
|
+
// brightness and place (the sun, the moon, the lamp). Key positions are metres
|
|
55
|
+
// around the stage centre; a low y is a low sun. Tuned by eye on the clay
|
|
56
|
+
// figure through the shot camera, so treat them as starting points to tweak.
|
|
57
|
+
const recipe = (lighting, keyLight) => Object.freeze({ lighting: Object.freeze({ ...DEFAULT_LIGHTING, ...lighting }), keyLight: Object.freeze(keyLight) });
|
|
58
|
+
export const LIGHTING_RECIPES = Object.freeze({
|
|
59
|
+
studio: recipe({}, { ...DEFAULT_KEY_LIGHT }),
|
|
60
|
+
day: recipe({ sky: "#bcd2e8", skyTop: "#4f86c6", ambient: 1.15, hemiSky: "#c4dcff", hemiGround: "#6b6253", fill: 0.7, fog: 0.12, shadowSoftness: 0.15, envLight: 1, ao: 0.8 },
|
|
61
|
+
{ x: 5, y: 14, z: 7, intensity: 2.1, color: "#fff3df" }),
|
|
62
|
+
overcast: recipe({ sky: "#c2c7ce", skyTop: "#8f97a3", ambient: 1.9, hemiSky: "#e3e8ef", hemiGround: "#5c5f64", fill: 1.2, fog: 0.3, exposure: 1.05, shadowSoftness: 1, envLight: 1, ao: 1 },
|
|
63
|
+
{ x: 2, y: 14, z: 3, intensity: 0.55, color: "#e8ecf2" }),
|
|
64
|
+
golden: recipe({ sky: "#f0a868", skyTop: "#6a8cc0", ambient: 0.75, hemiSky: "#9fb3d6", hemiGround: "#6e5136", fill: 0.45, fog: 0.3, shadowSoftness: 0.35, envLight: 1, ao: 0.8 },
|
|
65
|
+
{ x: 9, y: 3.2, z: 4, intensity: 1.9, color: "#ffad5c" }),
|
|
66
|
+
dusk: recipe({ sky: "#8a6f8c", skyTop: "#18223f", ambient: 0.8, hemiSky: "#5b6c9c", hemiGround: "#29242e", fill: 0.35, fog: 0.4, shadowSoftness: 0.6, envLight: 1, ao: 0.8 },
|
|
67
|
+
{ x: -9, y: 1.8, z: -4, intensity: 0.7, color: "#ff9466" }),
|
|
68
|
+
night: recipe({ sky: "#0b1426", skyTop: "#020408", ambient: 0.6, hemiSky: "#34507f", hemiGround: "#0a0c12", fill: 0.25, fog: 0.45, shadowSoftness: 0.75, envLight: 1, ao: 0.8 },
|
|
69
|
+
{ x: -6, y: 6.5, z: 0.5, intensity: 0.6, color: "#9db6e6" }),
|
|
70
|
+
interior: recipe({ sky: "#2b2621", ambient: 0.7, hemiSky: "#ffe2bf", hemiGround: "#3d3128", fill: 0.55, fog: 0.35, shadowSoftness: 0.55, envLight: 1, ao: 0.9 },
|
|
71
|
+
{ x: 2, y: 4.5, z: 2.5, intensity: 1.35, color: "#ffd29c" }),
|
|
72
|
+
stage: recipe({ sky: "#000000", ambient: 0, hemiSky: "#000000", hemiGround: "#000000", fill: 0, fog: 0.7, shadowSoftness: 0.2, ao: 0.6 },
|
|
73
|
+
{ x: 1.5, y: 9, z: 3, intensity: 2.6, color: "#fff0d8" }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** What stage.applyLighting { keep } may hold back from the recipe. "lighting.hdr"
|
|
77
|
+
* keeps the HDR environment with its rotation, intensity and backdrop switch. */
|
|
78
|
+
export const LIGHTING_KEEP_PATHS = Object.freeze(["keyLight.position", "keyLight.intensity", "keyLight.color",
|
|
79
|
+
...["sky", "skyTop", "ambient", "hemiSky", "hemiGround", "fill", "fog", "exposure", "shadowSoftness", "envLight", "ao", "hdr"].map((key) => `lighting.${key}`)]);
|
|
80
|
+
|
|
81
|
+
/** The stage's { lighting, keyLight } after one recipe: every field written
|
|
82
|
+
* from it except the `keep` paths, which stay as they are. Studio also resets
|
|
83
|
+
* warmth, so it renders the tuned rig exactly; other recipes set a key colour,
|
|
84
|
+
* which overrides warmth. */
|
|
85
|
+
export function applyLightingRecipe(stage, preset, keep = []) {
|
|
86
|
+
const chosen = LIGHTING_RECIPES[preset];
|
|
87
|
+
if (!chosen) throw new Error(`Unknown lighting preset: ${preset}`);
|
|
88
|
+
const before = { lighting: normalizeLighting(stage?.lighting), keyLight: createKeyLight(stage?.keyLight) };
|
|
89
|
+
const lighting = { ...chosen.lighting, preset };
|
|
90
|
+
for (const path of keep) if (path.startsWith("lighting.")) lighting[path.slice(9)] = before.lighting[path.slice(9)];
|
|
91
|
+
if (keep.includes("lighting.hdr")) for (const key of HDR_FIELDS) lighting[key] = before.lighting[key];
|
|
92
|
+
const keyLight = { ...before.keyLight, ...chosen.keyLight };
|
|
93
|
+
if (keep.includes("keyLight.position")) Object.assign(keyLight, { x: before.keyLight.x, y: before.keyLight.y, z: before.keyLight.z });
|
|
94
|
+
if (keep.includes("keyLight.intensity")) keyLight.intensity = before.keyLight.intensity;
|
|
95
|
+
if (keep.includes("keyLight.color")) Object.assign(keyLight, { color: before.keyLight.color, warmth: before.keyLight.warmth });
|
|
96
|
+
return { lighting: normalizeLighting(lighting), keyLight: createKeyLight(keyLight) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Linear fog, in metres from the camera. With fog 0 the viewport keeps its
|
|
100
|
+
// tuned 18..54 m horizon fade and exports their 55..95 m one (stage-capture.jsx);
|
|
101
|
+
// haze pulls both in together, reaching 2..14 m at fog 1 (a black-void pool of
|
|
102
|
+
// light under the stage recipe). The squared falloff makes small values a
|
|
103
|
+
// distant haze rather than a jump.
|
|
104
|
+
export function hazeRange(fog) {
|
|
105
|
+
const far = (1 - fog) ** 2;
|
|
106
|
+
return { near: 2 + 53 * far, far: 14 + 81 * far };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Everything the renderer needs for one look, as plain colours and numbers.
|
|
111
|
+
* A null sky keeps the UI theme's studio stage — its grey, its deck and its
|
|
112
|
+
* ground bounce — so the default renders exactly as before under both themes;
|
|
113
|
+
* the grid view's neutral rig applies only then. An explicit sky makes the look
|
|
114
|
+
* theme-independent: what the shot camera exports is the same for every user,
|
|
115
|
+
* and so does an HDR backdrop (stage.lighting.hdr with hdrBackground).
|
|
116
|
+
*/
|
|
117
|
+
export function stageLook({ lighting, keyLight, theme = "dark", grid = false }) {
|
|
118
|
+
const normalized = normalizeLighting(lighting), key = createKeyLight(keyLight);
|
|
119
|
+
// An HDR backdrop is no UI theme's stage either: a null sky under it is the dark stage's grey.
|
|
120
|
+
const l = normalized.sky === null && normalized.hdr && normalized.hdrBackground ? { ...normalized, sky: stageBackground("dark") } : normalized;
|
|
121
|
+
const themed = l.sky === null, neutral = grid && themed, lightTheme = themed && theme === "light";
|
|
122
|
+
const sky = themed ? (grid ? GRID_BACKGROUND : stageBackground(theme)) : l.sky;
|
|
123
|
+
const haze = hazeRange(l.fog), base = neutral ? GRID_FOG : { near: 18, far: 54 };
|
|
124
|
+
const dim = key.intensity / 1.12, lift = Math.min(1, 0.35 + 0.65 * dim);
|
|
125
|
+
// The environment map takes over the hemisphere's share as envLight rises (stage-environment.js).
|
|
126
|
+
const skyLight = 0.9 * lift * l.ambient, hemiShare = Math.max(0, 1 - l.envLight);
|
|
127
|
+
return {
|
|
128
|
+
sky, skyTop: l.skyTop,
|
|
129
|
+
fog: { color: sky, near: Math.min(base.near, haze.near), far: Math.min(base.far, haze.far) },
|
|
130
|
+
// Exports keep their own far horizon until haze asks for a nearer one.
|
|
131
|
+
captureFog: l.fog > 0 ? haze : null,
|
|
132
|
+
hemi: { sky: neutral ? "#ffffff" : l.hemiSky, ground: neutral ? "#3a3d42" : lightTheme ? "#d6d8de" : l.hemiGround, intensity: skyLight * hemiShare },
|
|
133
|
+
// three's IBL irradiance is pi x the map, a hemisphere's is its colour x intensity: the 1/pi matches them.
|
|
134
|
+
env: environmentSpec(l, key, sky), envIntensity: (skyLight * l.envLight) / Math.PI,
|
|
135
|
+
// An HDR environment replaces that map once it has loaded (stage-sky.jsx): its light is
|
|
136
|
+
// the file's radiance x hdrIntensity x envLight, its backdrop the file x hdrIntensity.
|
|
137
|
+
hdr: l.hdr ? { id: l.hdr, rotation: l.hdrRotation, intensity: l.hdrIntensity, background: l.hdrBackground, envIntensity: l.envLight * l.hdrIntensity } : null,
|
|
138
|
+
ao: l.ao,
|
|
139
|
+
ambient: (neutral ? 0.34 : lightTheme ? 0.32 : 0.18) * lift * l.ambient,
|
|
140
|
+
fill: 0.36 * dim * l.fill, rim: 0.22 * dim * l.fill,
|
|
141
|
+
exposure: l.exposure,
|
|
142
|
+
// shadow.radius in shadow-map texels (2048 over 28 m: ~1.4 cm each); 1 is the tuned hard
|
|
143
|
+
// edge. A wider kernel reaches the surface's own depth on curved clay and
|
|
144
|
+
// cross-hatches it, so the normal offset grows with it (metres).
|
|
145
|
+
shadowRadius: 1 + l.shadowSoftness * 7,
|
|
146
|
+
shadowNormalBias: 0.02 + l.shadowSoftness * 0.08,
|
|
147
|
+
lightDeck: lightTheme,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Ambient occlusion for the stage look (stage.lighting.ao): contact darkening
|
|
2
|
+
// where a body meets the floor or a prop, and in creases (an armpit, the back of
|
|
3
|
+
// a knee in a squat). Screen space, because the bodies are skinned and move: a
|
|
4
|
+
// baked aoMap cannot follow a skinned mesh, and contact blobs under the feet
|
|
5
|
+
// miss every crease.
|
|
6
|
+
//
|
|
7
|
+
// One hook on the scene (scene.onAfterRender) runs it after each draw of the
|
|
8
|
+
// stage through a perspective camera, so every path gets it with no code of its
|
|
9
|
+
// own: the viewport panes, the shot preview card, capture_frame, contact sheets
|
|
10
|
+
// and video export (stage-capture.jsx renders the same scene into its target).
|
|
11
|
+
// A draw with an override material (the outline prepass, the depth and normal
|
|
12
|
+
// plates) is left alone, and so is the orthographic plan.
|
|
13
|
+
//
|
|
14
|
+
// Per draw, at half resolution past 960 px wide: a depth-only prepass of the
|
|
15
|
+
// opaque set, the occlusion within AO_RADIUS of each pixel from SAMPLES samples
|
|
16
|
+
// (how far each sample's direction rises above the surface the depth holds,
|
|
17
|
+
// weighted toward the near ones and faded with the fog), a depth-aware 5x5
|
|
18
|
+
// blur, then a multiply over the frame just drawn. The prepass reuses the
|
|
19
|
+
// shadow maps the draw already made. On an Apple M4 Pro it adds about 0.4 ms of
|
|
20
|
+
// GPU time to a 1920x1080 export frame.
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
import { FullScreenQuad } from "three/examples/jsm/postprocessing/Pass.js";
|
|
23
|
+
import { GIZMO_LAYER, POSER_LAYER } from "./dualview.jsx";
|
|
24
|
+
|
|
25
|
+
const AO_RADIUS = 0.4;
|
|
26
|
+
const SAMPLES = 16;
|
|
27
|
+
// A sample whose direction rises less than this cosine above the normal is the
|
|
28
|
+
// same surface (floor noise at grazing angles), not an occluder.
|
|
29
|
+
const BIAS = 0.15;
|
|
30
|
+
// Occlusion to darkening, tuned by eye on the clay figure (a squat's creases, feet on the deck).
|
|
31
|
+
const GAIN = 3;
|
|
32
|
+
// Size buckets kept alive at once (the main pane, the preview card, the exports).
|
|
33
|
+
const CACHED_SIZES = 4;
|
|
34
|
+
|
|
35
|
+
const VERTEX = "varying vec2 vUv;\nvoid main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }";
|
|
36
|
+
const OCCLUSION = `
|
|
37
|
+
uniform sampler2D tDepth;
|
|
38
|
+
uniform mat4 projection;
|
|
39
|
+
uniform mat4 inverseProjection;
|
|
40
|
+
uniform vec2 size;
|
|
41
|
+
uniform vec2 fog;
|
|
42
|
+
varying vec2 vUv;
|
|
43
|
+
vec3 viewAt(vec2 uv, float depth) {
|
|
44
|
+
vec4 p = inverseProjection * vec4(vec3(uv, depth) * 2.0 - 1.0, 1.0);
|
|
45
|
+
return p.xyz / p.w;
|
|
46
|
+
}
|
|
47
|
+
vec3 viewAt(vec2 uv) { return viewAt(uv, texture2D(tDepth, uv).x); }
|
|
48
|
+
void main() {
|
|
49
|
+
float depth = texture2D(tDepth, vUv).x;
|
|
50
|
+
if (depth >= 1.0) { gl_FragColor = vec4(1.0); return; }
|
|
51
|
+
vec3 p = viewAt(vUv, depth);
|
|
52
|
+
// The normal of the surface the depth buffer holds (each axis from the nearer
|
|
53
|
+
// neighbour, so an edge does not bend it): flat facets of the low-poly clay
|
|
54
|
+
// then occlude nothing of their own, whatever their smoothed shading normal says.
|
|
55
|
+
vec2 texel = 1.0 / size;
|
|
56
|
+
vec3 right = viewAt(vUv + vec2(texel.x, 0.0)) - p, left = p - viewAt(vUv - vec2(texel.x, 0.0));
|
|
57
|
+
vec3 up = viewAt(vUv + vec2(0.0, texel.y)) - p, down = p - viewAt(vUv - vec2(0.0, texel.y));
|
|
58
|
+
vec3 n = normalize(cross(abs(right.z) < abs(left.z) ? right : left, abs(up.z) < abs(down.z) ? up : down));
|
|
59
|
+
float reach = min(${AO_RADIUS.toFixed(2)} * projection[1][1] * 0.5 * size.y / -p.z, size.y * 0.2);
|
|
60
|
+
float fade = 1.0 - smoothstep(fog.x, fog.y, -p.z);
|
|
61
|
+
if (reach < 1.0 || fade <= 0.0) { gl_FragColor = vec4(1.0); return; }
|
|
62
|
+
float spin = 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
|
63
|
+
// A weighted mean over the samples near the surface (weight falls to 0 at RADIUS),
|
|
64
|
+
// so an occluder close by counts fully however small it is on screen.
|
|
65
|
+
float occlusion = 0.0, weights = 0.25;
|
|
66
|
+
for (int i = 0; i < ${SAMPLES}; i++) {
|
|
67
|
+
float angle = float(i) * 2.39996323 + spin;
|
|
68
|
+
// snapped to a texel centre, so the depth read and the position rebuilt from it agree
|
|
69
|
+
vec2 uv = (floor((vUv + vec2(cos(angle), sin(angle)) * ((float(i) + 0.5) / ${SAMPLES}.0) * reach / size) * size) + 0.5) / size;
|
|
70
|
+
vec3 v = viewAt(uv) - p;
|
|
71
|
+
float distance = length(v), weight = pow(max(0.0, 1.0 - distance / ${AO_RADIUS.toFixed(2)}), 2.0);
|
|
72
|
+
occlusion += max(dot(v, n) / max(distance, 1e-4) - ${BIAS.toFixed(2)}, 0.0) * weight;
|
|
73
|
+
weights += weight;
|
|
74
|
+
}
|
|
75
|
+
float ao = clamp(1.0 - ${GAIN.toFixed(2)} * occlusion / (weights * ${(1 - BIAS).toFixed(2)}), 0.0, 1.0);
|
|
76
|
+
gl_FragColor = vec4(vec3(mix(1.0, ao, fade)), 1.0);
|
|
77
|
+
}`;
|
|
78
|
+
const BLUR = `
|
|
79
|
+
#include <packing>
|
|
80
|
+
uniform sampler2D tOcclusion;
|
|
81
|
+
uniform sampler2D tDepth;
|
|
82
|
+
uniform vec2 size;
|
|
83
|
+
uniform vec2 clip;
|
|
84
|
+
varying vec2 vUv;
|
|
85
|
+
float viewZ(vec2 uv) { return perspectiveDepthToViewZ(texture2D(tDepth, uv).x, clip.x, clip.y); }
|
|
86
|
+
void main() {
|
|
87
|
+
float z = viewZ(vUv), total = 0.0, weights = 0.0;
|
|
88
|
+
for (int x = -2; x <= 2; x++) for (int y = -2; y <= 2; y++) {
|
|
89
|
+
vec2 uv = vUv + vec2(float(x), float(y)) / size;
|
|
90
|
+
float weight = 1.0 - smoothstep(0.02, 0.06, abs(viewZ(uv) - z) / -z);
|
|
91
|
+
total += texture2D(tOcclusion, uv).r * weight;
|
|
92
|
+
weights += weight;
|
|
93
|
+
}
|
|
94
|
+
gl_FragColor = vec4(vec3(total / max(weights, 1e-4)), 1.0);
|
|
95
|
+
}`;
|
|
96
|
+
const COMPOSITE = `
|
|
97
|
+
uniform sampler2D tOcclusion;
|
|
98
|
+
uniform float strength;
|
|
99
|
+
varying vec2 vUv;
|
|
100
|
+
void main() { gl_FragColor = vec4(vec3(mix(1.0, texture2D(tOcclusion, vUv).r, strength)), 1.0); }`;
|
|
101
|
+
|
|
102
|
+
const pass = (fragmentShader, uniforms, extra = {}) => new THREE.ShaderMaterial({ vertexShader: VERTEX, fragmentShader, uniforms, depthTest: false, depthWrite: false, ...extra });
|
|
103
|
+
const target = (width, height, filter, depth = false) => new THREE.WebGLRenderTarget(width, height, {
|
|
104
|
+
minFilter: filter, magFilter: filter, depthBuffer: depth, generateMipmaps: false,
|
|
105
|
+
...(depth ? { depthTexture: Object.assign(new THREE.DepthTexture(width, height, THREE.UnsignedIntType), { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }) } : {}),
|
|
106
|
+
});
|
|
107
|
+
// What the prepass leaves out: light rather than surface (glow halos, smoke,
|
|
108
|
+
// see-through and faded parts, ghosts), picture cards whose alpha a solid
|
|
109
|
+
// depth pass would ignore, lines and points.
|
|
110
|
+
const seeThrough = (material) => [material].flat().some((m) => m && (m.transparent || m.depthWrite === false));
|
|
111
|
+
const leftOut = (node) => node.userData?.beautyOnly || node.userData?.cutoutTexture !== undefined || node.isSprite || node.isPoints || node.isLine || (node.isMesh && seeThrough(node.material));
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Put the occlusion pass on `scene`. `settings.strength` (0..1) is read at each
|
|
115
|
+
* draw, so a slider changes it without rebuilding anything. Answers a function
|
|
116
|
+
* that takes the hook off and frees the targets.
|
|
117
|
+
*/
|
|
118
|
+
export function installOcclusion(scene, settings) {
|
|
119
|
+
// Depth is all the pass reads: the prepass writes no colour at all.
|
|
120
|
+
const depthOnly = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, colorWrite: false });
|
|
121
|
+
const occlusion = pass(OCCLUSION, { tDepth: { value: null }, projection: { value: new THREE.Matrix4() }, inverseProjection: { value: new THREE.Matrix4() }, size: { value: new THREE.Vector2() }, fog: { value: new THREE.Vector2() } });
|
|
122
|
+
const blur = pass(BLUR, { tOcclusion: { value: null }, tDepth: { value: null }, size: { value: new THREE.Vector2() }, clip: { value: new THREE.Vector2() } });
|
|
123
|
+
const composite = pass(COMPOSITE, { tOcclusion: { value: null }, strength: { value: 1 } }, {
|
|
124
|
+
blending: THREE.CustomBlending, blendEquation: THREE.AddEquation, blendSrc: THREE.ZeroFactor, blendDst: THREE.SrcColorFactor,
|
|
125
|
+
blendSrcAlpha: THREE.ZeroFactor, blendDstAlpha: THREE.OneFactor, toneMapped: false,
|
|
126
|
+
});
|
|
127
|
+
const quad = new FullScreenQuad(), sizes = new Map(), viewport = new THREE.Vector4(), scissor = new THREE.Vector4();
|
|
128
|
+
let running = false;
|
|
129
|
+
const targetsFor = (width, height) => {
|
|
130
|
+
const key = `${width}x${height}`;
|
|
131
|
+
let set = sizes.get(key);
|
|
132
|
+
if (set) sizes.delete(key);
|
|
133
|
+
else set = { prepass: target(width, height, THREE.NearestFilter, true), raw: target(width, height, THREE.NearestFilter), smooth: target(width, height, THREE.LinearFilter) };
|
|
134
|
+
sizes.set(key, set);
|
|
135
|
+
if (sizes.size > CACHED_SIZES) {
|
|
136
|
+
const [oldest, stale] = sizes.entries().next().value;
|
|
137
|
+
sizes.delete(oldest);
|
|
138
|
+
for (const t of Object.values(stale)) { t.depthTexture?.dispose(); t.dispose(); }
|
|
139
|
+
}
|
|
140
|
+
return set;
|
|
141
|
+
};
|
|
142
|
+
scene.onAfterRender = (renderer, _scene, camera) => {
|
|
143
|
+
if (running || scene.overrideMaterial || !camera?.isPerspectiveCamera || !(settings.strength > 0)) return;
|
|
144
|
+
running = true;
|
|
145
|
+
const output = renderer.getRenderTarget(), autoClear = renderer.autoClear, background = scene.background, mask = camera.layers.mask;
|
|
146
|
+
const shadows = renderer.shadowMap, shadowAuto = shadows.autoUpdate, shadowNeeds = shadows.needsUpdate;
|
|
147
|
+
renderer.getViewport(viewport);
|
|
148
|
+
renderer.getScissor(scissor);
|
|
149
|
+
const pixels = renderer.getCurrentViewport(new THREE.Vector4()), half = pixels.z > 960 ? 0.5 : 1;
|
|
150
|
+
const width = Math.max(1, Math.round(pixels.z * half)), height = Math.max(1, Math.round(pixels.w * half));
|
|
151
|
+
const { prepass, raw, smooth } = targetsFor(width, height), hidden = [];
|
|
152
|
+
try {
|
|
153
|
+
scene.traverse((node) => { if (node.visible && leftOut(node)) { hidden.push(node); node.visible = false; } });
|
|
154
|
+
scene.background = null;
|
|
155
|
+
scene.overrideMaterial = depthOnly;
|
|
156
|
+
camera.layers.disable(GIZMO_LAYER);
|
|
157
|
+
camera.layers.disable(POSER_LAYER);
|
|
158
|
+
shadows.autoUpdate = false;
|
|
159
|
+
renderer.autoClear = true;
|
|
160
|
+
renderer.setRenderTarget(prepass);
|
|
161
|
+
renderer.render(scene, camera);
|
|
162
|
+
for (const node of hidden.splice(0)) node.visible = true;
|
|
163
|
+
scene.background = background;
|
|
164
|
+
scene.overrideMaterial = null;
|
|
165
|
+
camera.layers.mask = mask;
|
|
166
|
+
const u = occlusion.uniforms, b = blur.uniforms, fog = scene.fog;
|
|
167
|
+
u.tDepth.value = b.tDepth.value = prepass.depthTexture;
|
|
168
|
+
u.projection.value.copy(camera.projectionMatrix);
|
|
169
|
+
u.inverseProjection.value.copy(camera.projectionMatrixInverse);
|
|
170
|
+
u.size.value.set(width, height);
|
|
171
|
+
u.fog.value.set(fog?.near ?? 1e5, fog?.far ?? 2e5);
|
|
172
|
+
renderer.setRenderTarget(raw);
|
|
173
|
+
quad.material = occlusion;
|
|
174
|
+
quad.render(renderer);
|
|
175
|
+
b.tOcclusion.value = raw.texture;
|
|
176
|
+
b.size.value.set(width, height);
|
|
177
|
+
b.clip.value.set(camera.near, camera.far);
|
|
178
|
+
renderer.setRenderTarget(smooth);
|
|
179
|
+
quad.material = blur;
|
|
180
|
+
quad.render(renderer);
|
|
181
|
+
composite.uniforms.tOcclusion.value = smooth.texture;
|
|
182
|
+
composite.uniforms.strength.value = Math.min(1, settings.strength);
|
|
183
|
+
renderer.setRenderTarget(output);
|
|
184
|
+
// The canvas takes back the pane's own viewport (setRenderTarget(null) floors it, the draw rounded it).
|
|
185
|
+
if (output === null) {
|
|
186
|
+
renderer.setViewport(viewport);
|
|
187
|
+
renderer.setScissor(scissor);
|
|
188
|
+
}
|
|
189
|
+
renderer.autoClear = false;
|
|
190
|
+
quad.material = composite;
|
|
191
|
+
quad.render(renderer);
|
|
192
|
+
} finally {
|
|
193
|
+
for (const node of hidden) node.visible = true;
|
|
194
|
+
if (renderer.getRenderTarget() !== output) renderer.setRenderTarget(output);
|
|
195
|
+
scene.background = background;
|
|
196
|
+
scene.overrideMaterial = null;
|
|
197
|
+
camera.layers.mask = mask;
|
|
198
|
+
shadows.autoUpdate = shadowAuto;
|
|
199
|
+
shadows.needsUpdate = shadowNeeds;
|
|
200
|
+
renderer.autoClear = autoClear;
|
|
201
|
+
running = false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return () => {
|
|
205
|
+
delete scene.onAfterRender;
|
|
206
|
+
for (const set of sizes.values()) for (const t of Object.values(set)) { t.depthTexture?.dispose(); t.dispose(); }
|
|
207
|
+
for (const material of [depthOnly, occlusion, blur, composite]) material.dispose();
|
|
208
|
+
quad.dispose();
|
|
209
|
+
};
|
|
210
|
+
}
|