@solidrt/3d 0.0.62 → 0.0.64
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 +198 -17
- package/package.json +3 -2
- package/src/camera.ts +6 -2
- package/src/components/directional-light.tsx +6 -2
- package/src/components/hemisphere-light.tsx +12 -4
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +6 -2
- package/src/components/mesh.tsx +12 -1
- package/src/components/point-light.tsx +6 -2
- package/src/components/record-mesh.tsx +14 -2
- package/src/components/scene.tsx +8 -1
- package/src/components/splat-mesh.tsx +50 -0
- package/src/components/spot-light.tsx +7 -3
- package/src/environment.ts +55 -0
- package/src/glsl.ts +68 -14
- package/src/index.ts +9 -4
- package/src/light.ts +56 -4
- package/src/material.ts +26 -1
- package/src/mesh.ts +361 -6
- package/src/node.ts +3 -0
- package/src/scene-shadows.ts +8 -7
- package/src/scene.ts +225 -104
- package/src/splat-data.ts +617 -0
- package/src/splat.ts +401 -0
- package/tools/splat.ts +70 -0
package/src/scene.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawParams, setDrawRange, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
40
40
|
import * as spatial from "flux:spatial"
|
|
41
41
|
import type { BindDrawOptions, Impact as CoreImpact, NodeId, QueryFilter } from "flux:spatial"
|
|
42
|
-
import type { BufferId, DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureBindings, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
42
|
+
import type { BufferId, DrawId, FilterMode, InstanceOrder, 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"
|
|
@@ -50,19 +50,21 @@ import { cameraParams, cameraState, ensureCamera, makeCamera, updateCamera } fro
|
|
|
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 { geometryKey, geometryTopology, validateGeometry } from "./geometry.ts"
|
|
53
|
+
import { geometryKey, geometryTopology, layoutKey, 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
57
|
import { disposeResolve, makeResolve, replaceResolve, resizeResolve, setResolveBloom } from "./resolve.ts"
|
|
58
58
|
import type { BloomOptions, ResolveInput, ResolveOptions, ResolveRecord } from "./resolve.ts"
|
|
59
|
-
import { createEnvironmentPlaceholder, createPrefilter, bufferFormat } from "./environment.ts"
|
|
60
|
-
import type { Prefilter } from "./environment.ts"
|
|
59
|
+
import { checkCubeKnobs, createEnvironmentPlaceholder, createPrefilter, cubeTurn, bufferFormat, environmentParams } from "./environment.ts"
|
|
60
|
+
import type { EnvironmentOptions, Prefilter } from "./environment.ts"
|
|
61
|
+
|
|
62
|
+
export type { EnvironmentOptions } from "./environment.ts"
|
|
61
63
|
import type { Material } from "./material.ts"
|
|
62
64
|
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition } from "./node.ts"
|
|
63
65
|
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
64
|
-
import { checkInstancePairing, checkMask, instanceBinding, localBounds, publishRecords } from "./mesh.ts"
|
|
65
|
-
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
66
|
+
import { checkInstancePairing, checkMask, checkOrderPairing, drawCount, instanceBinding, localBounds, markLiveRecords, publishRecords } from "./mesh.ts"
|
|
67
|
+
import type { InstancedMesh, InstanceNode, Mesh, MeshInstances, ResolvedInstanceOrder } from "./mesh.ts"
|
|
66
68
|
import type { CastingLight, Light } from "./light.ts"
|
|
67
69
|
|
|
68
70
|
const IDENTITY = mat4()
|
|
@@ -337,64 +339,12 @@ export type SkyboxOptions = {
|
|
|
337
339
|
rotation?: number
|
|
338
340
|
}
|
|
339
341
|
|
|
340
|
-
/**
|
|
341
|
-
* The scene's environment: a cube map every `lit` material with
|
|
342
|
-
* `reflectivity` mirrors (scene.setEnvironment). The same three fields
|
|
343
|
-
* as a skybox, and typically the same cube - Three's `scene.environment`
|
|
344
|
-
* with `environmentIntensity` and `environmentRotation`; Unity's
|
|
345
|
-
* environment reflections source, Godot's reflected light from the sky.
|
|
346
|
-
*/
|
|
347
|
-
export type EnvironmentOptions = {
|
|
348
|
-
/** A cube map (loadEnvironment's baked chain; equirectToCube for an LDR
|
|
349
|
-
* panorama; createCubeTexture with `mipmap: true` for a hand-baked
|
|
350
|
-
* sky), faces in +X, -X, +Y, -Y, +Z, -Z order, looked up like the
|
|
351
|
-
* skybox (no flip). A 2D texture id throws. */
|
|
352
|
-
cube: TextureId
|
|
353
|
-
/** Multiplier on the reflected color, >= 0; default 1. */
|
|
354
|
-
intensity?: number
|
|
355
|
-
/** Turn about world y in RADIANS (default 0), the skybox's convention:
|
|
356
|
-
* the environment turns as a node with rotation [0, r, 0] would. */
|
|
357
|
-
rotation?: number
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// The uniform turn a rotated cube map is looked up through: the INVERSE
|
|
361
|
-
// of the sky's turn, because a lookup along view direction v must find
|
|
362
|
-
// the texel that sat at R(-r) v before the sky turned by +r.
|
|
363
|
-
function cubeTurn(rotation: number): Mat4 {
|
|
364
|
-
let c = Math.cos(rotation)
|
|
365
|
-
let n = Math.sin(rotation)
|
|
366
|
-
// prettier-ignore
|
|
367
|
-
return [
|
|
368
|
-
c, 0, n, 0,
|
|
369
|
-
0, 1, 0, 0,
|
|
370
|
-
-n, 0, c, 0,
|
|
371
|
-
0, 0, 0, 1,
|
|
372
|
-
]
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function checkCubeKnobs(o: { intensity?: number; rotation?: number }, site: string): { intensity: number; rotation: number } {
|
|
376
|
-
let intensity = o.intensity ?? 1
|
|
377
|
-
let rotation = o.rotation ?? 0
|
|
378
|
-
if (!Number.isFinite(intensity) || intensity < 0) throw new Error(site + ": intensity must be a finite number >= 0, got " + intensity)
|
|
379
|
-
if (!Number.isFinite(rotation)) throw new Error(site + ": rotation must be a finite angle in radians, got " + rotation)
|
|
380
|
-
return { intensity, rotation }
|
|
381
|
-
}
|
|
382
|
-
|
|
383
342
|
// The entry params a skybox compiles to.
|
|
384
343
|
function skyboxParams(sky: SkyboxOptions, site: string): ShaderParams {
|
|
385
344
|
let k = checkCubeKnobs(sky, site)
|
|
386
345
|
return { uSkyIntensity: k.intensity, uSkyRotation: cubeTurn(k.rotation) }
|
|
387
346
|
}
|
|
388
347
|
|
|
389
|
-
// The shared params an environment compiles to (null = off: uEnvOn 0
|
|
390
|
-
// makes every reflective material's term vanish; the set ENVIRONMENT in
|
|
391
|
-
// `@solidrt/3d/glsl` declares).
|
|
392
|
-
function environmentParams(env: EnvironmentOptions | null): ShaderParams {
|
|
393
|
-
if (env === null) return { uEnvIntensity: 0, uEnvRotation: cubeTurn(0), uEnvOn: 0 }
|
|
394
|
-
let k = checkCubeKnobs(env, "scene.setEnvironment")
|
|
395
|
-
return { uEnvIntensity: k.intensity, uEnvRotation: cubeTurn(k.rotation), uEnvOn: 1 }
|
|
396
|
-
}
|
|
397
|
-
|
|
398
348
|
export type { BloomOptions, ResolveInput, ResolveOptions } from "./resolve.ts"
|
|
399
349
|
|
|
400
350
|
/** The resolve's tone mapping (setToneMapping): "none" clamps, "aces" is
|
|
@@ -404,6 +354,20 @@ export type { BloomOptions, ResolveInput, ResolveOptions } from "./resolve.ts"
|
|
|
404
354
|
* where a filmic curve shifts them. */
|
|
405
355
|
export type ToneMapping = "none" | "aces" | "agx" | "neutral"
|
|
406
356
|
|
|
357
|
+
/** The buffer's blend space (SceneOptions.blendSpace). "linear" is the
|
|
358
|
+
* default contract: fragments write linear light into a half-float
|
|
359
|
+
* buffer and one resolve pass applies exposure, tone mapping and the
|
|
360
|
+
* sRGB encode. "display" turns the scene display-referred, the way the
|
|
361
|
+
* web renders: fragments encode to sRGB at output (BLEND_SPACE in the
|
|
362
|
+
* shared tail), BLENDING runs on encoded values, the buffer is rgba8
|
|
363
|
+
* and IS the displayed image - no resolve pass, no HDR, and exposure,
|
|
364
|
+
* tone mapping, bloom, a custom resolve and reflection probes are all
|
|
365
|
+
* unavailable (they act on linear radiance). The one reason it exists:
|
|
366
|
+
* content authored or TRAINED against sRGB compositing - a gaussian
|
|
367
|
+
* splat capture above all - only looks right when the blend itself is
|
|
368
|
+
* sRGB; a linear blend lays a milky veil over its stacked translucents. */
|
|
369
|
+
export type BlendSpace = "linear" | "display"
|
|
370
|
+
|
|
407
371
|
// The uToneMapping value per mode; the RESOLVE set branches on it.
|
|
408
372
|
const TONE_MAPPING_CODE: Record<ToneMapping, number> = { none: 0, aces: 1, agx: 2, neutral: 3 }
|
|
409
373
|
// The core's draw queues (flux:spatial bindDraw), drawn in this order.
|
|
@@ -414,15 +378,35 @@ const QUEUE_TRANSPARENT = 3
|
|
|
414
378
|
let drawQueue = (m: Material): number =>
|
|
415
379
|
m.transparent === true ? QUEUE_TRANSPARENT : m.cutout === true ? QUEUE_CUTOUT : QUEUE_OPAQUE
|
|
416
380
|
// A mesh entry's draw sink: what the core writes into it and where it
|
|
417
|
-
// sorts, from the material the entry draws with.
|
|
418
|
-
|
|
381
|
+
// sorts, from the material the entry draws with. `orderFeed` (the scene
|
|
382
|
+
// entry of a position-ordered population) has the core steer the entry's
|
|
383
|
+
// record order from the target's view.
|
|
384
|
+
let drawBinding = (material: Material, mesh: Mesh, inst: MeshInstances | null, orderFeed = false): BindDrawOptions => ({
|
|
419
385
|
normal: material.normalMatrix === true,
|
|
420
|
-
count: inst !== null ? inst
|
|
386
|
+
count: inst !== null ? drawCount(inst) : 1,
|
|
421
387
|
fade: material.lodFade === true,
|
|
422
388
|
queue: drawQueue(material),
|
|
423
389
|
renderOrder: mesh.renderOrder,
|
|
390
|
+
orderFeed,
|
|
424
391
|
})
|
|
425
392
|
|
|
393
|
+
// The engine's instanceOrder for the scene entry of an ordered mesh: the
|
|
394
|
+
// key stream resolved to its pipeline buffer index (the geometry's
|
|
395
|
+
// buffers precede the instance buffers, and the material's declaration
|
|
396
|
+
// order is the pipeline's), the float offsets resolved at creation, and
|
|
397
|
+
// for a position key a seed direction the core-side feed (bindDraw
|
|
398
|
+
// orderFeed) replaces at the first flush.
|
|
399
|
+
const ORDER_SEED_DIRECTION: [number, number, number] = [0, 0, 1]
|
|
400
|
+
let entryInstanceOrder = (order: ResolvedInstanceOrder, material: Material, geometryBuffers: number): InstanceOrder => {
|
|
401
|
+
let at = (material.instanceBuffers ?? []).findIndex(b => layoutKey(b.attributes) === order.key)
|
|
402
|
+
if (at < 0) throw new Error("Mesh material declares no instance buffer with the mesh's instanceOrder key layout (" + order.key + ")")
|
|
403
|
+
let buffer = geometryBuffers + at
|
|
404
|
+
let indices = order.indices !== null ? { indices: order.indices } : {}
|
|
405
|
+
return order.position
|
|
406
|
+
? { position: order.floats, direction: ORDER_SEED_DIRECTION, descending: order.descending, buffer, retain: order.retain, ...indices }
|
|
407
|
+
: { field: order.floats, descending: order.descending, buffer }
|
|
408
|
+
}
|
|
409
|
+
|
|
426
410
|
/** The RESOLVE set's params for a resolve pass of your own (an app-owned
|
|
427
411
|
* atlas tiled views render into, resolved through `resolveFragment` from
|
|
428
412
|
* `/glsl`): `uToneMapping` and `uExposure` as the scene's setToneMapping
|
|
@@ -455,6 +439,12 @@ export type SceneOptions = {
|
|
|
455
439
|
toneMapping?: ToneMapping
|
|
456
440
|
/** Output exposure, default 1; see setExposure. */
|
|
457
441
|
exposure?: number
|
|
442
|
+
/** The buffer's blend space, default "linear"; fixed at creation. See
|
|
443
|
+
* BlendSpace: "display" blends sRGB-encoded values in an rgba8 buffer
|
|
444
|
+
* that is itself the displayed image (`texture` is the buffer, there
|
|
445
|
+
* is no resolve), and rejects resolve, bloom, toneMapping, exposure
|
|
446
|
+
* and reflection probes. */
|
|
447
|
+
blendSpace?: BlendSpace
|
|
458
448
|
/** LOD quality knob, default 1: every measured projected size is
|
|
459
449
|
* multiplied by it before the level select, so below 1 the groups hand
|
|
460
450
|
* over to their far levels sooner (Unity's lodBias). Live via
|
|
@@ -693,12 +683,15 @@ const PROBE_SIZE = 128
|
|
|
693
683
|
const PROBE_FOV = 90
|
|
694
684
|
export type Scene = {
|
|
695
685
|
/** The scene's output: an ordinary texture id (`<texture src>`) - the
|
|
696
|
-
* resolve of the buffer, encoded display pixels.
|
|
686
|
+
* resolve of the buffer, encoded display pixels. In a blendSpace
|
|
687
|
+
* "display" scene there is no resolve and this IS the buffer. */
|
|
697
688
|
texture: TextureId
|
|
698
|
-
/** The scene's BUFFER: the target the meshes draw into,
|
|
699
|
-
* linear light (half float where the
|
|
700
|
-
* bufferFormat)
|
|
701
|
-
* bloom chain) whose result a custom resolve
|
|
689
|
+
/** The scene's BUFFER: the target the meshes draw into, sampler-only.
|
|
690
|
+
* Premultiplied linear light in a linear scene (half float where the
|
|
691
|
+
* device renders it, see bufferFormat) - the input of a
|
|
692
|
+
* radiance-reading pass (a bloom chain) whose result a custom resolve
|
|
693
|
+
* adds back in. In a blendSpace "display" scene it holds encoded
|
|
694
|
+
* display pixels and equals `texture`. */
|
|
702
695
|
hdrTexture: TextureId
|
|
703
696
|
/** The buffer's depth as a sampler-only texture id when created with
|
|
704
697
|
* `depth: "texture"`, else null. */
|
|
@@ -719,8 +712,9 @@ export type Scene = {
|
|
|
719
712
|
size(): { width: number; height: number }
|
|
720
713
|
/**
|
|
721
714
|
* Scene-wide uniforms: merge app-owned names into the target's SHARED
|
|
722
|
-
* params, beside the standard uViewProj/uCamPos/uCamRight/uCamUp
|
|
723
|
-
* camera writes. One write per
|
|
715
|
+
* params, beside the standard uViewProj/uCamPos/uCamRight/uCamUp/
|
|
716
|
+
* uViewport (the target's pixel size) the camera writes. One write per
|
|
717
|
+
* frame however many meshes read the name
|
|
724
718
|
* (a clock, a sun direction, fog) - the per-mesh channel is
|
|
725
719
|
* setMeshParams. Merge semantics, no unset; a material that does not
|
|
726
720
|
* declare a name simply skips it. Frame-rate-safe like setTransform.
|
|
@@ -1062,10 +1056,15 @@ function meshRange(mesh: Mesh): { firstIndex: number; indexCount: number } {
|
|
|
1062
1056
|
// discard everything).
|
|
1063
1057
|
const LOD_SOLID = [1, 1]
|
|
1064
1058
|
|
|
1065
|
-
function entrySeed(material: Material, params: ShaderParams | null): ShaderParams {
|
|
1059
|
+
function entrySeed(material: Material, params: ShaderParams | null, layers: number): ShaderParams {
|
|
1066
1060
|
let seed: ShaderParams = { uModel: IDENTITY }
|
|
1067
1061
|
if (material.normalMatrix) seed.uNormal = IDENTITY
|
|
1068
1062
|
if (material.lodFade) seed.uLodFade = LOD_SOLID
|
|
1063
|
+
// The mesh's layer membership, which lightVector masks lights against;
|
|
1064
|
+
// `| 0` because GLSL ES ints are signed 32-bit (all bits = -1). An
|
|
1065
|
+
// unwritten int uniform reads 0 - no light at all - so a lit entry is
|
|
1066
|
+
// always seeded.
|
|
1067
|
+
if (material.layered) seed.uLayers = layers | 0
|
|
1069
1068
|
return { ...seed, ...material.params, ...params }
|
|
1070
1069
|
}
|
|
1071
1070
|
|
|
@@ -1122,10 +1121,16 @@ function entryTextures(material: Material, mesh: Mesh, morph: MorphEntry | null)
|
|
|
1122
1121
|
return textures
|
|
1123
1122
|
}
|
|
1124
1123
|
|
|
1125
|
-
// A buffer's clear color: the sRGB option decoded to premultiplied
|
|
1126
|
-
// light
|
|
1127
|
-
|
|
1124
|
+
// A buffer's clear color: the sRGB option decoded to premultiplied
|
|
1125
|
+
// linear light in a linear scene (what the buffer holds and the resolve
|
|
1126
|
+
// tone maps); a display-space buffer holds encoded pixels, so the sRGB
|
|
1127
|
+
// components premultiply as they are.
|
|
1128
|
+
function bufferClear(color: [number, number, number, number] | undefined, display: boolean): [number, number, number, number] | undefined {
|
|
1128
1129
|
if (color === undefined) return undefined
|
|
1130
|
+
if (display) {
|
|
1131
|
+
let a = color[3]
|
|
1132
|
+
return [color[0] * a, color[1] * a, color[2] * a, a]
|
|
1133
|
+
}
|
|
1129
1134
|
let c = premultipliedColor(color)
|
|
1130
1135
|
return [c[0]!, c[1]!, c[2]!, c[3]!]
|
|
1131
1136
|
}
|
|
@@ -1145,19 +1150,34 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1145
1150
|
// scene cannot see must not darken it).
|
|
1146
1151
|
let sceneMask = checkMask(opts?.layers ?? 1, "createScene")
|
|
1147
1152
|
let label = opts?.label ?? "scene"
|
|
1153
|
+
let blendSpace = opts?.blendSpace ?? "linear"
|
|
1154
|
+
if (blendSpace !== "linear" && blendSpace !== "display") {
|
|
1155
|
+
throw new Error('createScene: expected blendSpace "linear" or "display", got ' + blendSpace)
|
|
1156
|
+
}
|
|
1157
|
+
let display = blendSpace === "display"
|
|
1158
|
+
if (display) {
|
|
1159
|
+
// Display space is LDR and has no resolve stage: everything that
|
|
1160
|
+
// acts on linear radiance past the fragment is off the table.
|
|
1161
|
+
for (let name of ["resolve", "bloom", "toneMapping", "exposure"] as const) {
|
|
1162
|
+
if (opts?.[name] !== undefined) throw new Error(`createScene: ${name} does not compose with blendSpace "display" (the buffer holds encoded display pixels; there is no resolve stage)`)
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1148
1165
|
// `texture` is the scene's BUFFER throughout this file: the draw target
|
|
1149
1166
|
// every entry, sink, sort and param write names. The handle's `texture`
|
|
1150
|
-
// is its resolve (below)
|
|
1167
|
+
// is its resolve (below); a display-space scene has no resolve and the
|
|
1168
|
+
// buffer is itself the displayable id.
|
|
1151
1169
|
let texture = createDrawTarget(width, height, null, {
|
|
1152
1170
|
depth: depthMode,
|
|
1153
|
-
format: bufferFormat(),
|
|
1154
|
-
clearColor: bufferClear(opts?.clearColor),
|
|
1171
|
+
format: display ? "rgba8" : bufferFormat(),
|
|
1172
|
+
clearColor: bufferClear(opts?.clearColor, display),
|
|
1155
1173
|
samples: opts?.samples,
|
|
1174
|
+
filter: display ? opts?.filter : undefined,
|
|
1175
|
+
wrap: display ? opts?.wrap : undefined,
|
|
1156
1176
|
label,
|
|
1157
1177
|
autoFree: false,
|
|
1158
1178
|
})
|
|
1159
1179
|
spatial.setDrawSort(texture, true)
|
|
1160
|
-
let resolve = makeResolve(texture, width, height, opts?.resolve, { filter: opts?.filter, wrap: opts?.wrap }, label)
|
|
1180
|
+
let resolve = display ? null : makeResolve(texture, width, height, opts?.resolve, { filter: opts?.filter, wrap: opts?.wrap }, label)
|
|
1161
1181
|
let disposed = false
|
|
1162
1182
|
let scheduled = false
|
|
1163
1183
|
|
|
@@ -1265,6 +1285,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1265
1285
|
let bias: number[] = []
|
|
1266
1286
|
let normalBias: number[] = []
|
|
1267
1287
|
let radius: number[] = []
|
|
1288
|
+
// Layer masks cross to GLSL as signed 32-bit ints (all bits = -1,
|
|
1289
|
+
// the no-hemisphere default; the shader only ANDs the bit pattern).
|
|
1290
|
+
let masks: number[] = []
|
|
1291
|
+
let hemiMask = -1
|
|
1268
1292
|
let count = 0
|
|
1269
1293
|
for (let light of lights) {
|
|
1270
1294
|
if (light.type === "hemisphere") {
|
|
@@ -1273,6 +1297,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1273
1297
|
let g = linearColor(light.ground)
|
|
1274
1298
|
sky = [s[0]! * k, s[1]! * k, s[2]! * k]
|
|
1275
1299
|
ground = [g[0]! * k, g[1]! * k, g[2]! * k]
|
|
1300
|
+
hemiMask = light.layers | 0
|
|
1276
1301
|
continue
|
|
1277
1302
|
}
|
|
1278
1303
|
// A light past the cap is not lit (sync reports the set once): its
|
|
@@ -1305,6 +1330,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1305
1330
|
bias.push(light.shadow.bias)
|
|
1306
1331
|
normalBias.push(light.shadow.normalBias)
|
|
1307
1332
|
radius.push(light.shadow.radius)
|
|
1333
|
+
masks.push(light.layers | 0)
|
|
1308
1334
|
count++
|
|
1309
1335
|
}
|
|
1310
1336
|
for (let i = count; i < MAX_LIGHTS; i++) {
|
|
@@ -1314,6 +1340,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1314
1340
|
bias.push(0)
|
|
1315
1341
|
normalBias.push(0)
|
|
1316
1342
|
radius.push(1)
|
|
1343
|
+
masks.push(0)
|
|
1317
1344
|
}
|
|
1318
1345
|
// The shadow set rides with the lights. Per casting light i: its
|
|
1319
1346
|
// map slots as uShadowFirst[i] + uShadowCount[i] (0 = a receiving
|
|
@@ -1340,10 +1367,12 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1340
1367
|
let params: ShaderParams = {
|
|
1341
1368
|
uHemiSky: sky,
|
|
1342
1369
|
uHemiGround: ground,
|
|
1370
|
+
uHemiMask: hemiMask,
|
|
1343
1371
|
uLightCount: count,
|
|
1344
1372
|
uLightType: types,
|
|
1345
1373
|
uLightColor: colors,
|
|
1346
1374
|
uLightParams: coneFalloff,
|
|
1375
|
+
uLightMask: masks,
|
|
1347
1376
|
uShadowFirst: first,
|
|
1348
1377
|
uShadowCount: counts,
|
|
1349
1378
|
uShadowBias: bias,
|
|
@@ -1484,8 +1513,12 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1484
1513
|
* m.castShadow). Re-evaluated per mesh by _setCast; also picks the
|
|
1485
1514
|
* caster's own shadow material variant over the depth override. */
|
|
1486
1515
|
shadowFilter: ((mesh: Mesh) => boolean) | null
|
|
1516
|
+
/** The casting light a SHADOW view renders for (null otherwise):
|
|
1517
|
+
* its `layers` compose the view's mask with the scene's, so a mesh
|
|
1518
|
+
* the light cannot see casts nothing into its map. */
|
|
1519
|
+
shadowLight: CastingLight | null
|
|
1487
1520
|
/** Layer mask: the view draws the meshes whose `layers` intersect
|
|
1488
|
-
* it. A shadow view's
|
|
1521
|
+
* it. A shadow view's is the scene's masked by its light's layers. */
|
|
1489
1522
|
mask: number
|
|
1490
1523
|
/** Names the view set itself (view.setParams, the fog option): the
|
|
1491
1524
|
* scene's fan-out skips them so a view override survives scene-wide
|
|
@@ -1524,6 +1557,21 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1524
1557
|
// environment (the faces, or the chain prefiltered from them): it
|
|
1525
1558
|
// samples the placeholder instead of itself.
|
|
1526
1559
|
let ownsEnvironment = (t: TextureId) => environment !== null && views.some(v => v.texture === t && v.probeCube === environment)
|
|
1560
|
+
// A mesh's own environment on one entry (Mesh.environment, applied
|
|
1561
|
+
// where the material reflects - Material.env): the same uEnv* names
|
|
1562
|
+
// the scene writes per target, written per entry, and the entry's
|
|
1563
|
+
// beat the target's (specific beats general). `probeCube` guards a
|
|
1564
|
+
// probe against sampling its own cube through an override - the
|
|
1565
|
+
// black placeholder stands in, writeLights' target-level rule at
|
|
1566
|
+
// entry grain (the engine rejects the same-pass feedback outright).
|
|
1567
|
+
let envOverride = (mesh: Mesh, material: Material, probeCube: TextureId | null): { params: ShaderParams; cube: TextureId | null } | null => {
|
|
1568
|
+
if (mesh.environment === undefined || material.env !== true) return null
|
|
1569
|
+
let e = mesh.environment
|
|
1570
|
+
return {
|
|
1571
|
+
params: environmentParams(e, "setEnvironment"),
|
|
1572
|
+
cube: e === null ? null : probeCube !== null && probeCube === e.cube ? envPlaceholder : e.cube,
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1527
1575
|
// The mesh's entry in the scene's OWN target - what mesh._entry is.
|
|
1528
1576
|
// Created when the scene mask admits the mesh, dropped when it stops:
|
|
1529
1577
|
// the same lifecycle a view entry has, so `_buffers` (not `_entry`) is
|
|
@@ -1539,18 +1587,32 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1539
1587
|
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1540
1588
|
// uModel.
|
|
1541
1589
|
let morph = morphEntry(mesh.material, mesh)
|
|
1542
|
-
|
|
1590
|
+
let env = envOverride(mesh, mesh.material, null)
|
|
1591
|
+
let params = morph === null ? mesh._params : { ...morph.params, ...mesh._params }
|
|
1592
|
+
if (env !== null) params = { ...env.params, ...params }
|
|
1593
|
+
let textures = entryTextures(mesh.material, mesh, morph)
|
|
1594
|
+
if (env !== null && env.cube !== null) textures = { ...textures, uEnv: env.cube }
|
|
1595
|
+
// The scene entry is the ONE ordered entry of an ordered population
|
|
1596
|
+
// (one buffer holds one order): views bind the same buffers and draw
|
|
1597
|
+
// them in the order the scene's camera decides.
|
|
1598
|
+
let order = inst?.order ?? null
|
|
1599
|
+
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(mesh.material, params, mesh.layers), {
|
|
1543
1600
|
buffers: [...bufs.buffers, ...(inst !== null ? instanceBinding(mesh.material, inst) : [])],
|
|
1544
1601
|
indexBuffer: bufs.index,
|
|
1545
1602
|
indexFormat: bufs.indexFormat,
|
|
1546
1603
|
...meshRange(mesh),
|
|
1547
|
-
textures
|
|
1604
|
+
textures,
|
|
1548
1605
|
instanceCount: 0,
|
|
1606
|
+
...(order !== null ? { instanceOrder: entryInstanceOrder(order, mesh.material, bufs.buffers.length) } : {}),
|
|
1549
1607
|
label: drawLabel(mesh),
|
|
1550
1608
|
})
|
|
1551
1609
|
// The core turns the entry on (with the world matrix) at the next
|
|
1552
1610
|
// flush, and off again whenever the node or an ancestor hides.
|
|
1553
|
-
spatial.bindDraw(mesh._node!, texture, mesh._entry, drawBinding(mesh.material, mesh, inst))
|
|
1611
|
+
spatial.bindDraw(mesh._node!, texture, mesh._entry, drawBinding(mesh.material, mesh, inst, order !== null && order.position))
|
|
1612
|
+
// An ordered population republishes whole with its fresh entry: the
|
|
1613
|
+
// engine-side order registry was just (re)built empty, and only a
|
|
1614
|
+
// full publish seeds the gathered buffer and the retained copy.
|
|
1615
|
+
if (order !== null) markLiveRecords(mesh)
|
|
1554
1616
|
}
|
|
1555
1617
|
let detachScene = (mesh: Mesh) => {
|
|
1556
1618
|
if (mesh._entry === null) return
|
|
@@ -1591,12 +1653,18 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1591
1653
|
let bufs = mesh._buffers!
|
|
1592
1654
|
let morph = morphEntry(material, mesh)
|
|
1593
1655
|
let params = v.override !== null ? (morph === null ? null : morph.params) : morph === null ? mesh._params : { ...morph.params, ...mesh._params }
|
|
1594
|
-
|
|
1656
|
+
// The mesh's own environment rides its own material only (an
|
|
1657
|
+
// override view follows its target, like mesh._params).
|
|
1658
|
+
let env = v.override === null ? envOverride(mesh, material, v.probeCube) : null
|
|
1659
|
+
if (env !== null) params = { ...env.params, ...params }
|
|
1660
|
+
let textures = entryTextures(material, mesh, morph)
|
|
1661
|
+
if (env !== null && env.cube !== null) textures = { ...textures, uEnv: env.cube }
|
|
1662
|
+
let entry = addDraw(v.texture, material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(material, params, mesh.layers), {
|
|
1595
1663
|
buffers: [...bufs.buffers, ...(inst !== null ? instanceBinding(material, inst) : [])],
|
|
1596
1664
|
indexBuffer: bufs.index,
|
|
1597
1665
|
indexFormat: bufs.indexFormat,
|
|
1598
1666
|
...meshRange(mesh),
|
|
1599
|
-
textures
|
|
1667
|
+
textures,
|
|
1600
1668
|
instanceCount: 0,
|
|
1601
1669
|
label: drawLabel(mesh),
|
|
1602
1670
|
})
|
|
@@ -1642,7 +1710,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1642
1710
|
updateCamera(cam, { position, target: [position[0] + d[0], position[1] + d[1], position[2] + d[2]], up: PROBE_FACE_UP[face] })
|
|
1643
1711
|
ensureCamera(cam, size, size)
|
|
1644
1712
|
cam.pending = false
|
|
1645
|
-
setTargetParams(target, cameraParams(cam))
|
|
1713
|
+
setTargetParams(target, cameraParams(cam, size, size))
|
|
1646
1714
|
perFace?.(face)
|
|
1647
1715
|
if (chain && face < last) renderTarget(target, face, 0)
|
|
1648
1716
|
else renderTarget(target, face)
|
|
@@ -1654,11 +1722,14 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1654
1722
|
// entry per mesh the filter admits.
|
|
1655
1723
|
let makeView = (
|
|
1656
1724
|
vopts: ViewOptions,
|
|
1657
|
-
|
|
1725
|
+
shadowLight: CastingLight | null,
|
|
1658
1726
|
cube: number | null = null,
|
|
1659
1727
|
mipmap = false,
|
|
1660
1728
|
sky = false,
|
|
1661
1729
|
): ViewRecord => {
|
|
1730
|
+
// Non-null shadowLight makes this a SHADOW view: the light's map
|
|
1731
|
+
// tile, drawing the casters its layers admit.
|
|
1732
|
+
let shadowFilter = shadowLight === null ? null : (m: Mesh) => m.castShadow
|
|
1662
1733
|
let override = vopts.overrideMaterial ?? null
|
|
1663
1734
|
if (override !== null) {
|
|
1664
1735
|
for (let mesh of meshes) if (mesh._instances === null) checkLayout(override, mesh.geometry, "View override material")
|
|
@@ -1666,6 +1737,12 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1666
1737
|
let tiled = vopts.into !== undefined
|
|
1667
1738
|
if (tiled && vopts.resolve !== undefined) throw new Error("createView: a tiled view (into) has no resolve of its own - resolve the atlas")
|
|
1668
1739
|
if (tiled && vopts.bloom !== undefined) throw new Error("createView: a tiled view (into) has no resolve to bloom on - resolve the atlas")
|
|
1740
|
+
if (display) {
|
|
1741
|
+
// The views of a display-space scene are display-referred like it:
|
|
1742
|
+
// no resolve of their own, and nothing that would act past one.
|
|
1743
|
+
if (vopts.resolve !== undefined) throw new Error('createView: resolve does not compose with blendSpace "display" (the view buffer is the displayed image)')
|
|
1744
|
+
if (vopts.bloom !== undefined) throw new Error('createView: bloom does not compose with blendSpace "display" (there is no resolve stage)')
|
|
1745
|
+
}
|
|
1669
1746
|
let viewLabel = vopts.label ?? label + (cube !== null ? "-probe" : "-view")
|
|
1670
1747
|
let buffer =
|
|
1671
1748
|
cube !== null
|
|
@@ -1675,15 +1752,17 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1675
1752
|
// solid angle: a generated chain, refreshed per face render.
|
|
1676
1753
|
mipmap,
|
|
1677
1754
|
format: bufferFormat(),
|
|
1678
|
-
clearColor: bufferClear(vopts.clearColor),
|
|
1755
|
+
clearColor: bufferClear(vopts.clearColor, display),
|
|
1679
1756
|
label: viewLabel,
|
|
1680
1757
|
autoFree: false,
|
|
1681
1758
|
})
|
|
1682
1759
|
: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1683
1760
|
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1684
|
-
format: tiled ? undefined : bufferFormat(),
|
|
1685
|
-
clearColor: bufferClear(vopts.clearColor),
|
|
1761
|
+
format: tiled ? undefined : display ? "rgba8" : bufferFormat(),
|
|
1762
|
+
clearColor: bufferClear(vopts.clearColor, display),
|
|
1686
1763
|
samples: vopts.samples,
|
|
1764
|
+
filter: display ? vopts.filter : undefined,
|
|
1765
|
+
wrap: display ? vopts.wrap : undefined,
|
|
1687
1766
|
label: viewLabel,
|
|
1688
1767
|
autoFree: false,
|
|
1689
1768
|
into: vopts.into,
|
|
@@ -1701,8 +1780,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1701
1780
|
texture: buffer,
|
|
1702
1781
|
// A shadow view is depth only, a probe's faces are read as a cube,
|
|
1703
1782
|
// a tile is the atlas's: none of them displays, so none resolves.
|
|
1783
|
+
// A display-space view's buffer is the displayed image itself.
|
|
1704
1784
|
resolve:
|
|
1705
|
-
cube === null && shadowFilter === null && !tiled
|
|
1785
|
+
cube === null && shadowFilter === null && !tiled && !display
|
|
1706
1786
|
? makeResolve(buffer, vopts.width, vopts.height, vopts.resolve, { filter: vopts.filter, wrap: vopts.wrap }, viewLabel)
|
|
1707
1787
|
: null,
|
|
1708
1788
|
ownBloom: vopts.bloom !== undefined,
|
|
@@ -1713,7 +1793,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1713
1793
|
skipped: new Set(),
|
|
1714
1794
|
skippedReported: false,
|
|
1715
1795
|
shadowFilter,
|
|
1716
|
-
|
|
1796
|
+
shadowLight,
|
|
1797
|
+
mask: shadowLight !== null ? (sceneMask & shadowLight.layers) >>> 0 : checkMask(vopts.layers ?? 1, "createView"),
|
|
1717
1798
|
ownNames: new Set(),
|
|
1718
1799
|
camera: makeCamera(),
|
|
1719
1800
|
entries: new Map(),
|
|
@@ -1815,7 +1896,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1815
1896
|
let cameraMoved = camera.pending
|
|
1816
1897
|
if (camera.pending) {
|
|
1817
1898
|
camera.pending = false
|
|
1818
|
-
setTargetParams(texture, cameraParams(camera))
|
|
1899
|
+
setTargetParams(texture, cameraParams(camera, width, height))
|
|
1819
1900
|
setView(texture, camera)
|
|
1820
1901
|
}
|
|
1821
1902
|
shadowSys.placeCameras(cameraMoved)
|
|
@@ -1823,7 +1904,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1823
1904
|
ensureCamera(v.camera, v.width, v.height)
|
|
1824
1905
|
if (v.camera.pending) {
|
|
1825
1906
|
v.camera.pending = false
|
|
1826
|
-
setTargetParams(v.texture, cameraParams(v.camera))
|
|
1907
|
+
setTargetParams(v.texture, cameraParams(v.camera, v.width, v.height))
|
|
1827
1908
|
setView(v.texture, v.camera)
|
|
1828
1909
|
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
1829
1910
|
}
|
|
@@ -1900,7 +1981,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1900
1981
|
_setLayers(mesh) {
|
|
1901
1982
|
if (mesh._buffers === null || disposed) return
|
|
1902
1983
|
if (mesh._node !== null) spatial.setLayers(mesh._node, mesh.layers)
|
|
1903
|
-
|
|
1984
|
+
// An instanced mesh's instance nodes follow its mask; a record mesh
|
|
1985
|
+
// has none.
|
|
1986
|
+
if (mesh._instances !== null && mesh._instances.nodes !== null) {
|
|
1904
1987
|
for (let n of instanceGroup(mesh as InstancedMesh)) spatial.setLayers(n, mesh.layers)
|
|
1905
1988
|
}
|
|
1906
1989
|
if ((mesh.layers & sceneMask) !== 0) attachScene(mesh)
|
|
@@ -1909,12 +1992,34 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1909
1992
|
if ((mesh.layers & v.mask) !== 0) attachView(v, mesh)
|
|
1910
1993
|
else detachView(v, mesh)
|
|
1911
1994
|
}
|
|
1995
|
+
// The surviving lit entries follow the membership too (uLayers, the
|
|
1996
|
+
// mask lightVector tests; a fresh attach above was seeded with it).
|
|
1997
|
+
let layered: ShaderParams = { uLayers: mesh.layers | 0 }
|
|
1998
|
+
if (mesh._entry !== null && mesh.material.layered === true) setDrawParams(texture, mesh._entry, layered)
|
|
1999
|
+
for (let v of views) {
|
|
2000
|
+
let entry = v.entries.get(mesh)
|
|
2001
|
+
if (entry !== undefined && viewMaterial(v, mesh).layered === true) setDrawParams(v.texture, entry, layered)
|
|
2002
|
+
}
|
|
1912
2003
|
hooks._schedule()
|
|
1913
2004
|
},
|
|
1914
2005
|
_lightChanged() {
|
|
1915
2006
|
lightsDirty = true
|
|
1916
2007
|
hooks._schedule()
|
|
1917
2008
|
},
|
|
2009
|
+
_lightLayersChanged(light) {
|
|
2010
|
+
if (disposed || light.type === "hemisphere") return
|
|
2011
|
+
// The light's shadow views re-filter their casters by the new mask;
|
|
2012
|
+
// the tiles and cameras stay (the uniform rewrite is _lightChanged's).
|
|
2013
|
+
for (let v of views) {
|
|
2014
|
+
if (v.shadowLight !== light) continue
|
|
2015
|
+
v.mask = (sceneMask & light.layers) >>> 0
|
|
2016
|
+
for (let mesh of meshes) {
|
|
2017
|
+
if ((mesh.layers & v.mask) !== 0) attachView(v, mesh)
|
|
2018
|
+
else detachView(v, mesh)
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
hooks._schedule()
|
|
2022
|
+
},
|
|
1918
2023
|
_attach(mesh) {
|
|
1919
2024
|
if (disposed) return
|
|
1920
2025
|
validateGeometry(mesh.geometry)
|
|
@@ -1939,6 +2044,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1939
2044
|
)
|
|
1940
2045
|
}
|
|
1941
2046
|
if (inst !== null) checkInstancePairing(mesh.material, inst, "Mesh material")
|
|
2047
|
+
// An ordered population's key stream must be among the entry's
|
|
2048
|
+
// buffers, whatever material the mesh wears now - errors here, at
|
|
2049
|
+
// add(), like the pairing above.
|
|
2050
|
+
if (inst !== null) checkOrderPairing(mesh.material, inst, "Mesh material")
|
|
1942
2051
|
// An anchored population's records are relative to the anchor, so
|
|
1943
2052
|
// it must be an ancestor (the core's contract: an anchor move then
|
|
1944
2053
|
// restages every record); anything else binds records that never
|
|
@@ -2075,7 +2184,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2075
2184
|
// The core composes the count with the visibility switch: a hidden
|
|
2076
2185
|
// entry stays at 0 and the unhide restores the new count.
|
|
2077
2186
|
if (mesh._buffers !== null && mesh._node !== null && !disposed && mesh._instances !== null) {
|
|
2078
|
-
spatial.setDrawCount(mesh._node, mesh._instances
|
|
2187
|
+
spatial.setDrawCount(mesh._node, drawCount(mesh._instances))
|
|
2079
2188
|
}
|
|
2080
2189
|
},
|
|
2081
2190
|
_setBuffer(mesh) {
|
|
@@ -2194,7 +2303,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2194
2303
|
}
|
|
2195
2304
|
|
|
2196
2305
|
let scene: Scene = {
|
|
2197
|
-
texture: resolve.target,
|
|
2306
|
+
texture: resolve === null ? texture : resolve.target,
|
|
2198
2307
|
hdrTexture: texture,
|
|
2199
2308
|
depthTexture: depthMode === "texture" ? depthTexture(texture) : null,
|
|
2200
2309
|
root,
|
|
@@ -2208,7 +2317,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2208
2317
|
width = w
|
|
2209
2318
|
height = h
|
|
2210
2319
|
setTargetSize(texture, w, h)
|
|
2211
|
-
resizeResolve(resolve, w, h)
|
|
2320
|
+
if (resolve !== null) resizeResolve(resolve, w, h)
|
|
2212
2321
|
camera.dirty = true
|
|
2213
2322
|
hooks._schedule()
|
|
2214
2323
|
},
|
|
@@ -2217,7 +2326,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2217
2326
|
if (disposed) return
|
|
2218
2327
|
Object.assign(sceneParams, params)
|
|
2219
2328
|
setTargetParams(texture, params)
|
|
2220
|
-
setTargetParams(resolve.target, params)
|
|
2329
|
+
if (resolve !== null) setTargetParams(resolve.target, params)
|
|
2221
2330
|
for (let v of views) {
|
|
2222
2331
|
// A view's own names (view.setParams, its fog) win over the
|
|
2223
2332
|
// scene-wide fan-out.
|
|
@@ -2251,13 +2360,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2251
2360
|
if ((mesh.layers & mask) !== 0) attachScene(mesh)
|
|
2252
2361
|
else detachScene(mesh)
|
|
2253
2362
|
}
|
|
2254
|
-
// Shadow views follow the scene's mask
|
|
2255
|
-
// must not darken it.
|
|
2363
|
+
// Shadow views follow the scene's mask (what the scene cannot see
|
|
2364
|
+
// must not darken it), narrowed by their light's own layers.
|
|
2256
2365
|
for (let v of views) {
|
|
2257
|
-
if (v.
|
|
2258
|
-
v.mask = mask
|
|
2366
|
+
if (v.shadowLight === null) continue
|
|
2367
|
+
v.mask = (mask & v.shadowLight.layers) >>> 0
|
|
2259
2368
|
for (let mesh of meshes) {
|
|
2260
|
-
if ((mesh.layers & mask) !== 0) attachView(v, mesh)
|
|
2369
|
+
if ((mesh.layers & v.mask) !== 0) attachView(v, mesh)
|
|
2261
2370
|
else detachView(v, mesh)
|
|
2262
2371
|
}
|
|
2263
2372
|
}
|
|
@@ -2292,7 +2401,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2292
2401
|
},
|
|
2293
2402
|
setEnvironment(env) {
|
|
2294
2403
|
if (disposed) return
|
|
2295
|
-
let params = environmentParams(env)
|
|
2404
|
+
let params = environmentParams(env, "scene.setEnvironment")
|
|
2296
2405
|
environment = env === null ? null : env.cube
|
|
2297
2406
|
let cube = environment ?? envPlaceholder
|
|
2298
2407
|
// A probe's own faces never sample the probe (see writeLights).
|
|
@@ -2300,21 +2409,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2300
2409
|
scene.setParams(params)
|
|
2301
2410
|
},
|
|
2302
2411
|
setToneMapping(mode) {
|
|
2412
|
+
if (resolve === null) throw new Error('scene.setToneMapping: a blendSpace "display" scene has no resolve stage to tone map in')
|
|
2303
2413
|
let code = TONE_MAPPING_CODE[mode]
|
|
2304
2414
|
if (code === undefined) throw new Error('scene.setToneMapping: expected "none", "aces", "agx" or "neutral", got ' + mode)
|
|
2305
2415
|
scene.setParams({ uToneMapping: code })
|
|
2306
2416
|
},
|
|
2307
2417
|
setExposure(exposure) {
|
|
2418
|
+
if (resolve === null) throw new Error('scene.setExposure: a blendSpace "display" scene has no resolve stage to expose in')
|
|
2308
2419
|
if (!Number.isFinite(exposure) || exposure < 0) throw new Error("scene.setExposure: expected a finite number >= 0, got " + exposure)
|
|
2309
2420
|
scene.setParams({ uExposure: exposure })
|
|
2310
2421
|
},
|
|
2311
2422
|
setResolve(r) {
|
|
2312
2423
|
if (disposed) return
|
|
2424
|
+
if (resolve === null) throw new Error('scene.setResolve: a blendSpace "display" scene has no resolve stage')
|
|
2313
2425
|
replaceResolve(resolve, texture, r)
|
|
2314
2426
|
hooks._schedule()
|
|
2315
2427
|
},
|
|
2316
2428
|
setBloom(bloom) {
|
|
2317
2429
|
if (disposed) return
|
|
2430
|
+
if (resolve === null) throw new Error('scene.setBloom: a blendSpace "display" scene has no resolve stage to bloom on')
|
|
2318
2431
|
sceneBloom = bloom
|
|
2319
2432
|
setResolveBloom(resolve, texture, width, height, bloom)
|
|
2320
2433
|
for (let v of views) if (v.resolve !== null && !v.ownBloom) setResolveBloom(v.resolve, v.texture, v.width, v.height, bloom)
|
|
@@ -2525,6 +2638,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2525
2638
|
},
|
|
2526
2639
|
createReflectionProbe(popts) {
|
|
2527
2640
|
if (disposed) throw new Error("createReflectionProbe: the scene is disposed")
|
|
2641
|
+
if (display) throw new Error('createReflectionProbe: a blendSpace "display" scene renders encoded pixels, which a probe would then sample as linear radiance')
|
|
2528
2642
|
let { cube, setPosition, update, dispose } = makeProbe(popts, checkMask(popts.layers ?? 1, "createReflectionProbe"))
|
|
2529
2643
|
return { cube, setPosition, update, dispose }
|
|
2530
2644
|
},
|
|
@@ -2559,7 +2673,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2559
2673
|
// Drain the zeroed direction slots the teardown queued while the
|
|
2560
2674
|
// targets still exist; afterwards their groups are gone.
|
|
2561
2675
|
spatial.flush()
|
|
2562
|
-
disposeResolve(resolve)
|
|
2676
|
+
if (resolve !== null) disposeResolve(resolve)
|
|
2563
2677
|
destroyTexture(texture)
|
|
2564
2678
|
for (let v of views.slice()) disposeView(v)
|
|
2565
2679
|
shadowSys.dispose()
|
|
@@ -2646,10 +2760,17 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2646
2760
|
scene.setParams({ uFogColor: [0, 0, 0], uFogNear: 0, uFogInv: 0, uFogDensity: 0, uFogHeight: 0, uFogHeightFalloff: 0 })
|
|
2647
2761
|
// Likewise the environment set starts at "none" (uEnvOn 0), so a
|
|
2648
2762
|
// reflective material has coverage before setEnvironment.
|
|
2649
|
-
scene.setParams(environmentParams(null))
|
|
2763
|
+
scene.setParams(environmentParams(null, "scene.setEnvironment"))
|
|
2650
2764
|
// And the resolve at its defaults (exposure 1, no tone mapping).
|
|
2651
2765
|
scene.setParams({ uExposure: 1, uToneMapping: TONE_MAPPING_CODE.none })
|
|
2652
2766
|
if (opts?.fog !== undefined) scene.setFog(opts.fog)
|
|
2767
|
+
// The one shader-visible trace of the mode: stock fragments (and any
|
|
2768
|
+
// custom one composing BLEND_SPACE) branch on it; views inherit it
|
|
2769
|
+
// through the scene-params seed. Every scene writes it, 0 included: a
|
|
2770
|
+
// program is shared across scenes and its uniform keeps the last
|
|
2771
|
+
// drawn value, so a linear scene must overwrite what a display-space
|
|
2772
|
+
// one left behind.
|
|
2773
|
+
scene.setParams({ uBlendSpace: display ? 1 : 0 })
|
|
2653
2774
|
if (opts?.toneMapping !== undefined) scene.setToneMapping(opts.toneMapping)
|
|
2654
2775
|
if (opts?.exposure !== undefined) scene.setExposure(opts.exposure)
|
|
2655
2776
|
if (opts?.bloom !== undefined) scene.setBloom(opts.bloom)
|