@solidrt/3d 0.0.58 → 0.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. package/tools/model.ts +3 -1
package/src/profile.ts CHANGED
@@ -2,14 +2,11 @@
2
2
  // Profile is a closed simple polygon in the XY plane - bare [x, y] tuples
3
3
  // are sharp (creased) corners, tagged { p, smooth } points shade round -
4
4
  // and winding is normalized to CCW on use, so either authoring direction
5
- // works. fillet() and roundRect() produce smooth-tagged arc corners,
6
- // shape() fills a profile as a flat +z face in the shared vertex layout,
7
- // and triangulate() (the ear-clipping core behind every cap) is exported
8
- // for custom flat work. The swept-solid generators consuming this
9
- // vocabulary - extrude, lathe, sweep, tube - live in sweep.ts.
5
+ // works. fillet() and roundRect() produce smooth-tagged arc corners, and
6
+ // triangulate() (the ear-clipping core behind every cap) is exported for
7
+ // custom flat work. The generators consuming this vocabulary - polygon,
8
+ // extrude, lathe, sweep, tube - live in sweep.ts.
10
9
 
11
- import { packGeometry } from "./geometry.ts"
12
- import type { Geometry, GeometryOptions } from "./geometry.ts"
13
10
  import type { Vec2 } from "./math.ts"
14
11
 
