@solidrt/3d 0.0.52 → 0.0.54
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 +112 -41
- package/examples/README.md +8 -0
- package/examples/cascades.tsx +121 -0
- package/package.json +4 -4
- package/src/components.tsx +2 -2
- package/src/glsl.ts +95 -40
- package/src/gltf.ts +22 -4
- package/src/material.ts +219 -100
- package/src/math.ts +76 -0
- package/src/model.ts +7 -1
- package/src/scene.ts +316 -95
- package/tools/model.ts +52 -0
- package/demos/README.md +0 -15
- package/demos/assets/icon.svg +0 -23
- package/demos/package.json +0 -9
- package/demos/src/the-third-dimension.tsx +0 -866
- package/demos/tsconfig.json +0 -15
package/src/scene.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// params. They read world matrices back from the core, and only for the
|
|
29
29
|
// subtrees that moved since they last looked.
|
|
30
30
|
|
|
31
|
-
import { addDraw, createBuffer, createDrawTarget, createTexture, depthTexture, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setTargetParams, setTargetSize, setTargetTextures, writeBuffer } from "@solidrt/core/gpu"
|
|
31
|
+
import { addDraw, createBuffer, createDrawTarget, createTexture, depthTexture, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyTexture, limits, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setTargetParams, setTargetRect, setTargetSize, setTargetTextures, writeBuffer } from "@solidrt/core/gpu"
|
|
32
32
|
import * as spatial from "flux:spatial"
|
|
33
33
|
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
34
34
|
import { on } from "srt:events"
|
|
@@ -37,9 +37,9 @@ import { getOwner, onCleanup } from "@solidrt/core"
|
|
|
37
37
|
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
38
38
|
// The scene's lookAt() aims a node; math's builds a camera's view matrix -
|
|
39
39
|
// the same pairing (and the same name) as Three's Object3D/Matrix4.
|
|
40
|
-
import { compose, copy, eulerFromQuat, identity, lookAt as lookAtMatrix, mat4, multiply, orthographic, perspective, quat, quatFromFrame, transformPoint, transformVector, updateRotation, updateScale } from "./math.ts"
|
|
40
|
+
import { cascadeSplit, compose, copy, eulerFromQuat, frustumSliceSphere, identity, lookAt as lookAtMatrix, mat4, multiply, orthographic, perspective, quat, quatFromFrame, snapToGrid, transformPoint, transformVector, updateRotation, updateScale } from "./math.ts"
|
|
41
41
|
import type { Mat4, Quat, TransformUpdate, Vec3, Vec4 } from "./math.ts"
|
|
42
|
-
import { MAX_LIGHTS } from "./glsl.ts"
|
|
42
|
+
import { MAX_CASCADES, MAX_LIGHTS, MAX_SHADOW_MAPS } from "./glsl.ts"
|
|
43
43
|
import { geometryBounds, layoutKey, plane, validateGeometry } from "./geometry.ts"
|
|
44
44
|
import { acquireGeometryBuffers, releaseGeometryBuffers } from "./geometry-gpu.ts"
|
|
45
45
|
import type { GeometryBuffers } from "./geometry-gpu.ts"
|
|
@@ -50,6 +50,13 @@ import type { Material } from "./material.ts"
|
|
|
50
50
|
|
|
51
51
|
const IDENTITY = mat4()
|
|
52
52
|
const RESOLVED = Promise.resolve()
|
|
53
|
+
// How a cascaded light slices the camera range: 0 uniform, 1
|
|
54
|
+
// logarithmic, halfway the "practical" split (near slices small, far
|
|
55
|
+
// ones not starved).
|
|
56
|
+
const CASCADE_SPLIT_LAMBDA = 0.5
|
|
57
|
+
// |y| of a light direction above this is straight up or down, where
|
|
58
|
+
// world up cannot serve as the shadow map's roll reference.
|
|
59
|
+
const VERTICAL_LIGHT = 0.99
|
|
53
60
|
// lookAt()'s default roll reference. Read-only: quatFromFrame never
|
|
54
61
|
// writes its inputs, so one shared vector is safe.
|
|
55
62
|
const WORLD_UP: Vec3 = [0, 1, 0]
|
|
@@ -73,7 +80,7 @@ let pointScratch: Vec4 = [0, 0, 0, 0]
|
|
|
73
80
|
let declared = new Map<NodeId, SceneNode>()
|
|
74
81
|
let subscribed = false
|
|
75
82
|
|
|
76
|
-
function
|
|
83
|
+
function declareTransition(id: NodeId, node: SceneNode): void {
|
|
77
84
|
declared.set(id, node)
|
|
78
85
|
if (subscribed) return
|
|
79
86
|
subscribed = true
|
|
@@ -168,8 +175,25 @@ export type ShadowOptions = {
|
|
|
168
175
|
/** Offset a receiving point along its normal before the lookup, in
|
|
169
176
|
* world units (default 0) - the acne fix that keeps contact shadows. */
|
|
170
177
|
normalBias?: number
|
|
171
|
-
/** The light frustum; absent keys keep the defaults +-5, 0.5..500.
|
|
178
|
+
/** The light frustum; absent keys keep the defaults +-5, 0.5..500.
|
|
179
|
+
* Ignored by a cascaded light (its frustums follow the scene camera). */
|
|
172
180
|
camera?: Partial<ShadowCamera>
|
|
181
|
+
/** Split the shadow into this many cascades (1..MAX_CASCADES, default
|
|
182
|
+
* 1 = the `camera` box). With more, the light renders one map per slice
|
|
183
|
+
* of the SCENE camera's frustum (near .. far, tightest first), each
|
|
184
|
+
* `mapSize` texels wide and fitted every time the camera or the light
|
|
185
|
+
* moves, and a receiver samples the tightest one that has the point:
|
|
186
|
+
* sharp contact shadows near the camera and coarser ones toward the
|
|
187
|
+
* horizon, for a scene the box cannot cover at one map's resolution.
|
|
188
|
+
* Views (`scene.createView`) sample the same maps, fitted to the scene
|
|
189
|
+
* camera, not their own. Each cascade is a tile of the atlas. */
|
|
190
|
+
cascades?: number
|
|
191
|
+
/** How far from the scene camera a cascaded light shadows, in world
|
|
192
|
+
* units (default null = the camera's far). The cascades span
|
|
193
|
+
* near..distance and a point past it is lit, so pulling it in sharpens
|
|
194
|
+
* every cascade; it also bounds the maps' depth range, which is what
|
|
195
|
+
* `bias` is measured against. A box light ignores it. */
|
|
196
|
+
distance?: number | null
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
/** A directional light node: parallel rays travelling along `direction`
|
|
@@ -192,7 +216,7 @@ export type DirectionalLight = SceneNode & {
|
|
|
192
216
|
* `receiveShadow: false`). */
|
|
193
217
|
castShadow: boolean
|
|
194
218
|
/** The resolved shadow options (read; write through setLight). */
|
|
195
|
-
shadow: { mapSize: number; bias: number; normalBias: number; camera: ShadowCamera }
|
|
219
|
+
shadow: { mapSize: number; bias: number; normalBias: number; camera: ShadowCamera; cascades: number; distance: number | null }
|
|
196
220
|
}
|
|
197
221
|
|
|
198
222
|
/** The ambient term: a sky/ground gradient by the WORLD normal's
|
|
@@ -377,13 +401,23 @@ export type ViewOptions = {
|
|
|
377
401
|
*/
|
|
378
402
|
overrideMaterial?: Material
|
|
379
403
|
/** The view target's depth storage: true (default) for a buffer,
|
|
380
|
-
* "texture" for a sampleable one exposed as `view.depthTexture`.
|
|
404
|
+
* "texture" for a sampleable one exposed as `view.depthTexture`. Not
|
|
405
|
+
* with `into` (the depth is the parent's). */
|
|
381
406
|
depth?: true | "texture"
|
|
382
407
|
clearColor?: [number, number, number, number]
|
|
383
408
|
samples?: 1 | 2 | 4 | 8
|
|
384
409
|
filter?: FilterMode
|
|
385
410
|
wrap?: WrapMode
|
|
386
411
|
label?: string
|
|
412
|
+
/** Render into a rectangle of this draw target (an app-owned atlas)
|
|
413
|
+
* instead of a target of the view's own: every view into one atlas
|
|
414
|
+
* costs ONE pass. `x`/`y` (top-left origin, default 0) place the tile;
|
|
415
|
+
* display it with `<d-texture src={atlas} srcX srcY srcW srcH>`. The
|
|
416
|
+
* atlas carries depth and samples; `view.texture` is then the tile's id
|
|
417
|
+
* (a draw target, not a texture) and `view.depthTexture` is null. */
|
|
418
|
+
into?: TextureId
|
|
419
|
+
x?: number
|
|
420
|
+
y?: number
|
|
387
421
|
}
|
|
388
422
|
|
|
389
423
|
/** A second rendering of a scene from its own camera; see Scene.createView. */
|
|
@@ -396,6 +430,9 @@ export type View = {
|
|
|
396
430
|
/** Partial camera update, exactly scene.setCamera. */
|
|
397
431
|
setCamera(update: CameraUpdate): void
|
|
398
432
|
setSize(width: number, height: number): void
|
|
433
|
+
/** Move and resize a view created `into` an atlas (top-left origin);
|
|
434
|
+
* throws on a view with a target of its own. */
|
|
435
|
+
setRect(rect: { x: number; y: number; width: number; height: number }): void
|
|
399
436
|
/** View-owned shared params on the view's target (the scene's own
|
|
400
437
|
* setParams names fan out to every view already). */
|
|
401
438
|
setParams(params: ShaderParams): void
|
|
@@ -603,7 +640,7 @@ function entrySeed(material: Material, params: ShaderParams | null): ShaderParam
|
|
|
603
640
|
: { uModel: IDENTITY, ...material.params, ...params }
|
|
604
641
|
}
|
|
605
642
|
|
|
606
|
-
// The
|
|
643
|
+
// The uShadowAtlas binding while nothing casts: one white
|
|
607
644
|
// texel (depth 1, never shadowed), shared by every scene for the app.
|
|
608
645
|
let placeholder: TextureId | undefined
|
|
609
646
|
|
|
@@ -644,15 +681,27 @@ export type DirectionalLightOptions = {
|
|
|
644
681
|
}
|
|
645
682
|
export type HemisphereLightOptions = { sky?: Vec3; ground?: Vec3; intensity?: number }
|
|
646
683
|
|
|
647
|
-
/** Every directional light may cast:
|
|
648
|
-
*
|
|
684
|
+
/** Every directional light may cast: the cap is MAX_LIGHTS (every map
|
|
685
|
+
* is a tile of the scene's one shadow atlas, so the pass count does not
|
|
686
|
+
* follow it, the fill does). */
|
|
649
687
|
export const MAX_SHADOWS = MAX_LIGHTS
|
|
688
|
+
export { MAX_CASCADES }
|
|
650
689
|
|
|
651
690
|
function mergeShadow(into: DirectionalLight["shadow"], update: ShadowOptions): void {
|
|
652
691
|
if (update.mapSize !== undefined) into.mapSize = update.mapSize
|
|
653
692
|
if (update.bias !== undefined) into.bias = update.bias
|
|
654
693
|
if (update.normalBias !== undefined) into.normalBias = update.normalBias
|
|
655
694
|
if (update.camera !== undefined) Object.assign(into.camera, update.camera)
|
|
695
|
+
if (update.cascades !== undefined) {
|
|
696
|
+
let n = update.cascades
|
|
697
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_CASCADES) throw new Error("shadow.cascades must be an integer from 1 to " + MAX_CASCADES)
|
|
698
|
+
into.cascades = n
|
|
699
|
+
}
|
|
700
|
+
if (update.distance !== undefined) {
|
|
701
|
+
let d = update.distance
|
|
702
|
+
if (d !== null && !(d > 0)) throw new Error("shadow.distance must be a positive number or null")
|
|
703
|
+
into.distance = d
|
|
704
|
+
}
|
|
656
705
|
}
|
|
657
706
|
|
|
658
707
|
export function createDirectionalLight(opts: DirectionalLightOptions = {}): DirectionalLight {
|
|
@@ -662,7 +711,7 @@ export function createDirectionalLight(opts: DirectionalLightOptions = {}): Dire
|
|
|
662
711
|
light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
|
|
663
712
|
light.intensity = opts.intensity ?? 1
|
|
664
713
|
light.castShadow = opts.castShadow === true
|
|
665
|
-
light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, camera: { left: -5, right: 5, top: 5, bottom: -5, near: 0.5, far: 500 } }
|
|
714
|
+
light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, camera: { left: -5, right: 5, top: 5, bottom: -5, near: 0.5, far: 500 }, cascades: 1, distance: null }
|
|
666
715
|
if (opts.shadow !== undefined) mergeShadow(light.shadow, opts.shadow)
|
|
667
716
|
return light
|
|
668
717
|
}
|
|
@@ -902,7 +951,7 @@ function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
902
951
|
node._node = spatial.createNode(fillTransform(node), node.visible)
|
|
903
952
|
if (node._transition !== null) {
|
|
904
953
|
spatial.setTransition(node._node, node._transition)
|
|
905
|
-
|
|
954
|
+
declareTransition(node._node, node)
|
|
906
955
|
}
|
|
907
956
|
// The parent is in the scene already (add() enters the child only then),
|
|
908
957
|
// and the scene root is the one node without a parent.
|
|
@@ -966,7 +1015,7 @@ export function setTransition(node: SceneNode, transition: NodeTransition | stri
|
|
|
966
1015
|
if (node._node !== null) {
|
|
967
1016
|
spatial.setTransition(node._node, transition)
|
|
968
1017
|
if (transition === null) declared.delete(node._node)
|
|
969
|
-
else
|
|
1018
|
+
else declareTransition(node._node, node)
|
|
970
1019
|
}
|
|
971
1020
|
}
|
|
972
1021
|
|
|
@@ -1242,6 +1291,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1242
1291
|
let sky: Vec3 = [0, 0, 0]
|
|
1243
1292
|
let ground: Vec3 = [0, 0, 0]
|
|
1244
1293
|
let colors: number[] = []
|
|
1294
|
+
let bias: number[] = []
|
|
1295
|
+
let normalBias: number[] = []
|
|
1245
1296
|
let count = 0
|
|
1246
1297
|
for (let light of lights) {
|
|
1247
1298
|
if (light.type === "hemisphere") {
|
|
@@ -1258,42 +1309,45 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1258
1309
|
let c = light.color
|
|
1259
1310
|
let k = light.intensity
|
|
1260
1311
|
colors.push(c[0] * k, c[1] * k, c[2] * k)
|
|
1261
|
-
count++
|
|
1262
|
-
}
|
|
1263
|
-
for (let i = count; i < MAX_LIGHTS; i++) colors.push(0, 0, 0)
|
|
1264
|
-
// The shadow set rides with the lights, slot i = directional light i:
|
|
1265
|
-
// whether it casts (0 = a receiving material draws that light plain),
|
|
1266
|
-
// its biases, and its map bound as uShadowMap<i> - the light's depth
|
|
1267
|
-
// id when it casts, else the white placeholder, so every receiving
|
|
1268
|
-
// target always has all MAX_LIGHTS samplers bound.
|
|
1269
|
-
let cast: number[] = []
|
|
1270
|
-
let bias: number[] = []
|
|
1271
|
-
let normalBias: number[] = []
|
|
1272
|
-
let maps: Record<string, TextureId> = {}
|
|
1273
|
-
let i = 0
|
|
1274
|
-
for (let light of lights) {
|
|
1275
|
-
if (light.type !== "directional") continue
|
|
1276
|
-
let shadow = shadows.get(light)
|
|
1277
|
-
cast.push(shadow !== undefined ? 1 : 0)
|
|
1278
1312
|
bias.push(light.shadow.bias)
|
|
1279
1313
|
normalBias.push(light.shadow.normalBias)
|
|
1280
|
-
|
|
1281
|
-
i++
|
|
1314
|
+
count++
|
|
1282
1315
|
}
|
|
1283
|
-
for (; i < MAX_LIGHTS; i++) {
|
|
1284
|
-
|
|
1316
|
+
for (let i = count; i < MAX_LIGHTS; i++) {
|
|
1317
|
+
colors.push(0, 0, 0)
|
|
1285
1318
|
bias.push(0)
|
|
1286
1319
|
normalBias.push(0)
|
|
1287
|
-
maps["uShadowMap" + i] = shadowPlaceholder()
|
|
1288
1320
|
}
|
|
1321
|
+
// The shadow set rides with the lights. Per directional light i: its
|
|
1322
|
+
// map slots as uShadowFirst[i] + uShadowCount[i] (0 = a receiving
|
|
1323
|
+
// material draws that light plain) and its biases; per map slot j its
|
|
1324
|
+
// tile of the atlas as uShadowRect[j] in atlas UV (the whole map in
|
|
1325
|
+
// an unused slot - never read). The atlas depth binds once as
|
|
1326
|
+
// uShadowAtlas, the white placeholder when nothing casts, so every
|
|
1327
|
+
// receiving target always has the sampler bound.
|
|
1328
|
+
let first: number[] = new Array(MAX_LIGHTS).fill(0)
|
|
1329
|
+
let counts: number[] = new Array(MAX_LIGHTS).fill(0)
|
|
1330
|
+
let rects: number[] = []
|
|
1331
|
+
let atlas = shadowAtlas
|
|
1332
|
+
let maps: Record<string, TextureId> = { uShadowAtlas: atlas !== null ? depthTexture(atlas.texture) : shadowPlaceholder() }
|
|
1333
|
+
forEachShadowSlot((slot, i, shadow, c) => {
|
|
1334
|
+
if (c === 0) first[i] = slot
|
|
1335
|
+
counts[i] = counts[i]! + 1
|
|
1336
|
+
let r = shadow.rects[c]!
|
|
1337
|
+
let a = atlas!
|
|
1338
|
+
rects.push(r.x / a.width, r.y / a.height, r.width / a.width, r.height / a.height)
|
|
1339
|
+
})
|
|
1340
|
+
for (let slot = rects.length / 4; slot < MAX_SHADOW_MAPS; slot++) rects.push(0, 0, 1, 1)
|
|
1289
1341
|
let params: ShaderParams = {
|
|
1290
1342
|
uHemiSky: sky,
|
|
1291
1343
|
uHemiGround: ground,
|
|
1292
1344
|
uLightCount: count,
|
|
1293
1345
|
uLightColor: colors,
|
|
1294
|
-
|
|
1346
|
+
uShadowFirst: first,
|
|
1347
|
+
uShadowCount: counts,
|
|
1295
1348
|
uShadowBias: bias,
|
|
1296
1349
|
uShadowNormalBias: normalBias,
|
|
1350
|
+
uShadowRect: rects,
|
|
1297
1351
|
}
|
|
1298
1352
|
receivingTargets(t => {
|
|
1299
1353
|
setTargetParams(t, params)
|
|
@@ -1363,17 +1417,114 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1363
1417
|
let views: ViewRecord[] = []
|
|
1364
1418
|
// Every name scene.setParams has merged so far, replayed on a new view.
|
|
1365
1419
|
let sceneParams: ShaderParams = {}
|
|
1366
|
-
// The shadows, one per casting directional light: the internal
|
|
1367
|
-
// rendering its
|
|
1368
|
-
// only
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1420
|
+
// The shadows, one per casting directional light: the internal views
|
|
1421
|
+
// rendering its maps (tiles of the shadow atlas, depth override, casting
|
|
1422
|
+
// meshes only; one for a box light, `shadow.cascades` for a cascaded
|
|
1423
|
+
// one, tightest first) and `rects`, each tile's place in the atlas in
|
|
1424
|
+
// texels. `lastWorld` is the light's world matrix the shadow cameras
|
|
1425
|
+
// were last placed from; `dirty` forces a re-place (options changed).
|
|
1426
|
+
type ShadowRect = { x: number; y: number; width: number; height: number }
|
|
1427
|
+
type Shadow = { light: DirectionalLight; views: ViewRecord[]; lastWorld: Mat4; dirty: boolean; rects: ShadowRect[] }
|
|
1371
1428
|
let shadows = new Map<DirectionalLight, Shadow>()
|
|
1429
|
+
// The map slots: every casting light's maps in light order, a light's
|
|
1430
|
+
// cascades consecutive and tightest first. The ONE enumeration the
|
|
1431
|
+
// receiving side is dealt by - rects and first/count in writeLights,
|
|
1432
|
+
// matrices in sync - so uShadowFirst/uShadowCount agree with both.
|
|
1433
|
+
// `i` is the light's directional index (its uShadow*[i] slot).
|
|
1434
|
+
let forEachShadowSlot = (fn: (slot: number, i: number, shadow: Shadow, cascade: number) => void): void => {
|
|
1435
|
+
let slot = 0
|
|
1436
|
+
let i = 0
|
|
1437
|
+
for (let light of lights) {
|
|
1438
|
+
if (light.type !== "directional") continue
|
|
1439
|
+
let shadow = shadows.get(light)
|
|
1440
|
+
if (shadow !== undefined) for (let c = 0; c < shadow.views.length; c++) fn(slot++, i, shadow, c)
|
|
1441
|
+
i++
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
// The shadow atlas: ONE depth-texture target every casting light's map
|
|
1445
|
+
// is a tile of, so N maps render as one pass and receivers sample one
|
|
1446
|
+
// sampler through per-map rects (uShadowRect). Created with the first
|
|
1447
|
+
// caster, destroyed with the last. Laid out as a grid of cells the
|
|
1448
|
+
// largest mapSize wide, scaled down uniformly when that would exceed
|
|
1449
|
+
// the device's texture size: tile size follows the budget.
|
|
1450
|
+
let shadowAtlas: { texture: TextureId; width: number; height: number } | null = null
|
|
1451
|
+
let shadowLayout = (count: number, maxSize: number) => {
|
|
1452
|
+
let cols = Math.ceil(Math.sqrt(count))
|
|
1453
|
+
let rows = Math.ceil(count / cols)
|
|
1454
|
+
let scale = Math.min(1, limits.maxTextureSize / (cols * maxSize), limits.maxTextureSize / (rows * maxSize))
|
|
1455
|
+
let cell = Math.max(1, Math.floor(maxSize * scale))
|
|
1456
|
+
return { cols, cell, scale, width: cols * cell, height: rows * cell }
|
|
1457
|
+
}
|
|
1458
|
+
// Place every shadow tile for the current caster set plus `adding` (not
|
|
1459
|
+
// yet in `shadows`; its rects are returned for the view creates), in
|
|
1460
|
+
// light order, a light's cascades consecutive. Sizes the atlas, moves
|
|
1461
|
+
// tiles whose place changed, and drops the atlas when nothing casts.
|
|
1462
|
+
// The rects reach receivers through the next light rewrite.
|
|
1463
|
+
let placeShadows = (adding: DirectionalLight | null): ShadowRect[] | null => {
|
|
1464
|
+
let casters: DirectionalLight[] = []
|
|
1465
|
+
for (let l of lights) if (l.type === "directional" && (shadows.has(l) || l === adding)) casters.push(l)
|
|
1466
|
+
if (adding !== null && !casters.includes(adding)) casters.push(adding)
|
|
1467
|
+
lightsDirty = true
|
|
1468
|
+
if (casters.length === 0) {
|
|
1469
|
+
if (shadowAtlas !== null) {
|
|
1470
|
+
destroyTexture(shadowAtlas.texture)
|
|
1471
|
+
shadowAtlas = null
|
|
1472
|
+
}
|
|
1473
|
+
return null
|
|
1474
|
+
}
|
|
1475
|
+
let maxSize = 1
|
|
1476
|
+
let tiles = 0
|
|
1477
|
+
for (let l of casters) {
|
|
1478
|
+
maxSize = Math.max(maxSize, l.shadow.mapSize)
|
|
1479
|
+
tiles += l.shadow.cascades
|
|
1480
|
+
}
|
|
1481
|
+
let lay = shadowLayout(tiles, maxSize)
|
|
1482
|
+
if (shadowAtlas === null) {
|
|
1483
|
+
shadowAtlas = {
|
|
1484
|
+
texture: createDrawTarget(lay.width, lay.height, null, {
|
|
1485
|
+
depth: "texture",
|
|
1486
|
+
clearColor: [1, 1, 1, 1],
|
|
1487
|
+
label: (opts?.label ?? "scene") + "-shadow-atlas",
|
|
1488
|
+
autoFree: false,
|
|
1489
|
+
}),
|
|
1490
|
+
width: lay.width,
|
|
1491
|
+
height: lay.height,
|
|
1492
|
+
}
|
|
1493
|
+
} else if (shadowAtlas.width !== lay.width || shadowAtlas.height !== lay.height) {
|
|
1494
|
+
setTargetSize(shadowAtlas.texture, lay.width, lay.height)
|
|
1495
|
+
shadowAtlas.width = lay.width
|
|
1496
|
+
shadowAtlas.height = lay.height
|
|
1497
|
+
}
|
|
1498
|
+
let placed: ShadowRect[] | null = null
|
|
1499
|
+
let k = 0
|
|
1500
|
+
for (let l of casters) {
|
|
1501
|
+
let size = Math.max(1, Math.floor(l.shadow.mapSize * lay.scale))
|
|
1502
|
+
let shadow = shadows.get(l)
|
|
1503
|
+
for (let c = 0; c < l.shadow.cascades; c++, k++) {
|
|
1504
|
+
let rect: ShadowRect = { x: (k % lay.cols) * lay.cell, y: Math.floor(k / lay.cols) * lay.cell, width: size, height: size }
|
|
1505
|
+
if (shadow === undefined) {
|
|
1506
|
+
if (placed === null) placed = []
|
|
1507
|
+
placed.push(rect)
|
|
1508
|
+
continue
|
|
1509
|
+
}
|
|
1510
|
+
let r = shadow.rects[c]!
|
|
1511
|
+
if (r.x === rect.x && r.y === rect.y && r.width === rect.width && r.height === rect.height) continue
|
|
1512
|
+
shadow.rects[c] = rect
|
|
1513
|
+
let view = shadow.views[c]!
|
|
1514
|
+
setTargetRect(view.texture, rect)
|
|
1515
|
+
view.width = rect.width
|
|
1516
|
+
view.height = rect.height
|
|
1517
|
+
// A tile's texel size moved: the cascade fit snaps to it.
|
|
1518
|
+
shadow.dirty = true
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return placed
|
|
1522
|
+
}
|
|
1372
1523
|
let shadowDir: Vec3 = [0, 0, 0]
|
|
1373
1524
|
// uShadowMatrix is one array param (the engine writes whole arrays), so
|
|
1374
|
-
// any shadow camera move rewrites all
|
|
1375
|
-
// the slots that
|
|
1376
|
-
let shadowMatrices: number[] = new Array(
|
|
1525
|
+
// any shadow camera move rewrites all MAX_SHADOW_MAPS matrices, identity
|
|
1526
|
+
// in the slots that are not dealt.
|
|
1527
|
+
let shadowMatrices: number[] = new Array(MAX_SHADOW_MAPS * 16).fill(0)
|
|
1377
1528
|
let shadowMatricesDirty = false
|
|
1378
1529
|
// Every target a receiving material can draw into: the scene's and each
|
|
1379
1530
|
// view's but the shadow views (binding a target's own depth into it
|
|
@@ -1386,7 +1537,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1386
1537
|
let inst = mesh._instances
|
|
1387
1538
|
if (v.override !== null && inst !== null) return
|
|
1388
1539
|
if (v.filter !== null && !v.filter(mesh)) return
|
|
1389
|
-
|
|
1540
|
+
// A shadow view (the filtered kind) lets a caster's material pick its
|
|
1541
|
+
// own depth variant (its cull side); any other override view draws
|
|
1542
|
+
// exactly what it was given.
|
|
1543
|
+
let material = v.override !== null ? (v.filter !== null ? (mesh.material.shadow ?? v.override) : v.override) : mesh.material
|
|
1390
1544
|
let bufs = mesh._buffers!
|
|
1391
1545
|
let entry = addDraw(v.texture, material.pipeline(mesh.geometry.layout), entrySeed(material, v.override !== null ? null : mesh._params), {
|
|
1392
1546
|
buffer: bufs.buffer,
|
|
@@ -1437,15 +1591,19 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1437
1591
|
if (override !== null) {
|
|
1438
1592
|
for (let mesh of meshes) if (mesh._instances === null) checkLayout(override, mesh.geometry, "View override material")
|
|
1439
1593
|
}
|
|
1594
|
+
let tiled = vopts.into !== undefined
|
|
1440
1595
|
let v: ViewRecord = {
|
|
1441
1596
|
texture: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1442
|
-
depth: vopts.depth ?? true,
|
|
1597
|
+
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1443
1598
|
clearColor: vopts.clearColor,
|
|
1444
1599
|
filter: vopts.filter,
|
|
1445
1600
|
wrap: vopts.wrap,
|
|
1446
1601
|
samples: vopts.samples,
|
|
1447
1602
|
label: vopts.label ?? (opts?.label ?? "scene") + "-view",
|
|
1448
1603
|
autoFree: false,
|
|
1604
|
+
into: vopts.into,
|
|
1605
|
+
x: vopts.x,
|
|
1606
|
+
y: vopts.y,
|
|
1449
1607
|
}),
|
|
1450
1608
|
width: vopts.width,
|
|
1451
1609
|
height: vopts.height,
|
|
@@ -1466,69 +1624,118 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1466
1624
|
hooks._schedule()
|
|
1467
1625
|
return v
|
|
1468
1626
|
}
|
|
1469
|
-
// A shadow
|
|
1470
|
-
// meshes with the depth override from
|
|
1471
|
-
// rewrite
|
|
1627
|
+
// A shadow's views: one square tile of the shadow atlas per map drawing
|
|
1628
|
+
// the casting meshes with the depth override from that map's frustum.
|
|
1629
|
+
// The light rewrite writes the rects in the light's slots on every
|
|
1630
|
+
// receiving target.
|
|
1472
1631
|
let createShadow = (light: DirectionalLight) => {
|
|
1473
|
-
let
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1632
|
+
let rects = placeShadows(light)
|
|
1633
|
+
if (rects === null || shadowAtlas === null) return
|
|
1634
|
+
let atlas = shadowAtlas
|
|
1635
|
+
let views = rects.map(rect =>
|
|
1636
|
+
makeView(
|
|
1637
|
+
{
|
|
1638
|
+
width: rect.width,
|
|
1639
|
+
height: rect.height,
|
|
1640
|
+
into: atlas.texture,
|
|
1641
|
+
x: rect.x,
|
|
1642
|
+
y: rect.y,
|
|
1643
|
+
overrideMaterial: shadowDepthMaterial(),
|
|
1644
|
+
clearColor: [1, 1, 1, 1],
|
|
1645
|
+
label: (opts?.label ?? "scene") + "-shadow",
|
|
1646
|
+
},
|
|
1647
|
+
m => m.castShadow,
|
|
1648
|
+
),
|
|
1484
1649
|
)
|
|
1485
|
-
shadows.set(light, { light,
|
|
1650
|
+
shadows.set(light, { light, views, lastWorld: mat4(), dirty: true, rects })
|
|
1486
1651
|
lightsDirty = true
|
|
1487
1652
|
}
|
|
1488
1653
|
let destroyShadow = (light: DirectionalLight) => {
|
|
1489
1654
|
let shadow = shadows.get(light)
|
|
1490
1655
|
if (shadow === undefined) return
|
|
1491
1656
|
shadows.delete(light)
|
|
1492
|
-
|
|
1657
|
+
for (let v of shadow.views) disposeView(v)
|
|
1658
|
+
placeShadows(null)
|
|
1493
1659
|
lightsDirty = true
|
|
1494
1660
|
hooks._schedule()
|
|
1495
1661
|
}
|
|
1496
|
-
// Place a shadow
|
|
1497
|
-
// position, looking along its world direction, the light
|
|
1498
|
-
// orthographic extents. Compared against the matrix it
|
|
1499
|
-
// from, so a scene animating elsewhere rewrites nothing
|
|
1500
|
-
|
|
1662
|
+
// Place a shadow's cameras from its light's world matrix. A box light:
|
|
1663
|
+
// at its world position, looking along its world direction, the light
|
|
1664
|
+
// frustum as the orthographic extents. Compared against the matrix it
|
|
1665
|
+
// was last placed from, so a scene animating elsewhere rewrites nothing
|
|
1666
|
+
// here. A cascaded light also follows the scene camera (`cameraMoved`).
|
|
1667
|
+
let cascadeScratch = mat4()
|
|
1668
|
+
let cascadeCenter: Vec3 = [0, 0, 0]
|
|
1669
|
+
let placeShadowCamera = (shadow: Shadow, cameraMoved: boolean) => {
|
|
1501
1670
|
let light = shadow.light
|
|
1502
1671
|
let m = worldInto(worldScratch, light)
|
|
1503
|
-
|
|
1672
|
+
let cascaded = shadow.views.length > 1
|
|
1673
|
+
if (!shadow.dirty && !(cascaded && cameraMoved) && m.every((x, i) => x === shadow.lastWorld[i])) return
|
|
1504
1674
|
shadow.dirty = false
|
|
1505
1675
|
copy(shadow.lastWorld, m)
|
|
1506
1676
|
transformVector(shadowDir, m, light.direction)
|
|
1507
1677
|
let len = Math.hypot(shadowDir[0], shadowDir[1], shadowDir[2]) || 1
|
|
1508
1678
|
let d: Vec3 = [shadowDir[0] / len, shadowDir[1] / len, shadowDir[2] / len]
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1679
|
+
// A sun straight down is the common case and the degenerate one for
|
|
1680
|
+
// world up: roll about z then (the map's orientation is invisible).
|
|
1681
|
+
let up: Vec3 = Math.abs(d[1]) > VERTICAL_LIGHT ? [0, 0, 1] : [0, 1, 0]
|
|
1682
|
+
if (!cascaded) {
|
|
1683
|
+
let c = light.shadow.camera
|
|
1684
|
+
updateCamera(shadow.views[0]!.camera, {
|
|
1685
|
+
position: [m[12], m[13], m[14]],
|
|
1686
|
+
target: [m[12] + d[0], m[13] + d[1], m[14] + d[2]],
|
|
1687
|
+
up,
|
|
1688
|
+
ortho: { left: c.left, right: c.right, top: c.top, bottom: c.bottom },
|
|
1689
|
+
near: c.near,
|
|
1690
|
+
far: c.far,
|
|
1691
|
+
})
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
// Cascades: the scene camera's range near..far (far capped by
|
|
1695
|
+
// shadow.distance) sliced by cascadeSplit, each slice's bounding
|
|
1696
|
+
// sphere (frustumSliceSphere) as an orthographic box looking along
|
|
1697
|
+
// the light, its centre snapped to the map's texel grid in light
|
|
1698
|
+
// space (snapToGrid) so the shadow edges do not swim as the camera
|
|
1699
|
+
// moves. The box reaches back toward the light by the whole range,
|
|
1700
|
+
// so a caster outside the slice still casts into it.
|
|
1701
|
+
let n = shadow.views.length
|
|
1702
|
+
let near = camera.near
|
|
1703
|
+
let far = Math.min(camera.far, light.shadow.distance ?? Infinity)
|
|
1704
|
+
if (!(far > near)) far = near + 1
|
|
1705
|
+
// The light's rotation only: rows are its right, up and back axes.
|
|
1706
|
+
let basis = lookAtMatrix(cascadeScratch, [0, 0, 0], d, up)
|
|
1707
|
+
let aspect = width / height
|
|
1708
|
+
let zn = near
|
|
1709
|
+
for (let c = 0; c < n; c++) {
|
|
1710
|
+
let zf = cascadeSplit(near, far, c, n, CASCADE_SPLIT_LAMBDA)
|
|
1711
|
+
let radius = frustumSliceSphere(cascadeCenter, camera, aspect, zn, zf)
|
|
1712
|
+
zn = zf
|
|
1713
|
+
let view = shadow.views[c]!
|
|
1714
|
+
// A texel is 2r / mapSize world units.
|
|
1715
|
+
snapToGrid(cascadeCenter, cascadeCenter, basis, (2 * radius) / view.width)
|
|
1716
|
+
let back = radius + far
|
|
1717
|
+
updateCamera(view.camera, {
|
|
1718
|
+
position: [cascadeCenter[0] - d[0] * back, cascadeCenter[1] - d[1] * back, cascadeCenter[2] - d[2] * back],
|
|
1719
|
+
target: cascadeCenter,
|
|
1720
|
+
up,
|
|
1721
|
+
ortho: { left: -radius, right: radius, top: radius, bottom: -radius },
|
|
1722
|
+
near: 0,
|
|
1723
|
+
far: back + radius,
|
|
1724
|
+
})
|
|
1725
|
+
}
|
|
1520
1726
|
}
|
|
1521
1727
|
|
|
1522
1728
|
let sync = () => {
|
|
1523
1729
|
scheduled = false
|
|
1524
1730
|
if (disposed) return
|
|
1525
1731
|
ensureCamera(camera, width, height)
|
|
1732
|
+
let cameraMoved = camera.pending
|
|
1526
1733
|
if (camera.pending) {
|
|
1527
1734
|
camera.pending = false
|
|
1528
1735
|
setTargetParams(texture, cameraParams(camera))
|
|
1529
1736
|
if (transparentCount > 1) orderDirty = true
|
|
1530
1737
|
}
|
|
1531
|
-
for (let shadow of shadows.values()) placeShadowCamera(shadow)
|
|
1738
|
+
for (let shadow of shadows.values()) placeShadowCamera(shadow, cameraMoved)
|
|
1532
1739
|
for (let v of views) {
|
|
1533
1740
|
ensureCamera(v.camera, v.width, v.height)
|
|
1534
1741
|
if (v.camera.pending) {
|
|
@@ -1545,15 +1752,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1545
1752
|
// with: one array to every receiving target per shadow-camera move.
|
|
1546
1753
|
if (shadowMatricesDirty) {
|
|
1547
1754
|
shadowMatricesDirty = false
|
|
1548
|
-
let
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
let
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
for (; i < MAX_LIGHTS; i++) for (let k = 0; k < 16; k++) shadowMatrices[i * 16 + k] = IDENTITY[k]!
|
|
1755
|
+
let dealt = 0
|
|
1756
|
+
forEachShadowSlot((slot, _i, shadow, c) => {
|
|
1757
|
+
let m = shadow.views[c]!.camera.viewProj
|
|
1758
|
+
for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = m[k]!
|
|
1759
|
+
dealt = slot + 1
|
|
1760
|
+
})
|
|
1761
|
+
for (let slot = dealt; slot < MAX_SHADOW_MAPS; slot++) for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = IDENTITY[k]!
|
|
1557
1762
|
let params: ShaderParams = { uShadowMatrix: shadowMatrices }
|
|
1558
1763
|
receivingTargets(t => setTargetParams(t, params))
|
|
1559
1764
|
}
|
|
@@ -1608,12 +1813,15 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1608
1813
|
destroyShadow(light)
|
|
1609
1814
|
return
|
|
1610
1815
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1816
|
+
// A cascade count change is a different view set: rebuild it. A
|
|
1817
|
+
// mapSize change re-places every tile (the grid cell follows the
|
|
1818
|
+
// largest map).
|
|
1819
|
+
if (shadow.views.length !== light.shadow.cascades) {
|
|
1820
|
+
destroyShadow(light)
|
|
1821
|
+
createShadow(light)
|
|
1822
|
+
return
|
|
1616
1823
|
}
|
|
1824
|
+
placeShadows(null)
|
|
1617
1825
|
shadow.dirty = true
|
|
1618
1826
|
lightsDirty = true
|
|
1619
1827
|
hooks._schedule()
|
|
@@ -2003,7 +2211,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2003
2211
|
let v = makeView(vopts, null)
|
|
2004
2212
|
return {
|
|
2005
2213
|
texture: v.texture,
|
|
2006
|
-
depthTexture: vopts.depth === "texture" ? depthTexture(v.texture) : null,
|
|
2214
|
+
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
2007
2215
|
setCamera(update) {
|
|
2008
2216
|
updateCamera(v.camera, update)
|
|
2009
2217
|
hooks._schedule()
|
|
@@ -2016,6 +2224,15 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2016
2224
|
v.camera.dirty = true
|
|
2017
2225
|
hooks._schedule()
|
|
2018
2226
|
},
|
|
2227
|
+
setRect(rect) {
|
|
2228
|
+
if (v.disposed) return
|
|
2229
|
+
setTargetRect(v.texture, rect)
|
|
2230
|
+
if (rect.width === v.width && rect.height === v.height) return
|
|
2231
|
+
v.width = rect.width
|
|
2232
|
+
v.height = rect.height
|
|
2233
|
+
v.camera.dirty = true
|
|
2234
|
+
hooks._schedule()
|
|
2235
|
+
},
|
|
2019
2236
|
setParams(params) {
|
|
2020
2237
|
if (!v.disposed) setTargetParams(v.texture, params)
|
|
2021
2238
|
},
|
|
@@ -2043,6 +2260,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2043
2260
|
destroyTexture(texture)
|
|
2044
2261
|
shadows.clear()
|
|
2045
2262
|
for (let v of views.slice()) disposeView(v)
|
|
2263
|
+
if (shadowAtlas !== null) {
|
|
2264
|
+
destroyTexture(shadowAtlas.texture)
|
|
2265
|
+
shadowAtlas = null
|
|
2266
|
+
}
|
|
2046
2267
|
if (background !== null) {
|
|
2047
2268
|
// The entry died with the target; the pipeline and program are the
|
|
2048
2269
|
// scene's own (unlike shared material pipelines), so they go too.
|