reze-engine 0.50.12 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effect-schedule.d.ts +67 -0
- package/dist/effect-schedule.d.ts.map +1 -0
- package/dist/effect-schedule.js +96 -0
- package/dist/engine.d.ts +184 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +500 -96
- package/dist/graph/presets/unlit.d.ts +3 -0
- package/dist/graph/presets/unlit.d.ts.map +1 -0
- package/dist/graph/presets/unlit.js +34 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/shaders/anchor-table.d.ts +1 -1
- package/dist/shaders/anchor-table.js +3 -3
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-api.js +2 -2
- package/dist/shaders/directives.d.ts +73 -0
- package/dist/shaders/directives.d.ts.map +1 -0
- package/dist/shaders/directives.js +238 -0
- package/dist/shaders/lights.d.ts +0 -10
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +11 -19
- package/dist/shaders/lyrics-api.d.ts +11 -2
- package/dist/shaders/lyrics-api.d.ts.map +1 -1
- package/dist/shaders/lyrics-api.js +11 -2
- package/dist/shaders/passes/composite.d.ts +5 -5
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +12 -5
- package/dist/shaders/passes/grid.d.ts +0 -2
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +0 -9
- package/dist/shaders/passes/particles.d.ts +0 -6
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +15 -19
- package/dist/shaders/passes/scene-contract.d.ts +1 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +9 -4
- package/package.json +2 -2
- package/src/effect-schedule.ts +120 -0
- package/src/engine.ts +588 -95
- package/src/graph/presets/unlit.ts +37 -0
- package/src/index.ts +16 -0
- package/src/shaders/anchor-table.ts +3 -3
- package/src/shaders/cast-api.ts +2 -2
- package/src/shaders/directives.ts +289 -0
- package/src/shaders/lights.ts +11 -19
- package/src/shaders/lyrics-api.ts +11 -2
- package/src/shaders/passes/composite.ts +13 -6
- package/src/shaders/passes/grid.ts +0 -9
- package/src/shaders/passes/particles.ts +15 -21
- package/src/shaders/passes/scene-contract.ts +1 -1
- package/src/shaders/passes/trails.ts +9 -4
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
/**
|
|
35
35
|
* The bones an effect asked for, in declaration order — the slots rzAnchor reads.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* #anchor 左手首 trail
|
|
38
|
+
* #anchor 頭
|
|
39
39
|
*
|
|
40
40
|
* A declaration in the source, like the mounts: what a file names is what gets
|
|
41
41
|
* resolved and uploaded, so naming none costs nothing and nobody pays for a
|
|
42
42
|
* rig's other five hundred bones. Anchored to the start of a line so that
|
|
43
|
-
* writing the word
|
|
43
|
+
* writing the word #anchor in ordinary prose does not silently add a slot —
|
|
44
44
|
* which would shift every slot after it.
|
|
45
45
|
*
|
|
46
46
|
* `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
|
|
@@ -64,7 +64,7 @@ type CompositeEffectSource = {
|
|
|
64
64
|
hasBackground: boolean;
|
|
65
65
|
/** Defines `fn foreground(...)` — mount over the finished frame. */
|
|
66
66
|
hasForeground: boolean;
|
|
67
|
-
/** Grid resolution when the effect declared
|
|
67
|
+
/** Grid resolution when the effect declared `#grid`, else 0. */
|
|
68
68
|
gridSize: number;
|
|
69
69
|
/** Whether the scene pass carries the id attachment, so the field module can
|
|
70
70
|
* bind it. False emits accessors that answer 0 rather than nothing at all. */
|
|
@@ -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 /**\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";
|
|
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,
|
|
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,gEAAgE;IAChE,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,CA4EtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -56,13 +56,13 @@ import { gridReadApi } from "./grid";
|
|
|
56
56
|
/**
|
|
57
57
|
* The bones an effect asked for, in declaration order — the slots rzAnchor reads.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* #anchor 左手首 trail
|
|
60
|
+
* #anchor 頭
|
|
61
61
|
*
|
|
62
62
|
* A declaration in the source, like the mounts: what a file names is what gets
|
|
63
63
|
* resolved and uploaded, so naming none costs nothing and nobody pays for a
|
|
64
64
|
* rig's other five hundred bones. Anchored to the start of a line so that
|
|
65
|
-
* writing the word
|
|
65
|
+
* writing the word #anchor in ordinary prose does not silently add a slot —
|
|
66
66
|
* which would shift every slot after it.
|
|
67
67
|
*
|
|
68
68
|
* `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
|
|
@@ -73,7 +73,7 @@ import { gridReadApi } from "./grid";
|
|
|
73
73
|
* not have simply reports invalid.
|
|
74
74
|
*/
|
|
75
75
|
export function parseEffectAnchors(wgsl, max) {
|
|
76
|
-
return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]
|
|
76
|
+
return [...wgsl.matchAll(/^[ \t]*\/\/[ \t]*#anchor[ \t]+(\S+)([ \t]+trail)?[ \t]*$/gm)]
|
|
77
77
|
.map((m) => ({ bone: m[1], trail: m[2] !== undefined }))
|
|
78
78
|
.slice(0, max);
|
|
79
79
|
}
|
|
@@ -691,7 +691,7 @@ export function buildFieldShader(effect) {
|
|
|
691
691
|
* whose lightEmit read its own epoch disagreed with its own background()
|
|
692
692
|
* about what time it was. One buffer per effect, one answer.
|
|
693
693
|
*/
|
|
694
|
-
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f;
|
|
694
|
+
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, _, _)
|
|
695
695
|
|
|
696
696
|
@vertex fn fieldVs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
697
697
|
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
@@ -715,6 +715,13 @@ struct FieldOut {
|
|
|
715
715
|
out.fg = vec4f(0.0);
|
|
716
716
|
${bgLine}
|
|
717
717
|
${fgLine}
|
|
718
|
+
// WEIGHT, applied where the author cannot decline it.
|
|
719
|
+
//
|
|
720
|
+
// Alpha only: both field blends multiply the fragment's colour by src-alpha,
|
|
721
|
+
// so this is the fade for the alpha-over layer and the additive one alike.
|
|
722
|
+
// Scaling colour as well would fade as the square.
|
|
723
|
+
out.bg.a *= _rzFieldClock.y;
|
|
724
|
+
out.fg.a *= _rzFieldClock.y;
|
|
718
725
|
return out;
|
|
719
726
|
}
|
|
720
727
|
`);
|
|
@@ -2,8 +2,6 @@ import { type CastLayout } from "./particles";
|
|
|
2
2
|
export declare const GRID_MAX = 1024;
|
|
3
3
|
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
4
4
|
export declare const SIM_FORMAT: GPUTextureFormat;
|
|
5
|
-
/** `// @grid 256` — the grid's resolution, in texels per side. */
|
|
6
|
-
export declare function parseGridSize(wgsl: string, max: number): number;
|
|
7
5
|
/** Whether this effect drives a grid at all. */
|
|
8
6
|
export declare function gridEntryPoint(wgsl: string): boolean;
|
|
9
7
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,
|
|
1
|
+
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
|
|
@@ -36,15 +36,6 @@ import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
|
36
36
|
export const GRID_MAX = 1024;
|
|
37
37
|
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
38
38
|
export const SIM_FORMAT = "rgba16float";
|
|
39
|
-
/** `// @grid 256` — the grid's resolution, in texels per side. */
|
|
40
|
-
export function parseGridSize(wgsl, max) {
|
|
41
|
-
const m = /^\s*\/\/\s*@grid\s+(\d+)\s*$/m.exec(wgsl);
|
|
42
|
-
if (!m)
|
|
43
|
-
return 0;
|
|
44
|
-
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
45
|
-
// 4096 gets the most the engine will give and a scene that still runs.
|
|
46
|
-
return Math.max(8, Math.min(max, parseInt(m[1], 10)));
|
|
47
|
-
}
|
|
48
39
|
/** Whether this effect drives a grid at all. */
|
|
49
40
|
export function gridEntryPoint(wgsl) {
|
|
50
41
|
return /\bfn\s+gridStep\s*\(/.test(wgsl);
|
|
@@ -26,12 +26,6 @@ type ParticleSource = {
|
|
|
26
26
|
};
|
|
27
27
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
28
28
|
export declare const PARTICLE_STRIDE = 48;
|
|
29
|
-
/** `// @particles 4096` — how many live at once. */
|
|
30
|
-
export declare function parseParticleCount(wgsl: string, max: number): number;
|
|
31
|
-
/** `// @bloom` — opt in to the bloom pyramid. Sparks want it; rain does not. */
|
|
32
|
-
export declare function parseParticleBloom(wgsl: string): boolean;
|
|
33
|
-
/** `// @blend additive` — default is straight alpha. */
|
|
34
|
-
export declare function parseParticleBlend(wgsl: string): ParticleBlend;
|
|
35
29
|
/** Does the source define the particle contract? All three are required together. */
|
|
36
30
|
export declare function particleEntryPoints(wgsl: string): {
|
|
37
31
|
init: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAuDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAyHvF"}
|
|
@@ -51,6 +51,11 @@ struct ParticleU {
|
|
|
51
51
|
dt: f32,
|
|
52
52
|
count: u32,
|
|
53
53
|
frame: u32,
|
|
54
|
+
/** The effect's evaluated influence, applied at the one output site below. */
|
|
55
|
+
weight: f32,
|
|
56
|
+
_pad0: f32,
|
|
57
|
+
_pad1: f32,
|
|
58
|
+
_pad2: f32,
|
|
54
59
|
}
|
|
55
60
|
`;
|
|
56
61
|
/**
|
|
@@ -81,24 +86,6 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
81
86
|
}
|
|
82
87
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
83
88
|
`;
|
|
84
|
-
/** `// @particles 4096` — how many live at once. */
|
|
85
|
-
export function parseParticleCount(wgsl, max) {
|
|
86
|
-
const m = /^\s*\/\/\s*@particles\s+(\d+)\s*$/m.exec(wgsl);
|
|
87
|
-
if (!m)
|
|
88
|
-
return 0;
|
|
89
|
-
// Clamped rather than rejected: an author asking for a million gets the most
|
|
90
|
-
// the engine will give and a scene that still runs, which is a better failure
|
|
91
|
-
// than a compile error naming a number they had no way to know.
|
|
92
|
-
return Math.max(1, Math.min(max, parseInt(m[1], 10)));
|
|
93
|
-
}
|
|
94
|
-
/** `// @bloom` — opt in to the bloom pyramid. Sparks want it; rain does not. */
|
|
95
|
-
export function parseParticleBloom(wgsl) {
|
|
96
|
-
return /^\s*\/\/\s*@bloom\s*$/m.test(wgsl);
|
|
97
|
-
}
|
|
98
|
-
/** `// @blend additive` — default is straight alpha. */
|
|
99
|
-
export function parseParticleBlend(wgsl) {
|
|
100
|
-
return /^\s*\/\/\s*@blend\s+additive\s*$/m.test(wgsl) ? "additive" : "alpha";
|
|
101
|
-
}
|
|
102
89
|
/** Does the source define the particle contract? All three are required together. */
|
|
103
90
|
export function particleEntryPoints(wgsl) {
|
|
104
91
|
return {
|
|
@@ -256,7 +243,16 @@ ${sceneIdFieldWgsl()}}
|
|
|
256
243
|
@fragment
|
|
257
244
|
fn fs(in: VSOut) -> FSOut {
|
|
258
245
|
let p = particles[in.id];
|
|
259
|
-
|
|
246
|
+
// WEIGHT IS APPLIED HERE, on the author's result, before anything reads it.
|
|
247
|
+
//
|
|
248
|
+
// Alpha alone, and that is not a shortcut: this fragment is premultiplied
|
|
249
|
+
// two lines down, so scaling alpha scales the colour with it — and where the
|
|
250
|
+
// target blends additively the premultiply is what carries the fade. One
|
|
251
|
+
// multiply is the correct fade for both particle targets.
|
|
252
|
+
var c = particleShade(p, in.uv);
|
|
253
|
+
c.a *= pu.weight;
|
|
254
|
+
// Weight 0 therefore discards every fragment, so an effect faded out costs
|
|
255
|
+
// nothing past the vertex stage even on the frame the draw is still issued.
|
|
260
256
|
if (c.a <= 0.0) { discard; }
|
|
261
257
|
var out: FSOut;
|
|
262
258
|
// PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
|
|
@@ -61,7 +61,7 @@ type SceneRenderClass =
|
|
|
61
61
|
| "outline"
|
|
62
62
|
/** Particles and ribbons in their default, non-additive mode. */
|
|
63
63
|
| "particle"
|
|
64
|
-
/** Particles declaring
|
|
64
|
+
/** Particles declaring `#blend additive` — LIGHT rather than matter, so
|
|
65
65
|
* colour sums and alpha is left alone: a glow must not claim coverage it
|
|
66
66
|
* never occluded. The aux target sums with it, which is what lets an
|
|
67
67
|
* additive effect reach the bloom gate at all. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,
|
|
1
|
+
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6UR"}
|
|
@@ -83,7 +83,8 @@ struct TrailU {
|
|
|
83
83
|
// model is added or removed, and recompiling every trail shader for that
|
|
84
84
|
// would be absurd.
|
|
85
85
|
subjects: f32,
|
|
86
|
-
|
|
86
|
+
/** The effect's evaluated influence, applied at the one output site below. */
|
|
87
|
+
weight: f32,
|
|
87
88
|
_pad2: f32,
|
|
88
89
|
}
|
|
89
90
|
@group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
|
|
@@ -353,15 +354,19 @@ fn fs(in: VSOut) -> TrailFSOut {
|
|
|
353
354
|
// hardware does it, correctly and for free, and the reversed-Z trap that
|
|
354
355
|
// needed its own regression test goes with it.
|
|
355
356
|
let c = trailShade(in.uv.x, in.uv.y, in.age, in.weight, i32(in.slot));
|
|
356
|
-
|
|
357
|
+
// THE EFFECT'S weight, not the ribbon's — in.weight above is the strand's
|
|
358
|
+
// own taper and belongs to the author. This one is the scheduler's, and it
|
|
359
|
+
// scales alpha because the target below multiplies colour by src-alpha.
|
|
360
|
+
let a = c.a * tu.weight;
|
|
361
|
+
if (a <= 0.0) { discard; }
|
|
357
362
|
var o: TrailFSOut;
|
|
358
363
|
// STRAIGHT colour into an ADDITIVE target, which reverses the old MAX rule
|
|
359
364
|
// deliberately: max existed so parallel strands could not double into bright
|
|
360
365
|
// dashes on a layer composited after tone mapping. In HDR before bloom,
|
|
361
366
|
// overlapping light SHOULD sum — that is what neon does — and the tone
|
|
362
367
|
// mapper is what keeps the sum from clipping.
|
|
363
|
-
o.color = vec4f(c.rgb,
|
|
364
|
-
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0,
|
|
368
|
+
o.color = vec4f(c.rgb, a);
|
|
369
|
+
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, a);
|
|
365
370
|
${sceneIdPadWgsl("o")} return o;
|
|
366
371
|
}
|
|
367
372
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reze-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"@types/node": "^20",
|
|
42
42
|
"typescript": "^5"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHEN an effect is on, and how much of it.
|
|
3
|
+
*
|
|
4
|
+
* The engine evaluates this, not the app, and that is not an arbitrary split.
|
|
5
|
+
* Everything time-driven that lives above the engine has to be ticked by every
|
|
6
|
+
* loop that renders — playback, offline export, a warm-up pass — and the export
|
|
7
|
+
* loop already carries a scar about it: a score-driven effect once exported a
|
|
8
|
+
* still keyboard while the rest of the scene animated, because that loop
|
|
9
|
+
* advanced the audio clock and forgot the score's. A schedule evaluated where
|
|
10
|
+
* the scene clock advances cannot be forgotten by anyone.
|
|
11
|
+
*
|
|
12
|
+
* It is also where the three references put it. Unity's Timeline evaluates clip
|
|
13
|
+
* ease in its playable graph, Unreal's Sequencer evaluates section easing, and
|
|
14
|
+
* Blender's NLA evaluates strip influence — in all three the editor only edits
|
|
15
|
+
* the data. This is that.
|
|
16
|
+
*
|
|
17
|
+
* SECONDS, because that is the engine's clock. The document above works in
|
|
18
|
+
* MMD's 30fps frames and converts on the way in; nothing here knows the render
|
|
19
|
+
* rate, which is what lets a 60Hz preview and a 30 or 60fps export agree about
|
|
20
|
+
* which beat an effect lands on.
|
|
21
|
+
*
|
|
22
|
+
* The model is Blender's NLA strip, minus what needs a strip STACK: no
|
|
23
|
+
* extrapolation modes (outside a window is off, always), no auto-blend (it
|
|
24
|
+
* derives from neighbouring strips, and effects have a lane each), no repeat or
|
|
25
|
+
* scale (an author who wants a cycle can wrap their own clock).
|
|
26
|
+
*
|
|
27
|
+
* PURE, in its own file, for the reason param-track.ts is: everything else in
|
|
28
|
+
* this feature needs a GPU and this needs one number, so the edge cases can be
|
|
29
|
+
* tested exhaustively. They are where it goes wrong, and a wrong one is a beat
|
|
30
|
+
* missed in a video someone posts.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** One strip on an effect's LANE — and a lane holds as many as you place.
|
|
34
|
+
* Seconds throughout. */
|
|
35
|
+
export type EffectWindow = {
|
|
36
|
+
/** When it comes alive, and where its own clock reads zero. */
|
|
37
|
+
start: number
|
|
38
|
+
/** When it stops. Omitted = it runs to the end of the scene. */
|
|
39
|
+
end?: number
|
|
40
|
+
/** Ramp up over this long from `start`. Omitted or 0 is a HARD CUT, which is
|
|
41
|
+
* the right answer for a flash and the wrong one for a glow. */
|
|
42
|
+
blendIn?: number
|
|
43
|
+
/** Ramp down over this long back from `end`. Needs an `end` to measure from:
|
|
44
|
+
* an effect with no end has nothing to ramp toward. */
|
|
45
|
+
blendOut?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What an effect is doing at one instant: a weight for its mounts, and where
|
|
49
|
+
* its own clock has reached. */
|
|
50
|
+
export type EffectState = { weight: number; time: number }
|
|
51
|
+
|
|
52
|
+
/** Off, and stopped at its own beginning. */
|
|
53
|
+
const SILENT: EffectState = { weight: 0, time: 0 }
|
|
54
|
+
|
|
55
|
+
const clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : Number.isFinite(n) ? n : 0)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One effect's state at one moment of the scene.
|
|
59
|
+
*
|
|
60
|
+
* `influence` is the level it reaches INSIDE the window — Blender's word and
|
|
61
|
+
* Blender's meaning. The blends ramp toward it rather than toward 1, so a
|
|
62
|
+
* permanently half-strength effect and a scheduled one are the same dial
|
|
63
|
+
* instead of two that can disagree.
|
|
64
|
+
*
|
|
65
|
+
* LINEAR ramps, matching the NLA's. A curve is a better default for light and a
|
|
66
|
+
* worse one for everything else, and there is no control to shape it with — a
|
|
67
|
+
* curve nobody asked for is worse than a plain one. If eased blends are ever
|
|
68
|
+
* wanted they belong as a property ON the window, which is the same call
|
|
69
|
+
* param-track.ts made about bezier keys.
|
|
70
|
+
*/
|
|
71
|
+
export function effectState(
|
|
72
|
+
windows: readonly EffectWindow[] | null,
|
|
73
|
+
influence: number,
|
|
74
|
+
sceneTime: number,
|
|
75
|
+
): EffectState {
|
|
76
|
+
const level = clamp01(influence)
|
|
77
|
+
// UNSCHEDULED RUNS WITH THE SCENE, on the scene's own clock — so an effect
|
|
78
|
+
// nobody has scheduled behaves exactly as it did before windows existed.
|
|
79
|
+
if (!windows || windows.length === 0) return { weight: level, time: sceneTime }
|
|
80
|
+
|
|
81
|
+
// A LANE HOLDS MANY STRIPS, which is what lets one effect fire more than
|
|
82
|
+
// once. Each entry restarts the effect's own clock at its own start, so a hit
|
|
83
|
+
// placed at bar 8 and again at bar 24 plays its opening both times rather
|
|
84
|
+
// than resuming halfway through itself.
|
|
85
|
+
//
|
|
86
|
+
// The LATEST start that contains the time wins. Strips on one lane are not
|
|
87
|
+
// meant to overlap — that is the rule every NLE enforces within a track, and
|
|
88
|
+
// it keeps "which strip am I in" a question with one answer — but a document
|
|
89
|
+
// can be hand-edited, and "the one most recently entered" is the reading that
|
|
90
|
+
// matches what you would see if they were laid down in order.
|
|
91
|
+
let window: EffectWindow | null = null
|
|
92
|
+
for (const w of windows) {
|
|
93
|
+
if (sceneTime < w.start) continue
|
|
94
|
+
if (w.end !== undefined && (w.end <= w.start || sceneTime > w.end)) continue
|
|
95
|
+
if (!window || w.start > window.start) window = w
|
|
96
|
+
}
|
|
97
|
+
if (!window) return SILENT
|
|
98
|
+
|
|
99
|
+
const { start, end } = window
|
|
100
|
+
// The picker above already rejected an empty strip (an end at or before the
|
|
101
|
+
// start, reachable by dragging one edge past the other) and anything the
|
|
102
|
+
// time is outside of, so from here the strip is live and has a real length.
|
|
103
|
+
|
|
104
|
+
// ITS OWN CLOCK, from its entry. This is the reason the window owns the clock
|
|
105
|
+
// rather than being a visibility flag beside it: an effect entering at bar 33
|
|
106
|
+
// should play its own opening, not join the scene four minutes in.
|
|
107
|
+
const time = sceneTime - start
|
|
108
|
+
|
|
109
|
+
let ramp = 1
|
|
110
|
+
const fadeIn = window.blendIn ?? 0
|
|
111
|
+
if (fadeIn > 0) ramp = Math.min(ramp, time / fadeIn)
|
|
112
|
+
const fadeOut = end !== undefined ? (window.blendOut ?? 0) : 0
|
|
113
|
+
if (fadeOut > 0) ramp = Math.min(ramp, (end! - sceneTime) / fadeOut)
|
|
114
|
+
|
|
115
|
+
// The MINIMUM of the two ramps, so blends that overlap degrade instead of
|
|
116
|
+
// break: set both longer than the strip and it becomes a triangle — dimmer
|
|
117
|
+
// than asked for, still smooth, never out of range. Multiplying them would
|
|
118
|
+
// dip toward zero in the middle of a short strip, which is not a fade.
|
|
119
|
+
return { weight: level * clamp01(ramp), time }
|
|
120
|
+
}
|