@solidrt/3d 0.0.58 → 0.0.60
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 +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -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 +24 -2
- 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 +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- 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 +605 -185
- 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) => {
|
|
@@ -1330,27 +1549,32 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1330
1549
|
// instanced depth pass), whose attributes must fit the records like
|
|
1331
1550
|
// the main material's did at add().
|
|
1332
1551
|
if (v.override !== null && inst !== null) {
|
|
1333
|
-
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
|
+
}
|
|
1334
1559
|
checkInstancePairing(material, inst, "Shadow material")
|
|
1335
1560
|
}
|
|
1336
1561
|
let bufs = mesh._buffers!
|
|
1337
|
-
let
|
|
1338
|
-
|
|
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) : [])],
|
|
1339
1566
|
indexBuffer: bufs.index,
|
|
1340
1567
|
indexFormat: bufs.indexFormat,
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
// needs the mesh's palette); other override programs may not
|
|
1344
|
-
// declare the names, and per-entry bindings validate strictly.
|
|
1345
|
-
textures: (material === mesh.material || material.skinned === true) && mesh._textures !== null ? { ...material.textures, ...mesh._textures } : material.textures,
|
|
1346
|
-
...(inst !== null ? instanceBinding(material, inst) : {}),
|
|
1568
|
+
...meshRange(mesh),
|
|
1569
|
+
textures: entryTextures(material, mesh, morph),
|
|
1347
1570
|
instanceCount: 0,
|
|
1348
1571
|
})
|
|
1349
|
-
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)
|
|
1350
1573
|
v.entries.set(mesh, entry)
|
|
1351
1574
|
v.orderDirty = true
|
|
1352
1575
|
}
|
|
1353
1576
|
let detachView = (v: ViewRecord, mesh: Mesh) => {
|
|
1577
|
+
v.skipped.delete(mesh)
|
|
1354
1578
|
let entry = v.entries.get(mesh)
|
|
1355
1579
|
if (entry === undefined) return
|
|
1356
1580
|
v.entries.delete(mesh)
|
|
@@ -1374,8 +1598,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1374
1598
|
v.entries.clear()
|
|
1375
1599
|
for (let light of lights) if (light.type === "directional" && light._node !== null) spatial.unbindSlot(light._node, v.texture)
|
|
1376
1600
|
spatial.setFrustum(v.texture, null)
|
|
1601
|
+
spatial.setLodView(v.texture, null)
|
|
1377
1602
|
// Drain the zeroed direction slots while the target still exists.
|
|
1378
1603
|
spatial.flush()
|
|
1604
|
+
if (v.resolve !== null) disposeResolve(v.resolve)
|
|
1379
1605
|
destroyTexture(v.texture)
|
|
1380
1606
|
let i = views.indexOf(v)
|
|
1381
1607
|
if (i >= 0) views.splice(i, 1)
|
|
@@ -1418,35 +1644,47 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1418
1644
|
for (let mesh of meshes) if (mesh._instances === null) checkLayout(override, mesh.geometry, "View override material")
|
|
1419
1645
|
}
|
|
1420
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
|
+
})
|
|
1421
1673
|
let v: ViewRecord = {
|
|
1422
|
-
texture:
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
format: probeFormat(),
|
|
1431
|
-
clearColor: vopts.clearColor,
|
|
1432
|
-
label: vopts.label ?? (opts?.label ?? "scene") + "-probe",
|
|
1433
|
-
autoFree: false,
|
|
1434
|
-
})
|
|
1435
|
-
: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1436
|
-
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1437
|
-
clearColor: vopts.clearColor,
|
|
1438
|
-
filter: vopts.filter,
|
|
1439
|
-
wrap: vopts.wrap,
|
|
1440
|
-
samples: vopts.samples,
|
|
1441
|
-
label: vopts.label ?? (opts?.label ?? "scene") + "-view",
|
|
1442
|
-
autoFree: false,
|
|
1443
|
-
into: vopts.into,
|
|
1444
|
-
x: vopts.x,
|
|
1445
|
-
y: vopts.y,
|
|
1446
|
-
}),
|
|
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,
|
|
1447
1682
|
width: vopts.width,
|
|
1448
1683
|
height: vopts.height,
|
|
1684
|
+
label: viewLabel,
|
|
1449
1685
|
override,
|
|
1686
|
+
skipped: new Set(),
|
|
1687
|
+
skippedReported: false,
|
|
1450
1688
|
shadowFilter,
|
|
1451
1689
|
mask: shadowFilter !== null ? sceneMask : checkMask(vopts.layers ?? 1, "createView"),
|
|
1452
1690
|
ownNames: new Set(),
|
|
@@ -1454,6 +1692,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1454
1692
|
entries: new Map(),
|
|
1455
1693
|
orderDirty: true,
|
|
1456
1694
|
lastOrder: [],
|
|
1695
|
+
lodViewSet: false,
|
|
1457
1696
|
cube,
|
|
1458
1697
|
probeCube: null,
|
|
1459
1698
|
sky,
|
|
@@ -1472,6 +1711,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1472
1711
|
if (ownFog !== null) for (let k of Object.keys(ownFog)) v.ownNames.add(k)
|
|
1473
1712
|
setTargetParams(v.texture, ownFog === null ? sceneParams : withoutNames(sceneParams, v.ownNames))
|
|
1474
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
|
+
}
|
|
1475
1719
|
for (let mesh of meshes) attachView(v, mesh)
|
|
1476
1720
|
hooks._schedule()
|
|
1477
1721
|
return v
|
|
@@ -1484,7 +1728,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1484
1728
|
lights,
|
|
1485
1729
|
camera,
|
|
1486
1730
|
targetSize: () => ({ width, height }),
|
|
1487
|
-
label
|
|
1731
|
+
label,
|
|
1488
1732
|
makeView,
|
|
1489
1733
|
disposeView,
|
|
1490
1734
|
markLightsDirty: () => {
|
|
@@ -1503,36 +1747,101 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1503
1747
|
frustumScratch.set(cam.viewProj)
|
|
1504
1748
|
spatial.setFrustum(target, frustumScratch)
|
|
1505
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
|
+
}
|
|
1506
1770
|
let sync = () => {
|
|
1507
1771
|
scheduled = false
|
|
1508
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
|
|
1509
1803
|
ensureCamera(camera, width, height)
|
|
1510
1804
|
let cameraMoved = camera.pending
|
|
1805
|
+
let biasChanged = lodBiasDirty
|
|
1806
|
+
lodBiasDirty = false
|
|
1511
1807
|
if (camera.pending) {
|
|
1512
1808
|
camera.pending = false
|
|
1513
1809
|
setTargetParams(texture, cameraParams(camera))
|
|
1514
1810
|
setFrustum(texture, camera)
|
|
1515
1811
|
if (transparentCount > 1) orderDirty = true
|
|
1516
1812
|
}
|
|
1813
|
+
if (cameraMoved || biasChanged) setLodView(texture, camera)
|
|
1517
1814
|
shadowSys.placeCameras(cameraMoved)
|
|
1518
1815
|
for (let v of views) {
|
|
1519
1816
|
ensureCamera(v.camera, v.width, v.height)
|
|
1520
|
-
|
|
1817
|
+
let viewMoved = v.camera.pending
|
|
1818
|
+
if (viewMoved) {
|
|
1521
1819
|
v.camera.pending = false
|
|
1522
1820
|
setTargetParams(v.texture, cameraParams(v.camera))
|
|
1523
1821
|
setFrustum(v.texture, v.camera)
|
|
1524
1822
|
if (transparentCount > 1) v.orderDirty = true
|
|
1525
1823
|
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
1526
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
|
+
}
|
|
1527
1836
|
}
|
|
1528
1837
|
// Light bookkeeping first, so a fresh direction-slot bind is seeded
|
|
1529
1838
|
// by the flush below in the same sync.
|
|
1530
1839
|
if (lightsDirty) writeLights()
|
|
1531
|
-
// The
|
|
1532
|
-
// coalesced buffer write per
|
|
1533
|
-
if (
|
|
1534
|
-
for (let m of
|
|
1535
|
-
|
|
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()
|
|
1536
1845
|
}
|
|
1537
1846
|
// An instanced mesh without explicit bounds is culled by the union of
|
|
1538
1847
|
// its instances' boxes: the core's cull group (a skinned part's joint
|
|
@@ -1563,6 +1872,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1563
1872
|
if (orderDirty || views.some(v => v.orderDirty)) refreshCenters()
|
|
1564
1873
|
if (orderDirty) sortEntries()
|
|
1565
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
|
+
}
|
|
1566
1879
|
}
|
|
1567
1880
|
|
|
1568
1881
|
let hooks: SceneHooks = {
|
|
@@ -1573,23 +1886,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1573
1886
|
},
|
|
1574
1887
|
_attachLight(light) {
|
|
1575
1888
|
if (disposed) return
|
|
1576
|
-
if (light.type !== "hemisphere" && lights.filter(l => l.type !== "hemisphere").length >= MAX_LIGHTS) {
|
|
1577
|
-
throw new Error("A scene takes at most " + MAX_LIGHTS + " lights (directional, spot and point together)")
|
|
1578
|
-
}
|
|
1579
1889
|
lights.push(light)
|
|
1580
1890
|
lightsDirty = true
|
|
1581
|
-
if (light.type
|
|
1891
|
+
if (light.type === "hemisphere") return
|
|
1892
|
+
lightSetDirty = true
|
|
1893
|
+
if (light.castShadow) shadowSys.invalidate(light)
|
|
1582
1894
|
},
|
|
1583
1895
|
_detachLight(light) {
|
|
1584
1896
|
let i = lights.indexOf(light)
|
|
1585
1897
|
if (i >= 0) lights.splice(i, 1)
|
|
1586
1898
|
lightsDirty = true
|
|
1587
|
-
if (light.type !== "hemisphere")
|
|
1899
|
+
if (light.type !== "hemisphere") {
|
|
1900
|
+
lightSetDirty = true
|
|
1901
|
+
shadowSys.invalidate(light)
|
|
1902
|
+
}
|
|
1588
1903
|
hooks._schedule()
|
|
1589
1904
|
},
|
|
1590
1905
|
_shadowChanged(light) {
|
|
1591
1906
|
if (disposed) return
|
|
1592
|
-
shadowSys.
|
|
1907
|
+
shadowSys.invalidate(light)
|
|
1593
1908
|
},
|
|
1594
1909
|
_setCast(mesh) {
|
|
1595
1910
|
if (mesh._buffers === null || disposed) return
|
|
@@ -1622,19 +1937,23 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1622
1937
|
if (disposed) return
|
|
1623
1938
|
validateGeometry(mesh.geometry)
|
|
1624
1939
|
checkLayout(mesh.material, mesh.geometry, "Mesh material")
|
|
1940
|
+
checkMorph(mesh.material, mesh, "Mesh material")
|
|
1625
1941
|
// Every check before any mutation, so a rejected mesh is attached
|
|
1626
1942
|
// nowhere - the views' override materials included.
|
|
1627
1943
|
for (let v of views) {
|
|
1628
|
-
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
|
+
}
|
|
1629
1948
|
}
|
|
1630
1949
|
// Instancing pairs the same way layout does: the pipeline's instance
|
|
1631
1950
|
// attributes describe the mesh's record buffers, so one without the
|
|
1632
1951
|
// other (or a record stride from a different attribute list) would
|
|
1633
1952
|
// bind garbage - errors here, at add().
|
|
1634
1953
|
let inst = mesh._instances
|
|
1635
|
-
if (mesh.material.
|
|
1954
|
+
if (mesh.material.instanceBuffers !== undefined && inst === null) {
|
|
1636
1955
|
throw new Error(
|
|
1637
|
-
"Material declares
|
|
1956
|
+
"Material declares instanceBuffers - create its meshes with createInstancedMesh or createRecordMesh, not createMesh",
|
|
1638
1957
|
)
|
|
1639
1958
|
}
|
|
1640
1959
|
if (inst !== null) checkInstancePairing(mesh.material, inst, "Mesh material")
|
|
@@ -1642,30 +1961,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1642
1961
|
mesh._buffers = bufs
|
|
1643
1962
|
attachScene(mesh)
|
|
1644
1963
|
for (let v of views) attachView(v, mesh)
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
if (inst !== null && inst.
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
//
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
spatial.setShape(mesh._node!, inst === null && !mesh._sprite ? bufs.shape : null)
|
|
1660
|
-
// 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
|
|
1661
1978
|
// new geometry; their record bindings are untouched. On a first
|
|
1662
1979
|
// entry none has a core node yet - they enter after the mesh.
|
|
1663
1980
|
if (inst !== null && inst.nodes !== null) {
|
|
1664
1981
|
for (let n of inst.nodes.slots) {
|
|
1665
|
-
if (n !== null && n._node !== null)
|
|
1666
|
-
spatial.setBounds(n._node, geometryBounds(mesh.geometry))
|
|
1667
|
-
spatial.setShape(n._node, bufs.shape)
|
|
1668
|
-
}
|
|
1982
|
+
if (n !== null && n._node !== null) spatial.setShape(n._node, bufs.shape)
|
|
1669
1983
|
}
|
|
1670
1984
|
}
|
|
1671
1985
|
// Culling: the gate and margin only when off the defaults, and a
|
|
@@ -1690,7 +2004,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1690
2004
|
},
|
|
1691
2005
|
_detach(mesh) {
|
|
1692
2006
|
if (mesh._buffers !== null) {
|
|
1693
|
-
|
|
2007
|
+
recordsDirty.delete(mesh)
|
|
1694
2008
|
groupDirty.delete(mesh as InstancedMesh)
|
|
1695
2009
|
for (let v of views) detachView(v, mesh)
|
|
1696
2010
|
detachScene(mesh)
|
|
@@ -1715,21 +2029,33 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1715
2029
|
// geometry buffers and core node are live - a mesh masked out of
|
|
1716
2030
|
// the scene included, whose instances still pick.
|
|
1717
2031
|
if (mesh === null || mesh._buffers === null || mesh._node === null || instance._node === null) return
|
|
1718
|
-
spatial.setBounds(instance._node, geometryBounds(mesh.geometry))
|
|
1719
2032
|
spatial.setShape(instance._node, mesh._buffers.shape)
|
|
1720
2033
|
spatial.setLayers(instance._node, mesh.layers)
|
|
1721
2034
|
// The record is the instance's placement inside the mesh (the mesh
|
|
1722
2035
|
// node is the anchor), staged by the core at the next flush.
|
|
1723
|
-
|
|
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)
|
|
1724
2046
|
byNode.set(instance._node, instance)
|
|
1725
2047
|
if (mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
1726
2048
|
this._schedule()
|
|
1727
2049
|
},
|
|
2050
|
+
_exiting(mesh) {
|
|
2051
|
+
if (mesh._node !== null) byNode.delete(mesh._node)
|
|
2052
|
+
},
|
|
1728
2053
|
_detachInstance(instance) {
|
|
1729
|
-
// The core node is destroyed right after
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
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).
|
|
1733
2059
|
if (instance._node !== null) byNode.delete(instance._node)
|
|
1734
2060
|
let mesh = instance.mesh
|
|
1735
2061
|
if (mesh !== null && mesh._instances !== null && mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
@@ -1756,19 +2082,46 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1756
2082
|
// The entries keep their range (at most the old capacity, so the
|
|
1757
2083
|
// larger buffers always pass the swap's bounds check); the caller
|
|
1758
2084
|
// destroys the old buffers after this, which the entries held alive
|
|
1759
|
-
// 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).
|
|
1760
2088
|
let inst = mesh._instances
|
|
1761
|
-
|
|
1762
|
-
|
|
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)] })
|
|
1763
2092
|
for (let v of views) {
|
|
1764
2093
|
let entry = v.entries.get(mesh)
|
|
1765
|
-
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)] })
|
|
1766
2095
|
}
|
|
1767
2096
|
}
|
|
1768
2097
|
},
|
|
1769
|
-
|
|
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) {
|
|
1770
2123
|
if (disposed || mesh._buffers === null) return
|
|
1771
|
-
|
|
2124
|
+
recordsDirty.add(mesh)
|
|
1772
2125
|
this._schedule()
|
|
1773
2126
|
},
|
|
1774
2127
|
_reorder() {
|
|
@@ -1783,11 +2136,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1783
2136
|
}
|
|
1784
2137
|
this._schedule()
|
|
1785
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
|
+
},
|
|
1786
2152
|
}
|
|
1787
2153
|
|
|
1788
2154
|
let root = makeNode("group")
|
|
1789
2155
|
root._scene = hooks
|
|
1790
2156
|
root._node = spatial.createNode(fillTransform(root), true)
|
|
2157
|
+
if (opts?.stagger !== undefined) setTransition(root, { stagger: opts.stagger })
|
|
1791
2158
|
// The first light rewrite seeds the (empty) light set and the shadow
|
|
1792
2159
|
// slots - placeholders, no casts - so receivers draw plain from the
|
|
1793
2160
|
// first frame.
|
|
@@ -1798,8 +2165,37 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1798
2165
|
// pointer walk scene-pointer.ts runs behind scene.handlers.
|
|
1799
2166
|
let listeners = new Set<ScenePointerListener>()
|
|
1800
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
|
+
|
|
1801
2196
|
let scene: Scene = {
|
|
1802
|
-
texture,
|
|
2197
|
+
texture: resolve.target,
|
|
2198
|
+
hdrTexture: texture,
|
|
1803
2199
|
depthTexture: depthMode === "texture" ? depthTexture(texture) : null,
|
|
1804
2200
|
root,
|
|
1805
2201
|
setCamera(update) {
|
|
@@ -1812,24 +2208,35 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1812
2208
|
width = w
|
|
1813
2209
|
height = h
|
|
1814
2210
|
setTargetSize(texture, w, h)
|
|
2211
|
+
resizeResolve(resolve, w, h)
|
|
1815
2212
|
camera.dirty = true
|
|
1816
2213
|
hooks._schedule()
|
|
1817
2214
|
},
|
|
2215
|
+
size: () => ({ width, height }),
|
|
1818
2216
|
setParams(params) {
|
|
1819
2217
|
if (disposed) return
|
|
1820
2218
|
Object.assign(sceneParams, params)
|
|
1821
2219
|
setTargetParams(texture, params)
|
|
2220
|
+
setTargetParams(resolve.target, params)
|
|
1822
2221
|
for (let v of views) {
|
|
1823
2222
|
// A view's own names (view.setParams, its fog) win over the
|
|
1824
2223
|
// scene-wide fan-out.
|
|
1825
2224
|
let fanned = v.ownNames.size === 0 ? params : withoutNames(params, v.ownNames)
|
|
1826
2225
|
if (fanned !== params && Object.keys(fanned).length === 0) continue
|
|
1827
2226
|
setTargetParams(v.texture, fanned)
|
|
2227
|
+
if (v.resolve !== null) setTargetParams(v.resolve.target, fanned)
|
|
1828
2228
|
}
|
|
1829
2229
|
},
|
|
1830
2230
|
setFog(fog) {
|
|
1831
2231
|
scene.setParams(fogParams(fog))
|
|
1832
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
|
+
},
|
|
1833
2240
|
setLayers(mask) {
|
|
1834
2241
|
checkMask(mask, "scene.setLayers")
|
|
1835
2242
|
if (disposed || sceneMask === mask) return
|
|
@@ -1872,7 +2279,6 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1872
2279
|
background = null
|
|
1873
2280
|
}
|
|
1874
2281
|
if (source === null) return
|
|
1875
|
-
let label = opts?.label ?? "scene"
|
|
1876
2282
|
let built = sky === null ? backgroundPipeline(source as string, label + "-background") : backgroundPipeline(SKYBOX_FRAGMENT, label + "-skybox")
|
|
1877
2283
|
background = { pipeline: built.pipeline, program: built.program, sky, entries: new Map() }
|
|
1878
2284
|
// First in list order on every target that draws it: inserted before
|
|
@@ -1892,13 +2298,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1892
2298
|
},
|
|
1893
2299
|
setToneMapping(mode) {
|
|
1894
2300
|
let code = TONE_MAPPING_CODE[mode]
|
|
1895
|
-
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)
|
|
1896
2302
|
scene.setParams({ uToneMapping: code })
|
|
1897
2303
|
},
|
|
1898
2304
|
setExposure(exposure) {
|
|
1899
2305
|
if (!Number.isFinite(exposure) || exposure < 0) throw new Error("scene.setExposure: expected a finite number >= 0, got " + exposure)
|
|
1900
2306
|
scene.setParams({ uExposure: exposure })
|
|
1901
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
|
+
},
|
|
1902
2320
|
project(point) {
|
|
1903
2321
|
ensureCamera(camera, width, height)
|
|
1904
2322
|
transformPoint(clip, camera.viewProj, point)
|
|
@@ -1924,7 +2342,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1924
2342
|
},
|
|
1925
2343
|
pick(x, y) {
|
|
1926
2344
|
pixelRay(x, y)
|
|
1927
|
-
return
|
|
2345
|
+
return raycastAs(texture, pickOrigin, pickDir, undefined)
|
|
1928
2346
|
},
|
|
1929
2347
|
unproject(x, y, w, out = [0, 0, 0]) {
|
|
1930
2348
|
pixelRay(x, y)
|
|
@@ -1941,29 +2359,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1941
2359
|
}
|
|
1942
2360
|
},
|
|
1943
2361
|
raycast(origin, direction, rayOpts) {
|
|
1944
|
-
|
|
1945
|
-
// it, the same immediacy contract as lookAt()/project(). (The queued
|
|
1946
|
-
// microtask still runs and finds nothing dirty - harmless.)
|
|
1947
|
-
if (scheduled) sync()
|
|
1948
|
-
if (disposed) return []
|
|
1949
|
-
let f = queryFilter(rayOpts, "raycast")
|
|
1950
|
-
let hits: Hit[] = []
|
|
1951
|
-
rayOriginScratch[0] = origin[0]
|
|
1952
|
-
rayOriginScratch[1] = origin[1]
|
|
1953
|
-
rayOriginScratch[2] = origin[2]
|
|
1954
|
-
rayDirScratch[0] = direction[0]
|
|
1955
|
-
rayDirScratch[1] = direction[1]
|
|
1956
|
-
rayDirScratch[2] = direction[2]
|
|
1957
|
-
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
1958
|
-
let leaf = leafOf(h.node)
|
|
1959
|
-
if (leaf === null) continue
|
|
1960
|
-
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
1961
|
-
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
1962
|
-
if (h.face !== undefined) hit.face = h.face
|
|
1963
|
-
if (h.uv !== undefined) hit.uv = h.uv
|
|
1964
|
-
hits.push(hit)
|
|
1965
|
-
}
|
|
1966
|
-
return hits
|
|
2362
|
+
return raycastAs(texture, origin, direction, rayOpts)
|
|
1967
2363
|
},
|
|
1968
2364
|
overlap(volume, qopts) {
|
|
1969
2365
|
if (scheduled) sync()
|
|
@@ -2030,7 +2426,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2030
2426
|
// dispatch with its own capture and hover bookkeeping.
|
|
2031
2427
|
let viewListeners = new Set<ScenePointerListener>()
|
|
2032
2428
|
let handle: ViewHandle = {
|
|
2033
|
-
texture: v.texture,
|
|
2429
|
+
texture: v.resolve !== null ? v.resolve.target : v.texture,
|
|
2430
|
+
hdrTexture: v.resolve !== null ? v.texture : null,
|
|
2034
2431
|
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
2035
2432
|
setCamera(update) {
|
|
2036
2433
|
updateCamera(v.camera, update)
|
|
@@ -2042,9 +2439,24 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2042
2439
|
v.width = w
|
|
2043
2440
|
v.height = h
|
|
2044
2441
|
setTargetSize(v.texture, w, h)
|
|
2442
|
+
if (v.resolve !== null) resizeResolve(v.resolve, w, h)
|
|
2045
2443
|
v.camera.dirty = true
|
|
2046
2444
|
hooks._schedule()
|
|
2047
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
|
+
},
|
|
2048
2460
|
setRect(rect) {
|
|
2049
2461
|
if (v.disposed) return
|
|
2050
2462
|
setTargetRect(v.texture, rect)
|
|
@@ -2058,6 +2470,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2058
2470
|
if (v.disposed) return
|
|
2059
2471
|
for (let k of Object.keys(params)) v.ownNames.add(k)
|
|
2060
2472
|
setTargetParams(v.texture, params)
|
|
2473
|
+
if (v.resolve !== null) setTargetParams(v.resolve.target, params)
|
|
2061
2474
|
},
|
|
2062
2475
|
setLayers(mask) {
|
|
2063
2476
|
checkMask(mask, "view.setLayers")
|
|
@@ -2072,7 +2485,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2072
2485
|
pick(x, y) {
|
|
2073
2486
|
if (v.disposed) return []
|
|
2074
2487
|
pixelRayOf(v.camera, v.width, v.height, x, y)
|
|
2075
|
-
return
|
|
2488
|
+
return raycastAs(v.texture, pickOrigin, pickDir, undefined)
|
|
2076
2489
|
},
|
|
2077
2490
|
get handlers() {
|
|
2078
2491
|
return viewPointer.handlers
|
|
@@ -2110,7 +2523,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2110
2523
|
if (!Number.isInteger(size) || size < 1) throw new Error("scene.bakeBackground: size must be a positive integer, got " + size)
|
|
2111
2524
|
// A probe that sees no mesh (mask 0) draws the background alone: the
|
|
2112
2525
|
// same face cameras, linear output and prefilter as any probe.
|
|
2113
|
-
let probe = makeProbe({ position: [0, 0, 0], size, label:
|
|
2526
|
+
let probe = makeProbe({ position: [0, 0, 0], size, label: label + "-sky" }, 0)
|
|
2114
2527
|
probe.update()
|
|
2115
2528
|
return probe.finish()
|
|
2116
2529
|
},
|
|
@@ -2122,6 +2535,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2122
2535
|
// the scene (entries' geometry-buffer references and pick leaves
|
|
2123
2536
|
// dropped, core nodes freed), so a disposed scene leaves no
|
|
2124
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)
|
|
2125
2541
|
for (let c of root.children.slice()) leaveScene(c)
|
|
2126
2542
|
root._scene = null
|
|
2127
2543
|
if (root._node !== null) {
|
|
@@ -2129,9 +2545,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2129
2545
|
root._node = null
|
|
2130
2546
|
}
|
|
2131
2547
|
spatial.setFrustum(texture, null)
|
|
2548
|
+
spatial.setLodView(texture, null)
|
|
2132
2549
|
// Drain the zeroed direction slots the teardown queued while the
|
|
2133
2550
|
// targets still exist; afterwards their groups are gone.
|
|
2134
2551
|
spatial.flush()
|
|
2552
|
+
disposeResolve(resolve)
|
|
2135
2553
|
destroyTexture(texture)
|
|
2136
2554
|
for (let v of views.slice()) disposeView(v)
|
|
2137
2555
|
shadowSys.dispose()
|
|
@@ -2170,12 +2588,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2170
2588
|
)
|
|
2171
2589
|
v.camera.mirror = true
|
|
2172
2590
|
updateCamera(v.camera, { fov: PROBE_FOV, near: popts.near, far: popts.far })
|
|
2173
|
-
|
|
2174
|
-
// stage: no sRGB encode, no tone mapping, unit exposure - the
|
|
2175
|
-
// probe's own names, so the scene's fan-out leaves them alone.
|
|
2176
|
-
for (let k of Object.keys(LINEAR_OUTPUT)) v.ownNames.add(k)
|
|
2177
|
-
setTargetParams(v.texture, LINEAR_OUTPUT)
|
|
2178
|
-
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
|
|
2179
2592
|
v.probeCube = chain?.cube ?? v.texture
|
|
2180
2593
|
let position: Vec3 = [popts.position[0], popts.position[1], popts.position[2]]
|
|
2181
2594
|
return {
|
|
@@ -2196,6 +2609,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2196
2609
|
chain?.run()
|
|
2197
2610
|
},
|
|
2198
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)
|
|
2199
2619
|
chain?.dispose()
|
|
2200
2620
|
disposeView(v)
|
|
2201
2621
|
},
|
|
@@ -2215,12 +2635,12 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2215
2635
|
// Likewise the environment set starts at "none" (uEnvOn 0), so a
|
|
2216
2636
|
// reflective material has coverage before setEnvironment.
|
|
2217
2637
|
scene.setParams(environmentParams(null))
|
|
2218
|
-
// And the
|
|
2219
|
-
|
|
2220
|
-
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 })
|
|
2221
2640
|
if (opts?.fog !== undefined) scene.setFog(opts.fog)
|
|
2222
2641
|
if (opts?.toneMapping !== undefined) scene.setToneMapping(opts.toneMapping)
|
|
2223
2642
|
if (opts?.exposure !== undefined) scene.setExposure(opts.exposure)
|
|
2643
|
+
if (opts?.bloom !== undefined) scene.setBloom(opts.bloom)
|
|
2224
2644
|
if (opts?.background !== undefined) scene.setBackground(opts.background)
|
|
2225
2645
|
if (opts?.environment !== undefined) scene.setEnvironment(opts.environment)
|
|
2226
2646
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => scene.dispose())
|