@volter/editor-threejs 0.5.210 → 0.5.212
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/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +476 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/kit/viewport-surface-status.tsx +44 -0
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { invalidateStages } from '@volter/sdk/kit/stage-invalidation';
|
|
1
|
+
import { resetViewPresentation, setViewPresentation, type ViewportXray } from '@volter/sdk/viewport-presentation';
|
|
2
|
+
import { invalidateStages } from '@volter/sdk/stage-invalidation';
|
|
4
3
|
import type { AuthoringAdapter } from '@volter/project/adapter';
|
|
5
4
|
import { viewportCaptureOutputPass } from '@volter/editor-threejs/capture/output-pass';
|
|
6
5
|
import { resolveSceneLinearSize } from '@volter/editor-threejs/capture/linear-resolve';
|
|
6
|
+
import {
|
|
7
|
+
jitterProjection,
|
|
8
|
+
PHOTOGRAPH_SAMPLE_CAP,
|
|
9
|
+
PhotographAccumulation,
|
|
10
|
+
type PhotographSampling,
|
|
11
|
+
} from '@volter/editor-threejs/capture/photograph-sampling';
|
|
12
|
+
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
7
13
|
import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
|
|
8
14
|
import { setUserData } from '@volter/editor-threejs/ecs/user-data';
|
|
9
15
|
import {
|
|
@@ -17,10 +23,10 @@ import { DepthDrawMaterials } from '../../render/depth-draw-materials';
|
|
|
17
23
|
import type { DocumentDisplayTransform } from '../../render/document-display-transform';
|
|
18
24
|
import { threeObject } from '../../adapter/three-contract';
|
|
19
25
|
import { axisViewName, cameraPresetDirection, type ModelCameraPreset } from '../asset-workflow/model-inspection';
|
|
20
|
-
import {
|
|
21
|
-
import { editorConsole } from '@volter/sdk/
|
|
26
|
+
import { editorHost } from '@volter/sdk/host';
|
|
27
|
+
import { editorConsole } from '@volter/sdk/editor-console';
|
|
22
28
|
import { type EditorViewport } from '../editor-viewport';
|
|
23
|
-
import { nativeSelectionColors, subscribeNativeSelectionTheme } from '@volter/sdk/
|
|
29
|
+
import { nativeSelectionColors, subscribeNativeSelectionTheme } from '@volter/sdk/native-selection-style';
|
|
24
30
|
import { BoneSelectionHighlight } from '../three-viewport/bone-selection-highlight';
|
|
25
31
|
import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
|
|
26
32
|
import {
|
|
@@ -31,7 +37,7 @@ import {
|
|
|
31
37
|
} from '../three-viewport/selection-outline';
|
|
32
38
|
import { styleEditorSkeletonHelper } from '../three-viewport/skeleton-helper';
|
|
33
39
|
import { isEditorViewportShadingTarget } from '../viewport-shading-boundary';
|
|
34
|
-
import { setAuthoringSelection } from '@volter/sdk/
|
|
40
|
+
import { setAuthoringSelection } from '@volter/sdk/authoring/consumer-actions';
|
|
35
41
|
import type { ToolCameraView, ToolCameraViewSource } from '../../object3d-contributions';
|
|
36
42
|
|
|
37
43
|
export type Object3DDocumentViewMode = ViewportShadingMode | 'uv' | 'vertex-colors';
|
|
@@ -68,7 +74,7 @@ interface CameraFlight {
|
|
|
68
74
|
elapsed: number;
|
|
69
75
|
theta: number;
|
|
70
76
|
phi: number;
|
|
71
|
-
settle: (
|
|
77
|
+
settle: (outcome: DocumentLookOutcome) => void;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -80,12 +86,10 @@ interface CameraFlight {
|
|
|
80
86
|
*/
|
|
81
87
|
const TOPOLOGY_EDGE_THRESHOLD_DEGREES = 1;
|
|
82
88
|
|
|
83
|
-
/**
|
|
84
|
-
/** How long a presented-frame request waits on the host's draw loop before it
|
|
85
|
-
* gives the chrome door back `null` (see `requestPresentedFrame`). Two frames
|
|
86
|
-
* at 30fps is a loop that is running; longer means it is not. */
|
|
89
|
+
/** Maximum wait for the host draw loop before requestPresentedFrame returns null. */
|
|
87
90
|
const PRESENTED_FRAME_WAIT_MS = 700;
|
|
88
91
|
|
|
92
|
+
/** OrbitControls' own poles-excluded range; orbiting past them flips the view. */
|
|
89
93
|
const MIN_POLAR = 1e-3;
|
|
90
94
|
const MAX_POLAR = Math.PI - 1e-3;
|
|
91
95
|
|
|
@@ -112,15 +116,14 @@ const INITIAL_PRESENTATION: Object3DDocumentPresentationState = {
|
|
|
112
116
|
bounds: false,
|
|
113
117
|
};
|
|
114
118
|
|
|
119
|
+
/** The wire of an unselected object in the wireframe overlay: dark on the light clay body. */
|
|
120
|
+
const TOPOLOGY_WIRE_COLOR = 0x11161d;
|
|
121
|
+
|
|
115
122
|
/**
|
|
116
123
|
* Live presentation controls for one native Object3D document. The model
|
|
117
124
|
* graph remains truth; this session owns only editor chrome and diagnostics.
|
|
118
125
|
*/
|
|
119
|
-
/** The wire of an unselected object in the wireframe overlay: dark on the light clay body. */
|
|
120
|
-
const TOPOLOGY_WIRE_COLOR = 0x11161d;
|
|
121
|
-
|
|
122
126
|
export class Object3DDocumentSession {
|
|
123
|
-
readonly profiler = createPerformanceProfiler();
|
|
124
127
|
private readonly shading = new ViewportShadingRenderer();
|
|
125
128
|
private readonly depthDrawMaterials = new DepthDrawMaterials();
|
|
126
129
|
private readonly helpers: THREE.Object3D[] = [];
|
|
@@ -166,12 +169,75 @@ export class Object3DDocumentSession {
|
|
|
166
169
|
* shape (square by default, `{width, height}` when a caller asks for one)
|
|
167
170
|
* while the live camera's aspect is the panel's. */
|
|
168
171
|
private captureAspect: number | null = null;
|
|
169
|
-
/**
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
172
|
+
/** Target pixels per output pixel, non-null ONLY inside {@link captureImage}, which draws at
|
|
173
|
+
* twice the photograph's size and reduces. */
|
|
174
|
+
private capturePixelScale: number | null = null;
|
|
175
|
+
|
|
176
|
+
/** Target pixels per output pixel of the draw being made: the photograph's supersampling
|
|
177
|
+
* inside a capture, the device pixel ratio on screen. What a pixel-wide effect (Blender's
|
|
178
|
+
* object outline) spans. */
|
|
179
|
+
drawPixelScale(): number {
|
|
180
|
+
return this.capturePixelScale ?? this.renderer.getPixelRatio();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The draw being made is {@link captureImage}'s photograph. */
|
|
184
|
+
photographing(): boolean {
|
|
185
|
+
return this.capturePixelScale !== null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** How a photograph of each draw mode is anti-aliased, when the document states it
|
|
189
|
+
* (`PhotographSampling`): Blender's Solid by Workbench's own accumulation, its Rendered by
|
|
190
|
+
* EEVEE's film. A mode with none is drawn at twice the photograph's size and reduced. */
|
|
191
|
+
private photographSampling: Partial<Record<Object3DDocumentViewMode, PhotographSampling>> | null = null;
|
|
192
|
+
setPhotographSampling(sampling: Partial<Record<Object3DDocumentViewMode, PhotographSampling>> | null): void {
|
|
193
|
+
if (this.photographSampling === sampling) return;
|
|
194
|
+
this.photographSampling = sampling;
|
|
195
|
+
// The live view accumulates by the same sampling; what it gathered under the old is dropped.
|
|
196
|
+
this.live?.dispose();
|
|
197
|
+
this.live = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Accumulate the document’s viewport samples across unchanged redraws. Navigation, edits and resize reset the sum; release live targets while hidden and retain the finished display-encoded image after completion. */
|
|
201
|
+
private live: LiveAccumulation | null = null;
|
|
202
|
+
/** A failure inside the live accumulation is reported once per session. */
|
|
203
|
+
private liveFailureReported = false;
|
|
204
|
+
|
|
205
|
+
/** The sampling the live view accumulates by in the current mode, or null (drawn plain). */
|
|
206
|
+
private liveSampling(): PhotographSampling | null {
|
|
207
|
+
const mode = this.state.mode;
|
|
208
|
+
if (mode === 'uv' || mode === 'vertex-colors') return null;
|
|
209
|
+
return this.photographSampling?.[mode] ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The live view has samples still to draw while nothing changes: the host keeps drawing. */
|
|
213
|
+
accumulating(): boolean {
|
|
214
|
+
const sampling = this.liveSampling();
|
|
215
|
+
if (!sampling) return false;
|
|
216
|
+
const live = this.live;
|
|
217
|
+
if (!live || live.sampling !== sampling) return true;
|
|
218
|
+
return !live.failed && live.next < live.count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The stage is hidden: the live accumulation's targets go (it starts over when next shown). */
|
|
222
|
+
releaseLive(): void {
|
|
223
|
+
this.live?.dispose();
|
|
224
|
+
this.live = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The sub-pixel move of the sample being drawn, in a sampled photograph or the live view's accumulation. */
|
|
228
|
+
private captureJitter: { readonly offset: readonly [number, number]; readonly width: number; readonly height: number } | null = null;
|
|
229
|
+
/** Which sample of a sampled photograph is being drawn (0 for any other draw). */
|
|
230
|
+
private captureSample = 0;
|
|
231
|
+
/** The backdrop a sampled photograph leaves out of its samples, to lay its drawing over. */
|
|
232
|
+
private captureBackdrop: THREE.Color | null = null;
|
|
233
|
+
|
|
234
|
+
/** The sample being drawn: 0 on screen and in an unsampled photograph. A host reports a
|
|
235
|
+
* photograph's draw once, on its first sample. */
|
|
236
|
+
photographSampleIndex(): number {
|
|
237
|
+
return this.captureSample;
|
|
238
|
+
}
|
|
239
|
+
/** The camera used for the current capture. Blender renders use the scene camera while
|
|
240
|
+
* the modeling viewport keeps its own view. */
|
|
175
241
|
private cameraOverride: THREE.Camera | null = null;
|
|
176
242
|
/** The document's cameras a camera view can look through (`ToolObject3DAuthoringProps.cameraView`). */
|
|
177
243
|
private cameraViewSource: ToolCameraViewSource | null = null;
|
|
@@ -221,12 +287,10 @@ export class Object3DDocumentSession {
|
|
|
221
287
|
readonly scene: THREE.Scene,
|
|
222
288
|
readonly renderer: THREE.WebGLRenderer,
|
|
223
289
|
readonly viewport: EditorViewport,
|
|
224
|
-
private authoring
|
|
290
|
+
private authoring: AuthoringAdapter,
|
|
225
291
|
/** Asset Lab renders its neutral stage in editor chrome behind an alpha
|
|
226
292
|
* canvas; other document hosts may retain a real Three background. */
|
|
227
|
-
private neutralBackground: THREE.Color | THREE.Texture | null
|
|
228
|
-
/** When set, Frame and view presets fit this box instead of the root AABB. */
|
|
229
|
-
private readonly frameBox: THREE.Box3 | null = null,
|
|
293
|
+
private neutralBackground: THREE.Color | THREE.Texture | null,
|
|
230
294
|
) {
|
|
231
295
|
this.unsubscribeSelectionTheme = subscribeNativeSelectionTheme(
|
|
232
296
|
renderer.domElement,
|
|
@@ -250,14 +314,14 @@ export class Object3DDocumentSession {
|
|
|
250
314
|
private readonly axisView = (): boolean => {
|
|
251
315
|
this.leaveCameraView(false);
|
|
252
316
|
this.settleFlight('superseded');
|
|
253
|
-
if (
|
|
317
|
+
if (editorHost().keyboard.navigation().autoPerspective) this.setProjection('orthographic');
|
|
254
318
|
return true;
|
|
255
319
|
};
|
|
256
320
|
|
|
257
321
|
/** Auto Perspective, where the keymap states it (`KeymapNavigation.autoPerspective`): a rotate
|
|
258
322
|
* that starts from an orthographic view still down an axis draws in perspective. */
|
|
259
323
|
private readonly autoPerspective = (): void => {
|
|
260
|
-
if (!
|
|
324
|
+
if (!editorHost().keyboard.navigation().autoPerspective || this.cameraView() !== null) return;
|
|
261
325
|
if (this.state.projection !== 'orthographic') return;
|
|
262
326
|
const camera = this.viewport.camera;
|
|
263
327
|
const offset = camera.position.clone().sub(this.viewport.orbitControls.target);
|
|
@@ -319,12 +383,12 @@ export class Object3DDocumentSession {
|
|
|
319
383
|
}
|
|
320
384
|
|
|
321
385
|
private resolveFrameBounds(subject: 'selection' | 'all' = 'selection'): THREE.Box3 {
|
|
322
|
-
const selection = subject === 'all' ? [] : (this.authoring
|
|
386
|
+
const selection = subject === 'all' ? [] : (this.authoring.selection?.get() ?? []);
|
|
323
387
|
if (selection.length > 0) {
|
|
324
388
|
const selected = new THREE.Box3();
|
|
325
389
|
let found = false;
|
|
326
390
|
for (const id of selection) {
|
|
327
|
-
const object =
|
|
391
|
+
const object = threeObject(this.authoring.hierarchy, id);
|
|
328
392
|
if (!object) continue;
|
|
329
393
|
const bounds = contentWorldBounds(object);
|
|
330
394
|
if (bounds.isEmpty()) continue;
|
|
@@ -333,15 +397,14 @@ export class Object3DDocumentSession {
|
|
|
333
397
|
}
|
|
334
398
|
if (found) return selected;
|
|
335
399
|
}
|
|
336
|
-
if (this.frameBox && !this.frameBox.isEmpty()) return this.frameBox;
|
|
337
400
|
return contentWorldBounds(this.root);
|
|
338
401
|
}
|
|
339
402
|
|
|
340
403
|
/**
|
|
341
|
-
* Frame the subject: the selection if there is one, else
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* the
|
|
404
|
+
* Frame the subject: the selection if there is one, else the whole root.
|
|
405
|
+
* `subject: 'all'` passes over selection (Blender's View All).
|
|
406
|
+
* `fit` scales the fitted distance — 1 is
|
|
407
|
+
* the toolbar's tight fit, >1 pulls back.
|
|
345
408
|
*/
|
|
346
409
|
frame(fit = 1, subject: 'selection' | 'all' = 'selection'): boolean {
|
|
347
410
|
this.leaveCameraView(false);
|
|
@@ -358,7 +421,7 @@ export class Object3DDocumentSession {
|
|
|
358
421
|
positions += geometry.getAttribute('position')?.count ?? 0;
|
|
359
422
|
});
|
|
360
423
|
editorConsole.warn(
|
|
361
|
-
`frame: nothing to frame — root "${this.root.name}" (${this.root.children.length} children, ${meshes} meshes, ${positions} positions), selection ${JSON.stringify(this.authoring
|
|
424
|
+
`frame: nothing to frame — root "${this.root.name}" (${this.root.children.length} children, ${meshes} meshes, ${positions} positions), selection ${JSON.stringify(this.authoring.selection?.get() ?? [])}`,
|
|
362
425
|
'document',
|
|
363
426
|
);
|
|
364
427
|
return false;
|
|
@@ -446,6 +509,8 @@ export class Object3DDocumentSession {
|
|
|
446
509
|
spherical.radius > 1e-4
|
|
447
510
|
? spherical.radius
|
|
448
511
|
: Math.max(this.resolveFrameBounds().getSize(new THREE.Vector3()).length(), 1);
|
|
512
|
+
let settle!: CameraFlight['settle'];
|
|
513
|
+
const settled = new Promise<DocumentLookOutcome>((resolve) => { settle = resolve; });
|
|
449
514
|
const flight: CameraFlight = {
|
|
450
515
|
pivot,
|
|
451
516
|
radius,
|
|
@@ -460,12 +525,9 @@ export class Object3DDocumentSession {
|
|
|
460
525
|
elapsed: 0,
|
|
461
526
|
theta: spherical.theta,
|
|
462
527
|
phi: spherical.phi,
|
|
463
|
-
settle
|
|
528
|
+
settle,
|
|
464
529
|
};
|
|
465
530
|
this.flight = flight;
|
|
466
|
-
const settled = new Promise<DocumentLookOutcome>((resolve) => {
|
|
467
|
-
flight.settle = resolve;
|
|
468
|
-
});
|
|
469
531
|
// A zero-length move is still a move: apply it now so the pose is correct
|
|
470
532
|
// even on a surface whose next frame never comes.
|
|
471
533
|
this.applyFlightPose(flight, 0);
|
|
@@ -529,27 +591,27 @@ export class Object3DDocumentSession {
|
|
|
529
591
|
elevation: Math.PI / 2 - flight.phi,
|
|
530
592
|
seconds: Math.min(flight.elapsed, flight.duration),
|
|
531
593
|
};
|
|
532
|
-
flight.settle
|
|
594
|
+
flight.settle(cancelledBy === null ? ended : { ...ended, cancelledBy });
|
|
533
595
|
}
|
|
534
596
|
|
|
535
597
|
select(ids: readonly string[]): void {
|
|
536
598
|
invalidateStages();
|
|
537
|
-
|
|
599
|
+
setAuthoringSelection(this.authoring, [...ids]);
|
|
538
600
|
}
|
|
539
601
|
|
|
540
602
|
selection(): string[] {
|
|
541
|
-
return [...(this.authoring
|
|
603
|
+
return [...(this.authoring.selection?.get() ?? [])];
|
|
542
604
|
}
|
|
543
605
|
|
|
544
606
|
idForObject(object: THREE.Object3D): string | null {
|
|
545
|
-
return this.authoring
|
|
607
|
+
return this.authoring.hierarchy.idForNativeObject?.(object) ?? null;
|
|
546
608
|
}
|
|
547
609
|
|
|
548
610
|
/** Keep native hierarchy selection legible in this document's one viewport. */
|
|
549
611
|
syncSelectionPresentation(): void {
|
|
550
612
|
invalidateStages();
|
|
551
|
-
const selected = (this.authoring
|
|
552
|
-
.map((id) =>
|
|
613
|
+
const selected = (this.authoring.selection?.get() ?? [])
|
|
614
|
+
.map((id) => threeObject(this.authoring.hierarchy, id))
|
|
553
615
|
.filter((object): object is THREE.Object3D => object !== null);
|
|
554
616
|
this.selectedObjects = selected;
|
|
555
617
|
if (this.selectionOutline) {
|
|
@@ -581,14 +643,14 @@ export class Object3DDocumentSession {
|
|
|
581
643
|
}
|
|
582
644
|
|
|
583
645
|
frameSelection(): boolean {
|
|
584
|
-
return this.frameIds(this.authoring
|
|
646
|
+
return this.frameIds(this.authoring.selection?.get() ?? []);
|
|
585
647
|
}
|
|
586
648
|
|
|
587
649
|
frameIds(ids: readonly string[]): boolean {
|
|
588
650
|
this.leaveCameraView(false);
|
|
589
651
|
this.settleFlight('superseded');
|
|
590
652
|
const objects = ids
|
|
591
|
-
.map((id) =>
|
|
653
|
+
.map((id) => threeObject(this.authoring.hierarchy, id))
|
|
592
654
|
.filter((object): object is THREE.Object3D => object !== null);
|
|
593
655
|
if (objects.length === 0) return false;
|
|
594
656
|
if (objects.every((object) => (object as THREE.Bone).isBone)) {
|
|
@@ -599,18 +661,7 @@ export class Object3DDocumentSession {
|
|
|
599
661
|
return true;
|
|
600
662
|
}
|
|
601
663
|
|
|
602
|
-
/**
|
|
603
|
-
* An AXIS view is ORTHOGRAPHIC, the way Blender's numpad 1/3/7 are: the
|
|
604
|
-
* reference frame for numpad 1 (`modeling-front-ortho.png`) says so in its
|
|
605
|
-
* own view text, "Front Orthographic". Ours left the stage in perspective,
|
|
606
|
-
* so a front view still showed the cube's side faces converging.
|
|
607
|
-
*
|
|
608
|
-
* `isometric` is the perspective preset (it is the stage's 3/4 opening
|
|
609
|
-
* direction, not an axis) and stays perspective. Both callers of this door
|
|
610
|
-
* already separate the two: the relay's `view-preset` maps its own
|
|
611
|
-
* `perspective` onto `isometric`, and the document toolbar's view menu
|
|
612
|
-
* carries the projection pair as its own second group — so nothing here
|
|
613
|
-
* reads an axis preset expecting perspective.
|
|
664
|
+
/** Axis presets use orthographic projection; isometric uses the stage's perspective opening.
|
|
614
665
|
*
|
|
615
666
|
* The pose is still solved against the PERSPECTIVE camera, because the
|
|
616
667
|
* session's orthographic camera is derived from that pose every frame
|
|
@@ -734,9 +785,12 @@ export class Object3DDocumentSession {
|
|
|
734
785
|
const pose = through.locked
|
|
735
786
|
? `${free.position.toArray().join(',')}|${free.quaternion.toArray().join(',')}`
|
|
736
787
|
: '';
|
|
737
|
-
|
|
788
|
+
// THE SCENE'S CAMERA AS IT IS NOW, as Blender's camera view looks through `scene.camera`, which
|
|
789
|
+
// the Timeline's markers switch as the film plays; a LOCKED view keeps the camera it moves
|
|
790
|
+
const camera = through.locked ? through.camera : (source.camera() ?? through.camera);
|
|
791
|
+
const key = `${camera}|${region.width}|${region.height}|${through.zoom}|${through.offset.join(',')}|${pose}`;
|
|
738
792
|
if (this.viewMemo?.key === key) return this.viewMemo.view;
|
|
739
|
-
const seen = source.view(
|
|
793
|
+
const seen = source.view(camera, region, through.zoom, through.offset);
|
|
740
794
|
const view =
|
|
741
795
|
seen && through.locked
|
|
742
796
|
? {
|
|
@@ -956,7 +1010,7 @@ export class Object3DDocumentSession {
|
|
|
956
1010
|
const leftAxis =
|
|
957
1011
|
axisViewName(through.left.position.clone().sub(through.left.target), through.left.up) !== null;
|
|
958
1012
|
const projection =
|
|
959
|
-
|
|
1013
|
+
editorHost().keyboard.navigation().autoPerspective && leftAxis ? 'perspective' : through.left.projection;
|
|
960
1014
|
this.state = { ...this.state, projection };
|
|
961
1015
|
}
|
|
962
1016
|
viewport.camera.lookAt(viewport.orbitControls.target);
|
|
@@ -1116,8 +1170,7 @@ export class Object3DDocumentSession {
|
|
|
1116
1170
|
* `wireframe` flag, which draws each RENDERABLE triangle — so a quad shows
|
|
1117
1171
|
* its triangulation diagonal and a modeller reads noise where they expect
|
|
1118
1172
|
* face flow. On the Asset Lab document viewport, where the mesh IS the
|
|
1119
|
-
* subject, the mode
|
|
1120
|
-
* form are correctly occluded) under an edge overlay built with
|
|
1173
|
+
* subject, the mode draws neutral clay bodies under a depth-test-free edge overlay built with
|
|
1121
1174
|
* `EdgesGeometry`: coplanar diagonals fall out, quads read as quads.
|
|
1122
1175
|
*
|
|
1123
1176
|
* The overlay is editor chrome — tagged `editorHelper`, so picking, bakes
|
|
@@ -1135,11 +1188,8 @@ export class Object3DDocumentSession {
|
|
|
1135
1188
|
if (!mesh.isMesh || !mesh.geometry) return;
|
|
1136
1189
|
const line = new THREE.LineSegments(
|
|
1137
1190
|
new THREE.EdgesGeometry(mesh.geometry, TOPOLOGY_EDGE_THRESHOLD_DEGREES),
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
// itself away in z-fighting — photographed and read before this was
|
|
1141
|
-
// written. Off is also the truer read: Blender's wireframe shading
|
|
1142
|
-
// shows the far side too, and the far side is half the topology.
|
|
1191
|
+
// Draw dark wire edges without depth testing: coplanar lines would z-fight, and wireframe
|
|
1192
|
+
// shading includes the far-side topology.
|
|
1143
1193
|
new THREE.LineBasicMaterial({
|
|
1144
1194
|
color: TOPOLOGY_WIRE_COLOR,
|
|
1145
1195
|
toneMapped: false,
|
|
@@ -1258,7 +1308,7 @@ export class Object3DDocumentSession {
|
|
|
1258
1308
|
|
|
1259
1309
|
resetPresentation(): void {
|
|
1260
1310
|
invalidateStages();
|
|
1261
|
-
// Lighting, backdrop and tone are the view's presentation (
|
|
1311
|
+
// Lighting, backdrop and tone are the view's presentation (`@volter/sdk/viewport-presentation`).
|
|
1262
1312
|
resetViewPresentation(this.documentId);
|
|
1263
1313
|
this.clearDiagnosticPresentation();
|
|
1264
1314
|
this.state = INITIAL_PRESENTATION;
|
|
@@ -1275,11 +1325,9 @@ export class Object3DDocumentSession {
|
|
|
1275
1325
|
*
|
|
1276
1326
|
* The host keeps the document's own `THREE.Scene` nested inside the session's
|
|
1277
1327
|
* rendered scene and mirrors the world dressing (environment, fog, their
|
|
1278
|
-
* intensities/rotations, background) outward.
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
* synchronously before the capture was simply missing from it. Registering it
|
|
1282
|
-
* here puts `renderViewport` and `captureImage` on one path.
|
|
1328
|
+
* intensities/rotations, background) outward. In a per-frame `animate` an
|
|
1329
|
+
* offscreen photograph taken between two viewport ticks would miss a sky set
|
|
1330
|
+
* just before the capture. Registering it here puts `renderViewport` and `captureImage` on one path.
|
|
1283
1331
|
*
|
|
1284
1332
|
* The host owns the step and clears it in its own teardown; the session only
|
|
1285
1333
|
* holds the reference.
|
|
@@ -1288,26 +1336,7 @@ export class Object3DDocumentSession {
|
|
|
1288
1336
|
this.beforeRender = step;
|
|
1289
1337
|
}
|
|
1290
1338
|
|
|
1291
|
-
/**
|
|
1292
|
-
* THE PRESENTED FRAME — the stage exactly as the person is seeing it,
|
|
1293
|
-
* including everything drawn OVER the document's own render: the
|
|
1294
|
-
* orientation compass (`EditorViewport.renderViewCube`) above all.
|
|
1295
|
-
*
|
|
1296
|
-
* Why this is not {@link captureImage}: that door is an offscreen
|
|
1297
|
-
* re-render of the document's content, for thumbnails and asset previews,
|
|
1298
|
-
* and it deliberately draws the subject alone. The chrome door's promise is
|
|
1299
|
-
* the opposite one — "photograph the editor as the person sees it" — and
|
|
1300
|
-
* routing it through `captureImage` made the door LIE about this stage: the
|
|
1301
|
-
* compass renders every frame into the default framebuffer (measured: the
|
|
1302
|
-
* call is reached, `_threeSurfaceShowing` true, 24 objects, target=screen)
|
|
1303
|
-
* and never appeared in a single chrome frame.
|
|
1304
|
-
*
|
|
1305
|
-
* The canvas has no `preserveDrawingBuffer`, so the only place its pixels
|
|
1306
|
-
* can be read is inside the frame that drew them — which is why the host
|
|
1307
|
-
* serves the request at the end of its own `animate`
|
|
1308
|
-
* ({@link servePresentedFrame}), the same shape the ingest lane's
|
|
1309
|
-
* `LiveSession.snapshotFrame` uses.
|
|
1310
|
-
*/
|
|
1339
|
+
/** Capture the presented stage, including its orientation compass, at the end of animate through servePresentedFrame. captureImage separately renders document content offscreen for previews. */
|
|
1311
1340
|
private presentedFrameWaiters: Array<(frame: HTMLCanvasElement | null) => void> = [];
|
|
1312
1341
|
/** True while a host render loop is servicing {@link servePresentedFrame}. */
|
|
1313
1342
|
private presentsFrames = false;
|
|
@@ -1334,8 +1363,8 @@ export class Object3DDocumentSession {
|
|
|
1334
1363
|
resolve(frame);
|
|
1335
1364
|
};
|
|
1336
1365
|
this.presentedFrameWaiters.push(once);
|
|
1337
|
-
// A stage that draws on change draws for this request
|
|
1338
|
-
|
|
1366
|
+
// A stage that draws on change draws for this request (`needsFrame`); it is not a change
|
|
1367
|
+
// to the picture, so the stages are not invalidated (a live accumulation goes on).
|
|
1339
1368
|
setTimeout(() => once(null), PRESENTED_FRAME_WAIT_MS);
|
|
1340
1369
|
});
|
|
1341
1370
|
}
|
|
@@ -1372,6 +1401,16 @@ export class Object3DDocumentSession {
|
|
|
1372
1401
|
render(renderSolid: (camera: THREE.Camera) => void, interactive = false): void {
|
|
1373
1402
|
const finishDraw = this.beforeRender?.(interactive);
|
|
1374
1403
|
const restoreDepthDraw = this.depthDrawMaterials.bind(this.renderer);
|
|
1404
|
+
// A sampled photograph's jitter moves the projection of the camera the scene is drawn with,
|
|
1405
|
+
// AFTER the source prepared its draw (shadow fits stay put), and stays on through the source's
|
|
1406
|
+
// own finishing passes (Blender's outline is drawn per sample).
|
|
1407
|
+
const jitter = this.captureJitter;
|
|
1408
|
+
const jittered: { restore: (() => void) | null } = { restore: null };
|
|
1409
|
+
const unjittered = renderSolid;
|
|
1410
|
+
renderSolid = (camera) => {
|
|
1411
|
+
if (jitter && !jittered.restore) jittered.restore = jitterProjection(camera, jitter.offset, jitter.width, jitter.height);
|
|
1412
|
+
unjittered(camera);
|
|
1413
|
+
};
|
|
1375
1414
|
try {
|
|
1376
1415
|
this.boneSelectionHighlight?.update();
|
|
1377
1416
|
const camera = this.camera();
|
|
@@ -1406,8 +1445,7 @@ export class Object3DDocumentSession {
|
|
|
1406
1445
|
}
|
|
1407
1446
|
return;
|
|
1408
1447
|
}
|
|
1409
|
-
//
|
|
1410
|
-
// and the solid form behind it is what occludes the far side.
|
|
1448
|
+
// Draw clay bodies beneath the edge overlay, including far-side wires.
|
|
1411
1449
|
this.shading.render(
|
|
1412
1450
|
this.scene,
|
|
1413
1451
|
'clay',
|
|
@@ -1419,14 +1457,30 @@ export class Object3DDocumentSession {
|
|
|
1419
1457
|
this.shading.render(this.scene, mode, () => renderSolid(camera), isEditorViewportShadingTarget);
|
|
1420
1458
|
} finally {
|
|
1421
1459
|
restoreDepthDraw();
|
|
1422
|
-
|
|
1460
|
+
try {
|
|
1461
|
+
finishDraw?.();
|
|
1462
|
+
} finally {
|
|
1463
|
+
jittered.restore?.();
|
|
1464
|
+
}
|
|
1423
1465
|
}
|
|
1424
1466
|
}
|
|
1425
1467
|
|
|
1426
|
-
/** Visible authoring draw with the editor-only native selection silhouette.
|
|
1427
|
-
|
|
1468
|
+
/** Visible authoring draw with the editor-only native selection silhouette. `still`: nothing
|
|
1469
|
+
* has changed since the host's last drawn frame, so the live accumulation goes on; `revision`
|
|
1470
|
+
* names the state of the content the host drew it at. */
|
|
1471
|
+
renderViewport(deltaSeconds = 0, interactive = false, still = false, revision = ''): void {
|
|
1428
1472
|
this.advanceLook(deltaSeconds);
|
|
1429
1473
|
this.ensureComposer();
|
|
1474
|
+
const sampling = this.liveSampling();
|
|
1475
|
+
if (sampling && still && this.flight === null) {
|
|
1476
|
+
if (this.renderLiveFrame(sampling, revision)) return;
|
|
1477
|
+
// The sum is not yet better than the ordinary frame (or failed): the ordinary frame, at full
|
|
1478
|
+
// detail, without starting the sum over.
|
|
1479
|
+
interactive = false;
|
|
1480
|
+
} else {
|
|
1481
|
+
// Something changed: this frame is drawn plain, and the next still one starts over.
|
|
1482
|
+
this.live?.restart();
|
|
1483
|
+
}
|
|
1430
1484
|
const transform = this.materialDisplayTransform();
|
|
1431
1485
|
if (transform) {
|
|
1432
1486
|
const size = this.renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
@@ -1461,6 +1515,104 @@ export class Object3DDocumentSession {
|
|
|
1461
1515
|
}, interactive);
|
|
1462
1516
|
}
|
|
1463
1517
|
|
|
1518
|
+
/**
|
|
1519
|
+
* One more sample of the live accumulation, and the picture so far on screen once it is at least
|
|
1520
|
+
* as good as the ordinary frame; once every sample is in, the finished picture again (a redraw
|
|
1521
|
+
* with nothing changed: a presented-frame request). A sample is drawn as a photograph's is
|
|
1522
|
+
* (`captureImage`): the projection moved by its offset after the source prepared its draw, a flat
|
|
1523
|
+
* backdrop left out and merged under the sum. Answers false when the caller is to draw the
|
|
1524
|
+
* ordinary frame instead.
|
|
1525
|
+
*/
|
|
1526
|
+
private renderLiveFrame(sampling: PhotographSampling, revision: string): boolean {
|
|
1527
|
+
const size = this.renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
1528
|
+
let live = this.live;
|
|
1529
|
+
if (!live || live.sampling !== sampling || live.width !== size.x || live.height !== size.y) {
|
|
1530
|
+
live?.dispose();
|
|
1531
|
+
live = this.live = new LiveAccumulation(sampling, size.x, size.y);
|
|
1532
|
+
}
|
|
1533
|
+
if (live.failed) return false;
|
|
1534
|
+
// A sum drawn under another state of the content is neither added to nor shown.
|
|
1535
|
+
if (live.next > 0 && live.revision !== revision) live.restart();
|
|
1536
|
+
const transform = this.materialDisplayTransform();
|
|
1537
|
+
const previous = this.renderer.getRenderTarget();
|
|
1538
|
+
try {
|
|
1539
|
+
if (live.next < live.count) {
|
|
1540
|
+
const index = live.next;
|
|
1541
|
+
const { accumulation, sampleTarget } = live.working(this.renderer);
|
|
1542
|
+
if (index === 0) {
|
|
1543
|
+
accumulation.clear(this.renderer);
|
|
1544
|
+
live.backdrop = null;
|
|
1545
|
+
live.revision = revision;
|
|
1546
|
+
}
|
|
1547
|
+
const offset = accumulation.offsets[index]!;
|
|
1548
|
+
const mergeBackdrop = sampling.backdropMerge && !transform && this.renderer.toneMapping === THREE.NoToneMapping;
|
|
1549
|
+
// The first still frame draws the shadow maps for what it shows; the rest reuse them.
|
|
1550
|
+
const shadowsUpdate = this.renderer.shadowMap.autoUpdate;
|
|
1551
|
+
this.renderer.shadowMap.autoUpdate = shadowsUpdate && index === 0;
|
|
1552
|
+
this.captureBackdrop = null;
|
|
1553
|
+
this.captureJitter = {
|
|
1554
|
+
offset: [offset[0] * sampling.projectionScale, offset[1] * sampling.projectionScale],
|
|
1555
|
+
width: size.x, height: size.y,
|
|
1556
|
+
};
|
|
1557
|
+
try {
|
|
1558
|
+
this.renderer.setRenderTarget(sampleTarget);
|
|
1559
|
+
this.renderCapture(
|
|
1560
|
+
(camera) => this.renderLinearScene(camera, sampleTarget),
|
|
1561
|
+
false,
|
|
1562
|
+
mergeBackdrop && (index === 0 || live.backdrop !== null),
|
|
1563
|
+
);
|
|
1564
|
+
} finally {
|
|
1565
|
+
this.captureJitter = null;
|
|
1566
|
+
this.renderer.shadowMap.autoUpdate = shadowsUpdate;
|
|
1567
|
+
}
|
|
1568
|
+
if (index === 0) live.backdrop = this.captureBackdrop;
|
|
1569
|
+
this.captureBackdrop = null;
|
|
1570
|
+
accumulation.add(this.renderer, sampleTarget.texture, offset);
|
|
1571
|
+
live.next = index + 1;
|
|
1572
|
+
const drawing = accumulation.resolve(this.renderer);
|
|
1573
|
+
if (live.next < live.count) {
|
|
1574
|
+
if (live.next < live.showFrom) return false;
|
|
1575
|
+
this.presentLiveSum(live, drawing, transform, previous);
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
// Complete: the picture as shown is kept and the working targets go.
|
|
1579
|
+
this.presentLiveSum(live, drawing, transform, live.finishedTarget());
|
|
1580
|
+
live.releaseWorking();
|
|
1581
|
+
}
|
|
1582
|
+
if (!live.finished) return false;
|
|
1583
|
+
live.blitFinished(this.renderer, previous);
|
|
1584
|
+
return true;
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
// An accumulation that fails ends: the ordinary frame from here on, until something changes.
|
|
1587
|
+
live.failed = true;
|
|
1588
|
+
live.releaseWorking();
|
|
1589
|
+
if (!this.liveFailureReported) {
|
|
1590
|
+
this.liveFailureReported = true;
|
|
1591
|
+
editorConsole.warn(
|
|
1592
|
+
`live view: the accumulation failed and the view draws without it until it changes — ${
|
|
1593
|
+
error instanceof Error ? error.message : String(error)}`,
|
|
1594
|
+
'document',
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
return false;
|
|
1598
|
+
} finally {
|
|
1599
|
+
this.renderer.setRenderTarget(previous);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** The resolved sum through the view (merged over its backdrop, or the display transform, or
|
|
1604
|
+
* the output pass) into `target` (null: the canvas). */
|
|
1605
|
+
private presentLiveSum(
|
|
1606
|
+
live: LiveAccumulation,
|
|
1607
|
+
drawing: THREE.WebGLRenderTarget,
|
|
1608
|
+
transform: DocumentDisplayTransform | null,
|
|
1609
|
+
target: THREE.WebGLRenderTarget | null,
|
|
1610
|
+
): void {
|
|
1611
|
+
if (live.backdrop) live.working(this.renderer).accumulation.merge(this.renderer, drawing, live.backdrop, target);
|
|
1612
|
+
else if (transform) transform.render(this.renderer, drawing, target, false);
|
|
1613
|
+
else viewportCaptureOutputPass(false).render(this.renderer, target as THREE.WebGLRenderTarget, drawing, 0, false);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1464
1616
|
resize(width: number, height: number): void {
|
|
1465
1617
|
invalidateStages();
|
|
1466
1618
|
this.renderWidth = Math.max(1, width);
|
|
@@ -1468,15 +1620,25 @@ export class Object3DDocumentSession {
|
|
|
1468
1620
|
this.composer?.setSize(this.renderWidth, this.renderHeight);
|
|
1469
1621
|
}
|
|
1470
1622
|
|
|
1471
|
-
private renderCapture(
|
|
1623
|
+
private renderCapture(
|
|
1624
|
+
renderSolid: (camera: THREE.Camera) => void,
|
|
1625
|
+
transparent: boolean,
|
|
1626
|
+
/**
|
|
1627
|
+
* Retain a flat-color backdrop separately for composition beneath accumulated samples. Read it
|
|
1628
|
+
* after the host mirrors the current dressing into the scene.
|
|
1629
|
+
*/
|
|
1630
|
+
leaveColorBackdrop = false,
|
|
1631
|
+
): void {
|
|
1472
1632
|
this.render((camera) => {
|
|
1473
|
-
if (!transparent) {
|
|
1474
|
-
renderSolid(camera);
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
1633
|
// Apply after the host mirrors its world into this scene. Environment
|
|
1478
1634
|
// lighting stays intact; only the camera background is omitted.
|
|
1479
1635
|
const background = this.scene.background;
|
|
1636
|
+
const leaveBackdrop = !transparent && leaveColorBackdrop && (background as THREE.Color | null)?.isColor === true;
|
|
1637
|
+
if (!transparent && !leaveBackdrop) {
|
|
1638
|
+
renderSolid(camera);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (leaveBackdrop) this.captureBackdrop = (background as THREE.Color).clone();
|
|
1480
1642
|
const clearColor = this.renderer.getClearColor(new THREE.Color());
|
|
1481
1643
|
const clearAlpha = this.renderer.getClearAlpha();
|
|
1482
1644
|
try {
|
|
@@ -1491,18 +1653,9 @@ export class Object3DDocumentSession {
|
|
|
1491
1653
|
}
|
|
1492
1654
|
|
|
1493
1655
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* A NUMBER is a square of that size — the default shape, and the right one
|
|
1499
|
-
* for an unstaged look at a model. `{width, height}` renders the buffer AND
|
|
1500
|
-
* the camera at that aspect, so a video-shaped look comes back already
|
|
1501
|
-
* shaped instead of square-and-cropped. Whatever the shape, the camera is
|
|
1502
|
-
* matched to the BUFFER for the duration: rendering the panel's aspect
|
|
1503
|
-
* through a square buffer is what photographed a cube as a tall thin prism
|
|
1504
|
-
* on a 3.05:1 document panel, through the one look instrument an agent has.
|
|
1505
|
-
* The visible viewport is never touched.
|
|
1656
|
+
* Capture offscreen through this document’s renderer and diagnostic pipeline without
|
|
1657
|
+
* preserveDrawingBuffer. A number requests a square; width and height request a matching camera
|
|
1658
|
+
* aspect and buffer for the capture duration. Keep the visible viewport unchanged.
|
|
1506
1659
|
*/
|
|
1507
1660
|
captureImage(
|
|
1508
1661
|
size:
|
|
@@ -1521,8 +1674,12 @@ export class Object3DDocumentSession {
|
|
|
1521
1674
|
if (!Number.isFinite(requestedHeight) || requestedHeight < 1) return null;
|
|
1522
1675
|
const outputWidth = Math.min(2048, Math.round(requestedWidth));
|
|
1523
1676
|
const outputHeight = Math.min(2048, Math.round(requestedHeight));
|
|
1524
|
-
const
|
|
1525
|
-
const
|
|
1677
|
+
const mode = this.state.mode;
|
|
1678
|
+
const sampling = (mode !== 'uv' && mode !== 'vertex-colors' ? this.photographSampling?.[mode] : undefined) ?? null;
|
|
1679
|
+
// A sampled photograph is drawn at its own size, once per sample; otherwise at twice it.
|
|
1680
|
+
const scale = sampling ? 1 : 2;
|
|
1681
|
+
const renderWidth = outputWidth * scale;
|
|
1682
|
+
const renderHeight = outputHeight * scale;
|
|
1526
1683
|
// Three disables material tone mapping on ordinary render targets. A
|
|
1527
1684
|
// byte target clips lit surfaces to white before display conversion can
|
|
1528
1685
|
// recover them. Keep HDR values, then use the same output resolve as the
|
|
@@ -1533,28 +1690,84 @@ export class Object3DDocumentSession {
|
|
|
1533
1690
|
const transform = this.materialDisplayTransform();
|
|
1534
1691
|
const target = new THREE.WebGLRenderTarget(transform ? outputWidth : renderWidth, transform ? outputHeight : renderHeight);
|
|
1535
1692
|
let resolved: THREE.WebGLRenderTarget | null = null;
|
|
1693
|
+
const accumulation = sampling ? new PhotographAccumulation(sampling, outputWidth, outputHeight, this.renderer) : null;
|
|
1536
1694
|
const previousTarget = this.renderer.getRenderTarget();
|
|
1537
1695
|
const perspective = this.viewport.camera;
|
|
1538
1696
|
const previousAspect = perspective.aspect;
|
|
1539
1697
|
try {
|
|
1540
1698
|
this.captureAspect = outputWidth / outputHeight;
|
|
1699
|
+
this.capturePixelScale = scale;
|
|
1541
1700
|
const override = typeof size === 'number' ? undefined : size.camera;
|
|
1542
1701
|
if (override) this.cameraOverride = override;
|
|
1543
1702
|
else {
|
|
1544
1703
|
perspective.aspect = outputWidth / outputHeight;
|
|
1545
1704
|
perspective.updateProjectionMatrix();
|
|
1546
1705
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1706
|
+
const transparentCapture = typeof size !== 'number' && size.transparent === true;
|
|
1707
|
+
let merged = false;
|
|
1708
|
+
if (accumulation && sampling) {
|
|
1709
|
+
// The composer's buffers take the photograph's size once, not once per sample.
|
|
1710
|
+
const sizedComposer = this.sizeComposerForCapture(renderWidth, renderHeight);
|
|
1711
|
+
// The shadow maps the first sample drew serve them all: the jitter moves only the
|
|
1712
|
+
// projection, after the source fitted its shadows to the unmoved camera.
|
|
1713
|
+
const shadowsUpdate = this.renderer.shadowMap.autoUpdate;
|
|
1714
|
+
try {
|
|
1715
|
+
// Merged over its backdrop after the samples, as Blender's viewport does: each sample is
|
|
1716
|
+
// drawn with none when the backdrop is a flat colour (an opaque photograph of a
|
|
1717
|
+
// transparent drawing is laid over it below). The first sample decides; the rest follow.
|
|
1718
|
+
// The merge writes the display encoding itself; under a tone-mapping renderer the
|
|
1719
|
+
// drawing goes through the output pass as any other.
|
|
1720
|
+
const mergeBackdrop = sampling.backdropMerge && !transparentCapture && !transform &&
|
|
1721
|
+
this.renderer.toneMapping === THREE.NoToneMapping;
|
|
1722
|
+
this.captureBackdrop = null;
|
|
1723
|
+
accumulation.clear(this.renderer);
|
|
1724
|
+
accumulation.offsets.forEach((offset, index) => {
|
|
1725
|
+
this.captureSample = index;
|
|
1726
|
+
this.renderer.shadowMap.autoUpdate = shadowsUpdate && index === 0;
|
|
1727
|
+
this.captureJitter = {
|
|
1728
|
+
offset: [offset[0] * sampling.projectionScale, offset[1] * sampling.projectionScale],
|
|
1729
|
+
width: renderWidth, height: renderHeight,
|
|
1730
|
+
};
|
|
1731
|
+
this.renderer.setRenderTarget(sceneTarget);
|
|
1732
|
+
this.renderCapture(
|
|
1733
|
+
(camera) => this.renderSolidForCapture(camera, sceneTarget, renderWidth, renderHeight, sizedComposer),
|
|
1734
|
+
transparentCapture,
|
|
1735
|
+
mergeBackdrop && (index === 0 || this.captureBackdrop !== null),
|
|
1736
|
+
);
|
|
1737
|
+
this.captureJitter = null;
|
|
1738
|
+
accumulation.add(this.renderer, sceneTarget.texture, offset);
|
|
1739
|
+
});
|
|
1740
|
+
const drawing = accumulation.resolve(this.renderer);
|
|
1741
|
+
if (mergeBackdrop && this.captureBackdrop) {
|
|
1742
|
+
accumulation.merge(this.renderer, drawing, this.captureBackdrop, target);
|
|
1743
|
+
merged = true;
|
|
1744
|
+
}
|
|
1745
|
+
resolved = null;
|
|
1746
|
+
if (!merged) {
|
|
1747
|
+
if (transform) transform.render(this.renderer, drawing, target, transparentCapture);
|
|
1748
|
+
else viewportCaptureOutputPass(transparentCapture).render(this.renderer, target, drawing, 0, false);
|
|
1749
|
+
merged = true;
|
|
1750
|
+
}
|
|
1751
|
+
} finally {
|
|
1752
|
+
this.captureSample = 0;
|
|
1753
|
+
this.renderer.shadowMap.autoUpdate = shadowsUpdate;
|
|
1754
|
+
if (sizedComposer) this.restoreComposerAfterCapture(sizedComposer);
|
|
1755
|
+
}
|
|
1756
|
+
} else {
|
|
1757
|
+
this.renderer.setRenderTarget(sceneTarget);
|
|
1758
|
+
this.renderCapture(
|
|
1759
|
+
(camera) => this.renderSolidForCapture(camera, sceneTarget, renderWidth, renderHeight),
|
|
1760
|
+
transparentCapture,
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
const straightAlpha = transparentCapture;
|
|
1764
|
+
if (!merged) {
|
|
1765
|
+
if (transform) {
|
|
1766
|
+
resolved = resolveSceneLinearSize(this.renderer, sceneTarget, outputWidth, outputHeight);
|
|
1767
|
+
transform.render(this.renderer, resolved, target, straightAlpha);
|
|
1768
|
+
}
|
|
1769
|
+
else viewportCaptureOutputPass(straightAlpha).render(this.renderer, target, sceneTarget, 0, false);
|
|
1556
1770
|
}
|
|
1557
|
-
else viewportCaptureOutputPass(straightAlpha).render(this.renderer, target, sceneTarget, 0, false);
|
|
1558
1771
|
|
|
1559
1772
|
const rowBytes = target.width * 4;
|
|
1560
1773
|
const pixels = new Uint8Array(rowBytes * target.height);
|
|
@@ -1580,6 +1793,10 @@ export class Object3DDocumentSession {
|
|
|
1580
1793
|
return output.toDataURL('image/png');
|
|
1581
1794
|
} finally {
|
|
1582
1795
|
this.captureAspect = null;
|
|
1796
|
+
this.capturePixelScale = null;
|
|
1797
|
+
this.captureJitter = null;
|
|
1798
|
+
this.captureBackdrop = null;
|
|
1799
|
+
accumulation?.dispose();
|
|
1583
1800
|
this.cameraOverride = null;
|
|
1584
1801
|
perspective.aspect = previousAspect;
|
|
1585
1802
|
perspective.updateProjectionMatrix();
|
|
@@ -1612,6 +1829,8 @@ export class Object3DDocumentSession {
|
|
|
1612
1829
|
if (this.selectionOutline)
|
|
1613
1830
|
releaseThreeSelectionOutline(this.renderer, this.selectionOutline, this.selectionOutlinePass);
|
|
1614
1831
|
this.composer?.dispose();
|
|
1832
|
+
this.live?.dispose();
|
|
1833
|
+
this.live = null;
|
|
1615
1834
|
this.displayTarget?.dispose();
|
|
1616
1835
|
this.displayTarget = null;
|
|
1617
1836
|
this.displayTransform = null;
|
|
@@ -1631,34 +1850,8 @@ export class Object3DDocumentSession {
|
|
|
1631
1850
|
this.helpers.push(helper);
|
|
1632
1851
|
}
|
|
1633
1852
|
|
|
1634
|
-
/**
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
* The composer exists for ONE feature — the editor-owned selection
|
|
1638
|
-
* silhouette; `syncComposerOutput` hands the screen back to the plain scene
|
|
1639
|
-
* pass whenever nothing is outlined — so the library belongs with the first
|
|
1640
|
-
* frame of this document, not with the module that draws it. Everything an
|
|
1641
|
-
* Asset Lab document does before that first frame (mount, adopt, frame the
|
|
1642
|
-
* content) is untouched by it.
|
|
1643
|
-
*
|
|
1644
|
-
* Called from every `renderViewport`, so it must be cheap and re-entrant:
|
|
1645
|
-
* `composerLoading` keeps exactly one import in flight, and the continuation
|
|
1646
|
-
* re-checks `disposed` because a document can close mid-load.
|
|
1647
|
-
*/
|
|
1648
|
-
/**
|
|
1649
|
-
* WHETHER THIS DOCUMENT DRAWS THE SELECTION SILHOUETTE AT ALL — a LIVE
|
|
1650
|
-
* switch, not just a construction-time one.
|
|
1651
|
-
*
|
|
1652
|
-
* Two callers turn it off, for different reasons. The inspector's preview
|
|
1653
|
-
* lane shows a subject with nothing selectable inside it, so it never needs
|
|
1654
|
-
* the pass — or the effect it would build per selection — and says so once.
|
|
1655
|
-
* A modeling document says so whenever it is in EDIT mode: Blender draws no
|
|
1656
|
-
* object outline there (`modeling-edit-all.png`), and the silhouette
|
|
1657
|
-
* otherwise follows the hierarchy selection straight through the mode
|
|
1658
|
-
* change. That second caller flips it while the composer is already built,
|
|
1659
|
-
* so turning it off must drop the outline the composer is currently drawing
|
|
1660
|
-
* — gating construction alone leaves the frame exactly as it was.
|
|
1661
|
-
*/
|
|
1853
|
+
/** Enable the selection silhouette. Preview-only subjects and Edit mode
|
|
1854
|
+
* disable it; changing this also clears any already-built outline. */
|
|
1662
1855
|
get selectionOutlineEnabled(): boolean {
|
|
1663
1856
|
return this.selectionOutlineWanted;
|
|
1664
1857
|
}
|
|
@@ -1673,7 +1866,6 @@ export class Object3DDocumentSession {
|
|
|
1673
1866
|
}
|
|
1674
1867
|
private selectionOutlineWanted = true;
|
|
1675
1868
|
|
|
1676
|
-
/** Whether the view draws its selected objects' origins (`overlays.selection.origins`). */
|
|
1677
1869
|
/** The view's X-ray for its current draw mode (`ViewportModePresentation.xray`). */
|
|
1678
1870
|
private xray: ViewportXray = { enabled: false, alpha: 1 };
|
|
1679
1871
|
|
|
@@ -1683,6 +1875,7 @@ export class Object3DDocumentSession {
|
|
|
1683
1875
|
invalidateStages();
|
|
1684
1876
|
}
|
|
1685
1877
|
|
|
1878
|
+
/** Whether the view draws selected-object origins (`overlays.selection.origins`). */
|
|
1686
1879
|
set selectionOriginsEnabled(value: boolean) {
|
|
1687
1880
|
if (value === (this.selectionOrigins !== null)) return;
|
|
1688
1881
|
if (value) {
|
|
@@ -1696,6 +1889,8 @@ export class Object3DDocumentSession {
|
|
|
1696
1889
|
}
|
|
1697
1890
|
}
|
|
1698
1891
|
|
|
1892
|
+
/** Lazily import and construct the selection-outline composer. Keep one
|
|
1893
|
+
* import in flight and discard its result if the document closes. */
|
|
1699
1894
|
private ensureComposer(): void {
|
|
1700
1895
|
if (!this.selectionOutlineWanted) return;
|
|
1701
1896
|
if (this.composer || this.composerLoading || this.disposed) return;
|
|
@@ -1735,35 +1930,15 @@ export class Object3DDocumentSession {
|
|
|
1735
1930
|
});
|
|
1736
1931
|
}
|
|
1737
1932
|
|
|
1738
|
-
/**
|
|
1739
|
-
*
|
|
1740
|
-
* that pass without transferring screen ownership to the scene pass leaves
|
|
1741
|
-
* the unoutlined frame stranded in the composer's offscreen buffer. Keep
|
|
1742
|
-
* selection responsible only for the outline, never for whether the asset
|
|
1743
|
-
* itself reaches the canvas.
|
|
1744
|
-
*/
|
|
1745
|
-
/**
|
|
1746
|
-
* Draw the scene FOR A PHOTOGRAPH into `target` the way the screen sees it:
|
|
1747
|
-
* through the outline pass when a selection silhouette is on (the outline is
|
|
1748
|
-
* a pass, absent from a plain draw — a capture without it lied about the
|
|
1749
|
-
* screen), the plain draw otherwise.
|
|
1750
|
-
*
|
|
1751
|
-
* The two passes are driven DIRECTLY rather than through `composer.render()`,
|
|
1752
|
-
* because neither of the composer's own doors says where a frame landed:
|
|
1753
|
-
* `EffectComposer.render` ping-pongs LOCAL variables and never reassigns
|
|
1754
|
-
* `inputBuffer`/`outputBuffer`, and `CopyPass.render` ignores its
|
|
1755
|
-
* `outputBuffer` argument entirely (it always writes to its own render
|
|
1756
|
-
* target, 1x1 unless the composer sized it). Going through them wrote the
|
|
1757
|
-
* photograph nowhere — every capture of a selected object came back empty.
|
|
1758
|
-
* A pass's own contract is exact: `RenderPass` draws the scene into the
|
|
1759
|
-
* `inputBuffer` it is handed, `EffectPass` reads that and writes the
|
|
1760
|
-
* `outputBuffer` it is handed — so hand the outline pass the capture target.
|
|
1761
|
-
*/
|
|
1933
|
+
/** Capture through the current scene and outline passes. Resize only the
|
|
1934
|
+
* composer's offscreen buffers, then restore their live dimensions. */
|
|
1762
1935
|
private renderSolidForCapture(
|
|
1763
1936
|
camera: THREE.Camera,
|
|
1764
1937
|
target: THREE.WebGLRenderTarget,
|
|
1765
1938
|
width: number,
|
|
1766
1939
|
height: number,
|
|
1940
|
+
/** The composer is already at the photograph's size (a sampled photograph sized it once). */
|
|
1941
|
+
sized: { readonly width: number; readonly height: number } | null = null,
|
|
1767
1942
|
): void {
|
|
1768
1943
|
const composer = this.composer;
|
|
1769
1944
|
const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
|
|
@@ -1771,6 +1946,14 @@ export class Object3DDocumentSession {
|
|
|
1771
1946
|
this.renderLinearScene(camera, target);
|
|
1772
1947
|
return;
|
|
1773
1948
|
}
|
|
1949
|
+
if (sized) {
|
|
1950
|
+
try {
|
|
1951
|
+
this.renderLinearScene(camera, target);
|
|
1952
|
+
} finally {
|
|
1953
|
+
this.renderer.setRenderTarget(target);
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1774
1957
|
const previousSize = this.renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
1775
1958
|
try {
|
|
1776
1959
|
this.resizeComposerBuffers(width, height);
|
|
@@ -1782,6 +1965,21 @@ export class Object3DDocumentSession {
|
|
|
1782
1965
|
}
|
|
1783
1966
|
}
|
|
1784
1967
|
|
|
1968
|
+
/** A sampled photograph's composer sizing, once for all its samples; null when the capture
|
|
1969
|
+
* does not draw through the composer (nothing is outlined). */
|
|
1970
|
+
private sizeComposerForCapture(width: number, height: number): { readonly width: number; readonly height: number } | null {
|
|
1971
|
+
const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
|
|
1972
|
+
if (!this.composer || !outlined || !this.sceneRenderPass || !this.selectionOutlinePass) return null;
|
|
1973
|
+
const previous = this.renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
1974
|
+
this.resizeComposerBuffers(width, height);
|
|
1975
|
+
return { width: previous.x, height: previous.y };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
private restoreComposerAfterCapture(previous: { readonly width: number; readonly height: number }): void {
|
|
1979
|
+
this.resizeComposerBuffers(previous.width, previous.height);
|
|
1980
|
+
this.syncComposerOutput();
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1785
1983
|
/** Offscreen sizing must not call EffectComposer.setSize: it also resizes
|
|
1786
1984
|
* the renderer's visible canvas, clearing the frame a human is watching. */
|
|
1787
1985
|
private resizeComposerBuffers(width: number, height: number): void {
|
|
@@ -1823,6 +2021,7 @@ export class Object3DDocumentSession {
|
|
|
1823
2021
|
}
|
|
1824
2022
|
}
|
|
1825
2023
|
|
|
2024
|
+
/** Route screen output through the outline pass only while it has a selection. */
|
|
1826
2025
|
private syncComposerOutput(): void {
|
|
1827
2026
|
const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
|
|
1828
2027
|
if (this.sceneRenderPass) this.sceneRenderPass.renderToScreen = !outlined;
|
|
@@ -1945,30 +2144,7 @@ export class Object3DDocumentSession {
|
|
|
1945
2144
|
}
|
|
1946
2145
|
}
|
|
1947
2146
|
|
|
1948
|
-
// Compatibility for existing internal callers/tests; lookup-only consumers
|
|
1949
|
-
// import the registry directly so they do not pull this implementation graph.
|
|
1950
|
-
export {
|
|
1951
|
-
__resetObject3DDocumentSessionsForTest,
|
|
1952
|
-
allObject3DDocumentSessions,
|
|
1953
|
-
object3DDocumentSession,
|
|
1954
|
-
object3DDocumentSessionsVersion,
|
|
1955
|
-
registerObject3DDocumentSession,
|
|
1956
|
-
subscribeObject3DDocumentSessions,
|
|
1957
|
-
} from './object3d-document-session-registry';
|
|
1958
2147
|
|
|
1959
|
-
/**
|
|
1960
|
-
* OBJECT ORIGINS: a dot at the origin of each selected object, drawn over everything at a fixed
|
|
1961
|
-
* pixel size — Blender's "Origins" overlay (`overlays.selection.origins` in
|
|
1962
|
-
* `@volter/sdk/kit/viewport-presentation`). Blender draws it one pixel wider than its
|
|
1963
|
-
* Object Origin Size (default 6; `overlay_instance.cc`: `obcenter_dia + 1`) with a one-pixel
|
|
1964
|
-
* dark rim, and in the active object's
|
|
1965
|
-
* colour for the active object; the kit's rule for the active colour stands in for that: a lone
|
|
1966
|
-
* selected object is shown in the look's active colour, several in its selection colour, the
|
|
1967
|
-
* same rule the outline follows.
|
|
1968
|
-
*
|
|
1969
|
-
* The positions are read from each object's world matrix as the frame is drawn, so a dot follows
|
|
1970
|
-
* a drag without the selection having to change.
|
|
1971
|
-
*/
|
|
1972
2148
|
/** Blender's drawn origin diameter at the default Object Origin Size, in CSS pixels. three
|
|
1973
2149
|
* scales a point's size by the pixel ratio itself (`WebGLMaterials.refreshUniformsPoints`). */
|
|
1974
2150
|
const ORIGIN_SIZE_PX = 7;
|
|
@@ -1995,6 +2171,8 @@ function originDotTexture(): THREE.Texture {
|
|
|
1995
2171
|
return dotTexture;
|
|
1996
2172
|
}
|
|
1997
2173
|
|
|
2174
|
+
/** Draw selected-object origins at a fixed pixel size, using each object's
|
|
2175
|
+
* current world matrix and the active/selection colors. */
|
|
1998
2176
|
class SelectionOrigins extends THREE.Points<THREE.BufferGeometry, THREE.PointsMaterial> {
|
|
1999
2177
|
private objects: readonly THREE.Object3D[] = [];
|
|
2000
2178
|
private readonly origin = new THREE.Vector3();
|
|
@@ -2056,3 +2234,88 @@ class SelectionOrigins extends THREE.Points<THREE.BufferGeometry, THREE.PointsMa
|
|
|
2056
2234
|
this.material.dispose();
|
|
2057
2235
|
}
|
|
2058
2236
|
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* The live view's accumulation (`Object3DDocumentSession.renderLiveFrame`). Its working targets
|
|
2240
|
+
* (the sample, the sum) are half precision, made on the first sample and freed when the sum is
|
|
2241
|
+
* complete; the finished picture is kept display-encoded, 4 bytes a pixel.
|
|
2242
|
+
*/
|
|
2243
|
+
class LiveAccumulation {
|
|
2244
|
+
/** The next sample to draw: 0 starts over, `count` is done. */
|
|
2245
|
+
next = 0;
|
|
2246
|
+
/** The content revision the sum was begun at. */
|
|
2247
|
+
revision: string | null = null;
|
|
2248
|
+
/** The flat backdrop the samples leave out, to merge the sum over; null when drawn in. */
|
|
2249
|
+
backdrop: THREE.Color | null = null;
|
|
2250
|
+
/** The finished picture as shown, once every sample is in. */
|
|
2251
|
+
finished: THREE.WebGLRenderTarget | null = null;
|
|
2252
|
+
/** A failure ended the accumulation; cleared by the next change. */
|
|
2253
|
+
failed = false;
|
|
2254
|
+
readonly count: number;
|
|
2255
|
+
/** The sum is shown from this many samples on; the ordinary frame before. */
|
|
2256
|
+
readonly showFrom: number;
|
|
2257
|
+
private accumulation: PhotographAccumulation | null = null;
|
|
2258
|
+
private sampleTarget: THREE.WebGLRenderTarget | null = null;
|
|
2259
|
+
private readonly copy = new FullScreenQuad(new THREE.RawShaderMaterial({
|
|
2260
|
+
name: 'Live view finished picture', glslVersion: THREE.GLSL3, depthTest: false, depthWrite: false,
|
|
2261
|
+
blending: THREE.NoBlending, uniforms: { picture: { value: null } },
|
|
2262
|
+
vertexShader: `precision highp float; in vec3 position; in vec2 uv; out vec2 vUv;
|
|
2263
|
+
void main(){vUv=uv;gl_Position=vec4(position,1.0);}`,
|
|
2264
|
+
fragmentShader: `precision highp float; uniform sampler2D picture; in vec2 vUv; out vec4 outputColor;
|
|
2265
|
+
void main(){outputColor=texelFetch(picture, ivec2(vUv * vec2(textureSize(picture, 0))), 0);}`,
|
|
2266
|
+
}));
|
|
2267
|
+
|
|
2268
|
+
constructor(readonly sampling: PhotographSampling, readonly width: number, readonly height: number) {
|
|
2269
|
+
this.count = Math.min(sampling.offsets.length, PHOTOGRAPH_SAMPLE_CAP);
|
|
2270
|
+
this.showFrom = Math.min(this.count, Math.max(1, sampling.liveFrom ?? this.count));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** The working targets, made on demand. */
|
|
2274
|
+
working(renderer: THREE.WebGLRenderer): { accumulation: PhotographAccumulation; sampleTarget: THREE.WebGLRenderTarget } {
|
|
2275
|
+
this.accumulation ??= new PhotographAccumulation(this.sampling, this.width, this.height, renderer, 'half');
|
|
2276
|
+
this.sampleTarget ??= new THREE.WebGLRenderTarget(this.width, this.height, { type: THREE.HalfFloatType });
|
|
2277
|
+
return { accumulation: this.accumulation, sampleTarget: this.sampleTarget };
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
finishedTarget(): THREE.WebGLRenderTarget {
|
|
2281
|
+
this.finished ??= new THREE.WebGLRenderTarget(this.width, this.height, { depthBuffer: false });
|
|
2282
|
+
return this.finished;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
blitFinished(renderer: THREE.WebGLRenderer, target: THREE.WebGLRenderTarget | null): void {
|
|
2286
|
+
if (!this.finished) return;
|
|
2287
|
+
(this.copy.material as THREE.RawShaderMaterial).uniforms['picture']!.value = this.finished.texture;
|
|
2288
|
+
const autoClear = renderer.autoClear;
|
|
2289
|
+
renderer.setRenderTarget(target);
|
|
2290
|
+
renderer.autoClear = false;
|
|
2291
|
+
try {
|
|
2292
|
+
this.copy.render(renderer);
|
|
2293
|
+
} finally {
|
|
2294
|
+
renderer.autoClear = autoClear;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/** A change: the next still frame begins a new sum. */
|
|
2299
|
+
restart(): void {
|
|
2300
|
+
this.next = 0;
|
|
2301
|
+
this.revision = null;
|
|
2302
|
+
this.failed = false;
|
|
2303
|
+
this.finished?.dispose();
|
|
2304
|
+
this.finished = null;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
releaseWorking(): void {
|
|
2308
|
+
this.accumulation?.dispose();
|
|
2309
|
+
this.accumulation = null;
|
|
2310
|
+
this.sampleTarget?.dispose();
|
|
2311
|
+
this.sampleTarget = null;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
dispose(): void {
|
|
2315
|
+
this.releaseWorking();
|
|
2316
|
+
this.finished?.dispose();
|
|
2317
|
+
this.finished = null;
|
|
2318
|
+
this.copy.material.dispose();
|
|
2319
|
+
this.copy.dispose();
|
|
2320
|
+
}
|
|
2321
|
+
}
|