@solidrt/3d 0.0.63 → 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/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
- let drawBinding = (material: Material, mesh: Mesh, inst: { count: number } | null): BindDrawOptions => ({
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.count : 1,
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, premultiplied
699
- * linear light (half float where the device renders it, see
700
- * bufferFormat), sampler-only - the input of a radiance-reading pass (a
701
- * bloom chain) whose result a custom resolve adds back in. */
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 the
723
- * camera writes. One write per frame however many meshes read the name
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 linear
1126
- // light, what the buffer holds and the resolve tone maps.
1127
- function bufferClear(color: [number, number, number, number] | undefined): [number, number, number, number] | undefined {
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), the displayable id.
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 follows the scene'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
- mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(mesh.material, morph === null ? mesh._params : { ...morph.params, ...mesh._params }), {
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: entryTextures(mesh.material, mesh, morph),
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
- let entry = addDraw(v.texture, material.pipeline(mesh.geometry, geometryTopology(mesh.geometry)), entrySeed(material, params), {
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: entryTextures(material, mesh, morph),
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
- shadowFilter: ((mesh: Mesh) => boolean) | null,
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
- mask: shadowFilter !== null ? sceneMask : checkMask(vopts.layers ?? 1, "createView"),
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
- if (mesh._instances !== null) {
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.count)
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: what the scene cannot see
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.shadowFilter === null) continue
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)