reze-engine 0.50.7 → 0.50.8
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 +71 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +114 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +35 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-api.d.ts.map +1 -1
- package/dist/shaders/cast-api.js +11 -0
- package/dist/shaders/materials/common.d.ts +20 -0
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +85 -1
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.js +21 -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 +19 -1
- package/package.json +1 -1
- package/src/engine.ts +139 -1
- package/src/graph/slots.ts +37 -4
- package/src/index.ts +1 -0
- package/src/shaders/cast-api.ts +11 -0
- package/src/shaders/materials/common.ts +86 -1
- package/src/shaders/passes/depth-prepass.ts +21 -0
- package/src/shaders/passes/shadow.ts +20 -1
|
@@ -66,7 +66,16 @@ struct MaterialUniforms {
|
|
|
66
66
|
// just empty. f32 because the buffer is written as floats; the shader casts.
|
|
67
67
|
materialId: f32,
|
|
68
68
|
objectId: f32,
|
|
69
|
-
|
|
69
|
+
// How much of this material is still THERE: 1 whole, 0 gone. Rides the last
|
|
70
|
+
// of the padding this struct already carried, for the same reason the ids do
|
|
71
|
+
// — the buffer's size and layout are untouched, so the indirect-draw path and
|
|
72
|
+
// every existing pipeline keep working, and a material that never dissolves
|
|
73
|
+
// pays one float it was already paying.
|
|
74
|
+
//
|
|
75
|
+
// A material morph rebuilds this block from its base copy, which is why the
|
|
76
|
+
// engine writes the value into that copy as well as into the live buffer: a
|
|
77
|
+
// face morphing while she dissolves must not come back solid for those frames.
|
|
78
|
+
dissolve: f32,
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
struct VertexOutput {
|
|
@@ -256,6 +265,81 @@ const COMMON_VS_WGSL = /* wgsl */ `
|
|
|
256
265
|
export function commonFsOutWgsl() {
|
|
257
266
|
return `\n\n${sceneFsOutWgsl()}\n`;
|
|
258
267
|
}
|
|
268
|
+
// ─── Dissolve ───────────────────────────────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* Whether a fragment survives the dissolve, and how close it is to the front.
|
|
271
|
+
*
|
|
272
|
+
* ONE implementation, shared by the colour pass and the depth prepass, and that
|
|
273
|
+
* is the whole reason it lives here rather than in either of them. The prepass
|
|
274
|
+
* claims depth for fragments the colour pass will shade; if the two disagreed
|
|
275
|
+
* about which flakes are gone, the model would keep writing depth where it no
|
|
276
|
+
* longer draws — holes that occlude the floor behind her and read as sky.
|
|
277
|
+
*
|
|
278
|
+
* OBJECT SPACE, off the bind-pose position, for the same reason the procedural
|
|
279
|
+
* texture nodes use restPos: a world-space field would let the flakes swim
|
|
280
|
+
* through her as she moves, and a screen-space one would leave them hanging in
|
|
281
|
+
* the air while she turns. In object space the pattern is painted ON the
|
|
282
|
+
* surface — the flakes stay where they were on her arm as the arm moves.
|
|
283
|
+
*
|
|
284
|
+
* The hash is value noise rather than a bare hash so the flakes come apart in
|
|
285
|
+
* clumps of a few millimetres rather than as single-pixel snow, which at any
|
|
286
|
+
* distance is just a fade.
|
|
287
|
+
*/
|
|
288
|
+
export const DISSOLVE_WGSL = /* wgsl */ `
|
|
289
|
+
/** Flake size, in object-space units — MMD's are roughly centimetres. */
|
|
290
|
+
const RZ_DISSOLVE_GRAIN: f32 = 0.55;
|
|
291
|
+
/** How wide the glowing front is, in threshold units. */
|
|
292
|
+
const RZ_DISSOLVE_EDGE: f32 = 0.16;
|
|
293
|
+
/** What the front burns with. Emission, added after lighting and after the
|
|
294
|
+
* graph — a colour that ramps or takes a shadow reads as paint, not as heat.
|
|
295
|
+
* Above 1 on purpose: it is meant to reach the bloom pyramid. */
|
|
296
|
+
const RZ_BURN_COLOR: vec3f = vec3f(0.55, 1.85, 2.60);
|
|
297
|
+
|
|
298
|
+
fn rz_dissolve_hash(p: vec3f) -> f32 {
|
|
299
|
+
let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));
|
|
300
|
+
let r = q * 17.0 * (q + 34.0);
|
|
301
|
+
return fract(r.x * r.y * r.z);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Value noise over that hash — smooth within a flake, uncorrelated between. */
|
|
305
|
+
fn rz_dissolve_field(p: vec3f) -> f32 {
|
|
306
|
+
let i = floor(p);
|
|
307
|
+
let f = fract(p);
|
|
308
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
309
|
+
let c000 = rz_dissolve_hash(i);
|
|
310
|
+
let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));
|
|
311
|
+
let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));
|
|
312
|
+
let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));
|
|
313
|
+
let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));
|
|
314
|
+
let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));
|
|
315
|
+
let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));
|
|
316
|
+
let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));
|
|
317
|
+
let x00 = mix(c000, c100, u.x);
|
|
318
|
+
let x10 = mix(c010, c110, u.x);
|
|
319
|
+
let x01 = mix(c001, c101, u.x);
|
|
320
|
+
let x11 = mix(c011, c111, u.x);
|
|
321
|
+
return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The threshold this fragment is measured against.
|
|
326
|
+
*
|
|
327
|
+
* Tilted by HEIGHT so the dissolve sweeps up the body instead of arriving
|
|
328
|
+
* everywhere at once: a body that comes apart from the feet reads as something
|
|
329
|
+
* happening TO her, and one that fades uniformly reads as an opacity slider.
|
|
330
|
+
* The tilt is gentle — a third of the range — so the noise still decides which
|
|
331
|
+
* flake goes when, and the sweep only decides roughly where it is.
|
|
332
|
+
*
|
|
333
|
+
* FEET FIRST, head last: the face is the last thing to go and the first thing
|
|
334
|
+
* back, which is the order every disappearance in film is cut in. Object space
|
|
335
|
+
* puts the floor at y = 0 on any PMX, so the tilt needs no rig measurement.
|
|
336
|
+
*/
|
|
337
|
+
fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
|
|
338
|
+
let n = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);
|
|
339
|
+
let low = 1.0 - clamp(restPos.y * 0.045, 0.0, 1.0);
|
|
340
|
+
return clamp(n * 0.68 + low * 0.32, 0.0, 1.0);
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
259
343
|
// ─── Convenience: full shared prelude ───────────────────────────────
|
|
260
344
|
// Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
|
|
261
345
|
// pull in everything structural. Each material then adds its own constants + fs().
|
|
@@ -92,7 +92,7 @@ type CompositeEffectSource = {
|
|
|
92
92
|
* Depends on `viewU` and `_rzCast` being declared by the including module, and
|
|
93
93
|
* on nothing else: no textures, no samplers.
|
|
94
94
|
*/
|
|
95
|
-
export declare const EFFECT_SCENE_API = "\n// \u2500\u2500 The effect API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Named rz*, for the engine. The prefix earns its place twice: user code is\n// concatenated into THIS module, so an unprefixed rzAnchor() would collide with\n// exactly the helper an author would write, and the old bg* prefix stopped being\n// true in 0.41.0 when effects gained a mount over the finished frame.\n//\n// The bg* names below are permanent aliases, not a deprecation with an end date.\n// A published link is immutable, so a scene pinned to a bg* effect has to keep\n// compiling forever. They are one-line and inlined; no new function gets one.\n\n/** Canvas size in pixels \u2014 for aspect correction. */\nfn rzResolution() -> vec2f { return viewU[6].zw; }\n\n/** The camera's world position. */\nfn rzCameraPos() -> vec3f { return viewU[10].xyz; }\n\n/** How many characters are in the scene, up to four. */\nfn rzSubjectCount() -> i32 { return i32(viewU[10].w); }\n\n/**\n * A world point as the camera sees it: xy the uv it lands on, z its distance\n * along the VIEW AXIS in metres.\n *\n * The exact inverse of the ray this pass builds per pixel, so it is the cheap way\n * to work with anything anchored in the world. Marching a curve or a trail in 3D\n * costs a distance evaluation per sample per pixel; projecting its points once\n * and measuring in 2D costs a subtraction, which is the difference between a\n * ribbon that runs at 4K and one that does not.\n *\n * z is directly comparable to the depth handed to foreground(), so occlusion is\n * a single test: draw where your z is nearer than the scene's. It is returned\n * SIGNED and unclamped \u2014 behind the camera is negative, and worth rejecting\n * before you use the uv, which is meaningless there.\n */\nfn rzProject(p: vec3f) -> vec3f {\n let d = p - viewU[10].xyz;\n let z = dot(d, viewU[5].xyz);\n // Guard only the divide. z itself is returned as it is, so the caller can see\n // the sign; clamping it here would put points behind the lens on the horizon.\n let inv = 1.0 / select(z, 1e-4, z < 1e-4);\n let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);\n return vec3f(ndc * 0.5 + 0.5, z);\n}\n\nfn rzCamPos() -> vec3f { return rzCameraPos(); }\nfn rzCameraRight() -> vec3f { return viewU[3].xyz; }\nfn rzCameraUp() -> vec3f { return viewU[4].xyz; }\nfn rzCameraForward() -> vec3f { return viewU[5].xyz; }\n\n// The cast \u2014 subjects, anchors, trails \u2014 is CAST_API, shared verbatim with the\n// particle and trail modules. It used to be written out here, a second time, and\n// the two copies had drifted: this one had rzAnchor and that one did not.\n\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 16;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 204 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n\n\n// The hashes, the noise and rzFalloff \u2014 EFFECT_MATH_API, the same text the\n// particle and trail modules get. The field module used to be missing most of\n// it, so a helper an author wrote for one mount failed to compile in another\n// for no reason visible in the file.\n\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(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 n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n\n\n\nfn bgResolution() -> vec2f { return rzResolution(); }\nfn bgCameraPos() -> vec3f { return rzCameraPos(); }\nfn bgSubjectCount() -> i32 { return rzSubjectCount(); }\n\n/**\n * Where a character IS, in world space \u2014 at the hips, not on the floor.\n *\n * An effect that wants to RESPOND to the cast \u2014 a glow that follows someone,\n * dust kicked up where they are \u2014 needs to know where they are, and the ray and\n * the depth cannot tell it: they describe the pixel, not the scene.\n *\n * The value is model.position + \u30BB\u30F3\u30BF\u30FC + \u5168\u3066\u306E\u89AA. \u30BB\u30F3\u30BF\u30FC sits at hip\n * height on every standard MMD rig, so this is a point in the middle of the\n * body. It is NOT the contact point: a ripple drawn here appears at the waist.\n * Ground effects want the .xz of this and their own floor height, which is what\n * the effects that shipped against it already do.\n *\n * The comment here used to claim it was \"between the feet on the floor\", which\n * is where that habit came from. Left as it is regardless of the name: a\n * published link is immutable, so every shared scene pinning an effect that\n * reads this depends on it meaning exactly what it has always meant.\n *\n * Clamped rather than bounds-checked: an effect looping past the count reads the\n * last subject instead of sampling whatever follows the array, which is a wrong\n * ripple rather than an undefined one.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }\n\nfn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }\n\n/** Where in the WORLD the scene drew this pixel \u2014 the depth handed to\n * foreground() turned into a place. Without it an effect can only think in\n * distances from the lens, which is no use to anything that belongs somewhere:\n * fog lying on the ground has to know where the ground is.\n *\n * depth measures along the VIEW AXIS, not along the ray, so it is divided by\n * the ray's projection onto camera-forward before being walked out. At the far\n * plane (nothing drawn) this lands a very long way off, which is what a sky\n * should do to anything reading it. */\nfn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {\n let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);\n return rzCameraPos() + normalize(ray) * (depth / axis);\n}\n\nfn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }\n\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n\n\n/** Color grading, applied to the tonemapped SCENE (not the background \u2014 see the\n * call site). The core is ASC CDL, the film-industry interchange standard:\n *\n * out = (in \u00B7 slope + offset) ^ power then saturation (SOP \u2192 SAT)\n *\n * Using the real standard rather than invented controls means a look authored\n * here maps onto any grading tool. slope/offset/power are derived on the CPU\n * from the UI's shadow/midtone/highlight colors (see setColorGrading), so the\n * per-pixel cost is one mul-add, one pow, one lerp. */\nfn grade(c: vec3f) -> vec3f {\n var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);\n // Contrast pivots on 0.5 \u2014 display-referred midpoint, since we grade post-Filmic.\n x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);\n // Rec.709 luma, matching the ASC SAT node.\n let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));\n return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));\n}\n";
|
|
95
|
+
export declare const EFFECT_SCENE_API = "\n// \u2500\u2500 The effect API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Named rz*, for the engine. The prefix earns its place twice: user code is\n// concatenated into THIS module, so an unprefixed rzAnchor() would collide with\n// exactly the helper an author would write, and the old bg* prefix stopped being\n// true in 0.41.0 when effects gained a mount over the finished frame.\n//\n// The bg* names below are permanent aliases, not a deprecation with an end date.\n// A published link is immutable, so a scene pinned to a bg* effect has to keep\n// compiling forever. They are one-line and inlined; no new function gets one.\n\n/** Canvas size in pixels \u2014 for aspect correction. */\nfn rzResolution() -> vec2f { return viewU[6].zw; }\n\n/** The camera's world position. */\nfn rzCameraPos() -> vec3f { return viewU[10].xyz; }\n\n/** How many characters are in the scene, up to four. */\nfn rzSubjectCount() -> i32 { return i32(viewU[10].w); }\n\n/**\n * A world point as the camera sees it: xy the uv it lands on, z its distance\n * along the VIEW AXIS in metres.\n *\n * The exact inverse of the ray this pass builds per pixel, so it is the cheap way\n * to work with anything anchored in the world. Marching a curve or a trail in 3D\n * costs a distance evaluation per sample per pixel; projecting its points once\n * and measuring in 2D costs a subtraction, which is the difference between a\n * ribbon that runs at 4K and one that does not.\n *\n * z is directly comparable to the depth handed to foreground(), so occlusion is\n * a single test: draw where your z is nearer than the scene's. It is returned\n * SIGNED and unclamped \u2014 behind the camera is negative, and worth rejecting\n * before you use the uv, which is meaningless there.\n */\nfn rzProject(p: vec3f) -> vec3f {\n let d = p - viewU[10].xyz;\n let z = dot(d, viewU[5].xyz);\n // Guard only the divide. z itself is returned as it is, so the caller can see\n // the sign; clamping it here would put points behind the lens on the horizon.\n let inv = 1.0 / select(z, 1e-4, z < 1e-4);\n let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);\n return vec3f(ndc * 0.5 + 0.5, z);\n}\n\nfn rzCamPos() -> vec3f { return rzCameraPos(); }\nfn rzCameraRight() -> vec3f { return viewU[3].xyz; }\nfn rzCameraUp() -> vec3f { return viewU[4].xyz; }\nfn rzCameraForward() -> vec3f { return viewU[5].xyz; }\n\n// The cast \u2014 subjects, anchors, trails \u2014 is CAST_API, shared verbatim with the\n// particle and trail modules. It used to be written out here, a second time, and\n// the two copies had drifted: this one had rzAnchor and that one did not.\n\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 16;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 204 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n\n\n// The hashes, the noise and rzFalloff \u2014 EFFECT_MATH_API, the same text the\n// particle and trail modules get. The field module used to be missing most of\n// it, so a helper an author wrote for one mount failed to compile in another\n// for no reason visible in the file.\n\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(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 n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n\n\n\nfn bgResolution() -> vec2f { return rzResolution(); }\nfn bgCameraPos() -> vec3f { return rzCameraPos(); }\nfn bgSubjectCount() -> i32 { return rzSubjectCount(); }\n\n/**\n * Where a character IS, in world space \u2014 at the hips, not on the floor.\n *\n * An effect that wants to RESPOND to the cast \u2014 a glow that follows someone,\n * dust kicked up where they are \u2014 needs to know where they are, and the ray and\n * the depth cannot tell it: they describe the pixel, not the scene.\n *\n * The value is model.position + \u30BB\u30F3\u30BF\u30FC + \u5168\u3066\u306E\u89AA. \u30BB\u30F3\u30BF\u30FC sits at hip\n * height on every standard MMD rig, so this is a point in the middle of the\n * body. It is NOT the contact point: a ripple drawn here appears at the waist.\n * Ground effects want the .xz of this and their own floor height, which is what\n * the effects that shipped against it already do.\n *\n * The comment here used to claim it was \"between the feet on the floor\", which\n * is where that habit came from. Left as it is regardless of the name: a\n * published link is immutable, so every shared scene pinning an effect that\n * reads this depends on it meaning exactly what it has always meant.\n *\n * Clamped rather than bounds-checked: an effect looping past the count reads the\n * last subject instead of sampling whatever follows the array, which is a wrong\n * ripple rather than an undefined one.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }\n\nfn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }\n\n/** Where in the WORLD the scene drew this pixel \u2014 the depth handed to\n * foreground() turned into a place. Without it an effect can only think in\n * distances from the lens, which is no use to anything that belongs somewhere:\n * fog lying on the ground has to know where the ground is.\n *\n * depth measures along the VIEW AXIS, not along the ray, so it is divided by\n * the ray's projection onto camera-forward before being walked out. At the far\n * plane (nothing drawn) this lands a very long way off, which is what a sky\n * should do to anything reading it. */\nfn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {\n let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);\n return rzCameraPos() + normalize(ray) * (depth / axis);\n}\n\nfn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }\n\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n\n\n/** Color grading, applied to the tonemapped SCENE (not the background \u2014 see the\n * call site). The core is ASC CDL, the film-industry interchange standard:\n *\n * out = (in \u00B7 slope + offset) ^ power then saturation (SOP \u2192 SAT)\n *\n * Using the real standard rather than invented controls means a look authored\n * here maps onto any grading tool. slope/offset/power are derived on the CPU\n * from the UI's shadow/midtone/highlight colors (see setColorGrading), so the\n * per-pixel cost is one mul-add, one pow, one lerp. */\nfn grade(c: vec3f) -> vec3f {\n var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);\n // Contrast pivots on 0.5 \u2014 display-referred midpoint, since we grade post-Filmic.\n x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);\n // Rec.709 luma, matching the ASC SAT node.\n let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));\n return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));\n}\n";
|
|
96
96
|
export declare function buildCompositeShader(effect?: CompositeEffectSource | null): string;
|
|
97
97
|
export declare function buildFieldShader(effect: CompositeEffectSource): string;
|
|
98
98
|
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA4LD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAuDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAqEtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,CAsFpD"}
|
|
@@ -26,13 +26,24 @@
|
|
|
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
30
|
import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
30
31
|
export function transparentDepthPrepassWgsl() {
|
|
31
32
|
return /* wgsl */ `
|
|
33
|
+
${DISSOLVE_WGSL}
|
|
32
34
|
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
35
|
+
// The head of the material block, plus the one field at its tail. The middle is
|
|
36
|
+
// skipped rather than named: this pass shades nothing, and every field it
|
|
37
|
+
// declares is a field that must stay in step with the real struct for no gain.
|
|
38
|
+
// The padding is explicit so the offsets are checkable by eye — vec4 at 16 and
|
|
39
|
+
// 32, vec3 at 48, and dissolve last at 60.
|
|
33
40
|
struct MaterialUniforms {
|
|
34
41
|
diffuseColor: vec3f,
|
|
35
42
|
alpha: f32,
|
|
43
|
+
_skip0: vec4f,
|
|
44
|
+
_skip1: vec4f,
|
|
45
|
+
_skip2: vec3f,
|
|
46
|
+
dissolve: f32,
|
|
36
47
|
};
|
|
37
48
|
|
|
38
49
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
@@ -46,6 +57,9 @@ struct VSOut {
|
|
|
46
57
|
// must land on exactly the depths this wrote.
|
|
47
58
|
@builtin(position) @invariant position: vec4f,
|
|
48
59
|
@location(0) uv: vec2f,
|
|
60
|
+
// The bind-pose position, carried for one reason: the dissolve test below has
|
|
61
|
+
// to be the SAME test the colour pass runs, and that one is in object space.
|
|
62
|
+
@location(1) restPos: vec3f,
|
|
49
63
|
};
|
|
50
64
|
|
|
51
65
|
@vertex fn vs(
|
|
@@ -66,6 +80,7 @@ struct VSOut {
|
|
|
66
80
|
var o: VSOut;
|
|
67
81
|
o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
|
|
68
82
|
o.uv = uv;
|
|
83
|
+
o.restPos = position;
|
|
69
84
|
return o;
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -87,6 +102,12 @@ override CUTOFF: f32 = 0.5;
|
|
|
87
102
|
@fragment fn fs(in: VSOut) -> PrepassOut {
|
|
88
103
|
let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;
|
|
89
104
|
if (a < CUTOFF) { discard; }
|
|
105
|
+
// The dissolve, run identically to the colour pass — shared code, not a
|
|
106
|
+
// second copy of the same idea. A prepass that kept claiming depth for flakes
|
|
107
|
+
// the colour pass throws away would punch holes that occlude the floor behind
|
|
108
|
+
// her: you would see sky through her, which is the failure this line exists
|
|
109
|
+
// to prevent.
|
|
110
|
+
if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
|
|
90
111
|
var out: PrepassOut;
|
|
91
112
|
out.color = vec4f(0.0);
|
|
92
113
|
out.mask = vec4f(0.0);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SHADOW_DEPTH_SHADER_WGSL = "\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>;\nstruct MaterialDiffuse {
|
|
1
|
+
export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Flake size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.55;\n/** How wide the glowing front is, in threshold units. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.16;\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. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.55, 1.85, 2.60);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));\n let r = q * 17.0 * (q + 34.0);\n return fract(r.x * r.y * r.z);\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 * Tilted by HEIGHT so the dissolve sweeps up the body instead of arriving\n * everywhere at once: a body that comes apart from the feet reads as something\n * happening TO her, and one that fades uniformly reads as an opacity slider.\n * The tilt is gentle \u2014 a third of the range \u2014 so the noise still decides which\n * flake goes when, and the sweep only decides roughly where it is.\n *\n * FEET FIRST, head last: the face is the last thing to go and the first thing\n * back, which is the order every disappearance in film is cut in. Object space\n * puts the floor at y = 0 on any PMX, so the tilt needs no rig measurement.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let n = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);\n let low = 1.0 - clamp(restPos.y * 0.045, 0.0, 1.0);\n return clamp(n * 0.68 + low * 0.32, 0.0, 1.0);\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};\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 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 && rz_dissolve_threshold(in.restPos) > 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":"
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,y1IAiDpC,CAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DISSOLVE_WGSL } from "../materials/common";
|
|
1
2
|
// Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
|
|
2
3
|
// color targets): texels where diffuse-texture alpha × material alpha fall below
|
|
3
4
|
// the cutoff are discarded, so lace casts lace-shaped shadows and a true veil
|
|
@@ -5,17 +6,29 @@
|
|
|
5
6
|
// Group 1 is the main pass's per-material bind group reused as-is; only
|
|
6
7
|
// bindings 0/1 are declared here (a layout may carry bindings a shader ignores).
|
|
7
8
|
export const SHADOW_DEPTH_SHADER_WGSL = /* wgsl */ `
|
|
9
|
+
${DISSOLVE_WGSL}
|
|
8
10
|
struct LightVP { viewProj: mat4x4f, };
|
|
9
11
|
@group(0) @binding(0) var<uniform> lp: LightVP;
|
|
10
12
|
@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
|
|
11
13
|
@group(0) @binding(2) var texSampler: sampler;
|
|
12
14
|
@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
13
|
-
|
|
15
|
+
// The head of the material block and the one field at its tail — the same
|
|
16
|
+
// reach the depth prepass takes, and for the same reason: a shadow cast by a
|
|
17
|
+
// body that is no longer drawn is the tell that the vanishing is a trick.
|
|
18
|
+
struct MaterialDiffuse {
|
|
19
|
+
diffuse: vec4f,
|
|
20
|
+
_skip0: vec4f,
|
|
21
|
+
_skip1: vec4f,
|
|
22
|
+
_skip2: vec3f,
|
|
23
|
+
dissolve: f32,
|
|
24
|
+
};
|
|
14
25
|
@group(1) @binding(1) var<uniform> material: MaterialDiffuse;
|
|
15
26
|
|
|
16
27
|
struct VSOut {
|
|
17
28
|
@builtin(position) position: vec4f,
|
|
18
29
|
@location(0) uv: vec2f,
|
|
30
|
+
/** Bind-pose position, for the dissolve test — object space, as everywhere. */
|
|
31
|
+
@location(1) restPos: vec3f,
|
|
19
32
|
};
|
|
20
33
|
|
|
21
34
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
@@ -29,11 +42,16 @@ struct VSOut {
|
|
|
29
42
|
var out: VSOut;
|
|
30
43
|
out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
31
44
|
out.uv = uv;
|
|
45
|
+
out.restPos = position;
|
|
32
46
|
return out;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
@fragment fn fs(in: VSOut) {
|
|
36
50
|
let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;
|
|
37
51
|
if (alpha < 0.5) { discard; }
|
|
52
|
+
// The dissolve, run as the colour pass runs it. A flake that is gone stops
|
|
53
|
+
// casting: without this she leaves a whole shadow standing on the floor while
|
|
54
|
+
// her body is in the air.
|
|
55
|
+
if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
|
|
38
56
|
}
|
|
39
57
|
`;
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -666,12 +666,40 @@ interface PickDrawCall {
|
|
|
666
666
|
bindGroup: GPUBindGroup
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/**
|
|
670
|
+
* A repeating dissolve, in seconds within one cycle.
|
|
671
|
+
*
|
|
672
|
+
* Four moments rather than a duration and a delay: every one of them is a thing
|
|
673
|
+
* you can see happen, and an author tuning this is watching for exactly those
|
|
674
|
+
* four frames.
|
|
675
|
+
*/
|
|
676
|
+
export interface DissolveCycle {
|
|
677
|
+
period: number
|
|
678
|
+
/** She starts to come apart. */
|
|
679
|
+
breakAt: number
|
|
680
|
+
/** Fully gone. */
|
|
681
|
+
hiddenAt: number
|
|
682
|
+
/** She starts to come back. */
|
|
683
|
+
backAt: number
|
|
684
|
+
/** Whole again. */
|
|
685
|
+
doneAt: number
|
|
686
|
+
}
|
|
687
|
+
|
|
669
688
|
interface ModelInstance {
|
|
670
689
|
name: string
|
|
671
690
|
/** This model's id in the id attachment — 1-based, so 0 stays "nothing".
|
|
672
691
|
* The pick pass has always minted it; the cast carries it now too, so an
|
|
673
692
|
* effect can compare what it reads out of the id buffer against a subject. */
|
|
674
693
|
objectId: number
|
|
694
|
+
/** How much of this model is still THERE: 1 whole, 0 gone. Written into every
|
|
695
|
+
* material's uniform (see setModelDissolve) and mirrored into the cast, so
|
|
696
|
+
* the material shell can take her apart and an effect can draw what is
|
|
697
|
+
* leaving — both from one number rather than two clocks that must agree. */
|
|
698
|
+
dissolve: number
|
|
699
|
+
/** Every material's uniform buffer, in draw order. Kept because a dissolve
|
|
700
|
+
* writes ONE float into each of them and needs no other reason to hold a
|
|
701
|
+
* block: the whole 16-float copy exists only for materials that morph. */
|
|
702
|
+
materialUniformBuffers: GPUBuffer[]
|
|
675
703
|
model: Model
|
|
676
704
|
basePath: string
|
|
677
705
|
assetReader: AssetReader
|
|
@@ -1595,6 +1623,8 @@ export class Engine {
|
|
|
1595
1623
|
* every frame. */
|
|
1596
1624
|
private fieldClockScratch = new Float32Array(4)
|
|
1597
1625
|
/** Material parameters driven by the scene clock — see setStyleParamTrack. */
|
|
1626
|
+
/** Repeating dissolves, by model name — see setModelDissolveCycle. */
|
|
1627
|
+
private dissolveCycles = new Map<string, DissolveCycle>()
|
|
1598
1628
|
private paramTracks = new Map<
|
|
1599
1629
|
string,
|
|
1600
1630
|
{ modelName: string; groupId: string; paramId: string; keys: ParamKey[]; last: ParamValue | null }
|
|
@@ -8662,6 +8692,8 @@ export class Engine {
|
|
|
8662
8692
|
materialToGroup: new Map(),
|
|
8663
8693
|
styleGroupGen: new Map(),
|
|
8664
8694
|
objectId: this.modelInstances.size + 1,
|
|
8695
|
+
dissolve: 1,
|
|
8696
|
+
materialUniformBuffers: [],
|
|
8665
8697
|
cullModelIndex: 0,
|
|
8666
8698
|
// Seeded false: the first skin-matrix upload decides it, and until then the
|
|
8667
8699
|
// sphere path is the safe answer (it never culls something it should not).
|
|
@@ -9069,6 +9101,7 @@ export class Engine {
|
|
|
9069
9101
|
modelId,
|
|
9070
9102
|
)
|
|
9071
9103
|
inst.gpuBuffers.push(materialUniformBuffer)
|
|
9104
|
+
inst.materialUniformBuffers.push(materialUniformBuffer)
|
|
9072
9105
|
if (morphedMaterials.has(pmxMaterialIndex)) {
|
|
9073
9106
|
const base = this.materialUniformData(mat, sphereMode, headBoneIndex, materialId, modelId)
|
|
9074
9107
|
morphTargets.push({
|
|
@@ -9225,6 +9258,10 @@ export class Engine {
|
|
|
9225
9258
|
// bound and the indirect-draw path is untouched.
|
|
9226
9259
|
data[13] = materialId
|
|
9227
9260
|
data[14] = objectId
|
|
9261
|
+
// 15 is the last of that padding: how much of this material is there. ONE,
|
|
9262
|
+
// not zero — the default has to be "whole", or every model would load
|
|
9263
|
+
// already gone.
|
|
9264
|
+
data[15] = 1
|
|
9228
9265
|
return data
|
|
9229
9266
|
}
|
|
9230
9267
|
|
|
@@ -10329,6 +10366,7 @@ export class Engine {
|
|
|
10329
10366
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
10330
10367
|
// Material parameters on the scene clock, before anything reads their
|
|
10331
10368
|
// uniforms this frame.
|
|
10369
|
+
this.evaluateDissolveCycles()
|
|
10332
10370
|
this.evaluateParamTracks()
|
|
10333
10371
|
this.stepSim(encoder, deltaTime)
|
|
10334
10372
|
this.stepParticles(encoder, deltaTime)
|
|
@@ -10730,6 +10768,102 @@ export class Engine {
|
|
|
10730
10768
|
return true
|
|
10731
10769
|
}
|
|
10732
10770
|
|
|
10771
|
+
/**
|
|
10772
|
+
* How much of a model is still THERE: 1 whole, 0 gone.
|
|
10773
|
+
*
|
|
10774
|
+
* The instant tier, like setStyleParam — one float per material, no recompile,
|
|
10775
|
+
* no pipeline touched. What it drives is a THRESHOLD, not an opacity: the
|
|
10776
|
+
* material shell throws away every flake whose object-space threshold has
|
|
10777
|
+
* passed, and lights the ones about to go. So a model at 0.5 is not
|
|
10778
|
+
* half-transparent, it is half GONE, which is the difference between a fade
|
|
10779
|
+
* and a disintegration.
|
|
10780
|
+
*
|
|
10781
|
+
* Written into the depth prepass's copy of the same test as well, so the
|
|
10782
|
+
* flakes stop claiming depth the moment they stop being drawn — see
|
|
10783
|
+
* DISSOLVE_WGSL for why that has to be one implementation.
|
|
10784
|
+
*
|
|
10785
|
+
* Mirrored into the cast, so an effect can read rzSubject(i).dissolve and draw
|
|
10786
|
+
* the sparks that leave her in step with the body they came off. That is the
|
|
10787
|
+
* whole reason this lives on the model rather than in an effect's uniform: the
|
|
10788
|
+
* material pass runs long before any effect, and only the engine sees both.
|
|
10789
|
+
*/
|
|
10790
|
+
setModelDissolve(modelName: string, value: number): boolean {
|
|
10791
|
+
const inst = this.modelInstances.get(modelName)
|
|
10792
|
+
if (!inst) return false
|
|
10793
|
+
const v = Math.min(1, Math.max(0, value))
|
|
10794
|
+
if (inst.dissolve === v) return true
|
|
10795
|
+
inst.dissolve = v
|
|
10796
|
+
// Offset 60: the sixteenth float of MaterialUniforms. One four-byte write
|
|
10797
|
+
// per material rather than the whole block — the block only exists as a
|
|
10798
|
+
// copy for materials that morph.
|
|
10799
|
+
const one = new Float32Array([v])
|
|
10800
|
+
for (const buffer of inst.materialUniformBuffers) {
|
|
10801
|
+
this.device.queue.writeBuffer(buffer, 60, one)
|
|
10802
|
+
}
|
|
10803
|
+
// The morph path rebuilds a material's block from its `base` copy and
|
|
10804
|
+
// uploads it whole, which would put the old value straight back. Patching
|
|
10805
|
+
// `base` is what keeps a face that is morphing while she dissolves from
|
|
10806
|
+
// coming back solid for those frames; `last` is cleared so the next
|
|
10807
|
+
// comparison genuinely re-uploads rather than deciding nothing moved.
|
|
10808
|
+
if (inst.materialMorphTargets) {
|
|
10809
|
+
for (const t of inst.materialMorphTargets) {
|
|
10810
|
+
t.base[15] = v
|
|
10811
|
+
t.last[15] = Number.NaN
|
|
10812
|
+
}
|
|
10813
|
+
}
|
|
10814
|
+
return true
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
/**
|
|
10818
|
+
* A repeating dissolve, on the scene clock.
|
|
10819
|
+
*
|
|
10820
|
+
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
10821
|
+
* the wrong shape twice: an exported take stepped at another rate would land
|
|
10822
|
+
* on different values than the preview did, and the effect drawing the sparks
|
|
10823
|
+
* would be reading a number some other clock wrote. Here the engine samples it
|
|
10824
|
+
* where it samples everything else time-driven, so a take reproduces exactly
|
|
10825
|
+
* and rzSubject().dissolve is the same value the material shell used on that
|
|
10826
|
+
* very frame.
|
|
10827
|
+
*
|
|
10828
|
+
* The five numbers are seconds within one cycle: when she starts to go, when
|
|
10829
|
+
* she is fully gone, when she starts to come back, and when she is whole. The
|
|
10830
|
+
* gaps between them are the timing, and the hold between the middle two is how
|
|
10831
|
+
* long she is away.
|
|
10832
|
+
*/
|
|
10833
|
+
setModelDissolveCycle(modelName: string, cycle: DissolveCycle | null): boolean {
|
|
10834
|
+
if (!this.modelInstances.has(modelName)) return false
|
|
10835
|
+
if (!cycle) {
|
|
10836
|
+
if (this.dissolveCycles.delete(modelName)) this.setModelDissolve(modelName, 1)
|
|
10837
|
+
return true
|
|
10838
|
+
}
|
|
10839
|
+
this.dissolveCycles.set(modelName, cycle)
|
|
10840
|
+
return true
|
|
10841
|
+
}
|
|
10842
|
+
|
|
10843
|
+
/** Every dissolve cycle, at the current scene clock. Once per frame, before
|
|
10844
|
+
* the cast is written and long before any effect reads it. */
|
|
10845
|
+
private evaluateDissolveCycles(): void {
|
|
10846
|
+
if (this.dissolveCycles.size === 0) return
|
|
10847
|
+
for (const [name, c] of this.dissolveCycles) {
|
|
10848
|
+
const period = Math.max(c.period, 1e-3)
|
|
10849
|
+
const t = this.sceneClock - Math.floor(this.sceneClock / period) * period
|
|
10850
|
+
let v = 1
|
|
10851
|
+
if (t >= c.breakAt && t < c.hiddenAt) {
|
|
10852
|
+
v = 1 - (t - c.breakAt) / Math.max(c.hiddenAt - c.breakAt, 1e-4)
|
|
10853
|
+
} else if (t >= c.hiddenAt && t < c.backAt) {
|
|
10854
|
+
v = 0
|
|
10855
|
+
} else if (t >= c.backAt && t < c.doneAt) {
|
|
10856
|
+
v = (t - c.backAt) / Math.max(c.doneAt - c.backAt, 1e-4)
|
|
10857
|
+
}
|
|
10858
|
+
this.setModelDissolve(name, v)
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
|
|
10862
|
+
/** What setModelDissolve last set, or 1 for a model that has never dissolved. */
|
|
10863
|
+
getModelDissolve(modelName: string): number {
|
|
10864
|
+
return this.modelInstances.get(modelName)?.dissolve ?? 1
|
|
10865
|
+
}
|
|
10866
|
+
|
|
10733
10867
|
// Materials claimed by the model's currently-installed groups (for upsert/remove paths;
|
|
10734
10868
|
// applyStyleGroups derives claims from its input array instead).
|
|
10735
10869
|
private currentClaims(inst: ModelInstance): Map<string, string> {
|
|
@@ -11494,7 +11628,11 @@ export class Engine {
|
|
|
11494
11628
|
cd[b] = px
|
|
11495
11629
|
cd[b + 1] = floorY
|
|
11496
11630
|
cd[b + 2] = pz
|
|
11497
|
-
|
|
11631
|
+
// The root vec4's w, a constant 1 until now: how much of this subject is
|
|
11632
|
+
// still there. An effect that draws what is LEAVING her needs to know how
|
|
11633
|
+
// far along she is, and reading it here is what keeps the sparks in step
|
|
11634
|
+
// with the body without a second clock to agree with.
|
|
11635
|
+
cd[b + 3] = inst.dissolve
|
|
11498
11636
|
cd[b + 4] = px
|
|
11499
11637
|
cd[b + 5] = py
|
|
11500
11638
|
cd[b + 6] = pz
|
package/src/graph/slots.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// See docs/style-groups-spec.md §5, §7.
|
|
9
9
|
|
|
10
10
|
import { NODES_WGSL } from "../shaders/materials/nodes"
|
|
11
|
-
import { COMMON_MATERIAL_PRELUDE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
|
|
11
|
+
import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
|
|
12
12
|
import { sceneIdWriteWgsl } from "../shaders/passes/scene-contract"
|
|
13
13
|
import type { AlphaMode, RenderClass } from "./render-class"
|
|
14
14
|
|
|
@@ -65,7 +65,16 @@ const STYLE_UNIFORMS_WGSL = `struct StyleUniforms { p: array<vec4f, 16> };
|
|
|
65
65
|
`
|
|
66
66
|
|
|
67
67
|
function decls(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
68
|
-
|
|
68
|
+
// The dissolve helpers are ALWAYS declared, unlike the hashed-alpha ones.
|
|
69
|
+
// They cost nothing on a material that never dissolves — the branch that
|
|
70
|
+
// calls them is uniform across a draw and folds away — and making them
|
|
71
|
+
// conditional would mean deciding at COMPILE time which materials may ever
|
|
72
|
+
// come apart, which is the one thing a runtime value must not need.
|
|
73
|
+
return (
|
|
74
|
+
DISSOLVE_WGSL +
|
|
75
|
+
(alphaMode === "hashed" ? HASHED_ALPHA_DECLS : "") +
|
|
76
|
+
(renderClass === "hair" ? HAIR_OVER_EYES_DECL : "")
|
|
77
|
+
)
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
// fs() header up to and including the graph body's context locals. Composed so the
|
|
@@ -92,6 +101,25 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
92
101
|
let alpha = material.alpha * tex_s.a;
|
|
93
102
|
${discard}
|
|
94
103
|
|
|
104
|
+
// ── Dissolve ──
|
|
105
|
+
//
|
|
106
|
+
// A fragment is either THERE or it is not: the flake is thrown away rather
|
|
107
|
+
// than faded. A fade would be an opacity slider — she would go see-through
|
|
108
|
+
// and you would read the wall through her hair — while a threshold takes her
|
|
109
|
+
// apart in pieces, which is the thing being asked for. The depth prepass runs
|
|
110
|
+
// this identical test (rz_dissolve_threshold, shared), so depth and colour
|
|
111
|
+
// agree about which pieces are gone.
|
|
112
|
+
//
|
|
113
|
+
// The surviving edge glows: fragments within RZ_DISSOLVE_EDGE of the front
|
|
114
|
+
// are the ones about to go, and lighting them is what makes the boundary read
|
|
115
|
+
// as burning away rather than as a stencil moving over her.
|
|
116
|
+
var rz_burn = 0.0;
|
|
117
|
+
if (material.dissolve < 0.9995) {
|
|
118
|
+
let rz_t = rz_dissolve_threshold(input.restPos);
|
|
119
|
+
if (rz_t > material.dissolve) { discard; }
|
|
120
|
+
rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t);
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
var n = safe_normal(input.normal);
|
|
96
124
|
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
97
125
|
${gate}
|
|
@@ -132,18 +160,23 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
132
160
|
const LIT = ` + rzLightsDiffuse(input.worldPos, n) * albedo`
|
|
133
161
|
const ALBEDO = ` let albedo = tex_color;
|
|
134
162
|
`
|
|
163
|
+
// The dissolve's burning edge, ADDED after the graph and after the lights —
|
|
164
|
+
// it is emission, not a surface colour, so nothing may ramp or shadow it. Zero
|
|
165
|
+
// on every fragment of every material that is not dissolving, which is what
|
|
166
|
+
// keeps this out of the way of a scene that never uses it.
|
|
167
|
+
const BURN = ` + RZ_BURN_COLOR * rz_burn`
|
|
135
168
|
if (renderClass === "hair") {
|
|
136
169
|
return `${ALBEDO} var outAlpha = ${alphaBase};
|
|
137
170
|
if (IS_OVER_EYES) { outAlpha = ${alphaBase} * 0.25; }
|
|
138
171
|
|
|
139
172
|
var out: FSOut;
|
|
140
|
-
out.color = vec4f(final_color${LIT}, outAlpha);
|
|
173
|
+
out.color = vec4f(final_color${LIT}${BURN}, outAlpha);
|
|
141
174
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
142
175
|
${ID_WRITE} return out;
|
|
143
176
|
`
|
|
144
177
|
}
|
|
145
178
|
return `${ALBEDO} var out: FSOut;
|
|
146
|
-
out.color = vec4f(final_color${LIT}, ${alphaBase});
|
|
179
|
+
out.color = vec4f(final_color${LIT}${BURN}, ${alphaBase});
|
|
147
180
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
148
181
|
${ID_WRITE} return out;
|
|
149
182
|
`
|
package/src/index.ts
CHANGED
package/src/shaders/cast-api.ts
CHANGED
|
@@ -45,6 +45,16 @@ struct RzSubject {
|
|
|
45
45
|
center: vec3f,
|
|
46
46
|
/** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
|
|
47
47
|
bounds: vec4f,
|
|
48
|
+
/**
|
|
49
|
+
* How much of this character is still THERE: 1 whole, 0 gone.
|
|
50
|
+
*
|
|
51
|
+
* What setModelDissolve last set on them, which the material pass has already
|
|
52
|
+
* acted on by the time an effect runs — so an effect drawing what LEAVES a
|
|
53
|
+
* dissolving body (sparks, ash, a ghost) reads the same number the body was
|
|
54
|
+
* taken apart with, rather than keeping a clock of its own and hoping the two
|
|
55
|
+
* agree. 1 on a scene that never dissolves anybody.
|
|
56
|
+
*/
|
|
57
|
+
dissolve: f32,
|
|
48
58
|
/** False past the end of the cast, and every field is then zero. */
|
|
49
59
|
valid: bool,
|
|
50
60
|
}
|
|
@@ -73,6 +83,7 @@ fn rzSubject(i: i32) -> RzSubject {
|
|
|
73
83
|
if (!s.valid) { return s; }
|
|
74
84
|
let b = i * 3;
|
|
75
85
|
s.root = _rzCast[b].xyz;
|
|
86
|
+
s.dissolve = _rzCast[b].w;
|
|
76
87
|
s.center = _rzCast[b + 1].xyz;
|
|
77
88
|
s.bounds = _rzCast[b + 2];
|
|
78
89
|
return s;
|