@solidrt/3d 0.0.57 → 0.0.59
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/AGENTS.md +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +129 -0
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +608 -185
- package/src/skeleton.ts +211 -0
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/scene.ts
CHANGED
|
@@ -36,31 +36,33 @@
|
|
|
36
36
|
// and pointer subsystems are scene-shadows.ts / scene-pointer.ts,
|
|
37
37
|
// built here with the scene's seams as their deps.
|
|
38
38
|
|
|
39
|
-
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawOrder, setDrawParams, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
39
|
+
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
40
40
|
import * as spatial from "flux:spatial"
|
|
41
41
|
import type { Impact as CoreImpact, NodeId, QueryFilter } from "flux:spatial"
|
|
42
|
-
import type { DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
42
|
+
import type { BufferId, DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureBindings, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
43
43
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
44
44
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
45
45
|
import { copy, mat4, transformPoint } from "./math.ts"
|
|
46
|
-
import { linearColor } from "./color.ts"
|
|
46
|
+
import { linearColor, premultipliedColor } from "./color.ts"
|
|
47
47
|
import type { Mat4, Quat, Vec3, Vec4 } from "./math.ts"
|
|
48
48
|
import { MAX_LIGHTS, MAX_SHADOW_MAPS } from "./glsl.ts"
|
|
49
49
|
import { cameraParams, cameraState, ensureCamera, makeCamera, updateCamera } from "./camera.ts"
|
|
50
50
|
import type { Camera, CameraState, CameraUpdate } from "./camera.ts"
|
|
51
51
|
import { makeShadowSystem } from "./scene-shadows.ts"
|
|
52
52
|
import { makePointerInput } from "./scene-pointer.ts"
|
|
53
|
-
import {
|
|
53
|
+
import { geometryKey, geometryTopology, validateGeometry } from "./geometry.ts"
|
|
54
54
|
import type { Geometry } from "./geometry.ts"
|
|
55
55
|
import { acquireGeometryBuffers, releaseGeometryBuffers } from "./geometry-gpu.ts"
|
|
56
56
|
import { backgroundPipeline, missingAttributes, SKYBOX_FRAGMENT } from "./material.ts"
|
|
57
|
-
import {
|
|
57
|
+
import { disposeResolve, makeResolve, replaceResolve, resizeResolve, setResolveBloom } from "./resolve.ts"
|
|
58
|
+
import type { BloomOptions, ResolveInput, ResolveOptions, ResolveRecord } from "./resolve.ts"
|
|
59
|
+
import { createEnvironmentPlaceholder, createPrefilter, bufferFormat } from "./environment.ts"
|
|
58
60
|
import type { Prefilter } from "./environment.ts"
|
|
59
61
|
import type { Material } from "./material.ts"
|
|
60
62
|
import { orderEntries } from "./order.ts"
|
|
61
|
-
import { fillTransform, leaveScene, makeNode, worldInto } from "./node.ts"
|
|
63
|
+
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition, worldInto } from "./node.ts"
|
|
62
64
|
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
63
|
-
import { checkInstancePairing, checkMask, instanceBinding, localBounds,
|
|
65
|
+
import { checkInstancePairing, checkMask, instanceBinding, localBounds, publishRecords } from "./mesh.ts"
|
|
64
66
|
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
65
67
|
import type { CastingLight, Light } from "./light.ts"
|
|
66
68
|
|
|
@@ -397,25 +399,55 @@ function environmentParams(env: EnvironmentOptions | null): ShaderParams {
|
|
|
397
399
|
return { uEnvIntensity: k.intensity, uEnvRotation: cubeTurn(k.rotation), uEnvOn: 1 }
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
|
|
401
|
-
* filmic curve every engine ships (Three's ACESFilmic, Godot's ACES,
|
|
402
|
-
* Unity's ACES). */
|
|
403
|
-
export type ToneMapping = "none" | "aces"
|
|
402
|
+
export type { BloomOptions, ResolveInput, ResolveOptions } from "./resolve.ts"
|
|
404
403
|
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
/** The resolve's tone mapping (setToneMapping): "none" clamps, "aces" is
|
|
405
|
+
* the filmic curve every engine ships (Three's ACESFilmic, Godot's ACES,
|
|
406
|
+
* Unity's ACES), "agx" Blender's AgX (Three, Godot 4.3+) and "neutral"
|
|
407
|
+
* the Khronos PBR Neutral curve (Three, Unity) that keeps product colors
|
|
408
|
+
* where a filmic curve shifts them. */
|
|
409
|
+
export type ToneMapping = "none" | "aces" | "agx" | "neutral"
|
|
410
|
+
|
|
411
|
+
// The uToneMapping value per mode; the RESOLVE set branches on it.
|
|
412
|
+
const TONE_MAPPING_CODE: Record<ToneMapping, number> = { none: 0, aces: 1, agx: 2, neutral: 3 }
|
|
413
|
+
|
|
414
|
+
/** The RESOLVE set's params for a resolve pass of your own (an app-owned
|
|
415
|
+
* atlas tiled views render into, resolved through `resolveFragment` from
|
|
416
|
+
* `/glsl`): `uToneMapping` and `uExposure` as the scene's setToneMapping
|
|
417
|
+
* and setExposure would write them - a shader target validates its
|
|
418
|
+
* params strictly, so both are always written. */
|
|
419
|
+
export function resolveParams(opts?: { toneMapping?: ToneMapping; exposure?: number }): ShaderParams {
|
|
420
|
+
let mode = opts?.toneMapping ?? "none"
|
|
421
|
+
let code = TONE_MAPPING_CODE[mode]
|
|
422
|
+
if (code === undefined) throw new Error('resolveParams: expected "none", "aces", "agx" or "neutral", got ' + mode)
|
|
423
|
+
let exposure = opts?.exposure ?? 1
|
|
424
|
+
if (!Number.isFinite(exposure) || exposure < 0) throw new Error("resolveParams: expected a finite exposure >= 0, got " + exposure)
|
|
425
|
+
return { uToneMapping: code, uExposure: exposure }
|
|
426
|
+
}
|
|
407
427
|
|
|
408
428
|
export type SceneOptions = {
|
|
409
|
-
/** The
|
|
410
|
-
*
|
|
411
|
-
*
|
|
429
|
+
/** The buffer's clear: an sRGB color like every color option (decoded
|
|
430
|
+
* to linear light on write), so the backdrop takes the scene's exposure
|
|
431
|
+
* and tone mapping in the resolve exactly as a background does. */
|
|
412
432
|
clearColor?: [number, number, number, number]
|
|
433
|
+
/** The resolve fragment (see setResolve), or a function of the buffer
|
|
434
|
+
* id returning it - the chain over `hdrTexture` is built right there;
|
|
435
|
+
* absent is DEFAULT_RESOLVE. */
|
|
436
|
+
resolve?: ResolveInput
|
|
437
|
+
/** The stock bloom on the resolve; see setBloom. Views follow it
|
|
438
|
+
* unless they carry a `bloom` of their own. */
|
|
439
|
+
bloom?: BloomOptions
|
|
413
440
|
/** Scene-wide fog; see setFog. */
|
|
414
441
|
fog?: FogOptions
|
|
415
442
|
/** Output tone mapping, default "none"; see setToneMapping. */
|
|
416
443
|
toneMapping?: ToneMapping
|
|
417
444
|
/** Output exposure, default 1; see setExposure. */
|
|
418
445
|
exposure?: number
|
|
446
|
+
/** LOD quality knob, default 1: every measured projected size is
|
|
447
|
+
* multiplied by it before the level select, so below 1 the groups hand
|
|
448
|
+
* over to their far levels sooner (Unity's lodBias). Live via
|
|
449
|
+
* scene.setLodBias; views and shadow tiles follow it. */
|
|
450
|
+
lodBias?: number
|
|
419
451
|
/** The scene target's layer mask (bitmask, default 1): the scene draws
|
|
420
452
|
* the meshes whose `layers` intersect it. Live via scene.setLayers. */
|
|
421
453
|
layers?: number
|
|
@@ -431,6 +463,12 @@ export type SceneOptions = {
|
|
|
431
463
|
/** The cube map reflective materials mirror; see setEnvironment. */
|
|
432
464
|
environment?: EnvironmentOptions
|
|
433
465
|
label?: string
|
|
466
|
+
/** `stagger` (ms) on the scene's root node - `setTransition(scene.root,
|
|
467
|
+
* { stagger })` at creation: every node added straight under the root
|
|
468
|
+
* that enters or leaves in one frame is spaced by `index * stagger`,
|
|
469
|
+
* the whole-scene form of a Group's stagger (nested groups declaring
|
|
470
|
+
* their own win for what is under them). */
|
|
471
|
+
stagger?: number
|
|
434
472
|
/** `autoFree: false` opts out of owner-scoped auto-dispose (then call dispose yourself). */
|
|
435
473
|
autoFree?: boolean
|
|
436
474
|
filter?: FilterMode
|
|
@@ -438,6 +476,13 @@ export type SceneOptions = {
|
|
|
438
476
|
/** Multisample count of the target (1, 2, 4 or 8; default 1). Storage-only
|
|
439
477
|
* anti-aliasing of mesh edges; see createDrawTarget. */
|
|
440
478
|
samples?: 1 | 2 | 4 | 8
|
|
479
|
+
/** Where a per-scene budget error goes: the light cap and the
|
|
480
|
+
* shadow-slot budget are checked over the settled set at the sync and
|
|
481
|
+
* reported once per change of the set, the scene rendering on with
|
|
482
|
+
* what fits. Without a handler the sync throws it, uncaught (a
|
|
483
|
+
* microtask): the `<Scene>` component hands one in and rethrows inside
|
|
484
|
+
* the tree, so the app's error boundary shows it. */
|
|
485
|
+
onError?: (error: Error) => void
|
|
441
486
|
}
|
|
442
487
|
|
|
443
488
|
export type ViewOptions = {
|
|
@@ -467,17 +512,29 @@ export type ViewOptions = {
|
|
|
467
512
|
* "texture" for a sampleable one exposed as `view.depthTexture`. Not
|
|
468
513
|
* with `into` (the depth is the parent's). */
|
|
469
514
|
depth?: true | "texture"
|
|
515
|
+
/** The view buffer's clear, an sRGB color like the scene's. */
|
|
470
516
|
clearColor?: [number, number, number, number]
|
|
471
517
|
samples?: 1 | 2 | 4 | 8
|
|
472
518
|
filter?: FilterMode
|
|
473
519
|
wrap?: WrapMode
|
|
474
520
|
label?: string
|
|
521
|
+
/** The view's resolve fragment (see Scene.setResolve), or a function
|
|
522
|
+
* of the view's buffer id; not with `into` (a tile renders linear into
|
|
523
|
+
* the atlas, which the app resolves once). */
|
|
524
|
+
resolve?: ResolveInput
|
|
525
|
+
/** The view's own bloom: BloomOptions overrides the scene's, null turns
|
|
526
|
+
* it off in this view (a clean minimap). Absent follows the scene's
|
|
527
|
+
* setBloom, like `fog`. Not with `into`. */
|
|
528
|
+
bloom?: BloomOptions | null
|
|
475
529
|
/** Render into a rectangle of this draw target (an app-owned atlas)
|
|
476
530
|
* instead of a target of the view's own: every view into one atlas
|
|
477
531
|
* costs ONE pass. `x`/`y` (top-left origin, default 0) place the tile;
|
|
478
532
|
* display it with `<d-texture src={atlas} srcX srcY srcW srcH>`. The
|
|
479
|
-
* atlas carries depth and
|
|
480
|
-
* (a draw target, not a texture)
|
|
533
|
+
* atlas carries depth, samples and the format; `view.texture` is then
|
|
534
|
+
* the tile's id (a draw target, not a texture), `view.depthTexture` and
|
|
535
|
+
* `view.hdrTexture` are null, and the tile holds LINEAR light like every
|
|
536
|
+
* buffer - resolve the atlas once with a DEFAULT_RESOLVE pass over it
|
|
537
|
+
* (`uScene` bound to the atlas) and display that. */
|
|
481
538
|
into?: TextureId
|
|
482
539
|
x?: number
|
|
483
540
|
y?: number
|
|
@@ -485,16 +542,28 @@ export type ViewOptions = {
|
|
|
485
542
|
|
|
486
543
|
/** A second rendering of a scene from its own camera; see Scene.createView. */
|
|
487
544
|
export type ViewHandle = {
|
|
488
|
-
/** The view's output, an ordinary texture id
|
|
545
|
+
/** The view's output, an ordinary texture id: its resolve (or, tiled,
|
|
546
|
+
* the tile's id). */
|
|
489
547
|
texture: TextureId
|
|
490
|
-
/** The view
|
|
548
|
+
/** The view's buffer, the linear light its resolve reads (sampler-only;
|
|
549
|
+
* null for a tiled view). */
|
|
550
|
+
hdrTexture: TextureId | null
|
|
551
|
+
/** The view buffer's depth as a sampler-only texture id when created
|
|
491
552
|
* with `depth: "texture"` (the shadow-map input), else null. */
|
|
492
553
|
depthTexture: TextureId | null
|
|
554
|
+
/** Replace the view's resolve fragment; exactly Scene.setResolve. Throws
|
|
555
|
+
* on a tiled view. */
|
|
556
|
+
setResolve(resolve: ResolveInput): void
|
|
557
|
+
/** The view's own bloom from now on (null = off): the scene's setBloom
|
|
558
|
+
* skips this view afterwards. Throws on a tiled view. */
|
|
559
|
+
setBloom(bloom: BloomOptions | null): void
|
|
493
560
|
/** Partial camera update, exactly scene.setCamera. */
|
|
494
561
|
setCamera(update: CameraUpdate): void
|
|
495
562
|
/** Current camera state, exactly scene.camera. */
|
|
496
563
|
camera(): CameraState
|
|
497
564
|
setSize(width: number, height: number): void
|
|
565
|
+
/** The view's target size in pixels, as setSize/setRect last left it. */
|
|
566
|
+
size(): { width: number; height: number }
|
|
498
567
|
/** Move and resize a view created `into` an atlas (top-left origin);
|
|
499
568
|
* throws on a view with a target of its own. */
|
|
500
569
|
setRect(rect: { x: number; y: number; width: number; height: number }): void
|
|
@@ -564,7 +633,10 @@ export type ReflectionProbe = {
|
|
|
564
633
|
* sampled by the faces it renders (a black environment stands in): one
|
|
565
634
|
* bounce. */
|
|
566
635
|
update(): void
|
|
567
|
-
/** Destroy the cube (idempotent; probes also die with the scene).
|
|
636
|
+
/** Destroy the cube (idempotent; probes also die with the scene). A
|
|
637
|
+
* scene whose `environment` or `background` names this cube drops it
|
|
638
|
+
* first (setEnvironment(null), setBackground(null)), so nothing samples
|
|
639
|
+
* a destroyed texture: set another when the probe's owner leaves. */
|
|
568
640
|
dispose(): void
|
|
569
641
|
}
|
|
570
642
|
|
|
@@ -594,16 +666,17 @@ const PROBE_FACE_UP: Vec3[] = [
|
|
|
594
666
|
const PROBE_SIZE = 128
|
|
595
667
|
// A cube face spans a quarter turn.
|
|
596
668
|
const PROBE_FOV = 90
|
|
597
|
-
// The output stage of a cube that holds light rather than display pixels
|
|
598
|
-
// (a probe's faces, a baked sky): no sRGB encode, no tone mapping, unit
|
|
599
|
-
// exposure - so the environment lookups read radiance.
|
|
600
|
-
const LINEAR_OUTPUT: ShaderParams = { uOutputEncode: 0, uToneMapping: TONE_MAPPING_CODE.none, uExposure: 1 }
|
|
601
|
-
|
|
602
669
|
export type Scene = {
|
|
603
|
-
/** The scene's output: an ordinary texture id (`<texture src>`)
|
|
670
|
+
/** The scene's output: an ordinary texture id (`<texture src>`) - the
|
|
671
|
+
* resolve of the buffer, encoded display pixels. */
|
|
604
672
|
texture: TextureId
|
|
605
|
-
/** The scene
|
|
606
|
-
*
|
|
673
|
+
/** The scene's BUFFER: the target the meshes draw into, premultiplied
|
|
674
|
+
* linear light (half float where the device renders it, see
|
|
675
|
+
* bufferFormat), sampler-only - the input of a radiance-reading pass (a
|
|
676
|
+
* bloom chain) whose result a custom resolve adds back in. */
|
|
677
|
+
hdrTexture: TextureId
|
|
678
|
+
/** The buffer's depth as a sampler-only texture id when created with
|
|
679
|
+
* `depth: "texture"`, else null. */
|
|
607
680
|
depthTexture: TextureId | null
|
|
608
681
|
/** The tree root; add(scene.root, node) attaches top-level nodes. */
|
|
609
682
|
root: SceneNode
|
|
@@ -614,6 +687,11 @@ export type Scene = {
|
|
|
614
687
|
* back through setCamera). Reflects a pending setCamera immediately. */
|
|
615
688
|
camera(): CameraState
|
|
616
689
|
setSize(width: number, height: number): void
|
|
690
|
+
/** The target size in pixels, as setSize last left it (a fill-mode
|
|
691
|
+
* `<Scene>` sets it from its layout box, so before the first layout
|
|
692
|
+
* this is the creation size). The aspect a camera control's fit()
|
|
693
|
+
* frames against. */
|
|
694
|
+
size(): { width: number; height: number }
|
|
617
695
|
/**
|
|
618
696
|
* Scene-wide uniforms: merge app-owned names into the target's SHARED
|
|
619
697
|
* params, beside the standard uViewProj/uCamPos/uCamRight/uCamUp the
|
|
@@ -680,24 +758,55 @@ export type Scene = {
|
|
|
680
758
|
*/
|
|
681
759
|
setEnvironment(env: EnvironmentOptions | null): void
|
|
682
760
|
/**
|
|
683
|
-
* The
|
|
684
|
-
*
|
|
761
|
+
* The resolve's tone mapping, applied once per pixel to the buffer -
|
|
762
|
+
* every material, the background, the clearColor alike: "none"
|
|
685
763
|
* (default) clamps the linear result, "aces" compresses highlights on
|
|
686
|
-
* the filmic curve. One shared-params write (`uToneMapping`, the
|
|
687
|
-
* set in `@solidrt/3d/glsl` declares), like Three's
|
|
688
|
-
* renderer.toneMapping and Godot's Environment tonemap
|
|
689
|
-
* fragment that writes fragColor directly is untouched. The clearColor
|
|
690
|
-
* is not tone mapped: with a curve on, draw the backdrop as a
|
|
691
|
-
* background.
|
|
764
|
+
* the filmic curve. One shared-params write (`uToneMapping`, the
|
|
765
|
+
* RESOLVE set in `@solidrt/3d/glsl` declares), like Three's
|
|
766
|
+
* renderer.toneMapping and Godot's Environment tonemap.
|
|
692
767
|
*/
|
|
693
768
|
setToneMapping(mode: ToneMapping): void
|
|
694
769
|
/**
|
|
695
|
-
* The
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
770
|
+
* The resolve's exposure (default 1): the buffer is scaled by it before
|
|
771
|
+
* tone mapping, so a scene lit in physical-ish units is brought into
|
|
772
|
+
* range here rather than by dimming every light. One shared-params
|
|
773
|
+
* write (`uExposure`), like Three's toneMappingExposure.
|
|
699
774
|
*/
|
|
700
775
|
setExposure(exposure: number): void
|
|
776
|
+
/**
|
|
777
|
+
* Replace the scene's RESOLVE: the one full-screen pass that turns the
|
|
778
|
+
* buffer (`hdrTexture`, premultiplied linear light) into `texture`
|
|
779
|
+
* (display pixels) - the place a post effect that needs radiance
|
|
780
|
+
* lives (Godot's glow sits in its tonemap pass; Three's bloom reads the
|
|
781
|
+
* HDR render before OutputPass). Fragment GLSL with the shader-target
|
|
782
|
+
* contract (vUV 0..1 top-left origin, iResolution, fragColor; no
|
|
783
|
+
* `#version` line means the standard preamble), `uniform sampler2D
|
|
784
|
+
* uScene` (the buffer) and the RESOLVE set declared: end with
|
|
785
|
+
* `fragColor = resolveColor(rgb, alpha)` for the scene's exposure, tone
|
|
786
|
+
* mapping and encode. The object form binds extra textures the source
|
|
787
|
+
* declares (a bloom chain's result). Any name written through
|
|
788
|
+
* scene.setParams is readable. The default is DEFAULT_RESOLVE (one
|
|
789
|
+
* sample through resolveColor). A textures-only change rebinds in
|
|
790
|
+
* place; a source change recompiles. A function form receives the
|
|
791
|
+
* buffer id, like the option.
|
|
792
|
+
*/
|
|
793
|
+
setResolve(resolve: ResolveInput): void
|
|
794
|
+
/**
|
|
795
|
+
* The stock bloom, or null for none (default): radiance above
|
|
796
|
+
* `threshold` (default 1) blurred over `radius` rounds (default 2) at a
|
|
797
|
+
* quarter of the target's size and added back at `intensity` (default
|
|
798
|
+
* 0.5) - Godot's glow, Unity's Bloom, Three's UnrealBloomPass. A chain
|
|
799
|
+
* of small passes per resolving target, re-rendered when the buffer
|
|
800
|
+
* is; the default resolve composes it, and a custom one does by
|
|
801
|
+
* declaring `uBloom` / `uBloomIntensity` (start from BLOOM_RESOLVE in
|
|
802
|
+
* `@solidrt/3d/glsl`). Fans out to every view that has not set a
|
|
803
|
+
* bloom of its own, like setFog.
|
|
804
|
+
*/
|
|
805
|
+
setBloom(bloom: BloomOptions | null): void
|
|
806
|
+
/** The LOD quality knob (SceneOptions.lodBias): a multiplier on every
|
|
807
|
+
* measured projected size, for the scene, its views and its shadow
|
|
808
|
+
* tiles. Below 1 switches to far levels sooner. */
|
|
809
|
+
setLodBias(bias: number): void
|
|
701
810
|
/**
|
|
702
811
|
* Project a world point to scene pixels: origin top-left, y down - the
|
|
703
812
|
* output texture's own coordinate space, ready for overlay layout (HUD
|
|
@@ -865,9 +974,9 @@ export type Scene = {
|
|
|
865
974
|
* Bake the background into an environment: a reflection probe at the
|
|
866
975
|
* origin that sees no mesh (layer mask 0), so its six `size` faces
|
|
867
976
|
* (default 128) hold the background alone - the GLSL sky or the skybox,
|
|
868
|
-
* exactly as the scene draws it,
|
|
869
|
-
*
|
|
870
|
-
*
|
|
977
|
+
* exactly as the scene draws it, LINEAR like every buffer (a sky
|
|
978
|
+
* fragment writes light, never display pixels) - then GGX-prefiltered
|
|
979
|
+
* into the chain
|
|
871
980
|
* `standard` samples by roughness: Godot's sky-to-radiance bake, a
|
|
872
981
|
* procedural sky lighting the scene, or a hi-res LDR skybox reduced to
|
|
873
982
|
* an environment. A snapshot: bake again when the sky changed. Returns
|
|
@@ -889,23 +998,96 @@ export type Scene = {
|
|
|
889
998
|
// missing channel would have no home) - an error, like the rest of the
|
|
890
999
|
// strict entry path. Extra channels are fine.
|
|
891
1000
|
function checkLayout(material: Material, geometry: Geometry, what: string): void {
|
|
892
|
-
let missing = missingAttributes(material, geometry
|
|
1001
|
+
let missing = missingAttributes(material, geometry)
|
|
893
1002
|
if (missing.length > 0) {
|
|
894
1003
|
throw new Error(
|
|
895
|
-
what + " reads attributes the geometry layout (" +
|
|
1004
|
+
what + " reads attributes the geometry layout (" + geometryKey(geometry) + ") lacks: " +
|
|
896
1005
|
missing.map(a => a.name + " " + a.format).join(", ") +
|
|
897
1006
|
" - add the channel with withAttribute()/withColors(), or use a material that does not read it",
|
|
898
1007
|
)
|
|
899
1008
|
}
|
|
900
1009
|
}
|
|
901
1010
|
|
|
1011
|
+
// The index range a mesh's entries draw: its own (setDrawRange) or the
|
|
1012
|
+
// whole index list.
|
|
1013
|
+
function meshRange(mesh: Mesh): { firstIndex: number; indexCount: number } {
|
|
1014
|
+
return mesh._range === null ? { firstIndex: 0, indexCount: mesh.geometry.indices.length } : { firstIndex: mesh._range.first, indexCount: mesh._range.count }
|
|
1015
|
+
}
|
|
1016
|
+
|
|
902
1017
|
// An entry's initial params. The uNormal seed keys off the material flag
|
|
903
1018
|
// because entry params validate strictly - and a material declaring
|
|
904
1019
|
// uNormal without using it therefore throws right here, at add().
|
|
1020
|
+
// The solid uLodFade: a fresh entry draws whole until the core writes a
|
|
1021
|
+
// band position (a GL uniform reads zero until written, which would
|
|
1022
|
+
// discard everything).
|
|
1023
|
+
const LOD_SOLID = [1, 1]
|
|
1024
|
+
|
|
905
1025
|
function entrySeed(material: Material, params: ShaderParams | null): ShaderParams {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1026
|
+
let seed: ShaderParams = { uModel: IDENTITY }
|
|
1027
|
+
if (material.normalMatrix) seed.uNormal = IDENTITY
|
|
1028
|
+
if (material.lodFade) seed.uLodFade = LOD_SOLID
|
|
1029
|
+
return { ...seed, ...material.params, ...params }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// A morphing entry's bindings: the geometry's packed targets (uMorphs and
|
|
1033
|
+
// its width) and the weights texture (uMorphWeights): the mesh's owner's
|
|
1034
|
+
// one row, or the population's row-per-slot texture on an instanced mesh
|
|
1035
|
+
// (the shader reads row uMorphRow + gl_InstanceID). Only for a material
|
|
1036
|
+
// whose program declares them (Material.morphed - the mesh's own, a
|
|
1037
|
+
// morphed shadow stand-in); any other validates its bindings strictly.
|
|
1038
|
+
// A plain owner's register is bound here if it was not yet (a mesh owner
|
|
1039
|
+
// nothing wrote before its first morphing entry).
|
|
1040
|
+
type MorphEntry = { textures: TextureBindings; params: ShaderParams }
|
|
1041
|
+
function morphEntry(material: Material, mesh: Mesh): MorphEntry | null {
|
|
1042
|
+
if (material.morphed !== true) return null
|
|
1043
|
+
let packed = mesh._buffers?.morphs
|
|
1044
|
+
if (packed === null || packed === undefined) return null
|
|
1045
|
+
let weights = morphWeightsTexture(mesh)
|
|
1046
|
+
if (weights === null) return null
|
|
1047
|
+
return {
|
|
1048
|
+
textures: { uMorphs: packed.texture, uMorphWeights: weights },
|
|
1049
|
+
params: { uMorphWidth: packed.width, uMorphRow: 0 },
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// The weights texture a mesh's morphing entries bind, activating a plain
|
|
1054
|
+
// owner's register on the way; null when nothing owns weights.
|
|
1055
|
+
function morphWeightsTexture(mesh: Mesh): TextureId | null {
|
|
1056
|
+
let inst = mesh._instances
|
|
1057
|
+
if (inst !== null) return inst.morph?.texture ?? null
|
|
1058
|
+
return mesh._morphOwner === null ? null : activateMorph(mesh._morphOwner).texture
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// An entry's sampler bindings: the material's, the mesh's own (a skin's
|
|
1062
|
+
// uBones) when the material is the mesh's or a skinned stand-in (a
|
|
1063
|
+
// skinned shadow variant declares uBones and needs the mesh's palette;
|
|
1064
|
+
// other override programs may not declare the names, and per-entry
|
|
1065
|
+
// bindings validate strictly), and the morph bindings when it morphs.
|
|
1066
|
+
// A morphing material over geometry without targets has nothing to walk
|
|
1067
|
+
// (and its uMorphs binding nothing to bind): rejected at add(), like a
|
|
1068
|
+
// layout mismatch. A record mesh has no instance nodes to own weights,
|
|
1069
|
+
// so its records cannot morph; an instanced mesh morphs per instance.
|
|
1070
|
+
function checkMorph(material: Material, mesh: Mesh, site: string): void {
|
|
1071
|
+
if (material.morphed !== true) return
|
|
1072
|
+
if (mesh.geometry.morphs === undefined) throw new Error(site + " morphs (morph: true) but the geometry carries no morph targets")
|
|
1073
|
+
let inst = mesh._instances
|
|
1074
|
+
if (inst !== null && inst.nodes === null) throw new Error(site + " morphs, but a record mesh has no instance nodes to own weights (createInstancedMesh morphs per instance)")
|
|
1075
|
+
if (inst === null && mesh._morphOwner === null) throw new Error(site + " morphs but the mesh has no morph owner")
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function entryTextures(material: Material, mesh: Mesh, morph: MorphEntry | null): TextureBindings | undefined {
|
|
1079
|
+
let textures = material.textures
|
|
1080
|
+
if ((material === mesh.material || material.skinned === true) && mesh._textures !== null) textures = { ...textures, ...mesh._textures }
|
|
1081
|
+
if (morph !== null) textures = { ...textures, ...morph.textures }
|
|
1082
|
+
return textures
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// A buffer's clear color: the sRGB option decoded to premultiplied linear
|
|
1086
|
+
// light, what the buffer holds and the resolve tone maps.
|
|
1087
|
+
function bufferClear(color: [number, number, number, number] | undefined): [number, number, number, number] | undefined {
|
|
1088
|
+
if (color === undefined) return undefined
|
|
1089
|
+
let c = premultipliedColor(color)
|
|
1090
|
+
return [c[0]!, c[1]!, c[2]!, c[3]!]
|
|
909
1091
|
}
|
|
910
1092
|
|
|
911
1093
|
/**
|
|
@@ -922,15 +1104,19 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
922
1104
|
// The scene target's layer mask; shadow views follow it (a mesh the
|
|
923
1105
|
// scene cannot see must not darken it).
|
|
924
1106
|
let sceneMask = checkMask(opts?.layers ?? 1, "createScene")
|
|
1107
|
+
let label = opts?.label ?? "scene"
|
|
1108
|
+
// `texture` is the scene's BUFFER throughout this file: the draw target
|
|
1109
|
+
// every entry, sink, sort and param write names. The handle's `texture`
|
|
1110
|
+
// is its resolve (below), the displayable id.
|
|
925
1111
|
let texture = createDrawTarget(width, height, null, {
|
|
926
1112
|
depth: depthMode,
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
wrap: opts?.wrap,
|
|
1113
|
+
format: bufferFormat(),
|
|
1114
|
+
clearColor: bufferClear(opts?.clearColor),
|
|
930
1115
|
samples: opts?.samples,
|
|
931
|
-
label
|
|
1116
|
+
label,
|
|
932
1117
|
autoFree: false,
|
|
933
1118
|
})
|
|
1119
|
+
let resolve = makeResolve(texture, width, height, opts?.resolve, { filter: opts?.filter, wrap: opts?.wrap }, label)
|
|
934
1120
|
let disposed = false
|
|
935
1121
|
let scheduled = false
|
|
936
1122
|
|
|
@@ -946,8 +1132,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
946
1132
|
// One filter object per scene, refilled per query (the bindings read it
|
|
947
1133
|
// synchronously), so a query allocates nothing but its hits.
|
|
948
1134
|
let filter: QueryFilter = {}
|
|
949
|
-
|
|
1135
|
+
// `target` names the draw target whose LOD levels the query sees: the
|
|
1136
|
+
// scene's for its own queries, a view's for view.pick.
|
|
1137
|
+
let queryFilter = (qopts: QueryOptions | undefined, site: string, target: TextureId = texture): QueryFilter => {
|
|
950
1138
|
filter.root = root._node!
|
|
1139
|
+
filter.target = target
|
|
951
1140
|
filter.layers = qopts?.layers !== undefined ? checkMask(qopts.layers, site) : sceneMask
|
|
952
1141
|
if (qopts?.meshes === undefined) {
|
|
953
1142
|
filter.nodes = undefined
|
|
@@ -981,12 +1170,14 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
981
1170
|
// _moved flag): what the light and transparent-order bookkeeping
|
|
982
1171
|
// reacts to, since which meshes moved is the core's knowledge now.
|
|
983
1172
|
let moved: SceneNode[] = []
|
|
984
|
-
//
|
|
985
|
-
let
|
|
1173
|
+
// Populated meshes with record writes to publish at the next sync.
|
|
1174
|
+
let recordsDirty = new Set<Mesh>()
|
|
986
1175
|
// Instanced meshes whose cull group - their live instance nodes - changed
|
|
987
1176
|
// since the last sync: one setCullGroup per mesh however many entered
|
|
988
1177
|
// or left.
|
|
989
1178
|
let groupDirty = new Set<InstancedMesh>()
|
|
1179
|
+
// bindMatrixRecord's buffer list, refilled per instance.
|
|
1180
|
+
let instanceBuffers: BufferId[] = []
|
|
990
1181
|
let instanceGroup = (mesh: InstancedMesh): NodeId[] => {
|
|
991
1182
|
let members: NodeId[] = []
|
|
992
1183
|
for (let n of mesh._instances.nodes.slots) if (n !== null && n._node !== null) members.push(n._node)
|
|
@@ -1005,6 +1196,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1005
1196
|
// light params at the end of the sync - one write, however many meshes.
|
|
1006
1197
|
let lights: Light[] = []
|
|
1007
1198
|
let lightsDirty = false
|
|
1199
|
+
// The list changed shape (attach, detach) since sync last checked the
|
|
1200
|
+
// light cap.
|
|
1201
|
+
let lightSetDirty = false
|
|
1008
1202
|
// uLightDir and uLightPos are CORE-DRIVEN: each light's slots are
|
|
1009
1203
|
// shared-slot sinks following the node's world transform - the
|
|
1010
1204
|
// direction slot (bindDirectionSlot) with -direction as the local
|
|
@@ -1039,6 +1233,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1039
1233
|
ground = [g[0]! * k, g[1]! * k, g[2]! * k]
|
|
1040
1234
|
continue
|
|
1041
1235
|
}
|
|
1236
|
+
// A light past the cap is not lit (sync reports the set once): its
|
|
1237
|
+
// slots would overrun the MAX_LIGHTS arrays.
|
|
1238
|
+
if (count >= MAX_LIGHTS) continue
|
|
1042
1239
|
if (light.type !== "point") {
|
|
1043
1240
|
vecScratch[0] = -light.direction[0]
|
|
1044
1241
|
vecScratch[1] = -light.direction[1]
|
|
@@ -1147,7 +1344,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1147
1344
|
// light rewrite seeds it on new targets); null binds the placeholder,
|
|
1148
1345
|
// the scene's own 1x1 black cube.
|
|
1149
1346
|
let environment: TextureId | null = null
|
|
1150
|
-
let envPlaceholder = createEnvironmentPlaceholder(
|
|
1347
|
+
let envPlaceholder = createEnvironmentPlaceholder(label + "-env-none")
|
|
1151
1348
|
let sortEntries = () => {
|
|
1152
1349
|
orderDirty = false
|
|
1153
1350
|
let order = orderEntries(meshes, camera.view, background?.entries.get(texture))
|
|
@@ -1228,10 +1425,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1228
1425
|
// scene (view-space keys from the view's own camera); an overridden
|
|
1229
1426
|
// view is not sorted at all.
|
|
1230
1427
|
type ViewRecord = {
|
|
1428
|
+
/** The view's BUFFER (a 2D draw target, a tile, or a cube). */
|
|
1231
1429
|
texture: TextureId
|
|
1430
|
+
/** The view's resolve, the displayable output; null for a shadow
|
|
1431
|
+
* view, a probe and a tile (the atlas is the app's to resolve). */
|
|
1432
|
+
resolve: ResolveRecord | null
|
|
1433
|
+
/** Whether the view set its own bloom (the `bloom` option or
|
|
1434
|
+
* view.setBloom): the scene's setBloom then skips it. */
|
|
1435
|
+
ownBloom: boolean
|
|
1232
1436
|
width: number
|
|
1233
1437
|
height: number
|
|
1438
|
+
/** Debug label: the option, else the scene's plus -view/-probe. */
|
|
1439
|
+
label: string
|
|
1234
1440
|
override: Material | null
|
|
1441
|
+
/** The instanced meshes an override view leaves out: the override
|
|
1442
|
+
* declares no instanceBuffers, so it cannot place their records
|
|
1443
|
+
* (ViewOptions.overrideMaterial). Kept so sync() reports them once
|
|
1444
|
+
* per view, over the settled set. */
|
|
1445
|
+
skipped: Set<Mesh>
|
|
1446
|
+
skippedReported: boolean
|
|
1235
1447
|
/** Non-null marks a SHADOW view and names its caster set (m =>
|
|
1236
1448
|
* m.castShadow). Re-evaluated per mesh by _setCast; also picks the
|
|
1237
1449
|
* caster's own shadow material variant over the depth override. */
|
|
@@ -1247,6 +1459,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1247
1459
|
entries: Map<Mesh, DrawId>
|
|
1248
1460
|
orderDirty: boolean
|
|
1249
1461
|
lastOrder: DrawId[]
|
|
1462
|
+
/** A shadow tile measures LOD by the SCENE camera: set once at
|
|
1463
|
+
* creation, then on every scene camera move. */
|
|
1464
|
+
lodViewSet: boolean
|
|
1250
1465
|
/** A reflection probe's target is a cube draw target of this face
|
|
1251
1466
|
* edge, rendered face by face through probe.update(); null for a
|
|
1252
1467
|
* 2D view. */
|
|
@@ -1264,6 +1479,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1264
1479
|
let views: ViewRecord[] = []
|
|
1265
1480
|
// Every name scene.setParams has merged so far, replayed on a new view.
|
|
1266
1481
|
let sceneParams: ShaderParams = {}
|
|
1482
|
+
// The scene's bloom (setBloom), applied to every resolving view that
|
|
1483
|
+
// has not set its own.
|
|
1484
|
+
let sceneBloom: BloomOptions | null = null
|
|
1267
1485
|
// Every target a receiving material can draw into: the scene's and each
|
|
1268
1486
|
// view's but the shadow views (binding a target's own depth into it
|
|
1269
1487
|
// would be same-pass feedback).
|
|
@@ -1289,17 +1507,18 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1289
1507
|
// so added live it would draw at the seeded identity until then. The
|
|
1290
1508
|
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1291
1509
|
// uModel.
|
|
1292
|
-
|
|
1293
|
-
|
|
1510
|
+
let morph = morphEntry(mesh.material, mesh)
|
|
1511
|
+
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(mesh.material, morph === null ? mesh._params : { ...morph.params, ...mesh._params }), {
|
|
1512
|
+
buffers: [...bufs.buffers, ...(inst !== null ? instanceBinding(mesh.material, inst) : [])],
|
|
1294
1513
|
indexBuffer: bufs.index,
|
|
1295
1514
|
indexFormat: bufs.indexFormat,
|
|
1296
|
-
|
|
1297
|
-
|
|
1515
|
+
...meshRange(mesh),
|
|
1516
|
+
textures: entryTextures(mesh.material, mesh, morph),
|
|
1298
1517
|
instanceCount: 0,
|
|
1299
1518
|
})
|
|
1300
1519
|
// The core turns the entry on (with the world matrix) at the next
|
|
1301
1520
|
// flush, and off again whenever the node or an ancestor hides.
|
|
1302
|
-
spatial.bindDraw(mesh._node!, texture, mesh._entry, mesh.material.normalMatrix === true, inst !== null ? inst.count : 1)
|
|
1521
|
+
spatial.bindDraw(mesh._node!, texture, mesh._entry, mesh.material.normalMatrix === true, inst !== null ? inst.count : 1, mesh.material.lodFade === true)
|
|
1303
1522
|
orderDirty = true
|
|
1304
1523
|
}
|
|
1305
1524
|
let detachScene = (mesh: Mesh) => {
|
|
@@ -1319,6 +1538,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1319
1538
|
if (v.entries.has(mesh)) return
|
|
1320
1539
|
let inst = mesh._instances
|
|
1321
1540
|
if (v.shadowFilter !== null && !v.shadowFilter(mesh)) return
|
|
1541
|
+
// Lines and points cast nothing: a one-pixel wireframe in a shadow
|
|
1542
|
+
// map is speckle on the floor, never a shadow anyone wanted.
|
|
1543
|
+
if (v.shadowFilter !== null && geometryTopology(mesh.geometry) !== "triangles") return
|
|
1322
1544
|
if ((mesh.layers & v.mask) === 0) return
|
|
1323
1545
|
let material = viewMaterial(v, mesh)
|
|
1324
1546
|
// An override pipeline cannot know a populated mesh's record layout,
|
|
@@ -1327,27 +1549,32 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1327
1549
|
// instanced depth pass), whose attributes must fit the records like
|
|
1328
1550
|
// the main material's did at add().
|
|
1329
1551
|
if (v.override !== null && inst !== null) {
|
|
1330
|
-
if (material.
|
|
1552
|
+
if (material.instanceBuffers === undefined) {
|
|
1553
|
+
// A caster without an instanced shadow variant casts none by
|
|
1554
|
+
// that rule; an app's override view dropping a mesh is invisible
|
|
1555
|
+
// in its output, so it is recorded for sync() to report.
|
|
1556
|
+
if (v.shadowFilter === null) v.skipped.add(mesh)
|
|
1557
|
+
return
|
|
1558
|
+
}
|
|
1331
1559
|
checkInstancePairing(material, inst, "Shadow material")
|
|
1332
1560
|
}
|
|
1333
1561
|
let bufs = mesh._buffers!
|
|
1334
|
-
let
|
|
1335
|
-
|
|
1562
|
+
let morph = morphEntry(material, mesh)
|
|
1563
|
+
let params = v.override !== null ? (morph === null ? null : morph.params) : morph === null ? mesh._params : { ...morph.params, ...mesh._params }
|
|
1564
|
+
let entry = addDraw(v.texture, material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(material, params), {
|
|
1565
|
+
buffers: [...bufs.buffers, ...(inst !== null ? instanceBinding(material, inst) : [])],
|
|
1336
1566
|
indexBuffer: bufs.index,
|
|
1337
1567
|
indexFormat: bufs.indexFormat,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
// needs the mesh's palette); other override programs may not
|
|
1341
|
-
// declare the names, and per-entry bindings validate strictly.
|
|
1342
|
-
textures: (material === mesh.material || material.skinned === true) && mesh._textures !== null ? { ...material.textures, ...mesh._textures } : material.textures,
|
|
1343
|
-
...(inst !== null ? instanceBinding(material, inst) : {}),
|
|
1568
|
+
...meshRange(mesh),
|
|
1569
|
+
textures: entryTextures(material, mesh, morph),
|
|
1344
1570
|
instanceCount: 0,
|
|
1345
1571
|
})
|
|
1346
|
-
spatial.bindDraw(mesh._node!, v.texture, entry, material.normalMatrix === true, inst !== null ? inst.count : 1)
|
|
1572
|
+
spatial.bindDraw(mesh._node!, v.texture, entry, material.normalMatrix === true, inst !== null ? inst.count : 1, material.lodFade === true)
|
|
1347
1573
|
v.entries.set(mesh, entry)
|
|
1348
1574
|
v.orderDirty = true
|
|
1349
1575
|
}
|
|
1350
1576
|
let detachView = (v: ViewRecord, mesh: Mesh) => {
|
|
1577
|
+
v.skipped.delete(mesh)
|
|
1351
1578
|
let entry = v.entries.get(mesh)
|
|
1352
1579
|
if (entry === undefined) return
|
|
1353
1580
|
v.entries.delete(mesh)
|
|
@@ -1371,8 +1598,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1371
1598
|
v.entries.clear()
|
|
1372
1599
|
for (let light of lights) if (light.type === "directional" && light._node !== null) spatial.unbindSlot(light._node, v.texture)
|
|
1373
1600
|
spatial.setFrustum(v.texture, null)
|
|
1601
|
+
spatial.setLodView(v.texture, null)
|
|
1374
1602
|
// Drain the zeroed direction slots while the target still exists.
|
|
1375
1603
|
spatial.flush()
|
|
1604
|
+
if (v.resolve !== null) disposeResolve(v.resolve)
|
|
1376
1605
|
destroyTexture(v.texture)
|
|
1377
1606
|
let i = views.indexOf(v)
|
|
1378
1607
|
if (i >= 0) views.splice(i, 1)
|
|
@@ -1415,35 +1644,47 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1415
1644
|
for (let mesh of meshes) if (mesh._instances === null) checkLayout(override, mesh.geometry, "View override material")
|
|
1416
1645
|
}
|
|
1417
1646
|
let tiled = vopts.into !== undefined
|
|
1647
|
+
if (tiled && vopts.resolve !== undefined) throw new Error("createView: a tiled view (into) has no resolve of its own - resolve the atlas")
|
|
1648
|
+
if (tiled && vopts.bloom !== undefined) throw new Error("createView: a tiled view (into) has no resolve to bloom on - resolve the atlas")
|
|
1649
|
+
let viewLabel = vopts.label ?? label + (cube !== null ? "-probe" : "-view")
|
|
1650
|
+
let buffer =
|
|
1651
|
+
cube !== null
|
|
1652
|
+
? createCubeDrawTarget(cube, null, {
|
|
1653
|
+
depth: true,
|
|
1654
|
+
// The prefilter reads the faces at the lod of each sample's
|
|
1655
|
+
// solid angle: a generated chain, refreshed per face render.
|
|
1656
|
+
mipmap,
|
|
1657
|
+
format: bufferFormat(),
|
|
1658
|
+
clearColor: bufferClear(vopts.clearColor),
|
|
1659
|
+
label: viewLabel,
|
|
1660
|
+
autoFree: false,
|
|
1661
|
+
})
|
|
1662
|
+
: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1663
|
+
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1664
|
+
format: tiled ? undefined : bufferFormat(),
|
|
1665
|
+
clearColor: bufferClear(vopts.clearColor),
|
|
1666
|
+
samples: vopts.samples,
|
|
1667
|
+
label: viewLabel,
|
|
1668
|
+
autoFree: false,
|
|
1669
|
+
into: vopts.into,
|
|
1670
|
+
x: vopts.x,
|
|
1671
|
+
y: vopts.y,
|
|
1672
|
+
})
|
|
1418
1673
|
let v: ViewRecord = {
|
|
1419
|
-
texture:
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
format: probeFormat(),
|
|
1428
|
-
clearColor: vopts.clearColor,
|
|
1429
|
-
label: vopts.label ?? (opts?.label ?? "scene") + "-probe",
|
|
1430
|
-
autoFree: false,
|
|
1431
|
-
})
|
|
1432
|
-
: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1433
|
-
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1434
|
-
clearColor: vopts.clearColor,
|
|
1435
|
-
filter: vopts.filter,
|
|
1436
|
-
wrap: vopts.wrap,
|
|
1437
|
-
samples: vopts.samples,
|
|
1438
|
-
label: vopts.label ?? (opts?.label ?? "scene") + "-view",
|
|
1439
|
-
autoFree: false,
|
|
1440
|
-
into: vopts.into,
|
|
1441
|
-
x: vopts.x,
|
|
1442
|
-
y: vopts.y,
|
|
1443
|
-
}),
|
|
1674
|
+
texture: buffer,
|
|
1675
|
+
// A shadow view is depth only, a probe's faces are read as a cube,
|
|
1676
|
+
// a tile is the atlas's: none of them displays, so none resolves.
|
|
1677
|
+
resolve:
|
|
1678
|
+
cube === null && shadowFilter === null && !tiled
|
|
1679
|
+
? makeResolve(buffer, vopts.width, vopts.height, vopts.resolve, { filter: vopts.filter, wrap: vopts.wrap }, viewLabel)
|
|
1680
|
+
: null,
|
|
1681
|
+
ownBloom: vopts.bloom !== undefined,
|
|
1444
1682
|
width: vopts.width,
|
|
1445
1683
|
height: vopts.height,
|
|
1684
|
+
label: viewLabel,
|
|
1446
1685
|
override,
|
|
1686
|
+
skipped: new Set(),
|
|
1687
|
+
skippedReported: false,
|
|
1447
1688
|
shadowFilter,
|
|
1448
1689
|
mask: shadowFilter !== null ? sceneMask : checkMask(vopts.layers ?? 1, "createView"),
|
|
1449
1690
|
ownNames: new Set(),
|
|
@@ -1451,6 +1692,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1451
1692
|
entries: new Map(),
|
|
1452
1693
|
orderDirty: true,
|
|
1453
1694
|
lastOrder: [],
|
|
1695
|
+
lodViewSet: false,
|
|
1454
1696
|
cube,
|
|
1455
1697
|
probeCube: null,
|
|
1456
1698
|
sky,
|
|
@@ -1469,6 +1711,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1469
1711
|
if (ownFog !== null) for (let k of Object.keys(ownFog)) v.ownNames.add(k)
|
|
1470
1712
|
setTargetParams(v.texture, ownFog === null ? sceneParams : withoutNames(sceneParams, v.ownNames))
|
|
1471
1713
|
if (ownFog !== null) setTargetParams(v.texture, ownFog)
|
|
1714
|
+
if (v.resolve !== null) {
|
|
1715
|
+
setTargetParams(v.resolve.target, sceneParams)
|
|
1716
|
+
let bloom = vopts.bloom !== undefined ? vopts.bloom : sceneBloom
|
|
1717
|
+
if (bloom !== null) setResolveBloom(v.resolve, v.texture, vopts.width, vopts.height, bloom)
|
|
1718
|
+
}
|
|
1472
1719
|
for (let mesh of meshes) attachView(v, mesh)
|
|
1473
1720
|
hooks._schedule()
|
|
1474
1721
|
return v
|
|
@@ -1481,7 +1728,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1481
1728
|
lights,
|
|
1482
1729
|
camera,
|
|
1483
1730
|
targetSize: () => ({ width, height }),
|
|
1484
|
-
label
|
|
1731
|
+
label,
|
|
1485
1732
|
makeView,
|
|
1486
1733
|
disposeView,
|
|
1487
1734
|
markLightsDirty: () => {
|
|
@@ -1500,36 +1747,101 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1500
1747
|
frustumScratch.set(cam.viewProj)
|
|
1501
1748
|
spatial.setFrustum(target, frustumScratch)
|
|
1502
1749
|
}
|
|
1750
|
+
// A camera write is also the target's LOD view - eye, vertical focal
|
|
1751
|
+
// factor (the magnitude of proj[5] under either projection: the
|
|
1752
|
+
// projections bake in a y-down clip flip, so the element is negative),
|
|
1753
|
+
// the ortho flag and the scene's bias - what the core measures
|
|
1754
|
+
// projected size with. The scene and each view measure by their own
|
|
1755
|
+
// camera; a shadow tile by the SCENE camera, so a caster draws the
|
|
1756
|
+
// level the camera sees and its shadow matches.
|
|
1757
|
+
let lodBias = opts?.lodBias ?? 1
|
|
1758
|
+
if (!(lodBias > 0 && Number.isFinite(lodBias))) throw new Error("createScene: lodBias must be a positive number, got " + lodBias)
|
|
1759
|
+
let lodBiasDirty = false
|
|
1760
|
+
let lodScratch = new Float32Array(6)
|
|
1761
|
+
let setLodView = (target: TextureId, cam: Camera) => {
|
|
1762
|
+
lodScratch[0] = cam.eye[0]
|
|
1763
|
+
lodScratch[1] = cam.eye[1]
|
|
1764
|
+
lodScratch[2] = cam.eye[2]
|
|
1765
|
+
lodScratch[3] = Math.abs(cam.proj[5]!)
|
|
1766
|
+
lodScratch[4] = cam.ortho !== null ? 1 : 0
|
|
1767
|
+
lodScratch[5] = lodBias
|
|
1768
|
+
spatial.setLodView(target, lodScratch)
|
|
1769
|
+
}
|
|
1503
1770
|
let sync = () => {
|
|
1504
1771
|
scheduled = false
|
|
1505
1772
|
if (disposed) return
|
|
1773
|
+
// An override view's skipped instanced meshes (attachView), reported
|
|
1774
|
+
// once per view over the settled set, as the budgets below are: the
|
|
1775
|
+
// meshes of one flush are all in by this microtask, so the count is
|
|
1776
|
+
// the whole set, not the first arrival.
|
|
1777
|
+
for (let v of views) {
|
|
1778
|
+
if (v.skippedReported || v.skipped.size === 0) continue
|
|
1779
|
+
v.skippedReported = true
|
|
1780
|
+
let n = v.skipped.size
|
|
1781
|
+
console.warn(
|
|
1782
|
+
`View '${v.label}' override material skips ${n} instanced ${n === 1 ? "mesh" : "meshes"}: it declares no instanceBuffers, so it cannot place their records, and they draw in no pass of this view; give it an instanced variant to include them`,
|
|
1783
|
+
)
|
|
1784
|
+
}
|
|
1785
|
+
// A per-scene budget - the light cap, the shadow slots - is tested
|
|
1786
|
+
// here over the settled set, never at attach: a declarative swap
|
|
1787
|
+
// (<Show>, <Switch>) attaches the incoming branch before the outgoing
|
|
1788
|
+
// one detaches, inside one Solid flush, and both are gone by this
|
|
1789
|
+
// microtask. A set past a budget is reported once, at the end, after
|
|
1790
|
+
// every write below, so the scene is never left half-written.
|
|
1791
|
+
let budgetError: Error | null = null
|
|
1792
|
+
if (lightSetDirty) {
|
|
1793
|
+
lightSetDirty = false
|
|
1794
|
+
let n = lights.filter(l => l.type !== "hemisphere").length
|
|
1795
|
+
if (n > MAX_LIGHTS) {
|
|
1796
|
+
budgetError = new Error(
|
|
1797
|
+
"A scene takes at most " + MAX_LIGHTS + " lights (directional, spot and point together): " + n + " attached, the lights past the cap are not lit",
|
|
1798
|
+
)
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
let shadowError = shadowSys.settle()
|
|
1802
|
+
if (budgetError === null) budgetError = shadowError
|
|
1506
1803
|
ensureCamera(camera, width, height)
|
|
1507
1804
|
let cameraMoved = camera.pending
|
|
1805
|
+
let biasChanged = lodBiasDirty
|
|
1806
|
+
lodBiasDirty = false
|
|
1508
1807
|
if (camera.pending) {
|
|
1509
1808
|
camera.pending = false
|
|
1510
1809
|
setTargetParams(texture, cameraParams(camera))
|
|
1511
1810
|
setFrustum(texture, camera)
|
|
1512
1811
|
if (transparentCount > 1) orderDirty = true
|
|
1513
1812
|
}
|
|
1813
|
+
if (cameraMoved || biasChanged) setLodView(texture, camera)
|
|
1514
1814
|
shadowSys.placeCameras(cameraMoved)
|
|
1515
1815
|
for (let v of views) {
|
|
1516
1816
|
ensureCamera(v.camera, v.width, v.height)
|
|
1517
|
-
|
|
1817
|
+
let viewMoved = v.camera.pending
|
|
1818
|
+
if (viewMoved) {
|
|
1518
1819
|
v.camera.pending = false
|
|
1519
1820
|
setTargetParams(v.texture, cameraParams(v.camera))
|
|
1520
1821
|
setFrustum(v.texture, v.camera)
|
|
1521
1822
|
if (transparentCount > 1) v.orderDirty = true
|
|
1522
1823
|
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
1523
1824
|
}
|
|
1825
|
+
// Probe faces measure nothing (six cameras, one target), like their
|
|
1826
|
+
// frustum.
|
|
1827
|
+
if (v.cube !== null) continue
|
|
1828
|
+
if (v.shadowFilter !== null) {
|
|
1829
|
+
if (cameraMoved || biasChanged || !v.lodViewSet) {
|
|
1830
|
+
v.lodViewSet = true
|
|
1831
|
+
setLodView(v.texture, camera)
|
|
1832
|
+
}
|
|
1833
|
+
} else if (viewMoved || biasChanged) {
|
|
1834
|
+
setLodView(v.texture, v.camera)
|
|
1835
|
+
}
|
|
1524
1836
|
}
|
|
1525
1837
|
// Light bookkeeping first, so a fresh direction-slot bind is seeded
|
|
1526
1838
|
// by the flush below in the same sync.
|
|
1527
1839
|
if (lightsDirty) writeLights()
|
|
1528
|
-
// The
|
|
1529
|
-
// coalesced buffer write per
|
|
1530
|
-
if (
|
|
1531
|
-
for (let m of
|
|
1532
|
-
|
|
1840
|
+
// The populated meshes' record writes since the last sync: one
|
|
1841
|
+
// coalesced buffer write per dirty stream.
|
|
1842
|
+
if (recordsDirty.size > 0) {
|
|
1843
|
+
for (let m of recordsDirty) publishRecords(m)
|
|
1844
|
+
recordsDirty.clear()
|
|
1533
1845
|
}
|
|
1534
1846
|
// An instanced mesh without explicit bounds is culled by the union of
|
|
1535
1847
|
// its instances' boxes: the core's cull group (a skinned part's joint
|
|
@@ -1560,6 +1872,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1560
1872
|
if (orderDirty || views.some(v => v.orderDirty)) refreshCenters()
|
|
1561
1873
|
if (orderDirty) sortEntries()
|
|
1562
1874
|
for (let v of views) if (v.orderDirty) sortView(v)
|
|
1875
|
+
if (budgetError !== null) {
|
|
1876
|
+
if (opts?.onError !== undefined) opts.onError(budgetError)
|
|
1877
|
+
else throw budgetError
|
|
1878
|
+
}
|
|
1563
1879
|
}
|
|
1564
1880
|
|
|
1565
1881
|
let hooks: SceneHooks = {
|
|
@@ -1570,23 +1886,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1570
1886
|
},
|
|
1571
1887
|
_attachLight(light) {
|
|
1572
1888
|
if (disposed) return
|
|
1573
|
-
if (light.type !== "hemisphere" && lights.filter(l => l.type !== "hemisphere").length >= MAX_LIGHTS) {
|
|
1574
|
-
throw new Error("A scene takes at most " + MAX_LIGHTS + " lights (directional, spot and point together)")
|
|
1575
|
-
}
|
|
1576
1889
|
lights.push(light)
|
|
1577
1890
|
lightsDirty = true
|
|
1578
|
-
if (light.type
|
|
1891
|
+
if (light.type === "hemisphere") return
|
|
1892
|
+
lightSetDirty = true
|
|
1893
|
+
if (light.castShadow) shadowSys.invalidate(light)
|
|
1579
1894
|
},
|
|
1580
1895
|
_detachLight(light) {
|
|
1581
1896
|
let i = lights.indexOf(light)
|
|
1582
1897
|
if (i >= 0) lights.splice(i, 1)
|
|
1583
1898
|
lightsDirty = true
|
|
1584
|
-
if (light.type !== "hemisphere")
|
|
1899
|
+
if (light.type !== "hemisphere") {
|
|
1900
|
+
lightSetDirty = true
|
|
1901
|
+
shadowSys.invalidate(light)
|
|
1902
|
+
}
|
|
1585
1903
|
hooks._schedule()
|
|
1586
1904
|
},
|
|
1587
1905
|
_shadowChanged(light) {
|
|
1588
1906
|
if (disposed) return
|
|
1589
|
-
shadowSys.
|
|
1907
|
+
shadowSys.invalidate(light)
|
|
1590
1908
|
},
|
|
1591
1909
|
_setCast(mesh) {
|
|
1592
1910
|
if (mesh._buffers === null || disposed) return
|
|
@@ -1619,19 +1937,23 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1619
1937
|
if (disposed) return
|
|
1620
1938
|
validateGeometry(mesh.geometry)
|
|
1621
1939
|
checkLayout(mesh.material, mesh.geometry, "Mesh material")
|
|
1940
|
+
checkMorph(mesh.material, mesh, "Mesh material")
|
|
1622
1941
|
// Every check before any mutation, so a rejected mesh is attached
|
|
1623
1942
|
// nowhere - the views' override materials included.
|
|
1624
1943
|
for (let v of views) {
|
|
1625
|
-
if (v.override !== null && mesh._instances === null)
|
|
1944
|
+
if (v.override !== null && mesh._instances === null) {
|
|
1945
|
+
checkLayout(v.override, mesh.geometry, "View override material")
|
|
1946
|
+
checkMorph(v.override, mesh, "View override material")
|
|
1947
|
+
}
|
|
1626
1948
|
}
|
|
1627
1949
|
// Instancing pairs the same way layout does: the pipeline's instance
|
|
1628
1950
|
// attributes describe the mesh's record buffers, so one without the
|
|
1629
1951
|
// other (or a record stride from a different attribute list) would
|
|
1630
1952
|
// bind garbage - errors here, at add().
|
|
1631
1953
|
let inst = mesh._instances
|
|
1632
|
-
if (mesh.material.
|
|
1954
|
+
if (mesh.material.instanceBuffers !== undefined && inst === null) {
|
|
1633
1955
|
throw new Error(
|
|
1634
|
-
"Material declares
|
|
1956
|
+
"Material declares instanceBuffers - create its meshes with createInstancedMesh or createRecordMesh, not createMesh",
|
|
1635
1957
|
)
|
|
1636
1958
|
}
|
|
1637
1959
|
if (inst !== null) checkInstancePairing(mesh.material, inst, "Mesh material")
|
|
@@ -1639,30 +1961,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1639
1961
|
mesh._buffers = bufs
|
|
1640
1962
|
attachScene(mesh)
|
|
1641
1963
|
for (let v of views) attachView(v, mesh)
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
if (inst !== null && inst.
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
//
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
spatial.setShape(mesh._node!, inst === null && !mesh._sprite ? bufs.shape : null)
|
|
1657
|
-
// A rebuilt entry (setGeometry) re-boxes the live instances to the
|
|
1964
|
+
// Record writes made while the mesh was out of a scene kept their
|
|
1965
|
+
// dirty ranges: publish them now.
|
|
1966
|
+
if (inst !== null && inst.streams.some(s => s.dirty !== null)) recordsDirty.add(mesh)
|
|
1967
|
+
// Picking: an ordinary mesh's node carries its geometry's shape,
|
|
1968
|
+
// which is its box in the core index (following every
|
|
1969
|
+
// updateVertices) and its triangle narrowphase. A populated mesh is
|
|
1970
|
+
// box-only by its explicit population bounds (its instances are the
|
|
1971
|
+
// leaves that pick, each with the shape; a record mesh's records are
|
|
1972
|
+
// opaque, so without explicit bounds it is not picked at all), as is
|
|
1973
|
+
// a sprite by the unit box (its triangles lie wherever the camera
|
|
1974
|
+
// is, not where the geometry says).
|
|
1975
|
+
if (inst === null && !mesh._sprite) spatial.setShape(mesh._node!, bufs.shape)
|
|
1976
|
+
else spatial.setBounds(mesh._node!, localBounds(mesh))
|
|
1977
|
+
// A rebuilt entry (setGeometry) re-shapes the live instances to the
|
|
1658
1978
|
// new geometry; their record bindings are untouched. On a first
|
|
1659
1979
|
// entry none has a core node yet - they enter after the mesh.
|
|
1660
1980
|
if (inst !== null && inst.nodes !== null) {
|
|
1661
1981
|
for (let n of inst.nodes.slots) {
|
|
1662
|
-
if (n !== null && n._node !== null)
|
|
1663
|
-
spatial.setBounds(n._node, geometryBounds(mesh.geometry))
|
|
1664
|
-
spatial.setShape(n._node, bufs.shape)
|
|
1665
|
-
}
|
|
1982
|
+
if (n !== null && n._node !== null) spatial.setShape(n._node, bufs.shape)
|
|
1666
1983
|
}
|
|
1667
1984
|
}
|
|
1668
1985
|
// Culling: the gate and margin only when off the defaults, and a
|
|
@@ -1687,7 +2004,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1687
2004
|
},
|
|
1688
2005
|
_detach(mesh) {
|
|
1689
2006
|
if (mesh._buffers !== null) {
|
|
1690
|
-
|
|
2007
|
+
recordsDirty.delete(mesh)
|
|
1691
2008
|
groupDirty.delete(mesh as InstancedMesh)
|
|
1692
2009
|
for (let v of views) detachView(v, mesh)
|
|
1693
2010
|
detachScene(mesh)
|
|
@@ -1712,21 +2029,33 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1712
2029
|
// geometry buffers and core node are live - a mesh masked out of
|
|
1713
2030
|
// the scene included, whose instances still pick.
|
|
1714
2031
|
if (mesh === null || mesh._buffers === null || mesh._node === null || instance._node === null) return
|
|
1715
|
-
spatial.setBounds(instance._node, geometryBounds(mesh.geometry))
|
|
1716
2032
|
spatial.setShape(instance._node, mesh._buffers.shape)
|
|
1717
2033
|
spatial.setLayers(instance._node, mesh.layers)
|
|
1718
2034
|
// The record is the instance's placement inside the mesh (the mesh
|
|
1719
2035
|
// node is the anchor), staged by the core at the next flush.
|
|
1720
|
-
|
|
2036
|
+
// One buffer per LOD level of an instanced LOD (the population is
|
|
2037
|
+
// the group and the anchor, its levels its children at identity).
|
|
2038
|
+
let levels = mesh._instances.levels
|
|
2039
|
+
instanceBuffers.length = 0
|
|
2040
|
+
if (levels === null) instanceBuffers.push(mesh._instances.matrix)
|
|
2041
|
+
else for (let l of levels) instanceBuffers.push(l._instances.matrix)
|
|
2042
|
+
spatial.bindMatrixRecord(instance._node, instanceBuffers, instance._slot, mesh._node)
|
|
2043
|
+
// A morphing population: the instance's register takes its slot's
|
|
2044
|
+
// row now (a recycled slot's row is overwritten, not inherited).
|
|
2045
|
+
if (mesh._instances.morph !== null) activateMorph(instance)
|
|
1721
2046
|
byNode.set(instance._node, instance)
|
|
1722
2047
|
if (mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
1723
2048
|
this._schedule()
|
|
1724
2049
|
},
|
|
2050
|
+
_exiting(mesh) {
|
|
2051
|
+
if (mesh._node !== null) byNode.delete(mesh._node)
|
|
2052
|
+
},
|
|
1725
2053
|
_detachInstance(instance) {
|
|
1726
|
-
// The core node is destroyed right after
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
2054
|
+
// The core node is destroyed right after (or animates out, picked
|
|
2055
|
+
// by nothing): its box, shape and record binding go with it, and
|
|
2056
|
+
// the slot hides at the next flush. The mesh's group loses the
|
|
2057
|
+
// member at the sync (a mesh leaving with its instances is skipped
|
|
2058
|
+
// there: its node is gone by then).
|
|
1730
2059
|
if (instance._node !== null) byNode.delete(instance._node)
|
|
1731
2060
|
let mesh = instance.mesh
|
|
1732
2061
|
if (mesh !== null && mesh._instances !== null && mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
@@ -1753,19 +2082,46 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1753
2082
|
// The entries keep their range (at most the old capacity, so the
|
|
1754
2083
|
// larger buffers always pass the swap's bounds check); the caller
|
|
1755
2084
|
// destroys the old buffers after this, which the entries held alive
|
|
1756
|
-
// until now. Each entry swaps the
|
|
2085
|
+
// until now. Each entry swaps the buffers its own material binds
|
|
2086
|
+
// (the full list: the geometry's buffer stays, the instance
|
|
2087
|
+
// buffers follow the population).
|
|
1757
2088
|
let inst = mesh._instances
|
|
1758
|
-
|
|
1759
|
-
|
|
2089
|
+
let bufs = mesh._buffers
|
|
2090
|
+
if (bufs !== null && !disposed && inst !== null) {
|
|
2091
|
+
if (mesh._entry !== null) setDrawBuffers(texture, mesh._entry, { buffers: [...bufs.buffers, ...instanceBinding(mesh.material, inst)] })
|
|
1760
2092
|
for (let v of views) {
|
|
1761
2093
|
let entry = v.entries.get(mesh)
|
|
1762
|
-
if (entry !== undefined) setDrawBuffers(v.texture, entry, instanceBinding(viewMaterial(v, mesh), inst))
|
|
2094
|
+
if (entry !== undefined) setDrawBuffers(v.texture, entry, { buffers: [...bufs.buffers, ...instanceBinding(viewMaterial(v, mesh), inst)] })
|
|
1763
2095
|
}
|
|
1764
2096
|
}
|
|
1765
2097
|
},
|
|
1766
|
-
|
|
2098
|
+
_setMorphTexture(mesh) {
|
|
2099
|
+
// The replaced population texture goes to every morphing entry of
|
|
2100
|
+
// the mesh (its own, a morphed shadow stand-in's); the caller
|
|
2101
|
+
// destroys the old texture after this.
|
|
2102
|
+
let weights = mesh._instances?.morph?.texture
|
|
2103
|
+
if (weights === undefined || mesh._buffers === null || disposed) return
|
|
2104
|
+
if (mesh._entry !== null && mesh.material.morphed === true) setDrawTextures(texture, mesh._entry, { uMorphWeights: weights })
|
|
2105
|
+
for (let v of views) {
|
|
2106
|
+
let entry = v.entries.get(mesh)
|
|
2107
|
+
if (entry !== undefined && viewMaterial(v, mesh).morphed === true) setDrawTextures(v.texture, entry, { uMorphWeights: weights })
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
_setRange(mesh) {
|
|
2111
|
+
// The index range travels to every entry of the mesh; the instance
|
|
2112
|
+
// count (the core's visibility switch) is not in the update, so it
|
|
2113
|
+
// keeps whatever the core last wrote.
|
|
2114
|
+
if (mesh._buffers === null || disposed) return
|
|
2115
|
+
let range = meshRange(mesh)
|
|
2116
|
+
if (mesh._entry !== null) setDrawRange(texture, mesh._entry, range)
|
|
2117
|
+
for (let v of views) {
|
|
2118
|
+
let entry = v.entries.get(mesh)
|
|
2119
|
+
if (entry !== undefined) setDrawRange(v.texture, entry, range)
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
_setRecords(mesh) {
|
|
1767
2123
|
if (disposed || mesh._buffers === null) return
|
|
1768
|
-
|
|
2124
|
+
recordsDirty.add(mesh)
|
|
1769
2125
|
this._schedule()
|
|
1770
2126
|
},
|
|
1771
2127
|
_reorder() {
|
|
@@ -1780,11 +2136,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1780
2136
|
}
|
|
1781
2137
|
this._schedule()
|
|
1782
2138
|
},
|
|
2139
|
+
_bindLod(node) {
|
|
2140
|
+
if (disposed || node._node === null) return
|
|
2141
|
+
let config = node._lod
|
|
2142
|
+
let levels: { node?: NodeId; size: number }[] = []
|
|
2143
|
+
if (config !== null) {
|
|
2144
|
+
for (let l of config.levels) {
|
|
2145
|
+
if (l.node === null) levels.push({ size: l.size })
|
|
2146
|
+
else if (l.node._node !== null) levels.push({ node: l.node._node, size: l.size })
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
spatial.setLod(node._node, levels, config?.fade ?? 0, texture)
|
|
2150
|
+
this._schedule()
|
|
2151
|
+
},
|
|
1783
2152
|
}
|
|
1784
2153
|
|
|
1785
2154
|
let root = makeNode("group")
|
|
1786
2155
|
root._scene = hooks
|
|
1787
2156
|
root._node = spatial.createNode(fillTransform(root), true)
|
|
2157
|
+
if (opts?.stagger !== undefined) setTransition(root, { stagger: opts.stagger })
|
|
1788
2158
|
// The first light rewrite seeds the (empty) light set and the shadow
|
|
1789
2159
|
// slots - placeholders, no casts - so receivers draw plain from the
|
|
1790
2160
|
// first frame.
|
|
@@ -1795,8 +2165,37 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1795
2165
|
// pointer walk scene-pointer.ts runs behind scene.handlers.
|
|
1796
2166
|
let listeners = new Set<ScenePointerListener>()
|
|
1797
2167
|
|
|
2168
|
+
// The raycast every pick shares: `target` names the draw target whose
|
|
2169
|
+
// LOD levels the query sees (the scene's own, or a view's).
|
|
2170
|
+
let raycastAs = (target: TextureId, origin: Vec3, direction: Vec3, rayOpts: QueryOptions | undefined): Hit[] => {
|
|
2171
|
+
// Flush pending writes: picking sees the tree as the app just wrote
|
|
2172
|
+
// it, the same immediacy contract as lookAt()/project(). (The queued
|
|
2173
|
+
// microtask still runs and finds nothing dirty - harmless.)
|
|
2174
|
+
if (scheduled) sync()
|
|
2175
|
+
if (disposed) return []
|
|
2176
|
+
let f = queryFilter(rayOpts, "raycast", target)
|
|
2177
|
+
let hits: Hit[] = []
|
|
2178
|
+
rayOriginScratch[0] = origin[0]
|
|
2179
|
+
rayOriginScratch[1] = origin[1]
|
|
2180
|
+
rayOriginScratch[2] = origin[2]
|
|
2181
|
+
rayDirScratch[0] = direction[0]
|
|
2182
|
+
rayDirScratch[1] = direction[1]
|
|
2183
|
+
rayDirScratch[2] = direction[2]
|
|
2184
|
+
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
2185
|
+
let leaf = leafOf(h.node)
|
|
2186
|
+
if (leaf === null) continue
|
|
2187
|
+
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
2188
|
+
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
2189
|
+
if (h.face !== undefined) hit.face = h.face
|
|
2190
|
+
if (h.uv !== undefined) hit.uv = h.uv
|
|
2191
|
+
hits.push(hit)
|
|
2192
|
+
}
|
|
2193
|
+
return hits
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1798
2196
|
let scene: Scene = {
|
|
1799
|
-
texture,
|
|
2197
|
+
texture: resolve.target,
|
|
2198
|
+
hdrTexture: texture,
|
|
1800
2199
|
depthTexture: depthMode === "texture" ? depthTexture(texture) : null,
|
|
1801
2200
|
root,
|
|
1802
2201
|
setCamera(update) {
|
|
@@ -1809,24 +2208,35 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1809
2208
|
width = w
|
|
1810
2209
|
height = h
|
|
1811
2210
|
setTargetSize(texture, w, h)
|
|
2211
|
+
resizeResolve(resolve, w, h)
|
|
1812
2212
|
camera.dirty = true
|
|
1813
2213
|
hooks._schedule()
|
|
1814
2214
|
},
|
|
2215
|
+
size: () => ({ width, height }),
|
|
1815
2216
|
setParams(params) {
|
|
1816
2217
|
if (disposed) return
|
|
1817
2218
|
Object.assign(sceneParams, params)
|
|
1818
2219
|
setTargetParams(texture, params)
|
|
2220
|
+
setTargetParams(resolve.target, params)
|
|
1819
2221
|
for (let v of views) {
|
|
1820
2222
|
// A view's own names (view.setParams, its fog) win over the
|
|
1821
2223
|
// scene-wide fan-out.
|
|
1822
2224
|
let fanned = v.ownNames.size === 0 ? params : withoutNames(params, v.ownNames)
|
|
1823
2225
|
if (fanned !== params && Object.keys(fanned).length === 0) continue
|
|
1824
2226
|
setTargetParams(v.texture, fanned)
|
|
2227
|
+
if (v.resolve !== null) setTargetParams(v.resolve.target, fanned)
|
|
1825
2228
|
}
|
|
1826
2229
|
},
|
|
1827
2230
|
setFog(fog) {
|
|
1828
2231
|
scene.setParams(fogParams(fog))
|
|
1829
2232
|
},
|
|
2233
|
+
setLodBias(bias) {
|
|
2234
|
+
if (!(bias > 0 && Number.isFinite(bias))) throw new Error("scene.setLodBias: bias must be a positive number, got " + bias)
|
|
2235
|
+
if (disposed || bias === lodBias) return
|
|
2236
|
+
lodBias = bias
|
|
2237
|
+
lodBiasDirty = true
|
|
2238
|
+
hooks._schedule()
|
|
2239
|
+
},
|
|
1830
2240
|
setLayers(mask) {
|
|
1831
2241
|
checkMask(mask, "scene.setLayers")
|
|
1832
2242
|
if (disposed || sceneMask === mask) return
|
|
@@ -1869,7 +2279,6 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1869
2279
|
background = null
|
|
1870
2280
|
}
|
|
1871
2281
|
if (source === null) return
|
|
1872
|
-
let label = opts?.label ?? "scene"
|
|
1873
2282
|
let built = sky === null ? backgroundPipeline(source as string, label + "-background") : backgroundPipeline(SKYBOX_FRAGMENT, label + "-skybox")
|
|
1874
2283
|
background = { pipeline: built.pipeline, program: built.program, sky, entries: new Map() }
|
|
1875
2284
|
// First in list order on every target that draws it: inserted before
|
|
@@ -1889,13 +2298,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1889
2298
|
},
|
|
1890
2299
|
setToneMapping(mode) {
|
|
1891
2300
|
let code = TONE_MAPPING_CODE[mode]
|
|
1892
|
-
if (code === undefined) throw new Error('scene.setToneMapping: expected "none" or "
|
|
2301
|
+
if (code === undefined) throw new Error('scene.setToneMapping: expected "none", "aces", "agx" or "neutral", got ' + mode)
|
|
1893
2302
|
scene.setParams({ uToneMapping: code })
|
|
1894
2303
|
},
|
|
1895
2304
|
setExposure(exposure) {
|
|
1896
2305
|
if (!Number.isFinite(exposure) || exposure < 0) throw new Error("scene.setExposure: expected a finite number >= 0, got " + exposure)
|
|
1897
2306
|
scene.setParams({ uExposure: exposure })
|
|
1898
2307
|
},
|
|
2308
|
+
setResolve(r) {
|
|
2309
|
+
if (disposed) return
|
|
2310
|
+
replaceResolve(resolve, texture, r)
|
|
2311
|
+
hooks._schedule()
|
|
2312
|
+
},
|
|
2313
|
+
setBloom(bloom) {
|
|
2314
|
+
if (disposed) return
|
|
2315
|
+
sceneBloom = bloom
|
|
2316
|
+
setResolveBloom(resolve, texture, width, height, bloom)
|
|
2317
|
+
for (let v of views) if (v.resolve !== null && !v.ownBloom) setResolveBloom(v.resolve, v.texture, v.width, v.height, bloom)
|
|
2318
|
+
hooks._schedule()
|
|
2319
|
+
},
|
|
1899
2320
|
project(point) {
|
|
1900
2321
|
ensureCamera(camera, width, height)
|
|
1901
2322
|
transformPoint(clip, camera.viewProj, point)
|
|
@@ -1921,7 +2342,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1921
2342
|
},
|
|
1922
2343
|
pick(x, y) {
|
|
1923
2344
|
pixelRay(x, y)
|
|
1924
|
-
return
|
|
2345
|
+
return raycastAs(texture, pickOrigin, pickDir, undefined)
|
|
1925
2346
|
},
|
|
1926
2347
|
unproject(x, y, w, out = [0, 0, 0]) {
|
|
1927
2348
|
pixelRay(x, y)
|
|
@@ -1938,29 +2359,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1938
2359
|
}
|
|
1939
2360
|
},
|
|
1940
2361
|
raycast(origin, direction, rayOpts) {
|
|
1941
|
-
|
|
1942
|
-
// it, the same immediacy contract as lookAt()/project(). (The queued
|
|
1943
|
-
// microtask still runs and finds nothing dirty - harmless.)
|
|
1944
|
-
if (scheduled) sync()
|
|
1945
|
-
if (disposed) return []
|
|
1946
|
-
let f = queryFilter(rayOpts, "raycast")
|
|
1947
|
-
let hits: Hit[] = []
|
|
1948
|
-
rayOriginScratch[0] = origin[0]
|
|
1949
|
-
rayOriginScratch[1] = origin[1]
|
|
1950
|
-
rayOriginScratch[2] = origin[2]
|
|
1951
|
-
rayDirScratch[0] = direction[0]
|
|
1952
|
-
rayDirScratch[1] = direction[1]
|
|
1953
|
-
rayDirScratch[2] = direction[2]
|
|
1954
|
-
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
1955
|
-
let leaf = leafOf(h.node)
|
|
1956
|
-
if (leaf === null) continue
|
|
1957
|
-
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
1958
|
-
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
1959
|
-
if (h.face !== undefined) hit.face = h.face
|
|
1960
|
-
if (h.uv !== undefined) hit.uv = h.uv
|
|
1961
|
-
hits.push(hit)
|
|
1962
|
-
}
|
|
1963
|
-
return hits
|
|
2362
|
+
return raycastAs(texture, origin, direction, rayOpts)
|
|
1964
2363
|
},
|
|
1965
2364
|
overlap(volume, qopts) {
|
|
1966
2365
|
if (scheduled) sync()
|
|
@@ -2027,7 +2426,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2027
2426
|
// dispatch with its own capture and hover bookkeeping.
|
|
2028
2427
|
let viewListeners = new Set<ScenePointerListener>()
|
|
2029
2428
|
let handle: ViewHandle = {
|
|
2030
|
-
texture: v.texture,
|
|
2429
|
+
texture: v.resolve !== null ? v.resolve.target : v.texture,
|
|
2430
|
+
hdrTexture: v.resolve !== null ? v.texture : null,
|
|
2031
2431
|
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
2032
2432
|
setCamera(update) {
|
|
2033
2433
|
updateCamera(v.camera, update)
|
|
@@ -2039,9 +2439,24 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2039
2439
|
v.width = w
|
|
2040
2440
|
v.height = h
|
|
2041
2441
|
setTargetSize(v.texture, w, h)
|
|
2442
|
+
if (v.resolve !== null) resizeResolve(v.resolve, w, h)
|
|
2042
2443
|
v.camera.dirty = true
|
|
2043
2444
|
hooks._schedule()
|
|
2044
2445
|
},
|
|
2446
|
+
size: () => ({ width: v.width, height: v.height }),
|
|
2447
|
+
setResolve(r) {
|
|
2448
|
+
if (v.resolve === null) throw new Error("view.setResolve: a tiled view has no resolve of its own - resolve the atlas")
|
|
2449
|
+
if (v.disposed) return
|
|
2450
|
+
replaceResolve(v.resolve, v.texture, r)
|
|
2451
|
+
hooks._schedule()
|
|
2452
|
+
},
|
|
2453
|
+
setBloom(bloom) {
|
|
2454
|
+
if (v.resolve === null) throw new Error("view.setBloom: a tiled view has no resolve to bloom on - resolve the atlas")
|
|
2455
|
+
if (v.disposed) return
|
|
2456
|
+
v.ownBloom = true
|
|
2457
|
+
setResolveBloom(v.resolve, v.texture, v.width, v.height, bloom)
|
|
2458
|
+
hooks._schedule()
|
|
2459
|
+
},
|
|
2045
2460
|
setRect(rect) {
|
|
2046
2461
|
if (v.disposed) return
|
|
2047
2462
|
setTargetRect(v.texture, rect)
|
|
@@ -2055,6 +2470,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2055
2470
|
if (v.disposed) return
|
|
2056
2471
|
for (let k of Object.keys(params)) v.ownNames.add(k)
|
|
2057
2472
|
setTargetParams(v.texture, params)
|
|
2473
|
+
if (v.resolve !== null) setTargetParams(v.resolve.target, params)
|
|
2058
2474
|
},
|
|
2059
2475
|
setLayers(mask) {
|
|
2060
2476
|
checkMask(mask, "view.setLayers")
|
|
@@ -2069,7 +2485,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2069
2485
|
pick(x, y) {
|
|
2070
2486
|
if (v.disposed) return []
|
|
2071
2487
|
pixelRayOf(v.camera, v.width, v.height, x, y)
|
|
2072
|
-
return
|
|
2488
|
+
return raycastAs(v.texture, pickOrigin, pickDir, undefined)
|
|
2073
2489
|
},
|
|
2074
2490
|
get handlers() {
|
|
2075
2491
|
return viewPointer.handlers
|
|
@@ -2107,7 +2523,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2107
2523
|
if (!Number.isInteger(size) || size < 1) throw new Error("scene.bakeBackground: size must be a positive integer, got " + size)
|
|
2108
2524
|
// A probe that sees no mesh (mask 0) draws the background alone: the
|
|
2109
2525
|
// same face cameras, linear output and prefilter as any probe.
|
|
2110
|
-
let probe = makeProbe({ position: [0, 0, 0], size, label:
|
|
2526
|
+
let probe = makeProbe({ position: [0, 0, 0], size, label: label + "-sky" }, 0)
|
|
2111
2527
|
probe.update()
|
|
2112
2528
|
return probe.finish()
|
|
2113
2529
|
},
|
|
@@ -2119,6 +2535,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2119
2535
|
// the scene (entries' geometry-buffer references and pick leaves
|
|
2120
2536
|
// dropped, core nodes freed), so a disposed scene leaves no
|
|
2121
2537
|
// bookkeeping behind and the JS tree survives as plain data.
|
|
2538
|
+
// Destroyed nodes still animating out are no longer in the tree:
|
|
2539
|
+
// they free on the spot first.
|
|
2540
|
+
freeLeaving((_, s) => s === hooks)
|
|
2122
2541
|
for (let c of root.children.slice()) leaveScene(c)
|
|
2123
2542
|
root._scene = null
|
|
2124
2543
|
if (root._node !== null) {
|
|
@@ -2126,9 +2545,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2126
2545
|
root._node = null
|
|
2127
2546
|
}
|
|
2128
2547
|
spatial.setFrustum(texture, null)
|
|
2548
|
+
spatial.setLodView(texture, null)
|
|
2129
2549
|
// Drain the zeroed direction slots the teardown queued while the
|
|
2130
2550
|
// targets still exist; afterwards their groups are gone.
|
|
2131
2551
|
spatial.flush()
|
|
2552
|
+
disposeResolve(resolve)
|
|
2132
2553
|
destroyTexture(texture)
|
|
2133
2554
|
for (let v of views.slice()) disposeView(v)
|
|
2134
2555
|
shadowSys.dispose()
|
|
@@ -2167,12 +2588,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2167
2588
|
)
|
|
2168
2589
|
v.camera.mirror = true
|
|
2169
2590
|
updateCamera(v.camera, { fov: PROBE_FOV, near: popts.near, far: popts.far })
|
|
2170
|
-
|
|
2171
|
-
// stage: no sRGB encode, no tone mapping, unit exposure - the
|
|
2172
|
-
// probe's own names, so the scene's fan-out leaves them alone.
|
|
2173
|
-
for (let k of Object.keys(LINEAR_OUTPUT)) v.ownNames.add(k)
|
|
2174
|
-
setTargetParams(v.texture, LINEAR_OUTPUT)
|
|
2175
|
-
let chain = prefilter ? createPrefilter(size, v.texture, probeFormat(), (popts.label ?? (opts?.label ?? "scene") + "-probe") + "-chain") : null
|
|
2591
|
+
let chain = prefilter ? createPrefilter(size, v.texture, bufferFormat(), (popts.label ?? label + "-probe") + "-chain") : null
|
|
2176
2592
|
v.probeCube = chain?.cube ?? v.texture
|
|
2177
2593
|
let position: Vec3 = [popts.position[0], popts.position[1], popts.position[2]]
|
|
2178
2594
|
return {
|
|
@@ -2193,6 +2609,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2193
2609
|
chain?.run()
|
|
2194
2610
|
},
|
|
2195
2611
|
dispose() {
|
|
2612
|
+
if (v.disposed) return
|
|
2613
|
+
// The scene may sample this very cube - `environment={{ cube }}`,
|
|
2614
|
+
// a skybox `background` - so those drop first: a subtree that
|
|
2615
|
+
// owned the environment leaves the scene unlit by it, never
|
|
2616
|
+
// sampling a destroyed texture until something re-bakes.
|
|
2617
|
+
if (environment === v.probeCube) scene.setEnvironment(null)
|
|
2618
|
+
if (background !== null && background.sky !== null && background.sky.cube === v.probeCube) scene.setBackground(null)
|
|
2196
2619
|
chain?.dispose()
|
|
2197
2620
|
disposeView(v)
|
|
2198
2621
|
},
|
|
@@ -2212,12 +2635,12 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2212
2635
|
// Likewise the environment set starts at "none" (uEnvOn 0), so a
|
|
2213
2636
|
// reflective material has coverage before setEnvironment.
|
|
2214
2637
|
scene.setParams(environmentParams(null))
|
|
2215
|
-
// And the
|
|
2216
|
-
|
|
2217
|
-
scene.setParams({ uExposure: 1, uToneMapping: TONE_MAPPING_CODE.none, uOutputEncode: 1 })
|
|
2638
|
+
// And the resolve at its defaults (exposure 1, no tone mapping).
|
|
2639
|
+
scene.setParams({ uExposure: 1, uToneMapping: TONE_MAPPING_CODE.none })
|
|
2218
2640
|
if (opts?.fog !== undefined) scene.setFog(opts.fog)
|
|
2219
2641
|
if (opts?.toneMapping !== undefined) scene.setToneMapping(opts.toneMapping)
|
|
2220
2642
|
if (opts?.exposure !== undefined) scene.setExposure(opts.exposure)
|
|
2643
|
+
if (opts?.bloom !== undefined) scene.setBloom(opts.bloom)
|
|
2221
2644
|
if (opts?.background !== undefined) scene.setBackground(opts.background)
|
|
2222
2645
|
if (opts?.environment !== undefined) scene.setEnvironment(opts.environment)
|
|
2223
2646
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => scene.dispose())
|