15
12
  /** A profile point: `p` in profile space, `smooth` to share an averaged
@@ -291,21 +288,3 @@ export function roundRect(
291
288
  let corners: Vec2[] = [[-x, -y], [x, -y], [x, y], [-x, y]]
292
289
  return fillet(corners, radius, segments)
293
290
  }
294
-
295
- /**
296
- * The profile filled as a flat face in the XY plane facing +z - the
297
- * general case of circle()/ring() for arbitrary outlines. UVs map the
298
- * profile's bounding box to the unit square like plane(); rotate flat the
299
- * same way: `rotation={[-Math.PI / 2, 0, 0]}`.
300
- */
301
- export function shape(profile: Profile, options: GeometryOptions = {}): Geometry {
302
- let pts = normalizeProfile(profile)
303
- let { minX, maxY, w, h } = profileBounds(pts)
304
- let px = pts.map((p) => p.x)
305
- let py = pts.map((p) => p.y)
306
- let verts: number[] = []
307
- for (let p of pts) {
308
- verts.push(p.x, p.y, 0, 0, 0, 1, (p.x - minX) / w, (maxY - p.y) / h)
309
- }
310
- return packGeometry(verts, earClip(px, py), options)
311
- }
package/src/resolve.ts ADDED
@@ -0,0 +1,181 @@
1
+ // A target's RESOLVE: the rgba8 draw target the app displays, holding one
2
+ // covering-triangle entry that samples the buffer as `uScene` and writes
3
+ // display pixels (exposure, tone mapping, the encode - the RESOLVE set in
4
+ // glsl.ts). An ordinary auto target: it re-renders exactly when the buffer
5
+ // does, so a static scene still costs zero passes. The pipeline and
6
+ // program are the target's own (a custom source recompiles them). The
7
+ // stock bloom hangs on it: a chain of shader textures over the buffer
8
+ // (threshold, then blur across and down per round) at a fraction of the
9
+ // target's size, bound on the resolve target as `uBloom`.
10
+ import { addDraw, createDrawTarget, createShaderTexture, createTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, setDrawTextures, setTargetParams, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
11
+ import type { DrawId, FilterMode, ProgramId, RenderPipelineId, TextureBindings, TextureId, WrapMode } from "@solidrt/core/gpu"
12
+ import { BLOOM_BLUR, BLOOM_RESOLVE, BLOOM_THRESHOLD, DEFAULT_RESOLVE } from "./glsl.ts"
13
+ import { resolvePipeline } from "./material.ts"
14
+
15
+ /** A custom resolve (SceneOptions.resolve, scene.setResolve): the fragment
16
+ * source and the extra textures it declares (a chain's result), bound
17
+ * beside `uScene`. */
18
+ export type ResolveOptions = { source: string; textures?: TextureBindings }
19
+ /** What the `resolve` option takes: a source, ResolveOptions, or a
20
+ * function of the buffer id (`hdrTexture`) returning either - the form
21
+ * that builds a chain over the buffer before the resolve is made. */
22
+ export type ResolveInput = string | ResolveOptions | ((hdr: TextureId) => string | ResolveOptions)
23
+
24
+ /** The stock bloom (SceneOptions.bloom, scene.setBloom): radiance above
25
+ * `threshold` (default 1: what a white surface lit to 1 reaches, so only
26
+ * brighter blooms) is blurred over `radius` rounds (default 2) of a
27
+ * separable blur at a quarter of the target's size and added back with
28
+ * `intensity` (default 0.5). Three's UnrealBloomPass strength / radius /
29
+ * threshold, Unity's Bloom threshold / intensity / scatter, Godot's glow. */
30
+ export type BloomOptions = { threshold?: number; intensity?: number; radius?: number }
31
+
32
+ // The bloom defaults: only radiance above a fully lit white surface
33
+ // blooms, at half weight, blurred over two rounds.
34
+ const BLOOM_THRESHOLD_DEFAULT = 1
35
+ const BLOOM_INTENSITY_DEFAULT = 0.5
36
+ const BLOOM_RADIUS_DEFAULT = 2
37
+ // The chain renders at this fraction of the target on each axis: a blur
38
+ // is low-frequency, and a coarse chain is what makes a 9-tap kernel wide
39
+ // on screen.
40
+ const BLOOM_SCALE = 4
41
+
42
+ type Bloom = {
43
+ threshold: TextureId
44
+ blurs: TextureId[]
45
+ radius: number
46
+ }
47
+
48
+ export type ResolveRecord = {
49
+ target: TextureId
50
+ entry: DrawId
51
+ pipeline: RenderPipelineId
52
+ program: ProgramId
53
+ source: string
54
+ /** Whether the app gave the source (then bloom never swaps it). */
55
+ custom: boolean
56
+ textures: TextureBindings | undefined
57
+ bloom: Bloom | null
58
+ /** What `uBloom` binds while bloom is off (a target binding cannot be
59
+ * removed, and the chain it named is destroyed): a 1x1 black texture,
60
+ * made at the first switch. */
61
+ placeholder: TextureId | null
62
+ label: string
63
+ }
64
+
65
+ function resolveSource(r: ResolveInput | undefined, buffer: TextureId): ResolveOptions {
66
+ if (r === undefined) return { source: DEFAULT_RESOLVE }
67
+ if (typeof r === "function") r = r(buffer)
68
+ return typeof r === "string" ? { source: r } : r
69
+ }
70
+
71
+ export function makeResolve(
72
+ buffer: TextureId,
73
+ width: number,
74
+ height: number,
75
+ r: ResolveInput | undefined,
76
+ sampler: { filter?: FilterMode; wrap?: WrapMode },
77
+ label: string,
78
+ ): ResolveRecord {
79
+ let o = resolveSource(r, buffer)
80
+ let built = resolvePipeline(o.source, label + "-resolve")
81
+ let target = createDrawTarget(width, height, null, { filter: sampler.filter, wrap: sampler.wrap, label: label + "-resolve", autoFree: false })
82
+ let entry = addDraw(target, built.pipeline, null, { vertexCount: 3, textures: { ...o.textures, uScene: buffer } })
83
+ return { target, entry, pipeline: built.pipeline, program: built.program, source: o.source, custom: r !== undefined, textures: o.textures, bloom: null, placeholder: null, label }
84
+ }
85
+
86
+ // Swap the entry's program for `source`, keeping the bindings.
87
+ function recompile(rec: ResolveRecord, buffer: TextureId, source: string): void {
88
+ let built = resolvePipeline(source, rec.label + "-resolve")
89
+ removeDraw(rec.target, rec.entry)
90
+ destroyRenderPipeline(rec.pipeline)
91
+ destroyProgram(rec.program)
92
+ rec.entry = addDraw(rec.target, built.pipeline, null, { vertexCount: 3, textures: { ...rec.textures, uScene: buffer } })
93
+ rec.pipeline = built.pipeline
94
+ rec.program = built.program
95
+ rec.source = source
96
+ }
97
+
98
+ export function replaceResolve(rec: ResolveRecord, buffer: TextureId, r: ResolveInput): void {
99
+ let o = resolveSource(r, buffer)
100
+ rec.custom = true
101
+ rec.textures = o.textures
102
+ if (o.source === rec.source) {
103
+ setDrawTextures(rec.target, rec.entry, { ...o.textures, uScene: buffer })
104
+ return
105
+ }
106
+ recompile(rec, buffer, o.source)
107
+ }
108
+
109
+ function chainSize(size: number): number {
110
+ return Math.max(1, Math.round(size / BLOOM_SCALE))
111
+ }
112
+
113
+ function makeChain(rec: ResolveRecord, buffer: TextureId, width: number, height: number, threshold: number, radius: number): Bloom {
114
+ let w = chainSize(width)
115
+ let h = chainSize(height)
116
+ let label = rec.label + "-bloom"
117
+ let first = createShaderTexture(BLOOM_THRESHOLD, w, h, { uThreshold: threshold }, { textures: { uSource: buffer }, label: label + "-threshold", autoFree: false })
118
+ let blurs: TextureId[] = []
119
+ let source = first
120
+ for (let i = 0; i < radius; i++) {
121
+ source = createShaderTexture(BLOOM_BLUR, w, h, { uDir: [1, 0] }, { textures: { uSource: source }, label: label + "-x", autoFree: false })
122
+ blurs.push(source)
123
+ source = createShaderTexture(BLOOM_BLUR, w, h, { uDir: [0, 1] }, { textures: { uSource: source }, label: label + "-y", autoFree: false })
124
+ blurs.push(source)
125
+ }
126
+ return { threshold: first, blurs, radius }
127
+ }
128
+
129
+ function destroyChain(bloom: Bloom): void {
130
+ for (let t of bloom.blurs) destroyTexture(t)
131
+ destroyTexture(bloom.threshold)
132
+ }
133
+
134
+ /** Switch the stock bloom on (options) or off (null) on a resolve. */
135
+ export function setResolveBloom(rec: ResolveRecord, buffer: TextureId, width: number, height: number, opts: BloomOptions | null): void {
136
+ if (opts === null) {
137
+ if (rec.bloom === null) return
138
+ if (rec.placeholder === null) rec.placeholder = createTexture(new Uint8Array([0, 0, 0, 255]), 1, 1, { label: rec.label + "-bloom-none", autoFree: false })
139
+ setTargetTextures(rec.target, { uBloom: rec.placeholder })
140
+ setTargetParams(rec.target, { uBloomIntensity: 0 })
141
+ destroyChain(rec.bloom)
142
+ rec.bloom = null
143
+ if (!rec.custom) recompile(rec, buffer, DEFAULT_RESOLVE)
144
+ return
145
+ }
146
+ let threshold = opts.threshold ?? BLOOM_THRESHOLD_DEFAULT
147
+ let intensity = opts.intensity ?? BLOOM_INTENSITY_DEFAULT
148
+ let radius = opts.radius ?? BLOOM_RADIUS_DEFAULT
149
+ if (!Number.isFinite(threshold) || threshold < 0) throw new Error("bloom: threshold must be a finite number >= 0, got " + threshold)
150
+ if (!Number.isFinite(intensity) || intensity < 0) throw new Error("bloom: intensity must be a finite number >= 0, got " + intensity)
151
+ if (!Number.isInteger(radius) || radius < 1) throw new Error("bloom: radius must be a positive integer (blur rounds), got " + radius)
152
+ if (rec.bloom !== null && rec.bloom.radius !== radius) {
153
+ destroyChain(rec.bloom)
154
+ rec.bloom = null
155
+ }
156
+ if (rec.bloom === null) {
157
+ rec.bloom = makeChain(rec, buffer, width, height, threshold, radius)
158
+ setTargetTextures(rec.target, { uBloom: rec.bloom.blurs[rec.bloom.blurs.length - 1]! })
159
+ } else {
160
+ setTargetParams(rec.bloom.threshold, { uThreshold: threshold })
161
+ }
162
+ setTargetParams(rec.target, { uBloomIntensity: intensity })
163
+ if (!rec.custom && rec.source !== BLOOM_RESOLVE) recompile(rec, buffer, BLOOM_RESOLVE)
164
+ }
165
+
166
+ export function resizeResolve(rec: ResolveRecord, width: number, height: number): void {
167
+ setTargetSize(rec.target, width, height)
168
+ if (rec.bloom === null) return
169
+ let w = chainSize(width)
170
+ let h = chainSize(height)
171
+ setTargetSize(rec.bloom.threshold, w, h)
172
+ for (let t of rec.bloom.blurs) setTargetSize(t, w, h)
173
+ }
174
+
175
+ export function disposeResolve(rec: ResolveRecord): void {
176
+ if (rec.bloom !== null) destroyChain(rec.bloom)
177
+ destroyTexture(rec.target)
178
+ if (rec.placeholder !== null) destroyTexture(rec.placeholder)
179
+ destroyRenderPipeline(rec.pipeline)
180
+ destroyProgram(rec.program)
181
+ }
@@ -11,7 +11,7 @@ import { createDrawTarget, depthTexture, destroyTexture, limits, setTargetRect,
11
11
  import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
12
12
  import { cascadeSplit, copy, frustumSliceSphere, lookAt as lookAtMatrix, mat4, snapToGrid, transformVector } from "./math.ts"
13
13
  import type { Mat4, Vec3 } from "./math.ts"
14
- import { MAX_SHADOW_MAPS } from "./glsl.ts"
14
+ import { MAX_LIGHTS, MAX_SHADOW_MAPS } from "./glsl.ts"
15
15
  import { shadowDepthMaterial } from "./material.ts"
16
16
  import type { Material } from "./material.ts"
17
17
  import { updateCamera } from "./camera.ts"
@@ -113,14 +113,22 @@ export type ShadowSystem = {
113
113
  * uShadow*[i] slot): every non-hemisphere light counts, casting or
114
114
  * not, so it matches the lit loop's index. */
115
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
116
+ /** The light attached, detached, or its castShadow/shadow options
117
+ * changed: the shadow set settles at the next sync (`settle`), never
118
+ * here. A declarative swap attaches the incoming branch before the
119
+ * outgoing one detaches, inside one flush, so a budget summed at attach
120
+ * counts both. Safe for a light that never cast. */
121
+ invalidate(light: CastingLight): void
122
+ /** Settle the shadow set against the light list, early in the scene's
123
+ * sync: drop the shadows of lights that left, stopped casting or
124
+ * changed shape (cascade count), place every wanted caster's tiles in
125
+ * attach order while the budget holds (first fit: a later spot still
126
+ * gets its map when an earlier point light does not), create the views
127
+ * of new casters. A set past the budget returns the error for the sync
128
+ * to throw AFTER its own writes, so a failing set never leaves the
129
+ * scene half-written: the casters that fit keep their maps, the rest
130
+ * light the scene unshadowed. Reported once per change of the set. */
131
+ settle(): Error | null
124
132
  /** Re-place every caster's map cameras from its light's current world
125
133
  * matrix (each compares against the matrix it was last placed from, so
126
134
  * a scene animating elsewhere rewrites nothing). Run per sync;
@@ -184,37 +192,88 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
184
192
  let forEachShadowSlot = (fn: (slot: number, lightIndex: number, cascade: number, rect: ShadowRect) => void): void => {
185
193
  eachSlot((slot, i, shadow, c) => fn(slot, i, c, shadow.rects[c]!))
186
194
  }
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 => {
195
+ // The set changed (attach, detach, options) since the last settle.
196
+ let setDirty = false
197
+ let invalidate = (light: CastingLight) => {
198
+ // An option that moves no tile (near, the box camera) still
199
+ // re-places the map cameras.
200
+ let shadow = shadows.get(light)
201
+ if (shadow !== undefined) shadow.dirty = true
202
+ setDirty = true
203
+ deps.schedule()
204
+ }
205
+ // A caster's views: one square tile of the shadow atlas per map,
206
+ // drawing the casting meshes with the depth override from that map's
207
+ // frustum. The light rewrite writes the rects in the light's slots on
208
+ // every receiving target.
209
+ let createShadow = (light: CastingLight, rects: ShadowRect[]) => {
210
+ let atlas = shadowAtlas!
211
+ let views = rects.map(rect =>
212
+ deps.makeView(
213
+ {
214
+ width: rect.width,
215
+ height: rect.height,
216
+ into: atlas.texture,
217
+ x: rect.x,
218
+ y: rect.y,
219
+ overrideMaterial: shadowDepthMaterial(),
220
+ clearColor: [1, 1, 1, 1],
221
+ label: deps.label + "-shadow",
222
+ },
223
+ m => m.castShadow,
224
+ ),
225
+ )
226
+ shadows.set(light, { light, views, lastWorld: mat4(), dirty: true, rects })
227
+ }
228
+ // Settle the set (ShadowSystem.settle): the wanted casters in light
229
+ // order, as many as fit, then the atlas sized to them, tiles whose
230
+ // place changed moved, new casters' views made. The rects reach
231
+ // receivers through the light rewrite this marks.
232
+ let settle = (): Error | null => {
233
+ if (!setDirty) return null
234
+ setDirty = false
235
+ // Every casting light within the light cap (one past it is not lit,
236
+ // and its slots would name a uShadow*[i] that does not exist) whose
237
+ // tiles still fit the budget.
193
238
  let casters: CastingLight[] = []
239
+ let tiles = 0
240
+ let wanted = 0
241
+ let index = 0
194
242
  for (let l of deps.lights) {
195
- if (l.type !== "hemisphere" && (shadows.has(l) || l === adding)) casters.push(l)
243
+ if (l.type === "hemisphere") continue
244
+ let i = index++
245
+ if (!l.castShadow || i >= MAX_LIGHTS) continue
246
+ let n = shadowTiles(l)
247
+ wanted += n
248
+ if (tiles + n > MAX_SHADOW_MAPS) continue
249
+ casters.push(l)
250
+ tiles += n
251
+ }
252
+ // A shadow no longer wanted (its light left, stopped casting or fell
253
+ // past the budget) or of the wrong shape (a cascade count change is a
254
+ // different view set) goes; its light draws plain.
255
+ for (let [light, shadow] of shadows) {
256
+ if (casters.includes(light) && shadow.views.length === shadowTiles(light)) continue
257
+ shadows.delete(light)
258
+ for (let v of shadow.views) deps.disposeView(v)
196
259
  }
197
- if (adding !== null && !casters.includes(adding)) casters.push(adding)
198
260
  deps.markLightsDirty()
261
+ let error =
262
+ wanted > MAX_SHADOW_MAPS
263
+ ? new Error(
264
+ "The scene's shadow set is full: " + wanted + " maps over the " + MAX_SHADOW_MAPS +
265
+ "-slot budget, the casters that do not fit light the scene unshadowed (a cascaded light claims shadow.cascades slots, a point light six)",
266
+ )
267
+ : null
199
268
  if (casters.length === 0) {
200
269
  if (shadowAtlas !== null) {
201
270
  destroyTexture(shadowAtlas.texture)
202
271
  shadowAtlas = null
203
272
  }
204
- return null
273
+ return error
205
274
  }
206
275
  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
- }
276
+ for (let l of casters) maxSize = Math.max(maxSize, l.shadow.mapSize)
218
277
  let lay = shadowLayout(tiles, maxSize)
219
278
  if (shadowAtlas === null) {
220
279
  shadowAtlas = {
@@ -232,16 +291,15 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
232
291
  shadowAtlas.width = lay.width
233
292
  shadowAtlas.height = lay.height
234
293
  }
235
- let placed: ShadowRect[] | null = null
236
294
  let k = 0
237
295
  for (let l of casters) {
238
296
  let size = Math.max(1, Math.floor(l.shadow.mapSize * lay.scale))
239
297
  let shadow = shadows.get(l)
298
+ let fresh: ShadowRect[] = []
240
299
  for (let c = 0; c < shadowTiles(l); c++, k++) {
241
300
  let rect: ShadowRect = { x: (k % lay.cols) * lay.cell, y: Math.floor(k / lay.cols) * lay.cell, width: size, height: size }
242
301
  if (shadow === undefined) {
243
- if (placed === null) placed = []
244
- placed.push(rect)
302
+ fresh.push(rect)
245
303
  continue
246
304
  }
247
305
  let r = shadow.rects[c]!
@@ -254,43 +312,9 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
254
312
  // A tile's texel size moved: the cascade fit snaps to it.
255
313
  shadow.dirty = true
256
314
  }
315
+ if (shadow === undefined) createShadow(l, fresh)
257
316
  }
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()
317
+ return error
294
318
  }
295
319
  // Place a shadow's cameras from its light's world matrix. A box light:
296
320
  // at its world position, looking along its world direction, the light
@@ -407,31 +431,8 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
407
431
  return {
408
432
  atlas: () => shadowAtlas,
409
433
  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
- },
434
+ invalidate,
435
+ settle,
435
436
  placeCameras(sceneCameraMoved) {
436
437
  for (let shadow of shadows.values()) placeShadowCamera(shadow, sceneCameraMoved)
437
438
  },