reze-engine 0.58.4 → 0.60.0
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/dist/engine.d.ts +35 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +157 -33
- package/dist/graph/slots.js +2 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +22 -14
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +7 -3
- package/dist/shadow-cascades.d.ts +84 -34
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +101 -55
- package/package.json +1 -1
- package/src/engine.ts +151 -37
- package/src/graph/slots.ts +2 -2
- package/src/shaders/lights.ts +22 -14
- package/src/shaders/materials/common.ts +7 -3
- package/src/shadow-cascades.ts +159 -67
package/dist/shaders/lights.js
CHANGED
|
@@ -241,6 +241,11 @@ export function lightsApi(group, binding) {
|
|
|
241
241
|
@group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<vec4u>;
|
|
242
242
|
|
|
243
243
|
const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
|
|
244
|
+
// A lamp's bulb, in world units: the inverse square is held flat inside it,
|
|
245
|
+
// so the spike beside the lamp is finite. Aether Gazer's lamps are
|
|
246
|
+
// 0.1 m across (their shapeRadius, capping 1/d² at 1/0.1), which at MMD scale
|
|
247
|
+
// is 2.5 units.
|
|
248
|
+
const RZ_LAMP_NEAR: f32 = 2.5;
|
|
244
249
|
|
|
245
250
|
/** How many positional lights the scene has. Zero is the ordinary case. */
|
|
246
251
|
fn rzLightCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].x)), RZ_MAX_LIGHTS); }
|
|
@@ -282,19 +287,16 @@ fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
|
282
287
|
/**
|
|
283
288
|
* One light's contribution at a surface point.
|
|
284
289
|
*
|
|
285
|
-
*
|
|
290
|
+
* A LIGHT FALLS OFF AS THE INVERSE SQUARE, the curve Unity, Unreal, Blender and
|
|
291
|
+
* glTF all light with: intensity / max(d², RZ_LAMP_NEAR²), so its intensity is
|
|
292
|
+
* the brightness one unit away, windowed by (1 − (d/R)⁴)² so it is exactly zero
|
|
293
|
+
* at its radius and the bound the grid is built from is real.
|
|
286
294
|
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* as
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* So: intensity is the brightness AT the light, radius is where it reaches
|
|
294
|
-
* zero, and the curve between them is the same shape whatever the scene's
|
|
295
|
-
* scale. Both dials now mean what they say, which for a composer beats being
|
|
296
|
-
* right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
|
|
297
|
-
* radius, so the bound the grid is built from is real.
|
|
295
|
+
* THE UNITS ARE BLENDER'S. Intensity is radiant intensity, a point light's
|
|
296
|
+
* power over 4π, and a Lambertian surface returns albedo × irradiance / π —
|
|
297
|
+
* the π the sun term already carries. So a stage exported from Blender lights
|
|
298
|
+
* here as it lit there, and a lamp's intensity is what Blender's exporter
|
|
299
|
+
* writes in candela over 683.
|
|
298
300
|
*/
|
|
299
301
|
fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
300
302
|
let pr = _rzLightVec(i, 0u);
|
|
@@ -310,14 +312,16 @@ fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
|
310
312
|
let ndl = max(dot(n, toLight), 0.0);
|
|
311
313
|
if (ndl <= 0.0) { return vec3f(0.0); }
|
|
312
314
|
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
313
|
-
let
|
|
315
|
+
let t2 = t * t;
|
|
316
|
+
let window = 1.0 - t2 * t2;
|
|
317
|
+
let falloff = window * window / max(dist * dist, RZ_LAMP_NEAR * RZ_LAMP_NEAR);
|
|
314
318
|
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
315
319
|
// the outer one, squared for the same soft edge the falloff has. A point
|
|
316
320
|
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
317
321
|
// dot product and no branch.
|
|
318
322
|
let cone = rzLightCone(i);
|
|
319
323
|
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
320
|
-
return rzLightColor(i) * (ndl * falloff *
|
|
324
|
+
return rzLightColor(i) * (ndl * falloff * aim * aim);
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
/** The lamps named by one word of a cell's bits, lowest first. */
|
|
@@ -343,6 +347,10 @@ fn _rzLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
|
343
347
|
* every material must cost nothing until someone asks for a light.
|
|
344
348
|
*/
|
|
345
349
|
fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
350
|
+
return _rzLightsIrradiance(p, n) * (1.0 / 3.141592653589793);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn _rzLightsIrradiance(p: vec3f, n: vec3f) -> vec3f {
|
|
346
354
|
var acc = vec3f(0.0);
|
|
347
355
|
let count = rzLightCount();
|
|
348
356
|
let docs = _rzLightDocCount();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,QA0JhC,CAAC;AA8HF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -143,9 +143,13 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
|
143
143
|
@group(0) @binding(8) var worldEnvTexture: texture_2d<f32>;
|
|
144
144
|
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
145
145
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
146
|
+
// The light this model takes apart from the scene's — see Engine.setModelFill
|
|
147
|
+
// and Engine.setModelSun. fill is added to its ambient, zero for a model
|
|
148
|
+
// nobody gave one; sun replaces the scene's sun colour while its w is set,
|
|
149
|
+
// which is how a stage keeps the daylight its game lit it by while the cast
|
|
150
|
+
// keeps the key the scene set for them.
|
|
151
|
+
struct ModelLight { fill: vec4f, sun: vec4f }
|
|
152
|
+
@group(1) @binding(1) var<uniform> modelLight: ModelLight;
|
|
149
153
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
150
154
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
151
155
|
// Reserved for future sphere/toon graph nodes; graphs that don't read them get the
|
|
@@ -1,45 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* THE SUN'S SHADOW FOLLOWS THE CAMERA, the way Blender's does.
|
|
3
|
+
*
|
|
4
|
+
* Each cascade is fitted every frame to a slice of the view frustum: the near
|
|
5
|
+
* one to the stretch around what the camera is looking at, the far one to the
|
|
6
|
+
* whole of what it sees. Whatever is in view is in a cascade, and whatever
|
|
7
|
+
* could throw a shadow onto it is in the cascade's depth range, which is fitted
|
|
8
|
+
* to the scene's bounds along the light. A room forty metres across with its
|
|
9
|
+
* window frames behind the camera shadows its floor as the game does; a lone
|
|
10
|
+
* dancer on an empty floor keeps a crisp near map.
|
|
11
|
+
*
|
|
12
|
+
* The earlier shape was two fixed boxes around the camera target, 64 and 256
|
|
13
|
+
* units across, and a stage reached past them: the floor near the windows lay
|
|
14
|
+
* outside every box and was drawn lit, the shadows stopping at the box's edge
|
|
15
|
+
* in a straight line.
|
|
16
|
+
*
|
|
17
|
+
* INVARIANT the sampler and the cull both lean on: the outer cascade CONTAINS
|
|
18
|
+
* the inner one. The sampler falls from cascade 0 to 1 at the box edge, which
|
|
19
|
+
* is only seamless if 1 covers where 0 ends, and the cull tests the OUTERMOST
|
|
20
|
+
* frustum alone. Both hold because the outer slice is the whole frustum, of
|
|
21
|
+
* which the inner slice is a part, and both take the same depth range.
|
|
22
|
+
* tests/shadow-cascades.test.mjs pins it.
|
|
23
|
+
*/
|
|
24
|
+
export type ShadowCascade = {
|
|
9
25
|
/** Texels per side of this cascade's map — sets the snap quantum. */
|
|
10
26
|
mapSize: number;
|
|
11
27
|
};
|
|
28
|
+
export declare const SHADOW_CASCADES: readonly ShadowCascade[];
|
|
29
|
+
/** How far past the camera's point of interest the near cascade reaches, in
|
|
30
|
+
* world units: the dancer and the floor around her, at the near map's texel. */
|
|
31
|
+
export declare const NEAR_REACH = 40;
|
|
32
|
+
/** The view the cascades are fitted to: the camera's eye and basis (world
|
|
33
|
+
* space, left-handed, +Z forward as the projection is), its vertical field of
|
|
34
|
+
* view, aspect and clip planes, and how far away the thing it looks at is. */
|
|
35
|
+
export type ShadowView = {
|
|
36
|
+
eye: {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
z: number;
|
|
40
|
+
};
|
|
41
|
+
right: {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
z: number;
|
|
45
|
+
};
|
|
46
|
+
up: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
z: number;
|
|
50
|
+
};
|
|
51
|
+
forward: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
z: number;
|
|
55
|
+
};
|
|
56
|
+
fov: number;
|
|
57
|
+
aspect: number;
|
|
58
|
+
near: number;
|
|
59
|
+
far: number;
|
|
60
|
+
/** Distance from the eye to the camera target, along the view. */
|
|
61
|
+
focus: number;
|
|
62
|
+
};
|
|
63
|
+
/** World-space box around everything drawn, or null for an empty scene. */
|
|
64
|
+
export type ShadowBounds = {
|
|
65
|
+
min: [number, number, number];
|
|
66
|
+
max: [number, number, number];
|
|
67
|
+
} | null;
|
|
68
|
+
type XYZ = {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
z: number;
|
|
72
|
+
};
|
|
12
73
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - the sampler falls from cascade i to i+1 at the box edge, which is only
|
|
18
|
-
* seamless if i+1 covers where i ends, and
|
|
19
|
-
* - the cull tests ONE frustum — the outermost — and the rasterizer clips
|
|
20
|
-
* each cascade to its own box. That is the same argument that made
|
|
21
|
-
* single-volume shadow culling exact: anything rejected was contributing
|
|
22
|
-
* nothing anywhere. Concentric containment is what keeps it true for a
|
|
23
|
-
* LIST. tests/shadow-cascades.test.mjs pins it.
|
|
74
|
+
* Where each cascade's slice of the view starts and ends, as distances along
|
|
75
|
+
* the view: [near, split] and [near, farFit]. The far end is where the scene
|
|
76
|
+
* ends, so an empty floor with a dancer on it keeps a short, sharp frustum
|
|
77
|
+
* rather than the camera's far plane.
|
|
24
78
|
*/
|
|
25
|
-
export declare
|
|
79
|
+
export declare function cascadeSlices(view: ShadowView, bounds: ShadowBounds): [number, number][];
|
|
80
|
+
/** The eight corners of a slice, for tests and for the fit's own checks. */
|
|
81
|
+
export declare function sliceCorners(view: ShadowView, n: number, f: number): XYZ[];
|
|
26
82
|
/**
|
|
27
|
-
* One cascade's view-projection
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
83
|
+
* One cascade's view-projection: an orthographic box around the slice's
|
|
84
|
+
* sphere, snapped to the map's texel grid in the light's right/up plane so a
|
|
85
|
+
* moving camera doesn't shimmer its shadow edges, and reaching along the light
|
|
86
|
+
* from the nearest thing in the scene to the farthest, so everything that
|
|
87
|
+
* could cast into the slice does.
|
|
32
88
|
*
|
|
33
89
|
* Writes the 16 floats into `out` at `offset` and returns `out`.
|
|
34
90
|
*/
|
|
35
|
-
export declare function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
z: number;
|
|
39
|
-
}, sunDirection: {
|
|
40
|
-
x: number;
|
|
41
|
-
y: number;
|
|
42
|
-
z: number;
|
|
43
|
-
}, cascade: ShadowCascade, out: Float32Array, offset: number): Float32Array;
|
|
91
|
+
export declare function fitShadowVP(view: ShadowView, slice: [number, number], sunDirection: XYZ, bounds: ShadowBounds, cascade: ShadowCascade, out: Float32Array, offset: number): Float32Array;
|
|
92
|
+
/** Every cascade's view-projection in a row, inner to outer. */
|
|
93
|
+
export declare function buildShadowCascades(view: ShadowView, sunDirection: XYZ, bounds: ShadowBounds, out: Float32Array): Float32Array;
|
|
44
94
|
export {};
|
|
45
95
|
//# sourceMappingURL=shadow-cascades.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow-cascades.d.ts","sourceRoot":"","sources":["../src/shadow-cascades.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shadow-cascades.d.ts","sourceRoot":"","sources":["../src/shadow-cascades.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA2C,CAAA;AAE/F;iFACiF;AACjF,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B;;+EAE+E;AAC/E,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,EAAE,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,OAAO,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG;IAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAAA;AAElG,KAAK,GAAG,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAkBxF;AAwBD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAc1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACvB,YAAY,EAAE,GAAG,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,GACb,YAAY,CA0Cd;AAED,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,YAAY,CAI9H"}
|
package/dist/shadow-cascades.js
CHANGED
|
@@ -1,70 +1,116 @@
|
|
|
1
|
-
// The shadow volumes, as data — a list the engine iterates rather than one
|
|
2
|
-
// hardcoded box, so a second cascade is a list entry and not a rewrite.
|
|
3
|
-
//
|
|
4
|
-
// Pure math, its own module for the same reason param-track.ts is: the engine
|
|
5
|
-
// class needs a GPU to construct, and the one thing that ever goes WRONG with a
|
|
6
|
-
// shadow volume is arithmetic — a snap that stops snapping, an eye that lands
|
|
7
|
-
// inside the near plane. Headless tests can hold this half to golden values.
|
|
8
|
-
//
|
|
9
|
-
// The arithmetic is the shipped single-volume code, operation for operation:
|
|
10
|
-
// float addition is not associative, so "the same formula, reordered" is not
|
|
11
|
-
// the same matrix, and cascade 0 must be BIT-IDENTICAL to the volume every
|
|
12
|
-
// published scene was lit by.
|
|
13
1
|
import { Mat4, Vec3 } from "./math";
|
|
2
|
+
export const SHADOW_CASCADES = [{ mapSize: 4096 }, { mapSize: 2048 }];
|
|
3
|
+
/** How far past the camera's point of interest the near cascade reaches, in
|
|
4
|
+
* world units: the dancer and the floor around her, at the near map's texel. */
|
|
5
|
+
export const NEAR_REACH = 40;
|
|
14
6
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - the sampler falls from cascade i to i+1 at the box edge, which is only
|
|
20
|
-
* seamless if i+1 covers where i ends, and
|
|
21
|
-
* - the cull tests ONE frustum — the outermost — and the rasterizer clips
|
|
22
|
-
* each cascade to its own box. That is the same argument that made
|
|
23
|
-
* single-volume shadow culling exact: anything rejected was contributing
|
|
24
|
-
* nothing anywhere. Concentric containment is what keeps it true for a
|
|
25
|
-
* LIST. tests/shadow-cascades.test.mjs pins it.
|
|
7
|
+
* Where each cascade's slice of the view starts and ends, as distances along
|
|
8
|
+
* the view: [near, split] and [near, farFit]. The far end is where the scene
|
|
9
|
+
* ends, so an empty floor with a dancer on it keeps a short, sharp frustum
|
|
10
|
+
* rather than the camera's far plane.
|
|
26
11
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
12
|
+
export function cascadeSlices(view, bounds) {
|
|
13
|
+
let farFit = view.near + 200;
|
|
14
|
+
if (bounds) {
|
|
15
|
+
let deepest = 0;
|
|
16
|
+
for (let i = 0; i < 8; i++) {
|
|
17
|
+
const cx = (i & 1 ? bounds.max : bounds.min)[0] - view.eye.x;
|
|
18
|
+
const cy = (i & 2 ? bounds.max : bounds.min)[1] - view.eye.y;
|
|
19
|
+
const cz = (i & 4 ? bounds.max : bounds.min)[2] - view.eye.z;
|
|
20
|
+
deepest = Math.max(deepest, cx * view.forward.x + cy * view.forward.y + cz * view.forward.z);
|
|
21
|
+
}
|
|
22
|
+
farFit = deepest + 1;
|
|
23
|
+
}
|
|
24
|
+
farFit = Math.min(view.far, Math.max(view.near + 1, farFit));
|
|
25
|
+
const split = Math.min(farFit, Math.max(view.near + 8, view.focus + NEAR_REACH));
|
|
26
|
+
return [
|
|
27
|
+
[view.near, split],
|
|
28
|
+
[view.near, farFit],
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** The bounding sphere of a frustum slice: on the view axis, at the depth that
|
|
32
|
+
* balances the near and far rectangles' corners. */
|
|
33
|
+
function sliceSphere(view, n, f) {
|
|
34
|
+
const t = Math.tan(view.fov / 2);
|
|
35
|
+
const k2 = t * t * (1 + view.aspect * view.aspect);
|
|
36
|
+
let depth;
|
|
37
|
+
let radius;
|
|
38
|
+
if (k2 >= (f - n) / (f + n)) {
|
|
39
|
+
depth = f;
|
|
40
|
+
radius = f * Math.sqrt(k2);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
depth = 0.5 * (f + n) * (1 + k2);
|
|
44
|
+
radius = 0.5 * Math.sqrt((f - n) * (f - n) + 2 * (f * f + n * n) * k2 + (f + n) * (f + n) * k2 * k2);
|
|
45
|
+
}
|
|
46
|
+
const center = new Vec3(view.eye.x + view.forward.x * depth, view.eye.y + view.forward.y * depth, view.eye.z + view.forward.z * depth);
|
|
47
|
+
return { center, radius };
|
|
48
|
+
}
|
|
49
|
+
/** The eight corners of a slice, for tests and for the fit's own checks. */
|
|
50
|
+
export function sliceCorners(view, n, f) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const d of [n, f]) {
|
|
53
|
+
const h = d * Math.tan(view.fov / 2);
|
|
54
|
+
const w = h * view.aspect;
|
|
55
|
+
for (const sy of [-1, 1])
|
|
56
|
+
for (const sx of [-1, 1])
|
|
57
|
+
out.push({
|
|
58
|
+
x: view.eye.x + view.forward.x * d + view.right.x * w * sx + view.up.x * h * sy,
|
|
59
|
+
y: view.eye.y + view.forward.y * d + view.right.y * w * sx + view.up.y * h * sy,
|
|
60
|
+
z: view.eye.z + view.forward.z * d + view.right.z * w * sx + view.up.z * h * sy,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
39
65
|
/**
|
|
40
|
-
* One cascade's view-projection
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
66
|
+
* One cascade's view-projection: an orthographic box around the slice's
|
|
67
|
+
* sphere, snapped to the map's texel grid in the light's right/up plane so a
|
|
68
|
+
* moving camera doesn't shimmer its shadow edges, and reaching along the light
|
|
69
|
+
* from the nearest thing in the scene to the farthest, so everything that
|
|
70
|
+
* could cast into the slice does.
|
|
45
71
|
*
|
|
46
72
|
* Writes the 16 floats into `out` at `offset` and returns `out`.
|
|
47
73
|
*/
|
|
48
|
-
export function
|
|
74
|
+
export function fitShadowVP(view, slice, sunDirection, bounds, cascade, out, offset) {
|
|
49
75
|
const dir = new Vec3(sunDirection.x, sunDirection.y, sunDirection.z);
|
|
50
76
|
dir.normalize();
|
|
51
77
|
const up = Math.abs(dir.y) > 0.99 ? new Vec3(0, 0, -1) : new Vec3(0, 1, 0);
|
|
52
|
-
const t = new Vec3(target.x, target.y, target.z);
|
|
53
78
|
const right = Vec3.crossInto(up, dir, new Vec3(0, 0, 0)).normalize();
|
|
54
79
|
const upv = Vec3.crossInto(dir, right, new Vec3(0, 0, 0));
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
80
|
+
const { center, radius } = sliceSphere(view, slice[0], slice[1]);
|
|
81
|
+
// A texel of the map, in world units; the radius is rounded up onto the
|
|
82
|
+
// grid too, so the box's size does not drift with the fov by fractions.
|
|
83
|
+
const texel = Math.max((2 * radius) / cascade.mapSize, 1e-4);
|
|
84
|
+
const half = Math.ceil(radius / texel) * texel;
|
|
85
|
+
const tr = Math.round(center.dot(right) / texel) * texel;
|
|
86
|
+
const tu = Math.round(center.dot(upv) / texel) * texel;
|
|
87
|
+
const td = center.dot(dir);
|
|
59
88
|
const snapped = new Vec3(right.x * tr + upv.x * tu + dir.x * td, right.y * tr + upv.y * tu + dir.y * td, right.z * tr + upv.z * tu + dir.z * td);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
// Along the light: from the scene's nearest point to its farthest, with a
|
|
90
|
+
// margin so a caster on the box's own face is not clipped. With nothing to
|
|
91
|
+
// fit, the sphere itself.
|
|
92
|
+
let zmin = td - half;
|
|
93
|
+
let zmax = td + half;
|
|
94
|
+
if (bounds) {
|
|
95
|
+
for (let i = 0; i < 8; i++) {
|
|
96
|
+
const z = (i & 1 ? bounds.max : bounds.min)[0] * dir.x + (i & 2 ? bounds.max : bounds.min)[1] * dir.y + (i & 4 ? bounds.max : bounds.min)[2] * dir.z;
|
|
97
|
+
zmin = Math.min(zmin, z);
|
|
98
|
+
zmax = Math.max(zmax, z);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const margin = 2 + 0.02 * (zmax - zmin);
|
|
102
|
+
const back = td - zmin + margin;
|
|
103
|
+
const far = back + (zmax - td) + margin;
|
|
104
|
+
const eye = new Vec3(snapped.x - dir.x * back, snapped.y - dir.y * back, snapped.z - dir.z * back);
|
|
105
|
+
const viewM = Mat4.lookAt(eye, snapped, up);
|
|
106
|
+
const proj = Mat4.orthographicLh(-half, half, -half, half, 1, far + 1);
|
|
107
|
+
out.set(proj.multiply(viewM).values, offset);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** Every cascade's view-projection in a row, inner to outer. */
|
|
111
|
+
export function buildShadowCascades(view, sunDirection, bounds, out) {
|
|
112
|
+
const slices = cascadeSlices(view, bounds);
|
|
113
|
+
for (let i = 0; i < SHADOW_CASCADES.length; i++)
|
|
114
|
+
fitShadowVP(view, slices[i], sunDirection, bounds, SHADOW_CASCADES[i], out, i * 16);
|
|
69
115
|
return out;
|
|
70
116
|
}
|