@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.
- package/AGENTS.md +1083 -183
- package/README.md +15 -7
- package/examples/README.md +79 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +763 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1242 -1557
- package/tools/environment.ts +69 -0
- 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
|
+
}
|