reze-engine 0.56.9 → 0.56.11
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/README.md +7 -3
- package/dist/engine.d.ts +39 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +93 -10
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +9 -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 +7 -6
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +59 -16
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -1
- package/dist/shaders/passes/cast-distance.d.ts +5 -1
- package/dist/shaders/passes/cast-distance.d.ts.map +1 -1
- package/dist/shaders/passes/cast-distance.js +21 -5
- package/package.json +1 -1
- package/src/engine.ts +113 -14
- package/src/graph/registry.ts +10 -0
- package/src/index.ts +1 -0
- package/src/reflection.ts +3 -0
- package/src/shaders/lights.ts +59 -16
- package/src/shaders/materials/common.ts +10 -1
- package/src/shaders/passes/cast-distance.ts +21 -5
package/dist/shaders/lights.js
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
|
import { castDistanceStub } from "./passes/cast-distance";
|
|
29
34
|
import { audioApi } from "./audio-api";
|
|
30
35
|
import { midiApi } from "./midi-api";
|
|
@@ -35,14 +40,15 @@ import { idApi } from "./id-api";
|
|
|
35
40
|
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
36
41
|
export const LIGHT_HEADER = 4;
|
|
37
42
|
/** Floats per light — see the layout above. */
|
|
38
|
-
export const LIGHT_STRIDE =
|
|
43
|
+
export const LIGHT_STRIDE = 16;
|
|
39
44
|
/**
|
|
40
|
-
* The cap
|
|
41
|
-
* the
|
|
42
|
-
*
|
|
43
|
-
* the
|
|
45
|
+
* The cap. The loop below runs per fragment over the lights a scene has, so
|
|
46
|
+
* this bounds the worst case rather than the ordinary one — and it has to clear
|
|
47
|
+
* a game stage: a Unity rip of one arrives with thirty-odd point lights, which
|
|
48
|
+
* the old ceiling of sixteen cut in half. Past this the extras are dropped.
|
|
49
|
+
* Clustering is what replaces it when a scene wants hundreds.
|
|
44
50
|
*/
|
|
45
|
-
export const MAX_LIGHTS =
|
|
51
|
+
export const MAX_LIGHTS = 48;
|
|
46
52
|
/** Floats in the whole buffer. */
|
|
47
53
|
export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE;
|
|
48
54
|
/**
|
|
@@ -199,6 +205,19 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
199
205
|
_rzLightsOut[b + 4u] = c.x;
|
|
200
206
|
_rzLightsOut[b + 5u] = c.y;
|
|
201
207
|
_rzLightsOut[b + 6u] = c.z;
|
|
208
|
+
// An effect emits POINT lights: RzLight carries no aim, and widening it would
|
|
209
|
+
// fail to compile every emitter already written against it. The rest of the
|
|
210
|
+
// record is written anyway — these slots are reused frame to frame, and a spot
|
|
211
|
+
// the document placed here last frame would otherwise keep its cone.
|
|
212
|
+
_rzLightsOut[b + 7u] = 0.0;
|
|
213
|
+
_rzLightsOut[b + 8u] = 0.0;
|
|
214
|
+
_rzLightsOut[b + 9u] = 0.0;
|
|
215
|
+
_rzLightsOut[b + 10u] = 0.0;
|
|
216
|
+
_rzLightsOut[b + 11u] = -1.0;
|
|
217
|
+
_rzLightsOut[b + 12u] = -1.0;
|
|
218
|
+
_rzLightsOut[b + 13u] = 0.0;
|
|
219
|
+
_rzLightsOut[b + 14u] = 0.0;
|
|
220
|
+
_rzLightsOut[b + 15u] = 0.0;
|
|
202
221
|
}
|
|
203
222
|
`;
|
|
204
223
|
}
|
|
@@ -228,6 +247,19 @@ fn rzLightColor(i: u32) -> vec3f {
|
|
|
228
247
|
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
251
|
+
fn rzLightAim(i: u32) -> vec3f {
|
|
252
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
253
|
+
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
257
|
+
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
258
|
+
fn rzLightCone(i: u32) -> vec2f {
|
|
259
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 11u;
|
|
260
|
+
return vec2f(_rzLights[b], _rzLights[b + 1u]);
|
|
261
|
+
}
|
|
262
|
+
|
|
231
263
|
/**
|
|
232
264
|
* Every positional light's contribution at a surface point, as light — not as a
|
|
233
265
|
* finished colour. Multiply by whatever the surface's albedo is.
|
|
@@ -257,14 +289,25 @@ fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
|
257
289
|
for (var i = 0u; i < count; i = i + 1u) {
|
|
258
290
|
let d = rzLightPos(i) - p;
|
|
259
291
|
let dist = length(d);
|
|
292
|
+
// Out of reach before anything else is computed. The falloff is already
|
|
293
|
+
// exactly zero at the radius, so this changes no pixel — it is what keeps a
|
|
294
|
+
// stage rig's far lamps off the bill at every fragment they do not light.
|
|
295
|
+
if (dist >= rzLightRadius(i)) { continue; }
|
|
296
|
+
let toLight = d / max(dist, 1e-4);
|
|
260
297
|
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
261
298
|
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
262
299
|
// the ramp's business and not this one's.
|
|
263
|
-
let ndl = max(dot(n,
|
|
300
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
264
301
|
if (ndl <= 0.0) { continue; }
|
|
265
302
|
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
266
303
|
let falloff = 1.0 - t * t;
|
|
267
|
-
|
|
304
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
305
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
306
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
307
|
+
// dot product and no branch.
|
|
308
|
+
let cone = rzLightCone(i);
|
|
309
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
310
|
+
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
268
311
|
}
|
|
269
312
|
return acc;
|
|
270
313
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QA4IhC,CAAC;AAiIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -28,7 +28,16 @@ struct CameraUniforms {
|
|
|
28
28
|
view: mat4x4f,
|
|
29
29
|
projection: mat4x4f,
|
|
30
30
|
viewPos: vec3f,
|
|
31
|
-
|
|
31
|
+
/** Render-target height in device pixels. */
|
|
32
|
+
viewportHeight: f32,
|
|
33
|
+
/** The scene clock in seconds — what the time node reads. Paused with the
|
|
34
|
+
* transport and stepped exactly by an export, so an animated material lands
|
|
35
|
+
* in the file where it stood on screen. (No backticks in here: this WGSL
|
|
36
|
+
* lives in a template literal and one would end it.) */
|
|
37
|
+
time: f32,
|
|
38
|
+
// NOTHING AFTER THIS. A trailing vec3f aligns to 16 and pushes the struct to
|
|
39
|
+
// 176 bytes against a 160-byte buffer, which fails validation on every
|
|
40
|
+
// pipeline at once; ending on the f32 rounds the struct to exactly 160.
|
|
32
41
|
};
|
|
33
42
|
|
|
34
43
|
struct Light {
|
|
@@ -19,7 +19,11 @@ export declare const CAST_FIELD_DIV = 1;
|
|
|
19
19
|
*
|
|
20
20
|
* The ground, a stage and a media plane all write ids exactly as she does, so
|
|
21
21
|
* seeding "anything drawn" would grow a border around the floor: a rectangle
|
|
22
|
-
* round the frame rather than a sticker.
|
|
22
|
+
* round the frame rather than a sticker. The subject ids seed, and so do the
|
|
23
|
+
* props': a sword in her hand is part of her silhouette. A prop left out is a
|
|
24
|
+
* HOLE in the seeds wherever it covers her, and the field grows into it from
|
|
25
|
+
* her side, so the border ran round the inside of the prop instead of the
|
|
26
|
+
* outside.
|
|
23
27
|
*/
|
|
24
28
|
export declare function buildCastSeedShader(samples: number): string;
|
|
25
29
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B
|
|
1
|
+
{"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqF3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
|
|
@@ -74,7 +74,11 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
|
74
74
|
*
|
|
75
75
|
* The ground, a stage and a media plane all write ids exactly as she does, so
|
|
76
76
|
* seeding "anything drawn" would grow a border around the floor: a rectangle
|
|
77
|
-
* round the frame rather than a sticker.
|
|
77
|
+
* round the frame rather than a sticker. The subject ids seed, and so do the
|
|
78
|
+
* props': a sword in her hand is part of her silhouette. A prop left out is a
|
|
79
|
+
* HOLE in the seeds wherever it covers her, and the field grows into it from
|
|
80
|
+
* her side, so the border ran round the inside of the prop instead of the
|
|
81
|
+
* outside.
|
|
78
82
|
*/
|
|
79
83
|
export function buildCastSeedShader(samples) {
|
|
80
84
|
return (
|
|
@@ -85,6 +89,9 @@ const RZ_ID_SAMPLES: i32 = ${samples};
|
|
|
85
89
|
|
|
86
90
|
@group(0) @binding(0) var _rzIdTex: texture_multisampled_2d<u32>;
|
|
87
91
|
@group(0) @binding(1) var<storage, read> _rzCast: array<vec4f>;
|
|
92
|
+
/** The visible props' object ids, count first. Props are not subjects, so the
|
|
93
|
+
* cast buffer does not carry them. */
|
|
94
|
+
@group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
|
|
88
95
|
|
|
89
96
|
// The two cast accessors this pass needs, spelled out rather than pulled in.
|
|
90
97
|
// CAST_API brings subjects, trails, anchors and their aliases with it, and a
|
|
@@ -100,6 +107,17 @@ fn rzSubjectId(i: i32) -> u32 {
|
|
|
100
107
|
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
101
108
|
return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
|
|
102
109
|
}
|
|
110
|
+
/** Does this object seed the field: a subject, or a prop. */
|
|
111
|
+
fn rzSeeds(o: u32, subjects: i32) -> bool {
|
|
112
|
+
for (var i = 0; i < subjects; i++) {
|
|
113
|
+
if (o == rzSubjectId(i)) { return true; }
|
|
114
|
+
}
|
|
115
|
+
let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
|
|
116
|
+
for (var j = 1u; j <= props; j++) {
|
|
117
|
+
if (o == _rzSeedProps[j]) { return true; }
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
103
121
|
|
|
104
122
|
${FULLSCREEN_VS}
|
|
105
123
|
|
|
@@ -133,9 +151,7 @@ fn fs(@builtin(position) pos: vec4f) -> SeedOut {
|
|
|
133
151
|
for (var sample = 0; sample < RZ_ID_SAMPLES; sample++) {
|
|
134
152
|
let o = textureLoad(_rzIdTex, p, sample).y;
|
|
135
153
|
if (o == 0u) { continue; }
|
|
136
|
-
|
|
137
|
-
if (o == rzSubjectId(i)) { covered += 1.0; break; }
|
|
138
|
-
}
|
|
154
|
+
if (rzSeeds(o, n)) { covered += 1.0; }
|
|
139
155
|
}
|
|
140
156
|
var out: SeedOut;
|
|
141
157
|
out.coverage = covered / f32(RZ_ID_SAMPLES);
|
|
@@ -254,7 +270,7 @@ export function castDistanceApi(group, tex, samp, scale) {
|
|
|
254
270
|
*
|
|
255
271
|
* There is no ceiling. A pixel on the far side of the frame gets the real
|
|
256
272
|
* number. The ground, a stage and a media plane are not the cast and do not
|
|
257
|
-
* seed it.
|
|
273
|
+
* seed it. A prop does: what she holds is inside her border.
|
|
258
274
|
*
|
|
259
275
|
* Screen pixels whatever the field's own resolution is, so an author writes the
|
|
260
276
|
* width they mean and never has to know how this is built. uv is the effect's
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -826,9 +826,10 @@ interface ModelInstance {
|
|
|
826
826
|
/**
|
|
827
827
|
* A PROP: a PMX object a character holds or wears — a microphone, a fan, a
|
|
828
828
|
* sword. The third answer beside stage and plane. It keeps what a cast member
|
|
829
|
-
* has that scenery does not (physics, outlines, its own clip
|
|
830
|
-
*
|
|
831
|
-
* no
|
|
829
|
+
* has that scenery does not (physics, outlines, its own clip, a place in the
|
|
830
|
+
* cast's silhouette for rzCastDistance) and drops what makes one a performer:
|
|
831
|
+
* no effect subject id, no seeding of the scene clock, no bone picking. Like a
|
|
832
|
+
* card it leaves the floor alone. See addProp.
|
|
832
833
|
*/
|
|
833
834
|
isProp: boolean
|
|
834
835
|
/** Who this model hangs from, or null. Any model can: a prop by design, a
|
|
@@ -1593,6 +1594,23 @@ interface EffectInstance {
|
|
|
1593
1594
|
trails: EffectTrails | null
|
|
1594
1595
|
}
|
|
1595
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* A light the DOCUMENT places, as setLights takes it.
|
|
1599
|
+
*
|
|
1600
|
+
* `aim` is what separates the two kinds: with one the light is a spot pointing
|
|
1601
|
+
* that way, without one it is a point light. Angles are the whole cone in
|
|
1602
|
+
* degrees, the way a fixture and every DCC states them.
|
|
1603
|
+
*/
|
|
1604
|
+
export type SceneLight = {
|
|
1605
|
+
position: XYZ
|
|
1606
|
+
color: XYZ
|
|
1607
|
+
intensity?: number
|
|
1608
|
+
radius?: number
|
|
1609
|
+
aim?: XYZ
|
|
1610
|
+
angle?: number
|
|
1611
|
+
innerAngle?: number
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1596
1614
|
export class Engine {
|
|
1597
1615
|
private static instance: Engine | null = null
|
|
1598
1616
|
|
|
@@ -1611,7 +1629,7 @@ export class Engine {
|
|
|
1611
1629
|
// `!` field here is genuinely absent until init() — this one no longer is.
|
|
1612
1630
|
private camera: Camera
|
|
1613
1631
|
private cameraUniformBuffer!: GPUBuffer
|
|
1614
|
-
private cameraMatrixData = new Float32Array(
|
|
1632
|
+
private cameraMatrixData = new Float32Array(40)
|
|
1615
1633
|
// Blender-style scene config groups (resolved from EngineOptions)
|
|
1616
1634
|
private world!: { color: Vec3; strength: number }
|
|
1617
1635
|
private sun!: { color: Vec3; strength: number; direction: Vec3 }
|
|
@@ -1993,6 +2011,10 @@ export class Engine {
|
|
|
1993
2011
|
private castStepBindGroups: GPUBindGroup[] = []
|
|
1994
2012
|
private castResolveBindGroup: GPUBindGroup | null = null
|
|
1995
2013
|
private castStepStrideBuffers: GPUBuffer[] = []
|
|
2014
|
+
/** The visible props' object ids for the seed pass, count in slot 0. Grown in
|
|
2015
|
+
* powers of two; see writeCastSeedProps. */
|
|
2016
|
+
private castSeedPropBuffer: GPUBuffer | null = null
|
|
2017
|
+
private castSeedPropData = new Uint32Array(8)
|
|
1996
2018
|
/** Does anything installed actually read the field? Set from the effect list. */
|
|
1997
2019
|
private castDistanceWanted = false
|
|
1998
2020
|
|
|
@@ -3690,14 +3712,7 @@ export class Engine {
|
|
|
3690
3712
|
const strides: number[] = []
|
|
3691
3713
|
for (let k = 1 << Math.ceil(Math.log2(Math.max(w, h))); k >= 1; k >>= 1) strides.push(k)
|
|
3692
3714
|
|
|
3693
|
-
this.
|
|
3694
|
-
label: "cast distance seed",
|
|
3695
|
-
layout: this.castSeedPipeline.getBindGroupLayout(0),
|
|
3696
|
-
entries: [
|
|
3697
|
-
{ binding: 0, resource: this.idView! },
|
|
3698
|
-
{ binding: 1, resource: { buffer: this.castBuffer } },
|
|
3699
|
-
],
|
|
3700
|
-
})
|
|
3715
|
+
this.createCastSeedBindGroup()
|
|
3701
3716
|
strides.forEach((stride, i) => {
|
|
3702
3717
|
const buf = this.device!.createBuffer({
|
|
3703
3718
|
label: `cast distance stride ${stride}`,
|
|
@@ -3729,6 +3744,59 @@ export class Engine {
|
|
|
3729
3744
|
})
|
|
3730
3745
|
}
|
|
3731
3746
|
|
|
3747
|
+
/** The seed pass's inputs: the id attachment, the cast, and the prop ids. */
|
|
3748
|
+
private createCastSeedBindGroup(): void {
|
|
3749
|
+
const device = this.device!
|
|
3750
|
+
if (!this.castSeedPropBuffer) {
|
|
3751
|
+
this.castSeedPropBuffer = device.createBuffer({
|
|
3752
|
+
label: "cast distance prop seeds",
|
|
3753
|
+
size: this.castSeedPropData.byteLength,
|
|
3754
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
3755
|
+
})
|
|
3756
|
+
device.queue.writeBuffer(this.castSeedPropBuffer, 0, this.castSeedPropData)
|
|
3757
|
+
}
|
|
3758
|
+
this.castSeedBindGroup = device.createBindGroup({
|
|
3759
|
+
label: "cast distance seed",
|
|
3760
|
+
layout: this.castSeedPipeline!.getBindGroupLayout(0),
|
|
3761
|
+
entries: [
|
|
3762
|
+
{ binding: 0, resource: this.idView! },
|
|
3763
|
+
{ binding: 1, resource: { buffer: this.castBuffer } },
|
|
3764
|
+
{ binding: 2, resource: { buffer: this.castSeedPropBuffer } },
|
|
3765
|
+
],
|
|
3766
|
+
})
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
/**
|
|
3770
|
+
* Which props seed the field this frame: every visible one.
|
|
3771
|
+
*
|
|
3772
|
+
* A prop is not a subject — nothing follows it and no effect reads its bones —
|
|
3773
|
+
* but it is part of her silhouette. Uploaded only when the list changes.
|
|
3774
|
+
*/
|
|
3775
|
+
private writeCastSeedProps(): void {
|
|
3776
|
+
let n = 0
|
|
3777
|
+
this.forEachInstance((inst) => {
|
|
3778
|
+
if (inst.isProp && inst.model.visible) n++
|
|
3779
|
+
})
|
|
3780
|
+
let data = this.castSeedPropData
|
|
3781
|
+
let changed = data[0] !== n
|
|
3782
|
+
if (n + 1 > data.length) {
|
|
3783
|
+
data = new Uint32Array(1 << Math.ceil(Math.log2(n + 1)))
|
|
3784
|
+
this.castSeedPropData = data
|
|
3785
|
+
this.castSeedPropBuffer?.destroy()
|
|
3786
|
+
this.castSeedPropBuffer = null
|
|
3787
|
+
this.createCastSeedBindGroup()
|
|
3788
|
+
changed = true
|
|
3789
|
+
}
|
|
3790
|
+
data[0] = n
|
|
3791
|
+
let i = 1
|
|
3792
|
+
this.forEachInstance((inst) => {
|
|
3793
|
+
if (!inst.isProp || !inst.model.visible) return
|
|
3794
|
+
if (data[i] !== inst.objectId) changed = true
|
|
3795
|
+
data[i++] = inst.objectId
|
|
3796
|
+
})
|
|
3797
|
+
if (changed) this.device!.queue.writeBuffer(this.castSeedPropBuffer!, 0, data)
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3732
3800
|
/**
|
|
3733
3801
|
* The flood, encoded once a frame before the field pass reads it.
|
|
3734
3802
|
*
|
|
@@ -3738,6 +3806,7 @@ export class Engine {
|
|
|
3738
3806
|
*/
|
|
3739
3807
|
private encodeCastDistance(encoder: GPUCommandEncoder): void {
|
|
3740
3808
|
if (!this.castDistanceWanted || !this.castSeedBindGroup || !this.castResolveBindGroup) return
|
|
3809
|
+
this.writeCastSeedProps()
|
|
3741
3810
|
const seed = encoder.beginRenderPass({
|
|
3742
3811
|
label: "cast distance (seed)",
|
|
3743
3812
|
colorAttachments: [
|
|
@@ -9325,12 +9394,17 @@ export class Engine {
|
|
|
9325
9394
|
* keep the meaning the caller gave them, which is the same rule the anchor
|
|
9326
9395
|
* table follows. Passing none (or an empty list) turns the layer off and the
|
|
9327
9396
|
* scene renders exactly as it did before lights existed.
|
|
9397
|
+
*
|
|
9398
|
+
* A light with an `aim` is a SPOT: it reaches only inside `angle`, full
|
|
9399
|
+
* inside `innerAngle`, and the two are degrees from the axis the way every
|
|
9400
|
+
* tool states a cone. Without one it is a point light, which the shading
|
|
9401
|
+
* spells as a cone that covers everything rather than as a second case.
|
|
9328
9402
|
*/
|
|
9329
9403
|
setLights(
|
|
9330
9404
|
/** Structural {x,y,z} rather than the Vec3 class, the same choice effect
|
|
9331
9405
|
* params make: a scene document's JSON passes straight in, and so does a
|
|
9332
9406
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
9333
|
-
lights:
|
|
9407
|
+
lights: SceneLight[] | null,
|
|
9334
9408
|
): void {
|
|
9335
9409
|
const list = (lights ?? []).slice(0, MAX_LIGHTS)
|
|
9336
9410
|
this.docLightCount = list.length
|
|
@@ -9357,7 +9431,25 @@ export class Engine {
|
|
|
9357
9431
|
this.lightsData[b + 4] = Math.max(l.color.x * k, 0)
|
|
9358
9432
|
this.lightsData[b + 5] = Math.max(l.color.y * k, 0)
|
|
9359
9433
|
this.lightsData[b + 6] = Math.max(l.color.z * k, 0)
|
|
9360
|
-
//
|
|
9434
|
+
// A spot only where an aim was given AND it points somewhere: a zero
|
|
9435
|
+
// vector cannot be normalised, and a cone around nothing would light
|
|
9436
|
+
// nothing at all — a light that vanishes because its aim was left at the
|
|
9437
|
+
// default is worse than one that shines everywhere.
|
|
9438
|
+
const len = l.aim ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0
|
|
9439
|
+
this.lightsData[b + 7] = len > 0 ? 1 : 0
|
|
9440
|
+
this.lightsData[b + 8] = len > 0 ? l.aim!.x / len : 0
|
|
9441
|
+
this.lightsData[b + 9] = len > 0 ? l.aim!.y / len : 0
|
|
9442
|
+
this.lightsData[b + 10] = len > 0 ? l.aim!.z / len : 0
|
|
9443
|
+
// Half-angles in degrees, as cosines. Ordered so the outer edge is never
|
|
9444
|
+
// inside the inner one — a cone written the other way round would divide
|
|
9445
|
+
// by a negative span and light its own rim instead of its middle.
|
|
9446
|
+
const outer = len > 0 ? Math.cos((Math.min(Math.max(l.angle ?? 45, 0), 179) / 2) * (Math.PI / 180)) : -1
|
|
9447
|
+
const inner = len > 0 ? Math.cos((Math.min(Math.max(l.innerAngle ?? (l.angle ?? 45) * 0.8, 0), 179) / 2) * (Math.PI / 180)) : -1
|
|
9448
|
+
this.lightsData[b + 11] = outer
|
|
9449
|
+
this.lightsData[b + 12] = Math.max(inner, outer)
|
|
9450
|
+
this.lightsData[b + 13] = 0
|
|
9451
|
+
this.lightsData[b + 14] = 0
|
|
9452
|
+
this.lightsData[b + 15] = 0
|
|
9361
9453
|
}
|
|
9362
9454
|
if (list.length) {
|
|
9363
9455
|
this.device.queue.writeBuffer(
|
|
@@ -9466,6 +9558,8 @@ export class Engine {
|
|
|
9466
9558
|
inst.styleGroups.clear()
|
|
9467
9559
|
})
|
|
9468
9560
|
this.zeroStyleBuffer?.destroy()
|
|
9561
|
+
this.castSeedPropBuffer?.destroy()
|
|
9562
|
+
this.castSeedPropBuffer = null
|
|
9469
9563
|
this.releaseCullBuffers()
|
|
9470
9564
|
this.cullFrustaBuffer?.destroy()
|
|
9471
9565
|
this.cullFrustaBuffer = null
|
|
@@ -15811,6 +15905,11 @@ export class Engine {
|
|
|
15811
15905
|
// shader derives the full viewport (width via projection aspect) for its
|
|
15812
15906
|
// babylon-mmd constant-pixel edge extrusion.
|
|
15813
15907
|
this.cameraMatrixData[35] = this.canvas.height
|
|
15908
|
+
// The SCENE clock, which is what a material may animate on: it pauses with
|
|
15909
|
+
// the transport and an export steps it exactly, so a ripple is in the same
|
|
15910
|
+
// place in the file as it was on screen. Wall time would drift between the
|
|
15911
|
+
// two. The buffer was already allocated at 40 floats; this is one of them.
|
|
15912
|
+
this.cameraMatrixData[36] = this.sceneClock
|
|
15814
15913
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
15815
15914
|
|
|
15816
15915
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|
package/src/graph/registry.ts
CHANGED
|
@@ -132,6 +132,16 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
132
132
|
outputs: { color: "color", alpha: "float" },
|
|
133
133
|
contextOutputs: { color: "tex_color", alpha: "tex_s.a" },
|
|
134
134
|
},
|
|
135
|
+
// The scene clock, in seconds. A material is otherwise a function of position
|
|
136
|
+
// alone — this is what lets one SCROLL: ripples on water, a conveyor, a sign.
|
|
137
|
+
// It is the same clock the effects read, so a paused scene holds still and an
|
|
138
|
+
// export steps it frame by frame.
|
|
139
|
+
time: {
|
|
140
|
+
inputs: {},
|
|
141
|
+
outputs: { value: "float" },
|
|
142
|
+
contextOutputs: { value: "camera.time" },
|
|
143
|
+
},
|
|
144
|
+
|
|
135
145
|
geometry: {
|
|
136
146
|
inputs: {},
|
|
137
147
|
outputs: {
|
package/src/index.ts
CHANGED
package/src/reflection.ts
CHANGED
|
@@ -156,5 +156,8 @@ export function buildMirrorCamera(
|
|
|
156
156
|
out[33] = camera[33] - t * ny
|
|
157
157
|
out[34] = camera[34] - t * nz
|
|
158
158
|
out[35] = camera[35]
|
|
159
|
+
// The clock, unfolded: a mirrored draw of an animated material must be at the
|
|
160
|
+
// same instant as the one it reflects, or the ripples disagree across the pane.
|
|
161
|
+
out[36] = camera[36]
|
|
159
162
|
return out
|
|
160
163
|
}
|
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,15 @@ 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
|
-
* the
|
|
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 it has to clear
|
|
49
|
+
* a game stage: a Unity rip of one arrives with thirty-odd point lights, which
|
|
50
|
+
* the old ceiling of sixteen cut in half. Past this the extras are dropped.
|
|
51
|
+
* Clustering is what replaces it when a scene wants hundreds.
|
|
46
52
|
*/
|
|
47
|
-
export const MAX_LIGHTS =
|
|
53
|
+
export const MAX_LIGHTS = 48
|
|
48
54
|
/** Floats in the whole buffer. */
|
|
49
55
|
export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE
|
|
50
56
|
|
|
@@ -210,6 +216,19 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
210
216
|
_rzLightsOut[b + 4u] = c.x;
|
|
211
217
|
_rzLightsOut[b + 5u] = c.y;
|
|
212
218
|
_rzLightsOut[b + 6u] = c.z;
|
|
219
|
+
// An effect emits POINT lights: RzLight carries no aim, and widening it would
|
|
220
|
+
// fail to compile every emitter already written against it. The rest of the
|
|
221
|
+
// record is written anyway — these slots are reused frame to frame, and a spot
|
|
222
|
+
// the document placed here last frame would otherwise keep its cone.
|
|
223
|
+
_rzLightsOut[b + 7u] = 0.0;
|
|
224
|
+
_rzLightsOut[b + 8u] = 0.0;
|
|
225
|
+
_rzLightsOut[b + 9u] = 0.0;
|
|
226
|
+
_rzLightsOut[b + 10u] = 0.0;
|
|
227
|
+
_rzLightsOut[b + 11u] = -1.0;
|
|
228
|
+
_rzLightsOut[b + 12u] = -1.0;
|
|
229
|
+
_rzLightsOut[b + 13u] = 0.0;
|
|
230
|
+
_rzLightsOut[b + 14u] = 0.0;
|
|
231
|
+
_rzLightsOut[b + 15u] = 0.0;
|
|
213
232
|
}
|
|
214
233
|
`
|
|
215
234
|
}
|
|
@@ -240,6 +259,19 @@ fn rzLightColor(i: u32) -> vec3f {
|
|
|
240
259
|
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
241
260
|
}
|
|
242
261
|
|
|
262
|
+
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
263
|
+
fn rzLightAim(i: u32) -> vec3f {
|
|
264
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
265
|
+
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
269
|
+
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
270
|
+
fn rzLightCone(i: u32) -> vec2f {
|
|
271
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 11u;
|
|
272
|
+
return vec2f(_rzLights[b], _rzLights[b + 1u]);
|
|
273
|
+
}
|
|
274
|
+
|
|
243
275
|
/**
|
|
244
276
|
* Every positional light's contribution at a surface point, as light — not as a
|
|
245
277
|
* finished colour. Multiply by whatever the surface's albedo is.
|
|
@@ -269,14 +301,25 @@ fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
|
269
301
|
for (var i = 0u; i < count; i = i + 1u) {
|
|
270
302
|
let d = rzLightPos(i) - p;
|
|
271
303
|
let dist = length(d);
|
|
304
|
+
// Out of reach before anything else is computed. The falloff is already
|
|
305
|
+
// exactly zero at the radius, so this changes no pixel — it is what keeps a
|
|
306
|
+
// stage rig's far lamps off the bill at every fragment they do not light.
|
|
307
|
+
if (dist >= rzLightRadius(i)) { continue; }
|
|
308
|
+
let toLight = d / max(dist, 1e-4);
|
|
272
309
|
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
273
310
|
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
274
311
|
// the ramp's business and not this one's.
|
|
275
|
-
let ndl = max(dot(n,
|
|
312
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
276
313
|
if (ndl <= 0.0) { continue; }
|
|
277
314
|
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
278
315
|
let falloff = 1.0 - t * t;
|
|
279
|
-
|
|
316
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
317
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
318
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
319
|
+
// dot product and no branch.
|
|
320
|
+
let cone = rzLightCone(i);
|
|
321
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
322
|
+
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
280
323
|
}
|
|
281
324
|
return acc;
|
|
282
325
|
}
|
|
@@ -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 {
|