@dailies-studio/shot-editor 0.1.0
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/dist/assets/index-B7notge7.css +1 -0
- package/dist/assets/index-BBE6Ta9L.js +52 -0
- package/dist/index.html +13 -0
- package/package.json +45 -0
- package/src/api.ts +289 -0
- package/src/app/App.tsx +394 -0
- package/src/audio/index.tsx +74 -0
- package/src/audio/preview.ts +364 -0
- package/src/bridge.ts +45 -0
- package/src/bridgeTypes.ts +582 -0
- package/src/channels.ts +40 -0
- package/src/main.tsx +15 -0
- package/src/panels/ResizeHandle.tsx +113 -0
- package/src/panels/agent/AgentPanel.tsx +478 -0
- package/src/panels/agent/agent.css +322 -0
- package/src/panels/export/ExportDialog.tsx +284 -0
- package/src/panels/files/FilePanel.tsx +564 -0
- package/src/panels/files/McpSection.tsx +66 -0
- package/src/panels/files/icons.tsx +118 -0
- package/src/panels/header/DocSwitcher.tsx +85 -0
- package/src/panels/header/Header.tsx +132 -0
- package/src/panels/header/HistoryControls.tsx +20 -0
- package/src/panels/header/StatusBadges.tsx +49 -0
- package/src/panels/header/TransportBar.tsx +86 -0
- package/src/panels/header/ZoomControls.tsx +38 -0
- package/src/panels/help/HelpSection.tsx +190 -0
- package/src/panels/inspector/FramingRows.tsx +69 -0
- package/src/panels/inspector/InspectorPanel.tsx +178 -0
- package/src/panels/inspector/MarkerSection.tsx +63 -0
- package/src/panels/inspector/ParamRow.tsx +71 -0
- package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
- package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
- package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
- package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
- package/src/panels/inspector/animate/ShotSection.tsx +126 -0
- package/src/panels/inspector/animate/useBlocks.ts +43 -0
- package/src/panels/inspector/design/AlignRow.tsx +63 -0
- package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
- package/src/panels/inspector/design/ClonerSection.tsx +142 -0
- package/src/panels/inspector/design/ComponentSection.tsx +94 -0
- package/src/panels/inspector/design/CornerSection.tsx +52 -0
- package/src/panels/inspector/design/FillSection.tsx +89 -0
- package/src/panels/inspector/design/HtmlSection.tsx +50 -0
- package/src/panels/inspector/design/InstanceSection.tsx +302 -0
- package/src/panels/inspector/design/MediaSection.tsx +251 -0
- package/src/panels/inspector/design/MeshSection.tsx +172 -0
- package/src/panels/inspector/design/ShadowSection.tsx +73 -0
- package/src/panels/inspector/design/StrokeSection.tsx +85 -0
- package/src/panels/inspector/design/TransformSection.tsx +91 -0
- package/src/panels/inspector/design/TypographySection.tsx +170 -0
- package/src/panels/inspector/scene/CameraSection.tsx +59 -0
- package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
- package/src/panels/inspector/scene/SceneSection.tsx +71 -0
- package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
- package/src/panels/inspector/useDocWrite.ts +268 -0
- package/src/panels/material/MaterialPicker.tsx +172 -0
- package/src/panels/stage/StageMenu.tsx +38 -0
- package/src/panels/stage/StagePanel.tsx +131 -0
- package/src/panels/timeline/AudioLane.tsx +340 -0
- package/src/panels/timeline/BlockRow.tsx +242 -0
- package/src/panels/timeline/ChannelRow.tsx +266 -0
- package/src/panels/timeline/LifespanRow.tsx +165 -0
- package/src/panels/timeline/MarkerLane.tsx +146 -0
- package/src/panels/timeline/SceneStrip.tsx +145 -0
- package/src/panels/timeline/TimelineDock.tsx +488 -0
- package/src/panels/timeline/model.ts +641 -0
- package/src/panels/timeline/snap.ts +191 -0
- package/src/panels/toolbar/AddMenu.tsx +230 -0
- package/src/panels/toolbar/CameraViewBar.tsx +70 -0
- package/src/panels/toolbar/ShadingBar.tsx +42 -0
- package/src/panels/toolbar/ToolDockBar.tsx +110 -0
- package/src/panels/toolbar/ViewModeBar.tsx +49 -0
- package/src/shotTokens.css +131 -0
- package/src/stage/FrustumGizmo.tsx +184 -0
- package/src/stage/GridPlane.tsx +274 -0
- package/src/stage/MoveGizmo.tsx +411 -0
- package/src/stage/PerfHud.tsx +206 -0
- package/src/stage/SplitRenderer.tsx +511 -0
- package/src/stage/StageApp.tsx +3423 -0
- package/src/stage/StageTools.tsx +581 -0
- package/src/stage/bridgeClient.ts +278 -0
- package/src/stage/feed.ts +243 -0
- package/src/stage/frustumGeometry.ts +172 -0
- package/src/stage/gizmoMath.ts +494 -0
- package/src/stage/handles.ts +151 -0
- package/src/stage/instanceModules.ts +152 -0
- package/src/stage/interactions/move.ts +172 -0
- package/src/stage/interactions/panZoom.ts +60 -0
- package/src/stage/isolation.ts +155 -0
- package/src/stage/main.tsx +110 -0
- package/src/stage/modalState.ts +427 -0
- package/src/stage/overlays/Overlays.tsx +1046 -0
- package/src/stage/panes.ts +121 -0
- package/src/stage/penState.ts +112 -0
- package/src/stage/project.ts +402 -0
- package/src/stage/shading.ts +55 -0
- package/src/stage/tokenColor.ts +26 -0
- package/src/stage/view.ts +182 -0
- package/src/stage/workingView.ts +636 -0
- package/src/state/agentActions.ts +203 -0
- package/src/state/clientId.ts +25 -0
- package/src/state/context.tsx +33 -0
- package/src/state/insert.ts +507 -0
- package/src/state/keymap.ts +560 -0
- package/src/state/menu.ts +141 -0
- package/src/state/panelSize.ts +26 -0
- package/src/state/renderEvents.ts +100 -0
- package/src/state/store.ts +4094 -0
- package/src/state/triangles.ts +70 -0
- package/src/state/types.ts +384 -0
- package/src/styles.css +2948 -0
- package/src/tools.ts +181 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// The stage's render controller: two views of one scene, into one canvas.
|
|
2
|
+
//
|
|
3
|
+
// Mounted INSIDE `ShotRuntime`'s canvas, and only while a second pane is on
|
|
4
|
+
// screen. `renderControl: "external"` is what makes that legal — see the
|
|
5
|
+
// contract in packages/shot/src/ShotRuntime.tsx. `useFrame(…, 1)` is what hands
|
|
6
|
+
// the present over: r3f skips its own render whenever any subscriber has a
|
|
7
|
+
// priority above zero, which is the identical mechanism `PostPipeline` uses.
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THE CAMERA PANE'S POST GOES THROUGH A RENDER TARGET ──────────────
|
|
10
|
+
//
|
|
11
|
+
// It is not for the resolution and it is not for the colour. It is because a
|
|
12
|
+
// `RenderPipeline` rendered STRAIGHT TO A SCISSORED CANVAS wipes its neighbour
|
|
13
|
+
// on the WebGPU backend.
|
|
14
|
+
//
|
|
15
|
+
// The mechanism, measured against three 0.185.1: `RenderPipeline.render()`
|
|
16
|
+
// forces `outputColorSpace = workingColorSpace` for the duration of the chain,
|
|
17
|
+
// which makes `needsFrameBufferTarget` false and leaves the pass at the
|
|
18
|
+
// canvas's real sample count (4, because the stage asks for `antialias: true`).
|
|
19
|
+
// A plain render's `currentSamples` is 0, because it DOES take the framebuffer
|
|
20
|
+
// target. At 4 samples `WebGPUBackend.beginRender` attaches the swapchain
|
|
21
|
+
// texture as the MSAA RESOLVE TARGET — and a multisample resolve is a
|
|
22
|
+
// whole-attachment operation that `setScissorRect` does not reach. So the post
|
|
23
|
+
// pane's resolve stamps the entire canvas from a buffer that never held the
|
|
24
|
+
// other pane.
|
|
25
|
+
//
|
|
26
|
+
// Three things fix it and only one of them is honest. `antialias: false` costs
|
|
27
|
+
// the whole canvas its AA. "Always draw the post pane first" is unwritten
|
|
28
|
+
// ordering magic the next person will break. Rendering post into its OWN target
|
|
29
|
+
// and blitting that into the pane is backend-agnostic, AA-agnostic and
|
|
30
|
+
// order-agnostic, and it was measured identical on WebGPU and on the WebGL2
|
|
31
|
+
// fallback. It is also the only one CI can protect: the pinned Chromium in this
|
|
32
|
+
// repo cannot create a WebGPU device at all (dxil.dll), so every automated run
|
|
33
|
+
// of this lane takes the fallback and would never see the bug.
|
|
34
|
+
//
|
|
35
|
+
// THE RULE THAT FALLS OUT OF THAT, for whoever adds a third pane: never mix a
|
|
36
|
+
// plain render and a direct `pipeline.render()` across one scissored canvas.
|
|
37
|
+
// The failure is silent, it only appears on WebGPU with AA on, and it passes CI.
|
|
38
|
+
//
|
|
39
|
+
// ── …AND WHY IT IS setRenderTarget AND NOT setOutputRenderTarget ─────────
|
|
40
|
+
//
|
|
41
|
+
// Both bind the same texture. Only one of them tells the SHADER how big it is.
|
|
42
|
+
//
|
|
43
|
+
// `setOutputRenderTarget` leaves `renderer._renderTarget` null, and on that
|
|
44
|
+
// path `_renderScene` never calls `setRenderTarget()` (Renderer.js: the
|
|
45
|
+
// `else` branch just reads the output target). So `renderer.getRenderTarget()`
|
|
46
|
+
// stays null, and `ScreenNode.update` falls through to `getDrawingBufferSize()`
|
|
47
|
+
// — the WHOLE canvas — while `gl_FragCoord` only spans the pane-sized target.
|
|
48
|
+
// In a 50/50 split `screenUV` therefore runs 0..0.5 across the camera pane:
|
|
49
|
+
// `vignette` puts its bright centre on the pane's outer edge, `edgeFade` fades
|
|
50
|
+
// the wrong two sides, and `ghost` samples half the frame. The preview then
|
|
51
|
+
// disagrees with the export for every scene that uses them, from one toggle.
|
|
52
|
+
//
|
|
53
|
+
// `setRenderTarget` makes `_renderTarget` non-null, so `screenSize` is the pane
|
|
54
|
+
// and `screenUV` is 0..1 across it — matching the blit quad's own uv, which is
|
|
55
|
+
// what the un-suffixed `sceneTexture` samples with. The colour argument above
|
|
56
|
+
// survives unchanged: with `_renderTarget !== _outputRenderTarget` the renderer
|
|
57
|
+
// reports `NoToneMapping` + working space, so `needsFrameBufferTarget` is still
|
|
58
|
+
// false and there is still exactly one encode, at the blit. It additionally
|
|
59
|
+
// drops `currentSamples` to the target's own 0, which removes the MSAA resolve
|
|
60
|
+
// this header's first section is about rather than merely routing around it.
|
|
61
|
+
//
|
|
62
|
+
// NAMED CEILING: the scene PASS is still sized from the drawing buffer.
|
|
63
|
+
// `PassNode.updateBefore` honours an output target's size only when it is an XR
|
|
64
|
+
// target, so the pass renders the pane's frustum into a canvas-wide texture.
|
|
65
|
+
// The stretch cancels at the blit (quad uv 0..1 both ways) so the IMAGE is
|
|
66
|
+
// right, but bloom and chromatic aberration compute their kernels in that
|
|
67
|
+
// stretched space, so a round bloom is slightly elliptical in a split. Fixing
|
|
68
|
+
// it needs a pass whose target aspect can differ from the canvas's, which three
|
|
69
|
+
// does not expose — not another call here.
|
|
70
|
+
//
|
|
71
|
+
// ── COLOUR ───────────────────────────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// `outputColorTransform = false` on the pipeline is load-bearing. The target
|
|
74
|
+
// stays in WORKING space, `texture()` decodes nothing out of it, and the blit's
|
|
75
|
+
// own render applies `renderOutput()` exactly once. Leaving it true encodes at
|
|
76
|
+
// the pipeline and again at the blit: a linear 0.5 lands at 223 instead of 188,
|
|
77
|
+
// which looks plausible enough to ship. Same single-encode gate post.ts states.
|
|
78
|
+
//
|
|
79
|
+
// ── BINS ─────────────────────────────────────────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// A scene with a `mesh` layer draws in several passes (packages/shot/bins.ts).
|
|
82
|
+
// EVERY render here goes through `renderBins` or a `SceneSource` rather than
|
|
83
|
+
// `gl.render` / `pass()` directly, so both panes agree with each other and with
|
|
84
|
+
// the export about what covers what. `renderBins` degenerates to exactly one
|
|
85
|
+
// `gl.render` when there is nothing to split, so a scene with no model takes
|
|
86
|
+
// the identical call this file always made.
|
|
87
|
+
//
|
|
88
|
+
// ── NOTHING IS ALLOCATED PER FRAME ───────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// The rig, the clay, the blit quad and the scratch colour are all hoisted. The
|
|
91
|
+
// hot path is scissor rects and one uniform write.
|
|
92
|
+
import { useEffect, useMemo } from "react";
|
|
93
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
94
|
+
import { Color, ColorManagement, HalfFloatType, Layers, RenderTarget } from "three";
|
|
95
|
+
import { MeshBasicNodeMaterial, QuadMesh, RenderPipeline } from "three/webgpu";
|
|
96
|
+
import type { ColorSpace } from "three";
|
|
97
|
+
import type { WebGPURenderer } from "three/webgpu";
|
|
98
|
+
import { texture, uv } from "three/tsl";
|
|
99
|
+
import {
|
|
100
|
+
binSceneSource,
|
|
101
|
+
buildPostChain,
|
|
102
|
+
hasPostChain,
|
|
103
|
+
passSceneSource,
|
|
104
|
+
postSignature,
|
|
105
|
+
renderBins,
|
|
106
|
+
transitionTap,
|
|
107
|
+
} from "@dailies-studio/shot";
|
|
108
|
+
import type { RenderBin, SceneEffects } from "@dailies-studio/shot/doc";
|
|
109
|
+
import type { StageFeed } from "./feed.js";
|
|
110
|
+
import type { PaneRect } from "./panes.js";
|
|
111
|
+
import { workingDrawPlan } from "./shading.js";
|
|
112
|
+
import type { ShadingMode } from "./shading.js";
|
|
113
|
+
import { EDITOR_LAYER } from "./StageTools.js";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The working pane's backdrop and its clay.
|
|
117
|
+
*
|
|
118
|
+
* NOT tokens. These are a GL clear colour and a material colour, so they cannot
|
|
119
|
+
* read the CSS custom properties the overlays use; they are picked to sit just
|
|
120
|
+
* off `--panel` so the pane reads as a workspace rather than as the shot.
|
|
121
|
+
*/
|
|
122
|
+
export const CLAY_BACKDROP = new Color(0x141417);
|
|
123
|
+
const CLAY_FILL = new Color(0x8a8a93);
|
|
124
|
+
/** The wireframe's line colour. Brighter than the clay so edges read as lines
|
|
125
|
+
* on the same backdrop rather than as a dimmer clay. */
|
|
126
|
+
const WIRE_FILL = new Color(0xb8b8c2);
|
|
127
|
+
|
|
128
|
+
/** Scratch for saving the shot's own clear colour across the working pane's. */
|
|
129
|
+
const _prevClear = new Color();
|
|
130
|
+
|
|
131
|
+
export interface SplitRendererProps {
|
|
132
|
+
feed: StageFeed;
|
|
133
|
+
panes: { working: PaneRect | null; camera: PaneRect | null };
|
|
134
|
+
/** The active scene's effects, or null when nothing is on screen. */
|
|
135
|
+
effects: SceneEffects | null;
|
|
136
|
+
/** The active scene's render bins at this frame. Empty or single = one draw,
|
|
137
|
+
* which is every scene without a live mesh layer. */
|
|
138
|
+
bins: readonly RenderBin[];
|
|
139
|
+
/** Whether the scene contains a mesh layer AT ALL. Static (lifetimes
|
|
140
|
+
* ignored), because it picks the post chain's SHAPE — see ShotRuntime. */
|
|
141
|
+
hasMesh: boolean;
|
|
142
|
+
/** MASTER frame — the grain's seed, and the only clock the chain sees. */
|
|
143
|
+
frame: number;
|
|
144
|
+
/** How the WORKING pane draws. The camera pane is not addressable: it is
|
|
145
|
+
* always the graded render, whatever this says. */
|
|
146
|
+
shading: ShadingMode;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shading }: SplitRendererProps): null {
|
|
150
|
+
// The runtime class IS `WebGPURenderer` (createStageGl builds one, WebGL2 is
|
|
151
|
+
// a BACKEND of it, not a different class); r3f types `gl` as the classic
|
|
152
|
+
// renderer, so the cast happens once here rather than at five call sites.
|
|
153
|
+
const gl = useThree((s) => s.gl) as unknown as WebGPURenderer;
|
|
154
|
+
const scene = useThree((s) => s.scene);
|
|
155
|
+
const shotCamera = useThree((s) => s.camera);
|
|
156
|
+
const size = useThree((s) => s.size);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The clay override, ONE dedicated instance and never shared.
|
|
160
|
+
*
|
|
161
|
+
* `depthTest`/`depthWrite` stay OFF, matching every layer material. It is
|
|
162
|
+
* tempting to turn them on "because the working pane is the 3D one", and it
|
|
163
|
+
* would be a second truth: this lane's paint order is `renderOrder =
|
|
164
|
+
* layer.order` and nothing else (see ShotRuntime's header), so a depth-tested
|
|
165
|
+
* working pane would disagree with the camera pane about what covers what —
|
|
166
|
+
* and, because layers default to z = 0, would z-fight the common case.
|
|
167
|
+
*
|
|
168
|
+
* `transparent` is left false. three copies the SOURCE material's
|
|
169
|
+
* `transparent` onto the override per object anyway, which is what keeps the
|
|
170
|
+
* render-list assignment consistent; setting it here as well would only add a
|
|
171
|
+
* second opinion.
|
|
172
|
+
*/
|
|
173
|
+
const overrides = useMemo(() => {
|
|
174
|
+
const clay = new MeshBasicNodeMaterial();
|
|
175
|
+
clay.color.copy(CLAY_FILL);
|
|
176
|
+
clay.depthTest = false;
|
|
177
|
+
clay.depthWrite = false;
|
|
178
|
+
clay.toneMapped = false;
|
|
179
|
+
// The wireframe is the same material with the fill swapped for edges: the
|
|
180
|
+
// depth/tone rationale above holds unchanged, because a wireframe pane's
|
|
181
|
+
// paint order is still `renderOrder` and nothing else.
|
|
182
|
+
const wire = new MeshBasicNodeMaterial();
|
|
183
|
+
wire.color.copy(WIRE_FILL);
|
|
184
|
+
wire.wireframe = true;
|
|
185
|
+
wire.depthTest = false;
|
|
186
|
+
wire.depthWrite = false;
|
|
187
|
+
wire.toneMapped = false;
|
|
188
|
+
return { clay, wire };
|
|
189
|
+
}, []);
|
|
190
|
+
useEffect(
|
|
191
|
+
() => () => {
|
|
192
|
+
overrides.clay.dispose();
|
|
193
|
+
overrides.wire.dispose();
|
|
194
|
+
},
|
|
195
|
+
[overrides],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const signature = effects === null ? "" : `${postSignature(effects)}::${hasMesh}`;
|
|
199
|
+
const post = useMemo(() => {
|
|
200
|
+
if (effects === null || !hasPostChain(effects)) return null;
|
|
201
|
+
// Layer 0 only. This source is what feeds the graded image, so pinning it is
|
|
202
|
+
// what makes "an editor-only object can never appear in the camera pane"
|
|
203
|
+
// true by construction rather than by everyone remembering to set a mask.
|
|
204
|
+
// Both sources honour it identically.
|
|
205
|
+
const source = hasMesh
|
|
206
|
+
? binSceneSource(scene, shotCamera, new Layers())
|
|
207
|
+
: passSceneSource(scene, shotCamera, new Layers());
|
|
208
|
+
const chain = buildPostChain(source.textureNode, effects);
|
|
209
|
+
const pipeline = new RenderPipeline(
|
|
210
|
+
gl as unknown as ConstructorParameters<typeof RenderPipeline>[0],
|
|
211
|
+
);
|
|
212
|
+
pipeline.outputNode = chain.outputNode;
|
|
213
|
+
pipeline.outputColorTransform = false; // see the colour note in the header
|
|
214
|
+
// Half float, working space: the chain's output is scene-referred light and
|
|
215
|
+
// an 8-bit sRGB hop here would band every gradient the bloom touches.
|
|
216
|
+
const target = new RenderTarget(1, 1, {
|
|
217
|
+
type: HalfFloatType,
|
|
218
|
+
// Read off ColorManagement rather than written as `LinearSRGBColorSpace`:
|
|
219
|
+
// the pipeline forces the WORKING space for the chain, so this target has
|
|
220
|
+
// to be whatever that is, not whatever it is today.
|
|
221
|
+
colorSpace: ColorManagement.workingColorSpace as ColorSpace,
|
|
222
|
+
});
|
|
223
|
+
const material = new MeshBasicNodeMaterial();
|
|
224
|
+
material.colorNode = texture(target.texture, uv());
|
|
225
|
+
material.depthTest = false;
|
|
226
|
+
material.depthWrite = false;
|
|
227
|
+
material.toneMapped = false;
|
|
228
|
+
// THREE'S OWN full-screen primitive, not a hand-rolled plane. Its geometry
|
|
229
|
+
// carries the v-FLIPPED uv three uses everywhere it blits a render target
|
|
230
|
+
// (`QuadGeometry`: the bottom-left vertex is uv 0,1), and its camera covers
|
|
231
|
+
// NDC exactly — so uv 0..1 lands on the current VIEWPORT, whatever pane
|
|
232
|
+
// that is, and the image comes out the same way up on both backends. A
|
|
233
|
+
// `PlaneGeometry` looks equivalent and is upside down.
|
|
234
|
+
const blit = new QuadMesh(material);
|
|
235
|
+
return { source, chain, pipeline, target, material, blit };
|
|
236
|
+
// `effects` is deliberately not a dependency: its VALUES are uniforms
|
|
237
|
+
// written every frame, and only the SIGNATURE changes the graph's shape.
|
|
238
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
|
+
}, [gl, scene, shotCamera, signature]);
|
|
240
|
+
|
|
241
|
+
useEffect(
|
|
242
|
+
() => () => {
|
|
243
|
+
if (post === null) return;
|
|
244
|
+
post.chain.dispose();
|
|
245
|
+
post.pipeline.dispose();
|
|
246
|
+
post.source.dispose();
|
|
247
|
+
post.target.dispose();
|
|
248
|
+
post.material.dispose();
|
|
249
|
+
},
|
|
250
|
+
[post],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The WORKING pane's own post rig, for "rendered" shading. Mirrors `post`
|
|
255
|
+
* with two deliberate differences:
|
|
256
|
+
*
|
|
257
|
+
* 1. Its OWN source target, drawn by `renderBins` at FRAME time with
|
|
258
|
+
* `feed.workingCamera` — never `pass()`/`binSceneSource`, which bake a
|
|
259
|
+
* camera at build time. The working slot's camera OBJECT is swapped by
|
|
260
|
+
* isolation (StageTools puts an orthographic head-on rig in it), so the
|
|
261
|
+
* camera has to be read at draw time for the swap to cost nothing.
|
|
262
|
+
* 2. The source is sized to the PANE, so the scene is drawn at the pane's
|
|
263
|
+
* own resolution and `screenUV` spans it (the header's second section).
|
|
264
|
+
*
|
|
265
|
+
* NAMED CEILING, and it is the header's own, NOT escaped: `BloomNode`
|
|
266
|
+
* sizes its whole mip chain from `renderer.getDrawingBufferSize()` in
|
|
267
|
+
* `updateBefore` (three 0.185.1, BloomNode.js:347) rather than from the
|
|
268
|
+
* bound target, so the blur kernel is still computed in canvas space and a
|
|
269
|
+
* round bloom is as elliptical in this pane as in the camera one. Sizing
|
|
270
|
+
* the source to the pane does not reach that, and no call here can — it
|
|
271
|
+
* needs a pass whose target aspect may differ from the canvas's.
|
|
272
|
+
*
|
|
273
|
+
* Keyed on the mode so the rig exists only while rendered shading is on:
|
|
274
|
+
* solid users pay nothing, and leaving the mode disposes the pipeline. It
|
|
275
|
+
* is keyed on the EFFECTS signature alone and not on `signature`, which
|
|
276
|
+
* carries `hasMesh`: that bit picks the camera pane's SOURCE KIND (bin vs
|
|
277
|
+
* pass) and this rig has neither — it always draws its own target with
|
|
278
|
+
* `renderBins` — so folding it in would tear down and recompile the whole
|
|
279
|
+
* working pipeline every time a mesh layer entered or left the scene.
|
|
280
|
+
*/
|
|
281
|
+
const wantWorkingGrade = shading === "rendered";
|
|
282
|
+
const workingSignature = effects === null ? "" : postSignature(effects);
|
|
283
|
+
const wpost = useMemo(() => {
|
|
284
|
+
if (!wantWorkingGrade || effects === null || !hasPostChain(effects)) return null;
|
|
285
|
+
const src = new RenderTarget(1, 1, { type: HalfFloatType });
|
|
286
|
+
const chain = buildPostChain(texture(src.texture, uv()), effects);
|
|
287
|
+
const pipeline = new RenderPipeline(
|
|
288
|
+
gl as unknown as ConstructorParameters<typeof RenderPipeline>[0],
|
|
289
|
+
);
|
|
290
|
+
pipeline.outputNode = chain.outputNode;
|
|
291
|
+
pipeline.outputColorTransform = false; // one encode, at the blit — header
|
|
292
|
+
const target = new RenderTarget(1, 1, {
|
|
293
|
+
type: HalfFloatType,
|
|
294
|
+
colorSpace: ColorManagement.workingColorSpace as ColorSpace,
|
|
295
|
+
});
|
|
296
|
+
const material = new MeshBasicNodeMaterial();
|
|
297
|
+
material.colorNode = texture(target.texture, uv());
|
|
298
|
+
material.depthTest = false;
|
|
299
|
+
material.depthWrite = false;
|
|
300
|
+
material.toneMapped = false;
|
|
301
|
+
const blit = new QuadMesh(material);
|
|
302
|
+
return { src, chain, pipeline, target, material, blit };
|
|
303
|
+
// Same rule as `post`: `effects` values are uniforms written per frame,
|
|
304
|
+
// only the SIGNATURE changes the graph's shape.
|
|
305
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
306
|
+
}, [gl, workingSignature, wantWorkingGrade]);
|
|
307
|
+
|
|
308
|
+
useEffect(
|
|
309
|
+
() => () => {
|
|
310
|
+
if (wpost === null) return;
|
|
311
|
+
wpost.chain.dispose();
|
|
312
|
+
wpost.pipeline.dispose();
|
|
313
|
+
wpost.src.dispose();
|
|
314
|
+
wpost.target.dispose();
|
|
315
|
+
wpost.material.dispose();
|
|
316
|
+
},
|
|
317
|
+
[wpost],
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// The props are read straight out of the closure. r3f re-points `useFrame`'s
|
|
321
|
+
// callback ref in a layout effect on every render, and `ClockDriver` — the
|
|
322
|
+
// LAST child of the scene — is what calls `advance`, so the callback that
|
|
323
|
+
// draws is always the one this render produced.
|
|
324
|
+
useFrame(() => {
|
|
325
|
+
const working = panes.working;
|
|
326
|
+
const camera = panes.camera;
|
|
327
|
+
const fx = effects;
|
|
328
|
+
const at = frame;
|
|
329
|
+
const workingCamera = feed.workingCamera;
|
|
330
|
+
|
|
331
|
+
// The shot's own clear colour is ShotBackground's, written on a layout
|
|
332
|
+
// effect. Borrowing it for one pane and handing it back is cheaper than
|
|
333
|
+
// re-deriving the background here, and cannot drift from it.
|
|
334
|
+
gl.getClearColor(_prevClear);
|
|
335
|
+
const prevAlpha = gl.getClearAlpha();
|
|
336
|
+
|
|
337
|
+
// 0. A transition window owns the camera pane outright: the compositor
|
|
338
|
+
// below in @dailies-studio/shot has captured nothing yet, because under
|
|
339
|
+
// `renderControl: "external"` it deliberately does not present. Its
|
|
340
|
+
// capture goes here, in the same unscissored slot as our own post and
|
|
341
|
+
// for the same reason, at the PANE's size so the blend is not stretched
|
|
342
|
+
// across a frustum it was not rendered for. Without this the editor
|
|
343
|
+
// would show hard cuts in split view where the export dissolves.
|
|
344
|
+
const blend = camera === null ? null : transitionTap();
|
|
345
|
+
if (blend !== null && camera !== null) {
|
|
346
|
+
gl.setScissorTest(false);
|
|
347
|
+
blend.capture(camera.width, camera.height);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 1. The camera pane's post chain, OFFSCREEN and UNSCISSORED. See the
|
|
351
|
+
// header for why this cannot go straight to the canvas. Skipped inside a
|
|
352
|
+
// window: the compositor rendered this scene into its own operand target
|
|
353
|
+
// already, with the same chain.
|
|
354
|
+
if (blend === null && camera !== null && post !== null && fx !== null) {
|
|
355
|
+
// The target is PANE sized, in device px: the pane gets its own
|
|
356
|
+
// resolution for free, and `setSize` already no-ops when it matches.
|
|
357
|
+
const dpr = gl.getPixelRatio();
|
|
358
|
+
post.target.setSize(
|
|
359
|
+
Math.max(1, Math.round(camera.width * dpr)),
|
|
360
|
+
Math.max(1, Math.round(camera.height * dpr)),
|
|
361
|
+
);
|
|
362
|
+
post.chain.write(fx, at);
|
|
363
|
+
gl.setScissorTest(false);
|
|
364
|
+
// The bins first, into whatever the source owns. A `pass()`-backed source
|
|
365
|
+
// no-ops here and renders itself from inside `pipeline.render()` below,
|
|
366
|
+
// which is the untouched single-render path.
|
|
367
|
+
post.source.draw(gl, bins);
|
|
368
|
+
gl.setRenderTarget(post.target);
|
|
369
|
+
post.pipeline.render();
|
|
370
|
+
gl.setRenderTarget(null);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 1b. The WORKING pane's grade, offscreen and unscissored — rendered
|
|
374
|
+
// shading with a chain only. The same offscreen-then-blit construction
|
|
375
|
+
// as the camera pane, so the WebGPU MSAA resolve can never reach the
|
|
376
|
+
// neighbour. The source draw masks the working camera to LAYER 0: the
|
|
377
|
+
// gizmo and any future editor-layer object must never be graded — the
|
|
378
|
+
// same pin the camera source takes via `Layers`, done inline because
|
|
379
|
+
// this camera's mask is per-draw state we restore. The clear colour is
|
|
380
|
+
// still the shot's own (`_prevClear`): a graded working pane previews
|
|
381
|
+
// the export's image, so it sits on the export's background.
|
|
382
|
+
if (working !== null && workingCamera !== null && wpost !== null && fx !== null) {
|
|
383
|
+
const dpr = gl.getPixelRatio();
|
|
384
|
+
const w = Math.max(1, Math.round(working.width * dpr));
|
|
385
|
+
const h = Math.max(1, Math.round(working.height * dpr));
|
|
386
|
+
// Mirrors binSceneSource's target discipline: the renderer's own sample
|
|
387
|
+
// count and output buffer type, so the image is scene-referred with MSAA
|
|
388
|
+
// on the model silhouettes.
|
|
389
|
+
wpost.src.samples = gl.samples;
|
|
390
|
+
wpost.src.texture.type = gl.getOutputBufferType();
|
|
391
|
+
wpost.src.setSize(w, h);
|
|
392
|
+
wpost.target.setSize(w, h);
|
|
393
|
+
wpost.chain.write(fx, at);
|
|
394
|
+
gl.setScissorTest(false);
|
|
395
|
+
const prevMask = workingCamera.layers.mask;
|
|
396
|
+
workingCamera.layers.mask = 1; // bit 0 only
|
|
397
|
+
gl.setRenderTarget(wpost.src);
|
|
398
|
+
renderBins(gl, scene, workingCamera, bins);
|
|
399
|
+
gl.setRenderTarget(null);
|
|
400
|
+
workingCamera.layers.mask = prevMask;
|
|
401
|
+
gl.setRenderTarget(wpost.target);
|
|
402
|
+
wpost.pipeline.render();
|
|
403
|
+
gl.setRenderTarget(null);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
gl.setScissorTest(true);
|
|
407
|
+
|
|
408
|
+
// 2. The working pane. `autoClear` stays on, so each pane clears its own
|
|
409
|
+
// scissor rect with its own colour — measured to hold on both backends,
|
|
410
|
+
// because every render here takes the intermediate framebuffer target
|
|
411
|
+
// (the canvas's output colour space is sRGB and the working space is
|
|
412
|
+
// linear, so `needsFrameBufferTarget` is true whatever the tone mapping).
|
|
413
|
+
//
|
|
414
|
+
// What it draws is the shading mode's plan (shading.ts):
|
|
415
|
+
// solid / wireframe — the override path. The override reaches a model
|
|
416
|
+
// too (`allowOverride` is left at its default on the mesh materials,
|
|
417
|
+
// see mesh.tsx) and the gizmo escapes via its own
|
|
418
|
+
// `allowOverride = false`. Ceiling: three copies only
|
|
419
|
+
// `transparent`/`alphaTest`/`alphaMap` onto an override, not
|
|
420
|
+
// `depthTest`, so an overridden model does not self-occlude here.
|
|
421
|
+
// material — NO override: real layer materials, textures, opacity and
|
|
422
|
+
// alpha stencils, drawn ungraded. Two documented consequences, both
|
|
423
|
+
// correct for a materials preview and neither to be "fixed": the
|
|
424
|
+
// DOF mip-LOD (`layerLod`) is written from the SHOT camera's rig,
|
|
425
|
+
// so per-layer blur reflects the shot's focus and not this pane's
|
|
426
|
+
// viewpoint; and isolation's dimming becomes visible here, because
|
|
427
|
+
// dim rides the opacity uniform the clay used to strip.
|
|
428
|
+
// rendered — handled as `graded` when the scene has a chain (the
|
|
429
|
+
// `wpost` rig below); an effect-less scene degrades to the material
|
|
430
|
+
// plan, which is what `workingDrawPlan` returns for it.
|
|
431
|
+
// The camera pane is always the one that matches the export.
|
|
432
|
+
if (working !== null && workingCamera !== null) {
|
|
433
|
+
const plan = workingDrawPlan(shading, wpost !== null);
|
|
434
|
+
applyRect(gl, working);
|
|
435
|
+
if (plan.graded && wpost !== null) {
|
|
436
|
+
// The graded image already carries the shot's own background, so the
|
|
437
|
+
// pane clears with it rather than the clay backdrop.
|
|
438
|
+
gl.setClearColor(_prevClear, prevAlpha);
|
|
439
|
+
wpost.blit.render(gl);
|
|
440
|
+
// The EDITOR-LAYER overpaint: the gizmo draws crisp OVER the graded
|
|
441
|
+
// image, ungraded. Mask the camera to the editor bit alone and draw
|
|
442
|
+
// with `autoClear` off so the blit is kept; both are renderer/camera
|
|
443
|
+
// state we restore inline, on the same line of sight.
|
|
444
|
+
const prevMask = workingCamera.layers.mask;
|
|
445
|
+
const prevAutoClear = gl.autoClear;
|
|
446
|
+
workingCamera.layers.mask = 1 << EDITOR_LAYER;
|
|
447
|
+
gl.autoClear = false;
|
|
448
|
+
gl.render(scene, workingCamera);
|
|
449
|
+
gl.autoClear = prevAutoClear;
|
|
450
|
+
workingCamera.layers.mask = prevMask;
|
|
451
|
+
} else if (plan.override !== null) {
|
|
452
|
+
gl.setClearColor(CLAY_BACKDROP, 1);
|
|
453
|
+
scene.overrideMaterial = plan.override === "wire" ? overrides.wire : overrides.clay;
|
|
454
|
+
renderBins(gl, scene, workingCamera, bins);
|
|
455
|
+
scene.overrideMaterial = null;
|
|
456
|
+
} else {
|
|
457
|
+
// RENDERED WITHOUT A CHAIN keeps the SHOT's backdrop, not the clay
|
|
458
|
+
// one. It degrades to the material plan here (nothing to grade), but
|
|
459
|
+
// its job is still to preview the export — clearing to the workspace
|
|
460
|
+
// grey would disagree with the camera pane and the MP4, and would
|
|
461
|
+
// then snap to the shot's background the moment any effect was
|
|
462
|
+
// enabled, from a toggle that says nothing about the backdrop.
|
|
463
|
+
// Material mode keeps the clay backdrop on purpose: it previews the
|
|
464
|
+
// MATERIALS against the workspace, not the shot.
|
|
465
|
+
if (shading === "rendered") gl.setClearColor(_prevClear, prevAlpha);
|
|
466
|
+
else gl.setClearColor(CLAY_BACKDROP, 1);
|
|
467
|
+
renderBins(gl, scene, workingCamera, bins);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 3. The camera pane: the blit, or a plain render when the scene has no
|
|
472
|
+
// post at all (no pass, no target, no cost — the same call ShotRuntime
|
|
473
|
+
// would have made).
|
|
474
|
+
if (camera !== null) {
|
|
475
|
+
applyRect(gl, camera);
|
|
476
|
+
gl.setClearColor(_prevClear, prevAlpha);
|
|
477
|
+
// `QuadMesh.render` is `renderer.render(quad, itsOwnCamera)` — the same
|
|
478
|
+
// call, through the object that owns the right camera for it.
|
|
479
|
+
if (blend !== null) blend.quad.render(gl);
|
|
480
|
+
else if (post !== null && fx !== null) post.blit.render(gl);
|
|
481
|
+
else renderBins(gl, scene, shotCamera, bins);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 4. Hand the renderer back exactly as it was found. Every one of these is
|
|
485
|
+
// global state on a renderer this component does not own; leaking one
|
|
486
|
+
// surfaces on the NEXT frame, in a component that never touched it.
|
|
487
|
+
scene.overrideMaterial = null;
|
|
488
|
+
gl.setScissorTest(false);
|
|
489
|
+
gl.setViewport(0, 0, size.width, size.height);
|
|
490
|
+
gl.setScissor(0, 0, size.width, size.height);
|
|
491
|
+
gl.setClearColor(_prevClear, prevAlpha);
|
|
492
|
+
}, 1);
|
|
493
|
+
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Point the renderer at one pane.
|
|
499
|
+
*
|
|
500
|
+
* CSS px, not device px: three multiplies both rects by the pixel ratio itself
|
|
501
|
+
* (`renderContext.viewportValue.copy(viewport).multiplyScalar(pixelRatio)`), so
|
|
502
|
+
* a caller that pre-scaled would scale twice.
|
|
503
|
+
*
|
|
504
|
+
* The panes are full-height columns, so y is 0 in both and the backends' y
|
|
505
|
+
* origins cannot disagree here. Check that before building a top/bottom split
|
|
506
|
+
* or an inset: WebGPU's `setViewport` is top-left native and WebGL's is not.
|
|
507
|
+
*/
|
|
508
|
+
function applyRect(gl: WebGPURenderer, rect: PaneRect): void {
|
|
509
|
+
gl.setViewport(rect.x, rect.y, rect.width, rect.height);
|
|
510
|
+
gl.setScissor(rect.x, rect.y, rect.width, rect.height);
|
|
511
|
+
}
|