reze-engine 0.68.8 → 0.68.9
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 +2 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +39 -41
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +5 -4
- package/dist/jelly-eyes.d.ts.map +1 -1
- package/dist/jelly-eyes.js +43 -17
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/shaders/agx-lut.d.ts +11 -0
- package/dist/shaders/agx-lut.d.ts.map +1 -0
- package/dist/shaders/agx-lut.js +29 -0
- package/dist/shaders/dfg_lut.d.ts +3 -0
- package/dist/shaders/dfg_lut.d.ts.map +1 -0
- package/dist/shaders/dfg_lut.js +129 -0
- package/dist/shaders/ltc_mag_lut.d.ts +3 -0
- package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
- package/dist/shaders/ltc_mag_lut.js +1033 -0
- package/dist/shaders/materials/common.d.ts +6 -0
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +67 -60
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.js +11 -3
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ghosts.d.ts +22 -0
- package/dist/shaders/passes/ghosts.d.ts.map +1 -0
- package/dist/shaders/passes/ghosts.js +75 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/shadow.d.ts +1 -1
- package/dist/shaders/passes/shadow.d.ts.map +1 -1
- package/dist/shaders/passes/shadow.js +11 -3
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/engine.ts +39 -47
- package/src/graph/slots.ts +5 -4
- package/src/jelly-eyes.ts +41 -17
- package/src/shaders/materials/common.ts +68 -60
- package/src/shaders/passes/depth-prepass.ts +11 -3
- package/src/shaders/passes/shadow.ts +11 -3
- package/dist/_dbg-loader.mjs +0 -1100
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** The jelly eyes' vertex move (jelly-eyes.ts). Every pass that places model
|
|
2
|
+
* vertices takes it, or a pass that skips it draws the iris where it was —
|
|
3
|
+
* the shadow map then shades half the moved iris with its own unmoved copy.
|
|
4
|
+
* Reads `objectLight.jelly` and `material.materialId`, which the including
|
|
5
|
+
* module declares. */
|
|
6
|
+
export declare const JELLY_VERTEX_WGSL = "\nfn _rzJellyTurn(q: vec2f, a: f32) -> vec2f {\n let c = cos(a);\n let s = sin(a);\n return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);\n}\n\n/**\n * Where the jelly puts an iris vertex. The iris is the part of the eye the eye\n * bones carry, so it is moved as a whole, in bind space before skinning:\n * scaled about its centre (the CPU composes the blink's spring, the lid's\n * press and her motion's knock into one scale), turned and drifted a hair on\n * the spring, brought forward a hair, and rolled about the eye bone with a\n * closing expression \u2014 inside the white, which stays where it is. A highlight\n * layer (this draw's material is one of jelly[1].w's) takes the iris's move,\n * then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones\n * (jelly[2].w, jelly[3].w), so nothing else on the face moves.\n */\nfn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {\n let j = objectLight.jelly;\n let ids = u32(j[1].w);\n let mid = u32(material.materialId);\n let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));\n var out = p;\n for (var e = 0u; e < 2u; e++) {\n let bone = j[2u + e].w;\n if (bone < 0.0) { continue; }\n let b = u32(bone);\n let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));\n if (w <= 0.0) { continue; }\n let c = j[e].xyz;\n // A rig that carries both eyes on one bone: each vertex follows its own side.\n if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }\n if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }\n let t = j[4u + e];\n let m = j[6u + e];\n let h = j[10u + e];\n var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;\n // Forward is \u2212Z (MMD faces \u2212Z).\n var push = h.z;\n if (hl) {\n // Sliding over the iris's dome on its own, a highlight sheet a hair in\n // front of it would sink into it where the dome bulges more and the two\n // fight for depth; so it comes forward by as far as it slid \u2014 a slope of\n // one, steeper than the dome anywhere it draws.\n let before = q;\n q = _rzJellyTurn(q * h.xy, t.w) + m.zw;\n push += length(q - before);\n }\n var moved = vec3f(c.xy + q, p.z - push);\n let roll = j[8u + e];\n if (roll.w != 0.0) {\n let d = moved - roll.xyz;\n let rd = _rzJellyTurn(d.yz, roll.w);\n moved = roll.xyz + vec3f(d.x, rd.x, rd.y);\n }\n out = mix(p, moved, w);\n }\n return out;\n}\n";
|
|
1
7
|
export declare const COMMON_BINDINGS_WGSL: string;
|
|
2
8
|
export declare function commonFsOutWgsl(): string;
|
|
3
9
|
/**
|
|
@@ -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;;;;uBAIuB;AACvB,eAAO,MAAM,iBAAiB,8iFA4D7B,CAAA;AAED,eAAO,MAAM,oBAAoB,QAgQhC,CAAC;AAwHF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC6H,CAAA"}
|
|
@@ -23,6 +23,72 @@ import { WORLD_AMBIENT_WGSL } from "../lights";
|
|
|
23
23
|
// group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
|
|
24
24
|
// group(1): per-model skinning
|
|
25
25
|
// group(2): per-material (diffuse texture + material uniforms)
|
|
26
|
+
/** The jelly eyes' vertex move (jelly-eyes.ts). Every pass that places model
|
|
27
|
+
* vertices takes it, or a pass that skips it draws the iris where it was —
|
|
28
|
+
* the shadow map then shades half the moved iris with its own unmoved copy.
|
|
29
|
+
* Reads `objectLight.jelly` and `material.materialId`, which the including
|
|
30
|
+
* module declares. */
|
|
31
|
+
export const JELLY_VERTEX_WGSL = /* wgsl */ `
|
|
32
|
+
fn _rzJellyTurn(q: vec2f, a: f32) -> vec2f {
|
|
33
|
+
let c = cos(a);
|
|
34
|
+
let s = sin(a);
|
|
35
|
+
return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where the jelly puts an iris vertex. The iris is the part of the eye the eye
|
|
40
|
+
* bones carry, so it is moved as a whole, in bind space before skinning:
|
|
41
|
+
* scaled about its centre (the CPU composes the blink's spring, the lid's
|
|
42
|
+
* press and her motion's knock into one scale), turned and drifted a hair on
|
|
43
|
+
* the spring, brought forward a hair, and rolled about the eye bone with a
|
|
44
|
+
* closing expression — inside the white, which stays where it is. A highlight
|
|
45
|
+
* layer (this draw's material is one of jelly[1].w's) takes the iris's move,
|
|
46
|
+
* then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones
|
|
47
|
+
* (jelly[2].w, jelly[3].w), so nothing else on the face moves.
|
|
48
|
+
*/
|
|
49
|
+
fn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {
|
|
50
|
+
let j = objectLight.jelly;
|
|
51
|
+
let ids = u32(j[1].w);
|
|
52
|
+
let mid = u32(material.materialId);
|
|
53
|
+
let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));
|
|
54
|
+
var out = p;
|
|
55
|
+
for (var e = 0u; e < 2u; e++) {
|
|
56
|
+
let bone = j[2u + e].w;
|
|
57
|
+
if (bone < 0.0) { continue; }
|
|
58
|
+
let b = u32(bone);
|
|
59
|
+
let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));
|
|
60
|
+
if (w <= 0.0) { continue; }
|
|
61
|
+
let c = j[e].xyz;
|
|
62
|
+
// A rig that carries both eyes on one bone: each vertex follows its own side.
|
|
63
|
+
if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }
|
|
64
|
+
if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }
|
|
65
|
+
let t = j[4u + e];
|
|
66
|
+
let m = j[6u + e];
|
|
67
|
+
let h = j[10u + e];
|
|
68
|
+
var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;
|
|
69
|
+
// Forward is −Z (MMD faces −Z).
|
|
70
|
+
var push = h.z;
|
|
71
|
+
if (hl) {
|
|
72
|
+
// Sliding over the iris's dome on its own, a highlight sheet a hair in
|
|
73
|
+
// front of it would sink into it where the dome bulges more and the two
|
|
74
|
+
// fight for depth; so it comes forward by as far as it slid — a slope of
|
|
75
|
+
// one, steeper than the dome anywhere it draws.
|
|
76
|
+
let before = q;
|
|
77
|
+
q = _rzJellyTurn(q * h.xy, t.w) + m.zw;
|
|
78
|
+
push += length(q - before);
|
|
79
|
+
}
|
|
80
|
+
var moved = vec3f(c.xy + q, p.z - push);
|
|
81
|
+
let roll = j[8u + e];
|
|
82
|
+
if (roll.w != 0.0) {
|
|
83
|
+
let d = moved - roll.xyz;
|
|
84
|
+
let rd = _rzJellyTurn(d.yz, roll.w);
|
|
85
|
+
moved = roll.xyz + vec3f(d.x, rd.x, rd.y);
|
|
86
|
+
}
|
|
87
|
+
out = mix(p, moved, w);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
26
92
|
export const COMMON_BINDINGS_WGSL = /* wgsl */ `
|
|
27
93
|
|
|
28
94
|
struct CameraUniforms {
|
|
@@ -231,66 +297,7 @@ fn _rzCastShadow(wp: vec3f) -> f32 {
|
|
|
231
297
|
struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
|
|
232
298
|
@group(1) @binding(1) var<uniform> objectLight: ObjectLight;
|
|
233
299
|
|
|
234
|
-
|
|
235
|
-
let c = cos(a);
|
|
236
|
-
let s = sin(a);
|
|
237
|
-
return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Where the jelly puts an iris vertex. The iris is the part of the eye the eye
|
|
242
|
-
* bones carry, so it is moved as a whole, in bind space before skinning:
|
|
243
|
-
* scaled about its centre (the CPU composes the blink's spring, the lid's
|
|
244
|
-
* press and her motion's knock into one scale), turned and drifted a hair on
|
|
245
|
-
* the spring, brought forward a hair, and rolled about the eye bone with a
|
|
246
|
-
* closing expression — inside the white, which stays where it is. A highlight
|
|
247
|
-
* layer (this draw's material is one of jelly[1].w's) takes the iris's move,
|
|
248
|
-
* then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones
|
|
249
|
-
* (jelly[2].w, jelly[3].w), so nothing else on the face moves.
|
|
250
|
-
*/
|
|
251
|
-
fn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {
|
|
252
|
-
let j = objectLight.jelly;
|
|
253
|
-
let ids = u32(j[1].w);
|
|
254
|
-
let mid = u32(material.materialId);
|
|
255
|
-
let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));
|
|
256
|
-
var out = p;
|
|
257
|
-
for (var e = 0u; e < 2u; e++) {
|
|
258
|
-
let bone = j[2u + e].w;
|
|
259
|
-
if (bone < 0.0) { continue; }
|
|
260
|
-
let b = u32(bone);
|
|
261
|
-
let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));
|
|
262
|
-
if (w <= 0.0) { continue; }
|
|
263
|
-
let c = j[e].xyz;
|
|
264
|
-
// A rig that carries both eyes on one bone: each vertex follows its own side.
|
|
265
|
-
if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }
|
|
266
|
-
if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }
|
|
267
|
-
let t = j[4u + e];
|
|
268
|
-
let m = j[6u + e];
|
|
269
|
-
let h = j[10u + e];
|
|
270
|
-
var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;
|
|
271
|
-
// Forward is −Z (MMD faces −Z).
|
|
272
|
-
var push = h.z;
|
|
273
|
-
if (hl) {
|
|
274
|
-
// Sliding over the iris's dome on its own, a highlight sheet a hair in
|
|
275
|
-
// front of it would sink into it where the dome bulges more and the two
|
|
276
|
-
// fight for depth; so it comes forward by as far as it slid — a slope of
|
|
277
|
-
// one, steeper than the dome anywhere it draws.
|
|
278
|
-
let before = q;
|
|
279
|
-
q = _rzJellyTurn(q * h.xy, t.w) + m.zw;
|
|
280
|
-
push += length(q - before);
|
|
281
|
-
}
|
|
282
|
-
var moved = vec3f(c.xy + q, p.z - push);
|
|
283
|
-
let roll = j[8u + e];
|
|
284
|
-
if (roll.w != 0.0) {
|
|
285
|
-
let d = moved - roll.xyz;
|
|
286
|
-
let rd = _rzJellyTurn(d.yz, roll.w);
|
|
287
|
-
moved = roll.xyz + vec3f(d.x, rd.x, rd.y);
|
|
288
|
-
}
|
|
289
|
-
out = mix(p, moved, w);
|
|
290
|
-
}
|
|
291
|
-
return out;
|
|
292
|
-
}
|
|
293
|
-
|
|
300
|
+
${JELLY_VERTEX_WGSL}
|
|
294
301
|
/** The light arriving at a surface of this object facing n: its own ambient
|
|
295
302
|
* where loaded data gave it one (a stage's probes, a character's), else the
|
|
296
303
|
* world's. The same polynomial rzWorldAmbient evaluates. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,CAkGpD"}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// one: the fragment outputs below depend on whether the device carries the id
|
|
27
27
|
// attachment, and that answer does not exist at import time. The constant could
|
|
28
28
|
// not have taken the outputs at all, which is most of why it did not have them.
|
|
29
|
-
import { DISSOLVE_WGSL } from "../materials/common";
|
|
29
|
+
import { DISSOLVE_WGSL, JELLY_VERTEX_WGSL } from "../materials/common";
|
|
30
30
|
import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
31
31
|
export function transparentDepthPrepassWgsl() {
|
|
32
32
|
return /* wgsl */ `
|
|
@@ -42,13 +42,20 @@ struct MaterialUniforms {
|
|
|
42
42
|
alpha: f32,
|
|
43
43
|
_skip0: vec4f,
|
|
44
44
|
_skip1: vec4f,
|
|
45
|
-
_skip2:
|
|
45
|
+
_skip2: f32,
|
|
46
|
+
materialId: f32,
|
|
47
|
+
_skip3: f32,
|
|
46
48
|
dissolve: f32,
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
50
52
|
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
51
53
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
54
|
+
// The model's ObjectLight, for its jelly eyes: depth goes down where the colour
|
|
55
|
+
// pass will draw the iris.
|
|
56
|
+
struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
|
|
57
|
+
@group(1) @binding(1) var<uniform> objectLight: ObjectLight;
|
|
58
|
+
${JELLY_VERTEX_WGSL}
|
|
52
59
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
53
60
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
54
61
|
|
|
@@ -72,10 +79,11 @@ struct VSOut {
|
|
|
72
79
|
@location(3) joints0: vec4<u32>,
|
|
73
80
|
@location(4) weights0: vec4<f32>,
|
|
74
81
|
) -> VSOut {
|
|
75
|
-
let pos4 = vec4f(position, 1.0);
|
|
76
82
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
77
83
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
78
84
|
let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
85
|
+
var pos4 = vec4f(position, 1.0);
|
|
86
|
+
if (objectLight.jelly[0].w > 0.0) { pos4 = vec4f(rzJellyVertex(position, joints0, w), 1.0); }
|
|
79
87
|
var skinned = vec4f(0.0);
|
|
80
88
|
for (var i = 0u; i < 4u; i++) {
|
|
81
89
|
skinned += (skinMats[joints0[i]] * pos4) * w[i];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The field layer, drawn INTO the scene.
|
|
3
|
+
*
|
|
4
|
+
* Field effects render into their own targets at their own resolution — that
|
|
5
|
+
* part was always right. What was wrong was where the result landed: the
|
|
6
|
+
* composite sampled it AFTER tone mapping, which is why a field effect could
|
|
7
|
+
* never bloom, could never light anything, and tone mapped differently from the
|
|
8
|
+
* particle and ribbon mounts drawing the same kind of glow.
|
|
9
|
+
*
|
|
10
|
+
* Two draws inside the scene pass — background before any geometry, foreground
|
|
11
|
+
* after all of it — so the bloom pyramid sees field effects and one tone map
|
|
12
|
+
* covers the whole frame.
|
|
13
|
+
*
|
|
14
|
+
* NO DEPTH, neither written nor tested. A foreground is handed the scene's
|
|
15
|
+
* depth as a parameter and does its own occlusion; testing it here would
|
|
16
|
+
* occlude it a second time, and a background has nothing to test against.
|
|
17
|
+
*
|
|
18
|
+
* COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
|
|
19
|
+
* layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
|
|
20
|
+
* unpremultiply from dividing colour by nothing — but N additive effects can
|
|
21
|
+
* sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
|
|
22
|
+
* A coverage above 1 would darken what the layer is drawn over by more than
|
|
23
|
+
* fully covering it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fieldBlitShaderWgsl(): string;
|
|
26
|
+
//# sourceMappingURL=field-blit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-blit.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/field-blit.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuC5C"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sceneFsOutWgsl } from "./scene-contract";
|
|
2
|
+
/**
|
|
3
|
+
* The field layer, drawn INTO the scene.
|
|
4
|
+
*
|
|
5
|
+
* Field effects render into their own targets at their own resolution — that
|
|
6
|
+
* part was always right. What was wrong was where the result landed: the
|
|
7
|
+
* composite sampled it AFTER tone mapping, which is why a field effect could
|
|
8
|
+
* never bloom, could never light anything, and tone mapped differently from the
|
|
9
|
+
* particle and ribbon mounts drawing the same kind of glow.
|
|
10
|
+
*
|
|
11
|
+
* Two draws inside the scene pass — background before any geometry, foreground
|
|
12
|
+
* after all of it — so the bloom pyramid sees field effects and one tone map
|
|
13
|
+
* covers the whole frame.
|
|
14
|
+
*
|
|
15
|
+
* NO DEPTH, neither written nor tested. A foreground is handed the scene's
|
|
16
|
+
* depth as a parameter and does its own occlusion; testing it here would
|
|
17
|
+
* occlude it a second time, and a background has nothing to test against.
|
|
18
|
+
*
|
|
19
|
+
* COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
|
|
20
|
+
* layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
|
|
21
|
+
* unpremultiply from dividing colour by nothing — but N additive effects can
|
|
22
|
+
* sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
|
|
23
|
+
* A coverage above 1 would darken what the layer is drawn over by more than
|
|
24
|
+
* fully covering it.
|
|
25
|
+
*/
|
|
26
|
+
export function fieldBlitShaderWgsl() {
|
|
27
|
+
return /* wgsl */ `
|
|
28
|
+
@group(0) @binding(0) var _rzLayer: texture_2d<f32>;
|
|
29
|
+
@group(0) @binding(1) var _rzLayerSamp: sampler;
|
|
30
|
+
// (bloom, _, fullW, fullH). The size is the SCENE target's, not the layer's:
|
|
31
|
+
// the layer is often half resolution, and normalised uv is what makes that
|
|
32
|
+
// invisible.
|
|
33
|
+
@group(0) @binding(2) var<uniform> _rzBlitU: vec4f;
|
|
34
|
+
|
|
35
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
36
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
37
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
38
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${sceneFsOutWgsl()}
|
|
42
|
+
@fragment fn fs(@builtin(position) fragCoord: vec4f) -> FSOut {
|
|
43
|
+
var out: FSOut;
|
|
44
|
+
// Bilinear, because the layer is usually half resolution and the whole point
|
|
45
|
+
// of that choice is that low-frequency effects upsample invisibly.
|
|
46
|
+
let uv = fragCoord.xy / max(_rzBlitU.zw, vec2f(1.0));
|
|
47
|
+
let c = textureSampleLevel(_rzLayer, _rzLayerSamp, uv, 0.0);
|
|
48
|
+
let cov = clamp(c.a, 0.0, 1.0);
|
|
49
|
+
// PREMULTIPLIED, and blitted with a premultiplied-over blend — the field
|
|
50
|
+
// targets have been premultiplied since N effects had to compose into them
|
|
51
|
+
// associatively. Handing this to the material blend would scale rgb by alpha
|
|
52
|
+
// a second time and every layer would come out dark.
|
|
53
|
+
out.color = vec4f(max(c.rgb, vec3f(0.0)), cov);
|
|
54
|
+
// The aux target: bloom mask, then coverage. THIS is what finally makes
|
|
55
|
+
// // @bloom mean something on a field effect — the flag has been parsed and
|
|
56
|
+
// ignored for every mount but particles and ribbons, which is the kind of
|
|
57
|
+
// author-surface lie the install-time guards exist to kill.
|
|
58
|
+
//
|
|
59
|
+
// The mask is written UNWEIGHTED and the blend weights it by src.a — the
|
|
60
|
+
// convention the materials already follow (they write vec4f(1, 1, 0, alpha)).
|
|
61
|
+
out.mask = vec4f(_rzBlitU.x, 1.0, 0.0, cov);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Afterimages — the cast drawn again at the poses it held a moment ago.
|
|
3
|
+
*
|
|
4
|
+
* The VERTEX stage is the material one, unchanged: a ghost is the same mesh
|
|
5
|
+
* skinned by different matrices, and the engine binds a slot of its pose
|
|
6
|
+
* history where the live skinning matrices would be. Only the fragment is new.
|
|
7
|
+
*
|
|
8
|
+
* What a ghost looks like is Elden Ring's phantom: a body you can nearly see
|
|
9
|
+
* through, drawn by its EDGES. Three of them, all in the tint — the rim where
|
|
10
|
+
* the surface turns away (the silhouette), and inside it the lines a drawing
|
|
11
|
+
* would have: where the texture's brightness changes sharply from one pixel to
|
|
12
|
+
* the next (painted seams, trim, straps) and where the surface itself folds
|
|
13
|
+
* (the normal turning fast across a pixel). The body under them is the
|
|
14
|
+
* surface's own colour mixed toward the tint by a dial, at its own opacity.
|
|
15
|
+
*
|
|
16
|
+
* Built at first use rather than at import: the output struct depends on
|
|
17
|
+
* whether the device carries the id attachment, which is probed at init.
|
|
18
|
+
*/
|
|
19
|
+
/** One rank's uniforms, at a 256-byte stride for the dynamic offset. */
|
|
20
|
+
export declare const GHOST_UNIFORM_STRIDE = 256;
|
|
21
|
+
export declare function ghostShaderWgsl(): string;
|
|
22
|
+
//# sourceMappingURL=ghosts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghosts.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ghosts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,wEAAwE;AACxE,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,wBAAgB,eAAe,IAAI,MAAM,CAqDxC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NODES_WGSL } from "../materials/nodes";
|
|
2
|
+
import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL, commonFsOutWgsl } from "../materials/common";
|
|
3
|
+
import { sceneIdPadWgsl } from "./scene-contract";
|
|
4
|
+
/**
|
|
5
|
+
* Afterimages — the cast drawn again at the poses it held a moment ago.
|
|
6
|
+
*
|
|
7
|
+
* The VERTEX stage is the material one, unchanged: a ghost is the same mesh
|
|
8
|
+
* skinned by different matrices, and the engine binds a slot of its pose
|
|
9
|
+
* history where the live skinning matrices would be. Only the fragment is new.
|
|
10
|
+
*
|
|
11
|
+
* What a ghost looks like is Elden Ring's phantom: a body you can nearly see
|
|
12
|
+
* through, drawn by its EDGES. Three of them, all in the tint — the rim where
|
|
13
|
+
* the surface turns away (the silhouette), and inside it the lines a drawing
|
|
14
|
+
* would have: where the texture's brightness changes sharply from one pixel to
|
|
15
|
+
* the next (painted seams, trim, straps) and where the surface itself folds
|
|
16
|
+
* (the normal turning fast across a pixel). The body under them is the
|
|
17
|
+
* surface's own colour mixed toward the tint by a dial, at its own opacity.
|
|
18
|
+
*
|
|
19
|
+
* Built at first use rather than at import: the output struct depends on
|
|
20
|
+
* whether the device carries the id attachment, which is probed at init.
|
|
21
|
+
*/
|
|
22
|
+
/** One rank's uniforms, at a 256-byte stride for the dynamic offset. */
|
|
23
|
+
export const GHOST_UNIFORM_STRIDE = 256;
|
|
24
|
+
export function ghostShaderWgsl() {
|
|
25
|
+
return (NODES_WGSL +
|
|
26
|
+
COMMON_MATERIAL_PRELUDE_WGSL +
|
|
27
|
+
commonFsOutWgsl() +
|
|
28
|
+
DISSOLVE_WGSL +
|
|
29
|
+
/* wgsl */ `
|
|
30
|
+
struct GhostU {
|
|
31
|
+
tint: vec3f,
|
|
32
|
+
tintMix: f32,
|
|
33
|
+
alpha: f32,
|
|
34
|
+
glow: f32,
|
|
35
|
+
lines: f32,
|
|
36
|
+
body: f32,
|
|
37
|
+
}
|
|
38
|
+
@group(3) @binding(0) var<uniform> ghost: GhostU;
|
|
39
|
+
|
|
40
|
+
@fragment fn fsGhost(input: VertexOutput) -> FSOut {
|
|
41
|
+
let tex = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
42
|
+
let base = tex.rgb * material.diffuseColor;
|
|
43
|
+
let n = safe_normal(input.normal);
|
|
44
|
+
// Derivatives BEFORE the discard below: they need every pixel of the quad.
|
|
45
|
+
let lum = dot(base, vec3f(0.2126, 0.7152, 0.0722));
|
|
46
|
+
let detail = smoothstep(0.02, 0.12, fwidth(lum));
|
|
47
|
+
let crease = smoothstep(0.08, 0.35, length(fwidth(n)));
|
|
48
|
+
let cover = material.alpha * tex.a;
|
|
49
|
+
// Cutouts stay cut: lace and hair cards would otherwise ghost as solid cards.
|
|
50
|
+
if (cover < 0.1) { discard; }
|
|
51
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
52
|
+
let turn = 1.0 - abs(dot(n, v));
|
|
53
|
+
let rim = turn * turn * turn;
|
|
54
|
+
let edge = rim * ghost.glow + max(detail, crease) * ghost.lines;
|
|
55
|
+
// The dial arrives as the picked #rrggbb scaled to 0..1 — sRGB. Decoded, so
|
|
56
|
+
// the colour mixed with the linear surface and added as light is the colour
|
|
57
|
+
// that was picked.
|
|
58
|
+
let tint = pow(ghost.tint, vec3f(2.2));
|
|
59
|
+
let rgb = mix(base, tint, ghost.tintMix) * ghost.body + tint * edge;
|
|
60
|
+
let a = clamp(ghost.alpha * cover * (ghost.body + edge), 0.0, 1.0);
|
|
61
|
+
var out: FSOut;
|
|
62
|
+
out.color = vec4f(rgb, a);
|
|
63
|
+
out.mask = vec4f(1.0, 1.0, 0.0, a);
|
|
64
|
+
${sceneIdPadWgsl("out")} return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The cast mark writes stencil only; this satisfies the pass's attachments.
|
|
68
|
+
@fragment fn fsGhostMark(input: VertexOutput) -> FSOut {
|
|
69
|
+
var out: FSOut;
|
|
70
|
+
out.color = vec4f(0.0);
|
|
71
|
+
out.mask = vec4f(0.0);
|
|
72
|
+
${sceneIdPadWgsl("out")} return out;
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
|
|
3
|
+
* for a repeating texture). Mips are the point as much as the speed: procedural
|
|
4
|
+
* noise has no mip chain, so at grazing angles it shimmered.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildGroundNoiseMips(): Uint8Array[];
|
|
7
|
+
//# sourceMappingURL=ground-noise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ground-noise.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground-noise.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAsCnD"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Bakes the ground's frosted micro-texture into a seamless, mipmapped tile.
|
|
2
|
+
//
|
|
3
|
+
// The shader used to evaluate four octaves of value noise per pixel — sixteen
|
|
4
|
+
// hash rounds — and the ground covers the entire screen as soon as the camera
|
|
5
|
+
// tilts toward the horizon. This produces the same character of pattern once, at
|
|
6
|
+
// load, so the fragment shader pays one filtered fetch instead.
|
|
7
|
+
//
|
|
8
|
+
// SEAMLESS BY CONSTRUCTION. The original hash was not periodic, so a finite
|
|
9
|
+
// crop of it could never tile. Here every octave's lattice wraps at a whole
|
|
10
|
+
// number of cells across the tile, which makes the result repeat exactly at the
|
|
11
|
+
// edges. The pattern is therefore not pixel-identical to the analytic version —
|
|
12
|
+
// it is the same kind of noise, not the same noise. At the default strength of
|
|
13
|
+
// 0.05 it modulates brightness by ±2.5%, where the repeat is not findable by eye.
|
|
14
|
+
import { GROUND_NOISE_RES, GROUND_NOISE_TILE_WORLD } from "./ground";
|
|
15
|
+
/** Lattice cells across the tile at octave 0. The analytic version sampled fbm
|
|
16
|
+
* at worldXZ × 3, so a tile spanning 8 world units covers 24 base cells — the
|
|
17
|
+
* same spatial frequency the surface had before. */
|
|
18
|
+
const BASE_CELLS = GROUND_NOISE_TILE_WORLD * 3;
|
|
19
|
+
const OCTAVES = 4;
|
|
20
|
+
/** Deterministic lattice hash. Integer coordinates are wrapped by the caller,
|
|
21
|
+
* which is what makes the tile seamless. */
|
|
22
|
+
function hash(ix, iy) {
|
|
23
|
+
let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
|
|
24
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
25
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
26
|
+
}
|
|
27
|
+
/** Value noise on a lattice that repeats every `period` cells. */
|
|
28
|
+
function periodicValueNoise(x, y, period) {
|
|
29
|
+
const ix = Math.floor(x);
|
|
30
|
+
const iy = Math.floor(y);
|
|
31
|
+
const fx = x - ix;
|
|
32
|
+
const fy = y - iy;
|
|
33
|
+
// Smoothstep weights — same curve the WGSL used.
|
|
34
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
35
|
+
const uy = fy * fy * (3 - 2 * fy);
|
|
36
|
+
const x0 = ((ix % period) + period) % period;
|
|
37
|
+
const y0 = ((iy % period) + period) % period;
|
|
38
|
+
const x1 = (x0 + 1) % period;
|
|
39
|
+
const y1 = (y0 + 1) % period;
|
|
40
|
+
const a = hash(x0, y0);
|
|
41
|
+
const b = hash(x1, y0);
|
|
42
|
+
const c = hash(x0, y1);
|
|
43
|
+
const d = hash(x1, y1);
|
|
44
|
+
return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
|
|
48
|
+
* for a repeating texture). Mips are the point as much as the speed: procedural
|
|
49
|
+
* noise has no mip chain, so at grazing angles it shimmered.
|
|
50
|
+
*/
|
|
51
|
+
export function buildGroundNoiseMips() {
|
|
52
|
+
const levels = [];
|
|
53
|
+
const res = GROUND_NOISE_RES;
|
|
54
|
+
const base = new Uint8Array(res * res);
|
|
55
|
+
for (let py = 0; py < res; py++) {
|
|
56
|
+
for (let px = 0; px < res; px++) {
|
|
57
|
+
// Texel centre, in units of octave-0 lattice cells.
|
|
58
|
+
const x = ((px + 0.5) / res) * BASE_CELLS;
|
|
59
|
+
const y = ((py + 0.5) / res) * BASE_CELLS;
|
|
60
|
+
let v = 0;
|
|
61
|
+
let amp = 0.5;
|
|
62
|
+
let freq = 1;
|
|
63
|
+
for (let o = 0; o < OCTAVES; o++) {
|
|
64
|
+
v += amp * periodicValueNoise(x * freq, y * freq, BASE_CELLS * freq);
|
|
65
|
+
freq *= 2;
|
|
66
|
+
amp *= 0.5;
|
|
67
|
+
}
|
|
68
|
+
base[py * res + px] = Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
levels.push(base);
|
|
72
|
+
let src = base;
|
|
73
|
+
let size = res;
|
|
74
|
+
while (size > 1) {
|
|
75
|
+
const half = size >> 1;
|
|
76
|
+
const dst = new Uint8Array(half * half);
|
|
77
|
+
for (let y = 0; y < half; y++) {
|
|
78
|
+
for (let x = 0; x < half; x++) {
|
|
79
|
+
const s = y * 2 * size + x * 2;
|
|
80
|
+
dst[y * half + x] = (src[s] + src[s + 1] + src[s + size] + src[s + size + 1] + 2) >> 2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
levels.push(dst);
|
|
84
|
+
src = dst;
|
|
85
|
+
size = half;
|
|
86
|
+
}
|
|
87
|
+
return levels;
|
|
88
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres.\n *\n * Raised from 0.42: at that size the pieces are near enough to speckle that a\n * departure reads as a fade rather than as a body coming apart, which is the\n * thing the clumping exists to avoid. Hand-sized pieces are visible ones. */\nconst RZ_DISSOLVE_GRAIN: f32 = 1.2;\n/** How much the clumps pull a fragment off its own grit value \u2014 the whole of\n * the LOOK, and none of the rate. See rz_dissolve_threshold. */\nconst RZ_DISSOLVE_CLUMP: f32 = 0.6;\n/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what\n * keeps a material SMALLER than a clump from having one threshold for all of\n * it, and it has to be smaller than the SMALLEST material a model carries \u2014\n * an eye highlight, a button, a buckle \u2014 not merely small. Two and a half\n * millimetres on a normal model, which is under all of them. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.021;\n/** How wide the glowing front is, in threshold units. Thin: the front is a\n * rim, and a wide one lights whole limbs at once. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.11;\n/** What the front burns with. Emission, added after lighting and after the\n * graph \u2014 a colour that ramps or takes a shadow reads as paint, not as heat.\n * Above 1 on purpose: it is meant to reach the bloom pyramid. Blue-violet,\n * the same light the motes leaving her are drawn and lit with. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment\n // says it: above 24 bits an f32 loses the low ones. The float hash this\n // replaces multiplied three terms into the hundreds of millions and took\n // fract() of the product \u2014 which past 2^24 is not noise, it is a quantised\n // staircase, and wherever that staircase landed on zero the threshold was\n // zero. A surface whose threshold is zero never crosses it: it stays behind\n // while the rest of her leaves, and it sits inside the burning front the\n // whole time she is gone. Those were the white pieces that would not go.\n //\n // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and\n // +0.5 are the same cell, and half this model is at negative x.\n var h = vec3u(vec3i(floor(p)) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);\n}\n\n/** Value noise over that hash \u2014 smooth within a flake, uncorrelated between. */\nfn rz_dissolve_field(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let c000 = rz_dissolve_hash(i);\n let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));\n let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));\n let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));\n let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));\n let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));\n let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));\n let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(c000, c100, u.x);\n let x10 = mix(c010, c110, u.x);\n let x01 = mix(c001, c101, u.x);\n let x11 = mix(c011, c111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\n/**\n * The threshold this fragment is measured against.\n *\n * NOISE ALONE, so the whole body comes apart at once \u2014 a hand, a shoulder and\n * an ankle all thinning together rather than a line travelling up her. It was\n * tilted by height at first, on the argument that a sweep reads as something\n * happening TO her; what it actually reads as is a wipe, and a wipe has a\n * direction, an edge and a speed the rest of the effect knows nothing about.\n * Everywhere at once is also the honest match for what leaves: the motes are\n * shed from every limb in the same breath.\n *\n * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one\n * cell shared a single threshold: an eye highlight, a brow, a button, a hair\n * clip. All of it sat inside the glowing front at once, so the piece flashed\n * white as a whole and then popped \u2014 the two things a disintegration must never\n * do. The grit is a per-cell hash at a fraction of the clump, so a part a few\n * millimetres across still comes apart in pieces.\n *\n * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.\n * That ordering is what makes the dissolve smooth, and it is not obvious: the\n * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed\n * however uniform each half is. It piles up in the middle, so most of the\n * surface had a threshold near a half \u2014 all of it went as the dissolve swept\n * through the middle, and what remained was the thin tail near zero. That tail\n * was the second stage everyone could see: a body that mostly vanished, then a\n * scatter of glowing scraps, then nothing.\n *\n * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it\n * EXACTLY uniform, where scaling a sum back into range only flattens the middle\n * \u2014 measured over four hundred thousand samples, the sum removed 12% of her per\n * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum\n * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to\n * the last, a ratio of 1.0.\n *\n * That flatness IS the smoothness. An uneven rate is what produced the two\n * stages you could see \u2014 a body that mostly vanished at once, then a scatter of\n * scraps that took as long again to follow.\n *\n * The clumping survives the wrap: neighbouring fragments share a clump value,\n * so the offset moves them together. What wraps past 1 comes back at 0 and goes\n * at the other end instead, which costs a little speckle in a region and buys\n * an even rate everywhere.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;\n return fract(grit + clump * RZ_DISSOLVE_CLUMP);\n}\n\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\n// The head of the material block and the one field at its tail \u2014 the same\n// reach the depth prepass takes, and for the same reason: a shadow cast by a\n// body that is no longer drawn is the tell that the vanishing is a trick.\nstruct MaterialDiffuse {\n diffuse: vec4f,\n _skip0: vec4f,\n _skip1: vec4f,\n _skip2: vec3f,\n dissolve: f32,\n};\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n /** Bind-pose position, for the dissolve test \u2014 object space, as everywhere. */\n @location(1) restPos: vec3f,\n /** The triangle's own threshold, flat \u2014 see the material VertexOutput. A\n * face that is gone must stop casting, and stop as a whole face. */\n @location(2) @interpolate(flat) faceT: f32,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n out.restPos = position;\n out.faceT = rz_dissolve_threshold(position);\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n // The dissolve, run as the colour pass runs it. A flake that is gone stops\n // casting: without this she leaves a whole shadow standing on the floor while\n // her body is in the air.\n if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }\n}\n";
|
|
1
|
+
export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres.\n *\n * Raised from 0.42: at that size the pieces are near enough to speckle that a\n * departure reads as a fade rather than as a body coming apart, which is the\n * thing the clumping exists to avoid. Hand-sized pieces are visible ones. */\nconst RZ_DISSOLVE_GRAIN: f32 = 1.2;\n/** How much the clumps pull a fragment off its own grit value \u2014 the whole of\n * the LOOK, and none of the rate. See rz_dissolve_threshold. */\nconst RZ_DISSOLVE_CLUMP: f32 = 0.6;\n/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what\n * keeps a material SMALLER than a clump from having one threshold for all of\n * it, and it has to be smaller than the SMALLEST material a model carries \u2014\n * an eye highlight, a button, a buckle \u2014 not merely small. Two and a half\n * millimetres on a normal model, which is under all of them. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.021;\n/** How wide the glowing front is, in threshold units. Thin: the front is a\n * rim, and a wide one lights whole limbs at once. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.11;\n/** What the front burns with. Emission, added after lighting and after the\n * graph \u2014 a colour that ramps or takes a shadow reads as paint, not as heat.\n * Above 1 on purpose: it is meant to reach the bloom pyramid. Blue-violet,\n * the same light the motes leaving her are drawn and lit with. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment\n // says it: above 24 bits an f32 loses the low ones. The float hash this\n // replaces multiplied three terms into the hundreds of millions and took\n // fract() of the product \u2014 which past 2^24 is not noise, it is a quantised\n // staircase, and wherever that staircase landed on zero the threshold was\n // zero. A surface whose threshold is zero never crosses it: it stays behind\n // while the rest of her leaves, and it sits inside the burning front the\n // whole time she is gone. Those were the white pieces that would not go.\n //\n // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and\n // +0.5 are the same cell, and half this model is at negative x.\n var h = vec3u(vec3i(floor(p)) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);\n}\n\n/** Value noise over that hash \u2014 smooth within a flake, uncorrelated between. */\nfn rz_dissolve_field(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let c000 = rz_dissolve_hash(i);\n let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));\n let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));\n let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));\n let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));\n let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));\n let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));\n let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(c000, c100, u.x);\n let x10 = mix(c010, c110, u.x);\n let x01 = mix(c001, c101, u.x);\n let x11 = mix(c011, c111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\n/**\n * The threshold this fragment is measured against.\n *\n * NOISE ALONE, so the whole body comes apart at once \u2014 a hand, a shoulder and\n * an ankle all thinning together rather than a line travelling up her. It was\n * tilted by height at first, on the argument that a sweep reads as something\n * happening TO her; what it actually reads as is a wipe, and a wipe has a\n * direction, an edge and a speed the rest of the effect knows nothing about.\n * Everywhere at once is also the honest match for what leaves: the motes are\n * shed from every limb in the same breath.\n *\n * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one\n * cell shared a single threshold: an eye highlight, a brow, a button, a hair\n * clip. All of it sat inside the glowing front at once, so the piece flashed\n * white as a whole and then popped \u2014 the two things a disintegration must never\n * do. The grit is a per-cell hash at a fraction of the clump, so a part a few\n * millimetres across still comes apart in pieces.\n *\n * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.\n * That ordering is what makes the dissolve smooth, and it is not obvious: the\n * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed\n * however uniform each half is. It piles up in the middle, so most of the\n * surface had a threshold near a half \u2014 all of it went as the dissolve swept\n * through the middle, and what remained was the thin tail near zero. That tail\n * was the second stage everyone could see: a body that mostly vanished, then a\n * scatter of glowing scraps, then nothing.\n *\n * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it\n * EXACTLY uniform, where scaling a sum back into range only flattens the middle\n * \u2014 measured over four hundred thousand samples, the sum removed 12% of her per\n * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum\n * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to\n * the last, a ratio of 1.0.\n *\n * That flatness IS the smoothness. An uneven rate is what produced the two\n * stages you could see \u2014 a body that mostly vanished at once, then a scatter of\n * scraps that took as long again to follow.\n *\n * The clumping survives the wrap: neighbouring fragments share a clump value,\n * so the offset moves them together. What wraps past 1 comes back at 0 and goes\n * at the other end instead, which costs a little speckle in a region and buys\n * an even rate everywhere.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;\n return fract(grit + clump * RZ_DISSOLVE_CLUMP);\n}\n\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\n// The head of the material block and the one field at its tail \u2014 the same\n// reach the depth prepass takes, and for the same reason: a shadow cast by a\n// body that is no longer drawn is the tell that the vanishing is a trick.\nstruct MaterialDiffuse {\n diffuse: vec4f,\n _skip0: vec4f,\n _skip1: vec4f,\n _skip2: f32,\n materialId: f32,\n _skip3: f32,\n dissolve: f32,\n};\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n// The model's ObjectLight, for its jelly eyes: the map must hold the iris where\n// the colour pass draws it.\nstruct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }\n@group(0) @binding(3) var<uniform> objectLight: ObjectLight;\n\nfn _rzJellyTurn(q: vec2f, a: f32) -> vec2f {\n let c = cos(a);\n let s = sin(a);\n return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);\n}\n\n/**\n * Where the jelly puts an iris vertex. The iris is the part of the eye the eye\n * bones carry, so it is moved as a whole, in bind space before skinning:\n * scaled about its centre (the CPU composes the blink's spring, the lid's\n * press and her motion's knock into one scale), turned and drifted a hair on\n * the spring, brought forward a hair, and rolled about the eye bone with a\n * closing expression \u2014 inside the white, which stays where it is. A highlight\n * layer (this draw's material is one of jelly[1].w's) takes the iris's move,\n * then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones\n * (jelly[2].w, jelly[3].w), so nothing else on the face moves.\n */\nfn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {\n let j = objectLight.jelly;\n let ids = u32(j[1].w);\n let mid = u32(material.materialId);\n let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));\n var out = p;\n for (var e = 0u; e < 2u; e++) {\n let bone = j[2u + e].w;\n if (bone < 0.0) { continue; }\n let b = u32(bone);\n let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));\n if (w <= 0.0) { continue; }\n let c = j[e].xyz;\n // A rig that carries both eyes on one bone: each vertex follows its own side.\n if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }\n if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }\n let t = j[4u + e];\n let m = j[6u + e];\n let h = j[10u + e];\n var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;\n // Forward is \u2212Z (MMD faces \u2212Z).\n var push = h.z;\n if (hl) {\n // Sliding over the iris's dome on its own, a highlight sheet a hair in\n // front of it would sink into it where the dome bulges more and the two\n // fight for depth; so it comes forward by as far as it slid \u2014 a slope of\n // one, steeper than the dome anywhere it draws.\n let before = q;\n q = _rzJellyTurn(q * h.xy, t.w) + m.zw;\n push += length(q - before);\n }\n var moved = vec3f(c.xy + q, p.z - push);\n let roll = j[8u + e];\n if (roll.w != 0.0) {\n let d = moved - roll.xyz;\n let rd = _rzJellyTurn(d.yz, roll.w);\n moved = roll.xyz + vec3f(d.x, rd.x, rd.y);\n }\n out = mix(p, moved, w);\n }\n return out;\n}\n\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n /** Bind-pose position, for the dissolve test \u2014 object space, as everywhere. */\n @location(1) restPos: vec3f,\n /** The triangle's own threshold, flat \u2014 see the material VertexOutput. A\n * face that is gone must stop casting, and stop as a whole face. */\n @location(2) @interpolate(flat) faceT: f32,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var pos4 = vec4f(position, 1.0);\n if (objectLight.jelly[0].w > 0.0) { pos4 = vec4f(rzJellyVertex(position, joints0, nw), 1.0); }\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n out.restPos = position;\n out.faceT = rz_dissolve_threshold(position);\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n // The dissolve, run as the colour pass runs it. A flake that is gone stops\n // casting: without this she leaves a whole shadow standing on the floor while\n // her body is in the air.\n if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }\n}\n";
|
|
2
2
|
//# sourceMappingURL=shadow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,uyWA6DpC,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DISSOLVE_WGSL } from "../materials/common";
|
|
1
|
+
import { DISSOLVE_WGSL, JELLY_VERTEX_WGSL } from "../materials/common";
|
|
2
2
|
// Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
|
|
3
3
|
// color targets): texels where diffuse-texture alpha × material alpha fall below
|
|
4
4
|
// the cutoff are discarded, so lace casts lace-shaped shadows and a true veil
|
|
@@ -19,10 +19,17 @@ struct MaterialDiffuse {
|
|
|
19
19
|
diffuse: vec4f,
|
|
20
20
|
_skip0: vec4f,
|
|
21
21
|
_skip1: vec4f,
|
|
22
|
-
_skip2:
|
|
22
|
+
_skip2: f32,
|
|
23
|
+
materialId: f32,
|
|
24
|
+
_skip3: f32,
|
|
23
25
|
dissolve: f32,
|
|
24
26
|
};
|
|
25
27
|
@group(1) @binding(1) var<uniform> material: MaterialDiffuse;
|
|
28
|
+
// The model's ObjectLight, for its jelly eyes: the map must hold the iris where
|
|
29
|
+
// the colour pass draws it.
|
|
30
|
+
struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
|
|
31
|
+
@group(0) @binding(3) var<uniform> objectLight: ObjectLight;
|
|
32
|
+
${JELLY_VERTEX_WGSL}
|
|
26
33
|
|
|
27
34
|
struct VSOut {
|
|
28
35
|
@builtin(position) position: vec4f,
|
|
@@ -36,10 +43,11 @@ struct VSOut {
|
|
|
36
43
|
|
|
37
44
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
38
45
|
@location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {
|
|
39
|
-
let pos4 = vec4f(position, 1.0);
|
|
40
46
|
let ws = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
41
47
|
let inv = select(1.0, 1.0 / ws, ws > 0.0001);
|
|
42
48
|
let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
|
|
49
|
+
var pos4 = vec4f(position, 1.0);
|
|
50
|
+
if (objectLight.jelly[0].w > 0.0) { pos4 = vec4f(rzJellyVertex(position, joints0, nw), 1.0); }
|
|
43
51
|
var sp = vec4f(0.0);
|
|
44
52
|
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
45
53
|
var out: VSOut;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CastLayout } from "./particles";
|
|
2
|
+
export declare const SIM_MAX = 1024;
|
|
3
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
4
|
+
export declare const SIM_FORMAT: GPUTextureFormat;
|
|
5
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
6
|
+
export declare function parseSimSize(wgsl: string, max: number): number;
|
|
7
|
+
/** Whether this effect drives a grid at all. */
|
|
8
|
+
export declare function simEntryPoint(wgsl: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
11
|
+
*
|
|
12
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
13
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
14
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
15
|
+
*/
|
|
16
|
+
export declare function simReadApi(group: number, tex: number, samp: number, size: number): string;
|
|
17
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
18
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
19
|
+
export declare function simClockApi(timeExpr: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The step shader: one invocation per texel, once per frame.
|
|
22
|
+
*
|
|
23
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
24
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
25
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
26
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
27
|
+
*
|
|
28
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
29
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
30
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
31
|
+
* what turns smoke into a staircase.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildSimShader(wgsl: string, size: number, _cast: CastLayout): string;
|
|
34
|
+
//# sourceMappingURL=sim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/sim.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAgC7C,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BzF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAwDpF"}
|