@solidrt/3d 0.0.55 → 0.0.56

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.
Files changed (54) hide show
  1. package/AGENTS.md +1083 -183
  2. package/README.md +15 -7
  3. package/examples/README.md +79 -5
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +250 -0
  7. package/examples/first-person.tsx +119 -0
  8. package/examples/fleet.tsx +194 -0
  9. package/examples/fog.tsx +2 -3
  10. package/examples/instanced.tsx +72 -32
  11. package/examples/lamps.tsx +92 -0
  12. package/examples/lit.tsx +3 -5
  13. package/examples/materials.tsx +150 -0
  14. package/examples/model-load.tsx +95 -0
  15. package/examples/model.glb +0 -0
  16. package/examples/model.tsx +32 -6
  17. package/examples/pick.tsx +2 -4
  18. package/examples/probe.tsx +101 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +32 -3
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +86 -0
  24. package/examples/skybox.tsx +128 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +132 -0
  27. package/examples/sweep-paths.tsx +2 -4
  28. package/package.json +3 -3
  29. package/src/camera.ts +148 -0
  30. package/src/clip.ts +96 -0
  31. package/src/collision.ts +199 -0
  32. package/src/color.ts +32 -0
  33. package/src/components.tsx +763 -82
  34. package/src/environment-bake.ts +492 -0
  35. package/src/environment.ts +325 -0
  36. package/src/first-person.ts +306 -0
  37. package/src/geometry.ts +6 -2
  38. package/src/glsl.ts +1343 -82
  39. package/src/gltf.ts +432 -112
  40. package/src/index.ts +57 -9
  41. package/src/light.ts +383 -0
  42. package/src/material.ts +667 -232
  43. package/src/math.ts +110 -0
  44. package/src/mesh.ts +728 -0
  45. package/src/mixer.ts +476 -0
  46. package/src/model-file.ts +80 -15
  47. package/src/model.ts +204 -35
  48. package/src/node.ts +506 -0
  49. package/src/orbit.ts +97 -48
  50. package/src/scene-pointer.ts +166 -0
  51. package/src/scene-shadows.ts +471 -0
  52. package/src/scene.ts +1242 -1557
  53. package/tools/environment.ts +69 -0
  54. package/tools/model.ts +4 -3
