reze-engine 0.56.10 → 0.56.12
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/camera.d.ts +13 -1
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +25 -2
- package/dist/engine.d.ts +92 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +273 -56
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +29 -0
- package/dist/ibl.d.ts +28 -0
- package/dist/ibl.d.ts.map +1 -1
- package/dist/ibl.js +21 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +3 -0
- package/dist/shaders/lights.d.ts +17 -6
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +69 -16
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +17 -1
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +41 -0
- package/package.json +1 -1
- package/src/camera.ts +26 -2
- package/src/engine.ts +311 -64
- package/src/graph/registry.ts +31 -0
- package/src/ibl.ts +41 -0
- package/src/index.ts +1 -0
- package/src/reflection.ts +3 -0
- package/src/shaders/lights.ts +69 -16
- package/src/shaders/materials/common.ts +17 -1
- package/src/shaders/materials/nodes.ts +41 -0
package/src/shaders/lights.ts
CHANGED
|
@@ -10,21 +10,26 @@ import { SCENE_TAP_STUB } from "./scene-tap"
|
|
|
10
10
|
// renderer that bolted a second key light onto a toon shader has shipped. A
|
|
11
11
|
// light here brightens; it does not restate the shading.
|
|
12
12
|
//
|
|
13
|
-
// So there are no per-light shadows, no area lights and no clustering.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// So there are no per-light shadows, no area lights and no clustering. The loop
|
|
14
|
+
// runs over the lights a scene HAS, not over the cap, so a scene with four pays
|
|
15
|
+
// for four; the cap only bounds the buffer and the worst case.
|
|
16
16
|
//
|
|
17
17
|
// LAYOUT. A 4-float header (count, then padding that keeps the records
|
|
18
|
-
// vec4-aligned), then MAX_LIGHTS records of
|
|
18
|
+
// vec4-aligned), then MAX_LIGHTS records of 16 floats — four vec4s:
|
|
19
19
|
//
|
|
20
|
-
// [0..2] position, world space
|
|
20
|
+
// [0..2] position, world space [3] radius
|
|
21
21
|
// [4..6] colour PREMULTIPLIED by intensity [7] type
|
|
22
|
+
// [8..10] aim, unit, pointing away from the light [11] cos of the outer angle
|
|
23
|
+
// [12] cos of the inner angle [13..15] spare
|
|
22
24
|
//
|
|
23
25
|
// Colour carries intensity because nothing reads them apart: every use is the
|
|
24
26
|
// product, and storing two numbers that are only ever multiplied is two numbers
|
|
25
|
-
// that can disagree.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
27
|
+
// that can disagree.
|
|
28
|
+
//
|
|
29
|
+
// A POINT LIGHT IS A SPOT WITH NO CONE, and that is how the loop stays
|
|
30
|
+
// branchless: it stores aim (0,0,0) and both cosines at -1, which makes the
|
|
31
|
+
// cone term below saturate to exactly 1. `type` says which a light is for
|
|
32
|
+
// anyone reading the buffer; the shading never asks.
|
|
28
33
|
|
|
29
34
|
import { castDistanceStub } from "./passes/cast-distance"
|
|
30
35
|
import { audioApi } from "./audio-api"
|
|
@@ -37,14 +42,25 @@ import { idApi } from "./id-api"
|
|
|
37
42
|
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
38
43
|
export const LIGHT_HEADER = 4
|
|
39
44
|
/** Floats per light — see the layout above. */
|
|
40
|
-
export const LIGHT_STRIDE =
|
|
45
|
+
export const LIGHT_STRIDE = 16
|
|
41
46
|
/**
|
|
42
|
-
* The cap
|
|
43
|
-
* the
|
|
44
|
-
*
|
|
45
|
-
*
|
|
47
|
+
* The cap. The loop below runs per fragment over the lights a scene HAS, so
|
|
48
|
+
* this bounds the worst case rather than the ordinary one, and the buffer is
|
|
49
|
+
* storage rather than uniform — 128 records is 8 KiB, which costs nothing to
|
|
50
|
+
* declare and is paid for only by the scenes that fill it.
|
|
51
|
+
*
|
|
52
|
+
* Sized against the work, not against a round number: a Unity stage arrives
|
|
53
|
+
* with a lamp per fixture. One rip of a resort brought thirty-three, a lit
|
|
54
|
+
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
55
|
+
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
56
|
+
*
|
|
57
|
+
* What it does NOT buy: a scene that genuinely lights a fragment from a hundred
|
|
58
|
+
* lamps pays for a hundred iterations of the loop. The distance test below
|
|
59
|
+
* rejects most of them in a few instructions, which is what makes a rig of this
|
|
60
|
+
* size affordable at all; clustering is what replaces the linear walk when a
|
|
61
|
+
* scene wants every one of them close enough to matter.
|
|
46
62
|
*/
|
|
47
|
-
export const MAX_LIGHTS =
|
|
63
|
+
export const MAX_LIGHTS = 128
|
|
48
64
|
/** Floats in the whole buffer. */
|
|
49
65
|
export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE
|
|
50
66
|
|
|
@@ -210,6 +226,19 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
210
226
|
_rzLightsOut[b + 4u] = c.x;
|
|
211
227
|
_rzLightsOut[b + 5u] = c.y;
|
|
212
228
|
_rzLightsOut[b + 6u] = c.z;
|
|
229
|
+
// An effect emits POINT lights: RzLight carries no aim, and widening it would
|
|
230
|
+
// fail to compile every emitter already written against it. The rest of the
|
|
231
|
+
// record is written anyway — these slots are reused frame to frame, and a spot
|
|
232
|
+
// the document placed here last frame would otherwise keep its cone.
|
|
233
|
+
_rzLightsOut[b + 7u] = 0.0;
|
|
234
|
+
_rzLightsOut[b + 8u] = 0.0;
|
|
235
|
+
_rzLightsOut[b + 9u] = 0.0;
|
|
236
|
+
_rzLightsOut[b + 10u] = 0.0;
|
|
237
|
+
_rzLightsOut[b + 11u] = -1.0;
|
|
238
|
+
_rzLightsOut[b + 12u] = -1.0;
|
|
239
|
+
_rzLightsOut[b + 13u] = 0.0;
|
|
240
|
+
_rzLightsOut[b + 14u] = 0.0;
|
|
241
|
+
_rzLightsOut[b + 15u] = 0.0;
|
|
213
242
|
}
|
|
214
243
|
`
|
|
215
244
|
}
|
|
@@ -240,6 +269,19 @@ fn rzLightColor(i: u32) -> vec3f {
|
|
|
240
269
|
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
241
270
|
}
|
|
242
271
|
|
|
272
|
+
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
273
|
+
fn rzLightAim(i: u32) -> vec3f {
|
|
274
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
275
|
+
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
279
|
+
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
280
|
+
fn rzLightCone(i: u32) -> vec2f {
|
|
281
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 11u;
|
|
282
|
+
return vec2f(_rzLights[b], _rzLights[b + 1u]);
|
|
283
|
+
}
|
|
284
|
+
|
|
243
285
|
/**
|
|
244
286
|
* Every positional light's contribution at a surface point, as light — not as a
|
|
245
287
|
* finished colour. Multiply by whatever the surface's albedo is.
|
|
@@ -269,14 +311,25 @@ fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
|
269
311
|
for (var i = 0u; i < count; i = i + 1u) {
|
|
270
312
|
let d = rzLightPos(i) - p;
|
|
271
313
|
let dist = length(d);
|
|
314
|
+
// Out of reach before anything else is computed. The falloff is already
|
|
315
|
+
// exactly zero at the radius, so this changes no pixel — it is what keeps a
|
|
316
|
+
// stage rig's far lamps off the bill at every fragment they do not light.
|
|
317
|
+
if (dist >= rzLightRadius(i)) { continue; }
|
|
318
|
+
let toLight = d / max(dist, 1e-4);
|
|
272
319
|
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
273
320
|
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
274
321
|
// the ramp's business and not this one's.
|
|
275
|
-
let ndl = max(dot(n,
|
|
322
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
276
323
|
if (ndl <= 0.0) { continue; }
|
|
277
324
|
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
278
325
|
let falloff = 1.0 - t * t;
|
|
279
|
-
|
|
326
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
327
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
328
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
329
|
+
// dot product and no branch.
|
|
330
|
+
let cone = rzLightCone(i);
|
|
331
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
332
|
+
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
280
333
|
}
|
|
281
334
|
return acc;
|
|
282
335
|
}
|
|
@@ -31,7 +31,16 @@ struct CameraUniforms {
|
|
|
31
31
|
view: mat4x4f,
|
|
32
32
|
projection: mat4x4f,
|
|
33
33
|
viewPos: vec3f,
|
|
34
|
-
|
|
34
|
+
/** Render-target height in device pixels. */
|
|
35
|
+
viewportHeight: f32,
|
|
36
|
+
/** The scene clock in seconds — what the time node reads. Paused with the
|
|
37
|
+
* transport and stepped exactly by an export, so an animated material lands
|
|
38
|
+
* in the file where it stood on screen. (No backticks in here: this WGSL
|
|
39
|
+
* lives in a template literal and one would end it.) */
|
|
40
|
+
time: f32,
|
|
41
|
+
// NOTHING AFTER THIS. A trailing vec3f aligns to 16 and pushes the struct to
|
|
42
|
+
// 176 bytes against a 160-byte buffer, which fails validation on every
|
|
43
|
+
// pipeline at once; ending on the f32 rounds the struct to exactly 160.
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
struct Light {
|
|
@@ -127,6 +136,13 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
|
127
136
|
// keeps its shadows when the crisp near volume ends. Binding 7: 6 is the
|
|
128
137
|
// positional lights, 9 the BRDF LUT.
|
|
129
138
|
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
139
|
+
// The world's own sky, for a surface that REFLECTS it rather than merely being
|
|
140
|
+
// lit by it. Equirect, in the same layout and the same linear radiance the
|
|
141
|
+
// composite draws the dome from, and mipped — a rough surface reads a coarser
|
|
142
|
+
// level, which is what a prefiltered environment is for. A scene with no HDRI
|
|
143
|
+
// has the 1x1 fallback bound here; rzWorldSpecular checks the SH flag and
|
|
144
|
+
// answers with the flat ambient instead, so nothing samples the placeholder.
|
|
145
|
+
@group(0) @binding(8) var worldEnvTexture: texture_2d<f32>;
|
|
130
146
|
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
131
147
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
132
148
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
@@ -290,6 +290,47 @@ fn point_world_to_camera(p: vec3f) -> vec3f { return (camera.view * vec4f(p, 1.0
|
|
|
290
290
|
// One function per slot because WGSL has no texture arrays-of-bindings here; the
|
|
291
291
|
// slot is topology, exactly like a Math node's operation.
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The world's radiance arriving from a direction — what a reflection sees.
|
|
295
|
+
*
|
|
296
|
+
* The engine's ambient is irradiance: the SH fit of the sky integrated over a
|
|
297
|
+
* hemisphere, evaluated along the NORMAL. That is the right answer for diffuse
|
|
298
|
+
* light and the wrong one for a mirror, which needs radiance along the
|
|
299
|
+
* REFLECTION vector and needs it sharp. A stage ripped from a game asks for
|
|
300
|
+
* this on nearly every surface — it is what a Unity material's DEFAULT_REFLECTION
|
|
301
|
+
* keyword means —
|
|
302
|
+
* and so does every metal the app has ever drawn.
|
|
303
|
+
*
|
|
304
|
+
* ROUGHNESS PICKS A MIP, which is the cheap prefilter: the chain is built by
|
|
305
|
+
* successive halving at upload, so a level is a box average of the sky over a
|
|
306
|
+
* widening solid angle. A cosine-lobe convolution would be more correct; the
|
|
307
|
+
* visible difference is confined to the middle of the roughness range, and the
|
|
308
|
+
* chain costs one downsample per level instead of a render pass per level.
|
|
309
|
+
*
|
|
310
|
+
* Without an HDRI there is nothing to reflect but the flat world colour, which
|
|
311
|
+
* is what the ambient already is — so the two agree and a scene without a sky
|
|
312
|
+
* texture sees no change at all.
|
|
313
|
+
*/
|
|
314
|
+
fn rzWorldSpecular(dir: vec3f, roughness: f32) -> vec3f {
|
|
315
|
+
// A SKY TEXTURE, or the best the scene has. The flag is 2 when an HDRI is
|
|
316
|
+
// installed, 1 when the world is a gradient fitted to the same harmonics,
|
|
317
|
+
// and 0 when it is one flat colour — and in the last two cases the ambient
|
|
318
|
+
// already answers by direction, which is a blurred sky and exactly what a
|
|
319
|
+
// world without a picture can honestly reflect.
|
|
320
|
+
if (light.sh[0].w < 1.5) { return rzWorldAmbient(normalize(dir)); }
|
|
321
|
+
let d = normalize(dir);
|
|
322
|
+
// The composite's own convention, restated: LH world with +Z forward, which
|
|
323
|
+
// is what makes a reflection line up with the dome the viewer can see.
|
|
324
|
+
let u = 0.5 + atan2(d.x, d.z) * 0.15915494309;
|
|
325
|
+
let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) * 0.31830988618;
|
|
326
|
+
let levels = f32(textureNumLevels(worldEnvTexture) - 1u);
|
|
327
|
+
// sqrt, not linear: mip n covers roughly twice the angle of n-1, so a linear
|
|
328
|
+
// ramp spends most of its travel in the blurry end and a mirror-to-satin
|
|
329
|
+
// sweep happens entirely in the first tenth of the dial.
|
|
330
|
+
let lod = clamp(sqrt(clamp(roughness, 0.0, 1.0)) * levels, 0.0, levels);
|
|
331
|
+
return textureSampleLevel(worldEnvTexture, diffuseSampler, vec2f(u, v), lod).rgb * light.ambientColor.w;
|
|
332
|
+
}
|
|
333
|
+
|
|
293
334
|
fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
|
|
294
335
|
fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
|
|
295
336
|
fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
|