@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.
- package/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
+
}
|
package/src/scene-shadows.ts
CHANGED
|
@@ -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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
273
|
+
return error
|
|
205
274
|
}
|
|
206
275
|
let maxSize = 1
|
|
207
|
-
let
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
411
|
-
|
|
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
|
},
|