@@ -0,0 +1,471 @@
1
+ // The shadow half of a scene instance: the atlas every casting light's
2
+ // map is a tile of, the per-caster shadow views (made through the
3
+ // scene's own view machinery), and the map cameras (box, spot cone,
4
+ // cascades, point faces). One system per scene, built by makeShadowSystem with the
5
+ // scene's seams as `deps` - the deps type IS the coupling, documented.
6
+ // The receiving side (uShadowRect/uShadowFirst/uShadowMatrix writes onto
7
+ // targets) stays in scene.ts with the light fan-out; it reads the slots
8
+ // through forEachShadowSlot and the atlas through atlas().
9
+
10
+ import { createDrawTarget, depthTexture, destroyTexture, limits, setTargetRect, setTargetSize } from "@solidrt/core/gpu"
11
+ import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
12
+ import { cascadeSplit, copy, frustumSliceSphere, lookAt as lookAtMatrix, mat4, snapToGrid, transformVector } from "./math.ts"
13
+ import type { Mat4, Vec3 } from "./math.ts"
14
+ import { MAX_SHADOW_MAPS } from "./glsl.ts"
15
+ import { shadowDepthMaterial } from "./material.ts"
16
+ import type { Material } from "./material.ts"
17
+ import { updateCamera } from "./camera.ts"
18
+ import type { Camera } from "./camera.ts"
19
+ import { worldInto } from "./node.ts"
20
+ import type { CastingLight, Light } from "./light.ts"
21
+ import type { Mesh } from "./mesh.ts"
22
+
23
+ // How a cascaded light slices the camera range: 0 uniform, 1
24
+ // logarithmic, halfway the "practical" split (near slices small, far
25
+ // ones not starved).
26
+ const CASCADE_SPLIT_LAMBDA = 0.5
27
+ // |y| of a light direction above this is straight up or down, where
28
+ // world up cannot serve as the shadow map's roll reference.
29
+ const VERTICAL_LIGHT = 0.99
30
+ // A perspective shadow camera's far plane (spot cone, point face) when
31
+ // the light has no `distance` cutoff - the directional box default.
32
+ const PERSPECTIVE_SHADOW_FAR = 500
33
+
34
+ // Degrees added to a point-light face map's 90-degree fov: the guard
35
+ // band that keeps a face-seam fragment's occluder inside the map it
36
+ // samples (without it every seam shows a lit slit where each map's
37
+ // coverage ends at its edge). Costs a sliver of map resolution.
38
+ const POINT_SHADOW_FOV_GUARD = 4
39
+
40
+ // A point light's six face frusta in slot order (+X, -X, +Y, -Y, +Z,
41
+ // -Z - SHADOW_LOOKUP's dominant-axis select counts on it), each a
42
+ // square 90-degree perspective map from the light's world position.
43
+ // World-axis aligned: a point light has no direction and rotation does
44
+ // not matter. The up only orients the map inside its tile, which the
45
+ // lookup reads back through the same matrix, so any non-colinear
46
+ // choice works.
47
+ const POINT_FACES: { dir: Vec3; up: Vec3 }[] = [
48
+ { dir: [1, 0, 0], up: [0, 1, 0] },
49
+ { dir: [-1, 0, 0], up: [0, 1, 0] },
50
+ { dir: [0, 1, 0], up: [0, 0, 1] },
51
+ { dir: [0, -1, 0], up: [0, 0, 1] },
52
+ { dir: [0, 0, 1], up: [0, 1, 0] },
53
+ { dir: [0, 0, -1], up: [0, 1, 0] },
54
+ ]
55
+
56
+ const IDENTITY = mat4()
57
+
58
+ /** One tile's place in the atlas, in texels. */
59
+ export type ShadowRect = { x: number; y: number; width: number; height: number }
60
+
61
+ /** What the shadow system needs of a scene view: the tile target and the
62
+ * camera it renders the map from. The scene's ViewRecord satisfies it;
63
+ * the system never sees the rest. */
64
+ export type ShadowView = {
65
+ texture: TextureId
66
+ width: number
67
+ height: number
68
+ camera: Camera
69
+ }
70
+
71
+ /** The scene seams one shadow system runs on - the coupling, spelled
72
+ * out. Arrays are the scene's own (read live, never copied); functions
73
+ * close over the scene instance. */
74
+ export type ShadowSystemDeps<V extends ShadowView> = {
75
+ /** The scene's light list, attach order = light index. */
76
+ lights: Light[]
77
+ /** The scene camera (cascades follow it). */
78
+ camera: Camera
79
+ /** The scene target's current size (the cascade fit's aspect). */
80
+ targetSize(): { width: number; height: number }
81
+ /** Base debug label ("scene" by default). */
82
+ label: string
83
+ /** Make one shadow view: a tile of `into` drawing `filter`'s meshes
84
+ * with the depth override (the scene's makeView). */
85
+ makeView(
86
+ vopts: {
87
+ width: number
88
+ height: number
89
+ into: TextureId
90
+ x: number
91
+ y: number
92
+ overrideMaterial: Material
93
+ clearColor: [number, number, number, number]
94
+ label: string
95
+ },
96
+ filter: (mesh: Mesh) => boolean,
97
+ ): V
98
+ disposeView(view: V): void
99
+ /** The light set changed shape: the scene owes a writeLights. */
100
+ markLightsDirty(): void
101
+ /** Schedule the scene's sync microtask. */
102
+ schedule(): void
103
+ }
104
+
105
+ export type ShadowSystem = {
106
+ /** The atlas while anything casts, else null. `texture` is the draw
107
+ * target; its depth texture is the receivers' uShadowAtlas binding. */
108
+ atlas(): { texture: TextureId; width: number; height: number } | null
109
+ /** Enumerate the dealt map slots in light order, a light's cascades
110
+ * consecutive and tightest first - the ONE enumeration the receiving
111
+ * side is dealt by, so uShadowFirst/uShadowCount/uShadowRect and the
112
+ * matrices always agree. `lightIndex` is the light's LIST index (its
113
+ * uShadow*[i] slot): every non-hemisphere light counts, casting or
114
+ * not, so it matches the lit loop's index. */
115
+ forEachShadowSlot(fn: (slot: number, lightIndex: number, cascade: number, rect: ShadowRect) => void): void
116
+ /** The light started casting (attach, or castShadow flipped on). */
117
+ createShadow(light: CastingLight): void
118
+ /** The light stopped casting (detach, or castShadow flipped off).
119
+ * Safe when it never cast. */
120
+ destroyShadow(light: CastingLight): void
121
+ /** The light's castShadow/shadow options changed: rebuild, re-place or
122
+ * re-fit whatever the change touches (SceneHooks._shadowChanged). */
123
+ shadowChanged(light: CastingLight): void
124
+ /** Re-place every caster's map cameras from its light's current world
125
+ * matrix (each compares against the matrix it was last placed from, so
126
+ * a scene animating elsewhere rewrites nothing). Run per sync;
127
+ * `sceneCameraMoved` re-fits the cascaded lights too. */
128
+ placeCameras(sceneCameraMoved: boolean): void
129
+ /** The slot deal changed (writeLights) or a map camera wrote
130
+ * (a shadow view's pending camera): the uShadowMatrix array is owed. */
131
+ markMatricesDirty(): void
132
+ /** Write the uShadowMatrix array through `write` if owed - the
133
+ * matrices that render the maps are the ones receivers look up with. */
134
+ flushMatrices(write: (params: ShaderParams) => void): void
135
+ /** The uShadowAtlas binding while nothing casts: the depth texture of a
136
+ * one-texel target that renders nothing (its clear leaves depth 1, never
137
+ * shadowed). A real depth texture, because uShadowAtlas is a
138
+ * sampler2DShadow and the engine refuses a color texture behind a
139
+ * comparison sampler. Created on first use, owned by this system. */
140
+ placeholder(): TextureId
141
+ /** Drop the atlas, the placeholder and forget every caster. The shadow
142
+ * views die with the scene's own view teardown, not here. */
143
+ dispose(): void
144
+ }
145
+
146
+ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>): ShadowSystem {
147
+ // One per casting light: the views rendering its maps (one for a box
148
+ // or spot light, `shadow.cascades` for a cascaded one, tightest
149
+ // first) and `rects`, each tile's place in the atlas. `lastWorld` is
150
+ // the light's world matrix the cameras were last placed from; `dirty`
151
+ // forces a re-place (options changed).
152
+ type Shadow = { light: CastingLight; views: V[]; lastWorld: Mat4; dirty: boolean; rects: ShadowRect[] }
153
+ let shadows = new Map<CastingLight, Shadow>()
154
+ // The shadow atlas: ONE depth-texture target every casting light's map
155
+ // is a tile of, so N maps render as one pass and receivers sample one
156
+ // sampler through per-map rects (uShadowRect). Created with the first
157
+ // caster, destroyed with the last. Laid out as a grid of cells the
158
+ // largest mapSize wide, scaled down uniformly when that would exceed
159
+ // the device's texture size: tile size follows the budget.
160
+ let shadowAtlas: { texture: TextureId; width: number; height: number } | null = null
161
+ // The one-texel depth target behind placeholder(), made on first use.
162
+ let placeholderTarget: TextureId | null = null
163
+ let shadowLayout = (count: number, maxSize: number) => {
164
+ let cols = Math.ceil(Math.sqrt(count))
165
+ let rows = Math.ceil(count / cols)
166
+ let scale = Math.min(1, limits.maxTextureSize / (cols * maxSize), limits.maxTextureSize / (rows * maxSize))
167
+ let cell = Math.max(1, Math.floor(maxSize * scale))
168
+ return { cols, cell, scale, width: cols * cell, height: rows * cell }
169
+ }
170
+ // A caster's tile count: a directional light brings its cascades, a
171
+ // point light its six faces, a spot exactly one map.
172
+ let shadowTiles = (l: CastingLight): number =>
173
+ l.type === "directional" ? l.shadow.cascades : l.type === "point" ? POINT_FACES.length : 1
174
+ let eachSlot = (fn: (slot: number, lightIndex: number, shadow: Shadow, cascade: number) => void): void => {
175
+ let slot = 0
176
+ let i = 0
177
+ for (let light of deps.lights) {
178
+ if (light.type === "hemisphere") continue
179
+ let shadow = shadows.get(light)
180
+ if (shadow !== undefined) for (let c = 0; c < shadow.views.length; c++) fn(slot++, i, shadow, c)
181
+ i++
182
+ }
183
+ }
184
+ let forEachShadowSlot = (fn: (slot: number, lightIndex: number, cascade: number, rect: ShadowRect) => void): void => {
185
+ eachSlot((slot, i, shadow, c) => fn(slot, i, c, shadow.rects[c]!))
186
+ }
187
+ // Place every shadow tile for the current caster set plus `adding` (not
188
+ // yet in `shadows`; its rects are returned for the view creates), in
189
+ // light order, a light's cascades consecutive. Sizes the atlas, moves
190
+ // tiles whose place changed, and drops the atlas when nothing casts.
191
+ // The rects reach receivers through the next light rewrite.
192
+ let placeShadows = (adding: CastingLight | null): ShadowRect[] | null => {
193
+ let casters: CastingLight[] = []
194
+ for (let l of deps.lights) {
195
+ if (l.type !== "hemisphere" && (shadows.has(l) || l === adding)) casters.push(l)
196
+ }
197
+ if (adding !== null && !casters.includes(adding)) casters.push(adding)
198
+ deps.markLightsDirty()
199
+ if (casters.length === 0) {
200
+ if (shadowAtlas !== null) {
201
+ destroyTexture(shadowAtlas.texture)
202
+ shadowAtlas = null
203
+ }
204
+ return null
205
+ }
206
+ let maxSize = 1
207
+ let tiles = 0
208
+ for (let l of casters) {
209
+ maxSize = Math.max(maxSize, l.shadow.mapSize)
210
+ tiles += shadowTiles(l)
211
+ }
212
+ if (tiles > MAX_SHADOW_MAPS) {
213
+ throw new Error(
214
+ "The scene's shadow set is full: " + tiles + " maps over the " + MAX_SHADOW_MAPS +
215
+ "-slot budget (a cascaded light claims shadow.cascades slots, a point light six)",
216
+ )
217
+ }
218
+ let lay = shadowLayout(tiles, maxSize)
219
+ if (shadowAtlas === null) {
220
+ shadowAtlas = {
221
+ texture: createDrawTarget(lay.width, lay.height, null, {
222
+ depth: "texture",
223
+ clearColor: [1, 1, 1, 1],
224
+ label: deps.label + "-shadow-atlas",
225
+ autoFree: false,
226
+ }),
227
+ width: lay.width,
228
+ height: lay.height,
229
+ }
230
+ } else if (shadowAtlas.width !== lay.width || shadowAtlas.height !== lay.height) {
231
+ setTargetSize(shadowAtlas.texture, lay.width, lay.height)
232
+ shadowAtlas.width = lay.width
233
+ shadowAtlas.height = lay.height
234
+ }
235
+ let placed: ShadowRect[] | null = null
236
+ let k = 0
237
+ for (let l of casters) {
238
+ let size = Math.max(1, Math.floor(l.shadow.mapSize * lay.scale))
239
+ let shadow = shadows.get(l)
240
+ for (let c = 0; c < shadowTiles(l); c++, k++) {
241
+ let rect: ShadowRect = { x: (k % lay.cols) * lay.cell, y: Math.floor(k / lay.cols) * lay.cell, width: size, height: size }
242
+ if (shadow === undefined) {
243
+ if (placed === null) placed = []
244
+ placed.push(rect)
245
+ continue
246
+ }
247
+ let r = shadow.rects[c]!
248
+ if (r.x === rect.x && r.y === rect.y && r.width === rect.width && r.height === rect.height) continue
249
+ shadow.rects[c] = rect
250
+ let view = shadow.views[c]!
251
+ setTargetRect(view.texture, rect)
252
+ view.width = rect.width
253
+ view.height = rect.height
254
+ // A tile's texel size moved: the cascade fit snaps to it.
255
+ shadow.dirty = true
256
+ }
257
+ }
258
+ return placed
259
+ }
260
+ // A shadow's views: one square tile of the shadow atlas per map drawing
261
+ // the casting meshes with the depth override from that map's frustum.
262
+ // The light rewrite writes the rects in the light's slots on every
263
+ // receiving target.
264
+ let createShadow = (light: CastingLight) => {
265
+ let rects = placeShadows(light)
266
+ if (rects === null || shadowAtlas === null) return
267
+ let atlas = shadowAtlas
268
+ let views = rects.map(rect =>
269
+ deps.makeView(
270
+ {
271
+ width: rect.width,
272
+ height: rect.height,
273
+ into: atlas.texture,
274
+ x: rect.x,
275
+ y: rect.y,
276
+ overrideMaterial: shadowDepthMaterial(),
277
+ clearColor: [1, 1, 1, 1],
278
+ label: deps.label + "-shadow",
279
+ },
280
+ m => m.castShadow,
281
+ ),
282
+ )
283
+ shadows.set(light, { light, views, lastWorld: mat4(), dirty: true, rects })
284
+ deps.markLightsDirty()
285
+ }
286
+ let destroyShadow = (light: CastingLight) => {
287
+ let shadow = shadows.get(light)
288
+ if (shadow === undefined) return
289
+ shadows.delete(light)
290
+ for (let v of shadow.views) deps.disposeView(v)
291
+ placeShadows(null)
292
+ deps.markLightsDirty()
293
+ deps.schedule()
294
+ }
295
+ // Place a shadow's cameras from its light's world matrix. A box light:
296
+ // at its world position, looking along its world direction, the light
297
+ // frustum as the orthographic extents. A spot light: the same pose
298
+ // with a perspective camera, fov = its cone. A point light: the six
299
+ // POINT_FACES frusta at its world position. Compared against the
300
+ // matrix it was last placed from, so a scene animating elsewhere
301
+ // rewrites nothing here. A cascaded light also follows the scene
302
+ // camera (`cameraMoved`).
303
+ let worldScratch = mat4()
304
+ let shadowDir: Vec3 = [0, 0, 0]
305
+ let cascadeScratch = mat4()
306
+ let cascadeCenter: Vec3 = [0, 0, 0]
307
+ let placeShadowCamera = (shadow: Shadow, cameraMoved: boolean) => {
308
+ let light = shadow.light
309
+ let m = worldInto(worldScratch, light)
310
+ let cascaded = light.type === "directional" && shadow.views.length > 1
311
+ if (!shadow.dirty && !(cascaded && cameraMoved) && m.every((x, i) => x === shadow.lastWorld[i])) return
312
+ shadow.dirty = false
313
+ copy(shadow.lastWorld, m)
314
+ if (light.type === "point") {
315
+ let far = light.distance > 0 ? light.distance : PERSPECTIVE_SHADOW_FAR
316
+ for (let f = 0; f < POINT_FACES.length; f++) {
317
+ let face = POINT_FACES[f]!
318
+ updateCamera(shadow.views[f]!.camera, {
319
+ position: [m[12], m[13], m[14]],
320
+ target: [m[12] + face.dir[0], m[13] + face.dir[1], m[14] + face.dir[2]],
321
+ up: face.up,
322
+ // A cube face spans exactly 90 degrees (square tile, aspect
323
+ // 1); the guard band widens each map past its face so a
324
+ // fragment at a face seam still finds its occluder inside the
325
+ // map (the lookup's dominant-axis select is unchanged, so the
326
+ // overlap is never sampled twice) - URP's fovBias.
327
+ fov: 90 + POINT_SHADOW_FOV_GUARD,
328
+ near: light.shadow.near,
329
+ far,
330
+ })
331
+ }
332
+ return
333
+ }
334
+ transformVector(shadowDir, m, light.direction)
335
+ let len = Math.hypot(shadowDir[0], shadowDir[1], shadowDir[2]) || 1
336
+ let d: Vec3 = [shadowDir[0] / len, shadowDir[1] / len, shadowDir[2] / len]
337
+ // A sun straight down is the common case and the degenerate one for
338
+ // world up: roll about z then (the map's orientation is invisible).
339
+ let up: Vec3 = Math.abs(d[1]) > VERTICAL_LIGHT ? [0, 0, 1] : [0, 1, 0]
340
+ if (light.type === "spot") {
341
+ // The cone's circular footprint inscribes exactly in the square
342
+ // map at fov = 2 * angle (aspect 1; both in degrees); everything
343
+ // past the cone gets no light, so no shadow is lost to the
344
+ // corners' margin.
345
+ updateCamera(shadow.views[0]!.camera, {
346
+ position: [m[12], m[13], m[14]],
347
+ target: [m[12] + d[0], m[13] + d[1], m[14] + d[2]],
348
+ up,
349
+ fov: light.angle * 2,
350
+ near: light.shadow.near,
351
+ far: light.distance > 0 ? light.distance : PERSPECTIVE_SHADOW_FAR,
352
+ })
353
+ return
354
+ }
355
+ if (!cascaded) {
356
+ let c = light.shadow.camera
357
+ updateCamera(shadow.views[0]!.camera, {
358
+ position: [m[12], m[13], m[14]],
359
+ target: [m[12] + d[0], m[13] + d[1], m[14] + d[2]],
360
+ up,
361
+ ortho: { left: c.left, right: c.right, top: c.top, bottom: c.bottom },
362
+ near: c.near,
363
+ far: c.far,
364
+ })
365
+ return
366
+ }
367
+ // Cascades: the scene camera's range near..far (far capped by
368
+ // shadow.distance) sliced by cascadeSplit, each slice's bounding
369
+ // sphere (frustumSliceSphere) as an orthographic box looking along
370
+ // the light, its centre snapped to the map's texel grid in light
371
+ // space (snapToGrid) so the shadow edges do not swim as the camera
372
+ // moves. The box reaches back toward the light by the whole range,
373
+ // so a caster outside the slice still casts into it.
374
+ let camera = deps.camera
375
+ let n = shadow.views.length
376
+ let near = camera.near
377
+ let far = Math.min(camera.far, light.shadow.distance ?? Infinity)
378
+ if (!(far > near)) far = near + 1
379
+ // The light's rotation only: rows are its right, up and back axes.
380
+ let basis = lookAtMatrix(cascadeScratch, [0, 0, 0], d, up)
381
+ let size = deps.targetSize()
382
+ let aspect = size.width / size.height
383
+ let zn = near
384
+ for (let c = 0; c < n; c++) {
385
+ let zf = cascadeSplit(near, far, c, n, CASCADE_SPLIT_LAMBDA)
386
+ let radius = frustumSliceSphere(cascadeCenter, camera, aspect, zn, zf)
387
+ zn = zf
388
+ let view = shadow.views[c]!
389
+ // A texel is 2r / mapSize world units.
390
+ snapToGrid(cascadeCenter, cascadeCenter, basis, (2 * radius) / view.width)
391
+ let back = radius + far
392
+ updateCamera(view.camera, {
393
+ position: [cascadeCenter[0] - d[0] * back, cascadeCenter[1] - d[1] * back, cascadeCenter[2] - d[2] * back],
394
+ target: cascadeCenter,
395
+ up,
396
+ ortho: { left: -radius, right: radius, top: radius, bottom: -radius },
397
+ near: 0,
398
+ far: back + radius,
399
+ })
400
+ }
401
+ }
402
+ // uShadowMatrix is one array param (the engine writes whole arrays), so
403
+ // any shadow camera move rewrites all MAX_SHADOW_MAPS matrices, identity
404
+ // in the slots that are not dealt.
405
+ let shadowMatrices: number[] = new Array(MAX_SHADOW_MAPS * 16).fill(0)
406
+ let shadowMatricesDirty = false
407
+ return {
408
+ atlas: () => shadowAtlas,
409
+ forEachShadowSlot,
410
+ createShadow,
411
+ destroyShadow,
412
+ shadowChanged(light) {
413
+ let shadow = shadows.get(light)
414
+ if (shadow !== undefined) {
415
+ if (!light.castShadow) {
416
+ destroyShadow(light)
417
+ return
418
+ }
419
+ // A cascade count change is a different view set: rebuild it. A
420
+ // mapSize change re-places every tile (the grid cell follows the
421
+ // largest map).
422
+ if (shadow.views.length !== shadowTiles(light)) {
423
+ destroyShadow(light)
424
+ createShadow(light)
425
+ return
426
+ }
427
+ placeShadows(null)
428
+ shadow.dirty = true
429
+ deps.markLightsDirty()
430
+ deps.schedule()
431
+ } else if (light.castShadow) {
432
+ createShadow(light)
433
+ }
434
+ },
435
+ placeCameras(sceneCameraMoved) {
436
+ for (let shadow of shadows.values()) placeShadowCamera(shadow, sceneCameraMoved)
437
+ },
438
+ markMatricesDirty() {
439
+ shadowMatricesDirty = true
440
+ },
441
+ flushMatrices(write) {
442
+ if (!shadowMatricesDirty) return
443
+ shadowMatricesDirty = false
444
+ let dealt = 0
445
+ eachSlot((slot, _i, shadow, c) => {
446
+ let m = shadow.views[c]!.camera.viewProj
447
+ for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = m[k]!
448
+ dealt = slot + 1
449
+ })
450
+ for (let slot = dealt; slot < MAX_SHADOW_MAPS; slot++) for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = IDENTITY[k]!
451
+ write({ uShadowMatrix: shadowMatrices })
452
+ },
453
+ placeholder() {
454
+ if (placeholderTarget === null) {
455
+ placeholderTarget = createDrawTarget(1, 1, null, { depth: "texture", autoFree: false, label: deps.label + "-shadow-none" })
456
+ }
457
+ return depthTexture(placeholderTarget)
458
+ },
459
+ dispose() {
460
+ shadows.clear()
461
+ if (shadowAtlas !== null) {
462
+ destroyTexture(shadowAtlas.texture)
463
+ shadowAtlas = null
464
+ }
465
+ if (placeholderTarget !== null) {
466
+ destroyTexture(placeholderTarget)
467
+ placeholderTarget = null
468
+ }
469
+ },
470
+ }
471
+ }