@wave3d/core 0.8.0 → 0.9.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/README.md +3 -1
- package/dist/config/model.d.ts +48 -2
- package/dist/config/model.js +8 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/renderer/WaveRenderer.d.ts +142 -10
- package/dist/renderer/WaveRenderer.js +209 -53
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +291 -0
- package/dist/renderer/WaveRendererGPU.js.map +1 -0
- package/dist/renderer/gpu-loader.js +2 -0
- package/dist/renderer/index.d.ts +3 -2
- package/dist/renderer/interaction.d.ts +29 -0
- package/dist/renderer/interaction.js +77 -31
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/interactionGates.js +59 -0
- package/dist/renderer/interactionGates.js.map +1 -0
- package/dist/renderer/particleField.d.ts +1 -59
- package/dist/renderer/particleField.js +49 -29
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +141 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -0
- package/dist/renderer/tilt.d.ts +17 -0
- package/dist/renderer/tilt.js +124 -0
- package/dist/renderer/tilt.js.map +1 -0
- package/dist/renderer/tsl/color.js +129 -0
- package/dist/renderer/tsl/color.js.map +1 -0
- package/dist/renderer/tsl/noise.js +83 -0
- package/dist/renderer/tsl/noise.js.map +1 -0
- package/dist/renderer/tsl/packedArray.js +76 -0
- package/dist/renderer/tsl/packedArray.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +133 -0
- package/dist/renderer/tsl/particleMaterial.js.map +1 -0
- package/dist/renderer/tsl/particleUniforms.js +44 -0
- package/dist/renderer/tsl/particleUniforms.js.map +1 -0
- package/dist/renderer/tsl/pointerField.js +73 -0
- package/dist/renderer/tsl/pointerField.js.map +1 -0
- package/dist/renderer/tsl/post.js +63 -0
- package/dist/renderer/tsl/post.js.map +1 -0
- package/dist/renderer/tsl/postChain.js +56 -0
- package/dist/renderer/tsl/postChain.js.map +1 -0
- package/dist/renderer/tsl/postEffects.js +221 -0
- package/dist/renderer/tsl/postEffects.js.map +1 -0
- package/dist/renderer/tsl/types.js +28 -0
- package/dist/renderer/tsl/types.js.map +1 -0
- package/dist/renderer/tsl/uniforms.js +152 -0
- package/dist/renderer/tsl/uniforms.js.map +1 -0
- package/dist/renderer/tsl/waveMaterial.js +185 -0
- package/dist/renderer/tsl/waveMaterial.js.map +1 -0
- package/dist/renderer/tsl/waveShape.js +108 -0
- package/dist/renderer/tsl/waveShape.js.map +1 -0
- package/dist/shell/createWave.d.ts +29 -0
- package/dist/shell/createWave.js +39 -11
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.js +18 -1
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +1811 -1594
- package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
- package/dist/standalone.d.ts +10 -3
- package/dist/standalone.js +10 -3
- package/dist/standalone.js.map +1 -1
- package/dist/studio/StudioWaveRenderer.d.ts +4 -0
- package/dist/studio/StudioWaveRenderer.js +9 -0
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/StudioWaveRendererGPU.js +18 -0
- package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
- package/dist/studio/index.d.ts +12 -2
- package/dist/studio/index.js +14 -1
- package/dist/studio/index.js.map +1 -0
- package/package.json +12 -3
- package/skills/wave3d/SKILL.md +29 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Vector4 } from "three";
|
|
2
|
+
import { int, uniformArray } from "three/tsl";
|
|
3
|
+
//#region src/renderer/tsl/packedArray.ts
|
|
4
|
+
/**
|
|
5
|
+
* One uniform buffer holding every array-valued uniform.
|
|
6
|
+
*
|
|
7
|
+
* WebGPU caps `maxUniformBuffersPerShaderStage` at 12 by default, and TSL gives each
|
|
8
|
+
* `uniformArray()` its OWN buffer. The wave's fragment stage needs eleven logical arrays (palette
|
|
9
|
+
* stops, mesh points, lights, noise bands) which — with the scalar block and the pass bindings —
|
|
10
|
+
* overruns that limit and fails pipeline creation outright:
|
|
11
|
+
*
|
|
12
|
+
* GPUValidationError: The number of uniform buffers (13) in the Fragment stage exceeds the
|
|
13
|
+
* maximum per-stage limit (12).
|
|
14
|
+
*
|
|
15
|
+
* So all of them share a single `vec4` array, each logical array being a view over a slot range and
|
|
16
|
+
* a component mask. The arrays are tiny (8/8/8/4 entries), so the whole thing is ~52 vec4s — well
|
|
17
|
+
* inside any device's minimum buffer size.
|
|
18
|
+
*
|
|
19
|
+
* Crucially this is invisible above: each view still exposes the plain `value` array the renderer
|
|
20
|
+
* writes (`u.uColors.value[i].set(...)`), and {@link PackedArrays.sync} folds those into the shared
|
|
21
|
+
* slots once per frame. That keeps the ~116 config-sync writes in `refresh()` backend-agnostic.
|
|
22
|
+
*/
|
|
23
|
+
var PackedArrays = class {
|
|
24
|
+
slots = [];
|
|
25
|
+
views = [];
|
|
26
|
+
node;
|
|
27
|
+
/** Reserve `count` slots and return their base index. Views over the same base share slots. */
|
|
28
|
+
reserve(count) {
|
|
29
|
+
const base = this.slots.length;
|
|
30
|
+
for (let i = 0; i < count; i++) this.slots.push(new Vector4());
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
element(base, i) {
|
|
34
|
+
this.node ??= uniformArray(this.slots, "vec4");
|
|
35
|
+
return this.node.element(int(i).add(base));
|
|
36
|
+
}
|
|
37
|
+
view(base, count, mask, make, write) {
|
|
38
|
+
const value = Array.from({ length: count }, (_, i) => make(i));
|
|
39
|
+
this.views.push({ write: (slots) => {
|
|
40
|
+
for (let i = 0; i < count; i++) write(slots[base + i], value[i]);
|
|
41
|
+
} });
|
|
42
|
+
return {
|
|
43
|
+
value,
|
|
44
|
+
el: (i) => {
|
|
45
|
+
const slot = this.element(base, i);
|
|
46
|
+
return mask === "xyzw" ? slot : slot[mask];
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** A `vec3` view occupying `.xyz` of its slots. */
|
|
51
|
+
vec3(base, count, make) {
|
|
52
|
+
return this.view(base, count, "xyz", make, (s, v) => s.set(v.x, v.y, v.z, s.w));
|
|
53
|
+
}
|
|
54
|
+
/** A `vec2` view occupying `.xy` of its slots. */
|
|
55
|
+
vec2(base, count, make) {
|
|
56
|
+
return this.view(base, count, "xy", make, (s, v) => s.set(v.x, v.y, s.z, s.w));
|
|
57
|
+
}
|
|
58
|
+
/** A `vec4` view occupying whole slots. */
|
|
59
|
+
vec4(base, count, make) {
|
|
60
|
+
return this.view(base, count, "xyzw", make, (s, v) => s.copy(v));
|
|
61
|
+
}
|
|
62
|
+
/** A scalar view occupying one named component of its slots. */
|
|
63
|
+
scalar(base, count, make, component) {
|
|
64
|
+
return this.view(base, count, component, make, (s, v) => {
|
|
65
|
+
s[component] = v;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Fold every view's `value` array into the shared slots. Called once per frame before drawing. */
|
|
69
|
+
sync() {
|
|
70
|
+
for (const v of this.views) v.write(this.slots);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { PackedArrays };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=packedArray.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packedArray.js","names":[],"sources":["../../../src/renderer/tsl/packedArray.ts"],"sourcesContent":["/**\n * One uniform buffer holding every array-valued uniform.\n *\n * WebGPU caps `maxUniformBuffersPerShaderStage` at 12 by default, and TSL gives each\n * `uniformArray()` its OWN buffer. The wave's fragment stage needs eleven logical arrays (palette\n * stops, mesh points, lights, noise bands) which — with the scalar block and the pass bindings —\n * overruns that limit and fails pipeline creation outright:\n *\n * GPUValidationError: The number of uniform buffers (13) in the Fragment stage exceeds the\n * maximum per-stage limit (12).\n *\n * So all of them share a single `vec4` array, each logical array being a view over a slot range and\n * a component mask. The arrays are tiny (8/8/8/4 entries), so the whole thing is ~52 vec4s — well\n * inside any device's minimum buffer size.\n *\n * Crucially this is invisible above: each view still exposes the plain `value` array the renderer\n * writes (`u.uColors.value[i].set(...)`), and {@link PackedArrays.sync} folds those into the shared\n * slots once per frame. That keeps the ~116 config-sync writes in `refresh()` backend-agnostic.\n */\nimport { Vector4 } from \"three\";\nimport { uniformArray, int } from \"three/tsl\";\nimport type { FloatNode, Vec2Node, Vec3Node, Vec4Node } from \"./types\";\n\n/** Which components of each slot a view occupies. Views sharing a slot must not overlap. */\ntype Mask = \"x\" | \"y\" | \"z\" | \"w\" | \"xy\" | \"xyz\" | \"xyzw\";\n\n/** A single component, for scalar views. */\nexport type Component = \"x\" | \"y\" | \"z\" | \"w\";\n\ninterface Writable {\n write(slots: Vector4[]): void;\n}\n\n/** A view over a slot range: `value` is what the renderer mutates, `el(i)` is graph access. */\nexport interface PackedView<TValue, TNode> {\n value: TValue[];\n el: (i: unknown) => TNode;\n}\n\nexport class PackedArrays {\n private readonly slots: Vector4[] = [];\n private readonly views: Writable[] = [];\n private node?: ReturnType<typeof uniformArray>;\n\n /** Reserve `count` slots and return their base index. Views over the same base share slots. */\n reserve(count: number): number {\n const base = this.slots.length;\n for (let i = 0; i < count; i++) this.slots.push(new Vector4());\n return base;\n }\n\n private element(base: number, i: unknown) {\n this.node ??= uniformArray(this.slots as never, \"vec4\");\n return this.node.element(int(i as never).add(base) as never);\n }\n\n private view<TValue, TNode>(\n base: number,\n count: number,\n mask: Mask,\n make: (i: number) => TValue,\n write: (slot: Vector4, v: TValue) => void,\n ): PackedView<TValue, TNode> {\n const value = Array.from({ length: count }, (_, i) => make(i));\n this.views.push({\n write: (slots) => {\n for (let i = 0; i < count; i++) write(slots[base + i], value[i]);\n },\n });\n return {\n value,\n el: (i) => {\n // Swizzling by a computed key is beyond what @types/three declares for the element node,\n // so the mask is applied through an indexed lookup — the runtime proxy handles it.\n const slot = this.element(base, i) as unknown as Record<string, unknown>;\n return (mask === \"xyzw\" ? slot : slot[mask]) as TNode;\n },\n };\n }\n\n /** A `vec3` view occupying `.xyz` of its slots. */\n vec3(base: number, count: number, make: (i: number) => Vec3Like) {\n return this.view<Vec3Like, Vec3Node>(base, count, \"xyz\", make, (s, v) =>\n s.set(v.x, v.y, v.z, s.w),\n );\n }\n\n /** A `vec2` view occupying `.xy` of its slots. */\n vec2(base: number, count: number, make: (i: number) => Vec2Like) {\n return this.view<Vec2Like, Vec2Node>(base, count, \"xy\", make, (s, v) =>\n s.set(v.x, v.y, s.z, s.w),\n );\n }\n\n /** A `vec4` view occupying whole slots. */\n vec4(base: number, count: number, make: (i: number) => Vector4) {\n return this.view<Vector4, Vec4Node>(base, count, \"xyzw\", make, (s, v) => s.copy(v));\n }\n\n /** A scalar view occupying one named component of its slots. */\n scalar(base: number, count: number, make: (i: number) => number, component: Component) {\n return this.view<number, FloatNode>(base, count, component, make, (s, v) => {\n s[component] = v;\n });\n }\n\n /** Fold every view's `value` array into the shared slots. Called once per frame before drawing. */\n sync(): void {\n for (const v of this.views) v.write(this.slots);\n }\n}\n\ninterface Vec2Like {\n x: number;\n y: number;\n}\ninterface Vec3Like {\n x: number;\n y: number;\n z: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,eAAb,MAA0B;CACxB,QAAoC,CAAC;CACrC,QAAqC,CAAC;CACtC;;CAGA,QAAQ,OAAuB;EAC7B,MAAM,OAAO,KAAK,MAAM;EACxB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC;EAC7D,OAAO;CACT;CAEA,QAAgB,MAAc,GAAY;EACxC,KAAK,SAAS,aAAa,KAAK,OAAgB,MAAM;EACtD,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAU,CAAC,CAAC,IAAI,IAAI,CAAU;CAC7D;CAEA,KACE,MACA,OACA,MACA,MACA,OAC2B;EAC3B,MAAM,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC;EAC7D,KAAK,MAAM,KAAK,EACd,QAAQ,UAAU;GAChB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,MAAM,MAAM,OAAO,IAAI,MAAM,EAAE;EACjE,EACF,CAAC;EACD,OAAO;GACL;GACA,KAAK,MAAM;IAGT,MAAM,OAAO,KAAK,QAAQ,MAAM,CAAC;IACjC,OAAQ,SAAS,SAAS,OAAO,KAAK;GACxC;EACF;CACF;;CAGA,KAAK,MAAc,OAAe,MAA+B;EAC/D,OAAO,KAAK,KAAyB,MAAM,OAAO,OAAO,OAAO,GAAG,MACjE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAC1B;CACF;;CAGA,KAAK,MAAc,OAAe,MAA+B;EAC/D,OAAO,KAAK,KAAyB,MAAM,OAAO,MAAM,OAAO,GAAG,MAChE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAC1B;CACF;;CAGA,KAAK,MAAc,OAAe,MAA8B;EAC9D,OAAO,KAAK,KAAwB,MAAM,OAAO,QAAQ,OAAO,GAAG,MAAM,EAAE,KAAK,CAAC,CAAC;CACpF;;CAGA,OAAO,MAAc,OAAe,MAA6B,WAAsB;EACrF,OAAO,KAAK,KAAwB,MAAM,OAAO,WAAW,OAAO,GAAG,MAAM;GAC1E,EAAE,aAAa;EACjB,CAAC;CACH;;CAGA,OAAa;EACX,KAAK,MAAM,KAAK,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK;CAChD;AACF"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { simplexNoise } from "./noise.js";
|
|
2
|
+
import { applyTwist, waveShape } from "./waveShape.js";
|
|
3
|
+
import { pointerField } from "./pointerField.js";
|
|
4
|
+
import { PointsNodeMaterial } from "three/webgpu";
|
|
5
|
+
import { Fn, If, abs, atan, cameraProjectionMatrix, cameraViewMatrix, clamp, cos, cross, dot, exp, float, floor, fract, instanceIndex, instancedArray, length, max, mix, normalize, pow, select, sin, smoothstep, texture, uv, varying, vec2, vec3, vec4 } from "three/tsl";
|
|
6
|
+
//#region src/renderer/tsl/particleMaterial.ts
|
|
7
|
+
/**
|
|
8
|
+
* The particle field's material in TSL — the port of `particleVertexShader` /
|
|
9
|
+
* `particleFragmentShader` in `../shaders.ts`.
|
|
10
|
+
*
|
|
11
|
+
* The one structural change the backend forces: WebGPU point primitives are fixed at ONE pixel, so
|
|
12
|
+
* a `THREE.Points` sized through `gl_PointSize` cannot work. three's own `PointsNodeMaterial`
|
|
13
|
+
* documents that a size is honoured only when the material is attached to a `Sprite`, which draws
|
|
14
|
+
* the field as instanced quads instead. That costs 4 vertices and 2 triangles per particle where
|
|
15
|
+
* the point list needed 1 vertex, and it changes two things in the shader:
|
|
16
|
+
*
|
|
17
|
+
* - `gl_PointSize` becomes `sizeNode`, which three multiplies by the device pixel ratio itself —
|
|
18
|
+
* so the GLSL's explicit `uPixelRatio` factor must NOT be repeated here or size is squared.
|
|
19
|
+
* - `gl_PointCoord` becomes `uv()`. Their Y axes are opposite (`gl_PointCoord` runs DOWN from the
|
|
20
|
+
* top-left), which the GLSL already had to flip for sprite artwork. Every procedural shape but
|
|
21
|
+
* "streak" is symmetric about Y, so getting this wrong would show up only on that one.
|
|
22
|
+
*
|
|
23
|
+
* Everything else — the deterministic life, the shed emitter riding the shared wave deform, and the
|
|
24
|
+
* pointer weld/shove — is the same graph the ribbon uses, reading the OWNING WAVE's uniform
|
|
25
|
+
* registry directly rather than mirroring values across two materials.
|
|
26
|
+
*/
|
|
27
|
+
const TAU = 6.28318530718;
|
|
28
|
+
/** Alpha for the procedural shapes, indexed by `uShape` (0 glitter, 1 soft, 2 ring, 3 star, 4 streak). */
|
|
29
|
+
function shapeAlpha(shape, pc, dir) {
|
|
30
|
+
const d = length(pc).toVar("pcD");
|
|
31
|
+
const glitter = smoothstep(.5, 0, d);
|
|
32
|
+
const soft = exp(d.mul(d).mul(-7));
|
|
33
|
+
const ring = smoothstep(.09, 0, abs(d.sub(.34)));
|
|
34
|
+
const spike = pow(abs(cos(atan(pc.y, pc.x).mul(2))), 6);
|
|
35
|
+
const star = smoothstep(1, 0, d.div(spike.mul(.5).add(.14)));
|
|
36
|
+
const along = dot(pc, dir);
|
|
37
|
+
const perp = dot(pc, vec2(dir.y.negate(), dir.x));
|
|
38
|
+
const streak = smoothstep(.5, 0, length(vec2(along.mul(.42), perp.mul(2.2))));
|
|
39
|
+
const s = floor(shape.add(.5)).toVar("shapeIdx");
|
|
40
|
+
return select(s.equal(1), soft, select(s.equal(2), ring, select(s.equal(3), star, select(s.equal(4), streak, glitter))));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build one field's material.
|
|
44
|
+
*
|
|
45
|
+
* `wave` is the OWNING wave's uniform registry: the shed emitter and the pointer weld read it
|
|
46
|
+
* directly, so the dust and the ribbon are driven by one set of nodes.
|
|
47
|
+
*/
|
|
48
|
+
function buildParticleMaterial(wave, u, attrs, flags, spriteTexture) {
|
|
49
|
+
const aSeed = instancedArray(attrs.aSeed, "float").element(instanceIndex);
|
|
50
|
+
const aRnd = instancedArray(attrs.aRnd, "vec4").element(instanceIndex);
|
|
51
|
+
const aUv = instancedArray(attrs.aUv, "vec2").element(instanceIndex);
|
|
52
|
+
const material = new PointsNodeMaterial();
|
|
53
|
+
material.transparent = true;
|
|
54
|
+
material.depthTest = false;
|
|
55
|
+
material.depthWrite = false;
|
|
56
|
+
material.blending = 2;
|
|
57
|
+
material.sizeAttenuation = false;
|
|
58
|
+
const cyc = max(1, floor(u.uPartSpeed.add(.5)));
|
|
59
|
+
const age = fract(select(u.uLoopSeconds.greaterThan(0), u.uTime.div(u.uLoopSeconds).mul(cyc), u.uTime.mul(u.uPartSpeed).div(max(u.uLife, .001))).add(aSeed));
|
|
60
|
+
const fade = sin(age.mul(Math.PI));
|
|
61
|
+
const jitter = float(1).add(u.uSizeJitter.mul(aSeed.sub(.5)).mul(2));
|
|
62
|
+
material.sizeNode = max(u.uSize.mul(jitter).mul(fade), 0);
|
|
63
|
+
const ts = flags.loopMotion ? float(0) : u.uTime.mul(u.uShedSpeed).add(u.uShedSeed);
|
|
64
|
+
const loopTheta = u.uTime.mul(TAU).div(u.uLoopSeconds).add(u.uShedSeed);
|
|
65
|
+
const loopR = u.uShedSpeed.mul(u.uLoopSeconds).mul(.159154943092);
|
|
66
|
+
const loopOff = flags.loopMotion ? vec2(cos(loopTheta), sin(loopTheta)).mul(loopR) : vec2(0, 0);
|
|
67
|
+
material.positionNode = Fn(() => {
|
|
68
|
+
const ws = waveShape(wave, flags, vec3(aUv.y.sub(.5).mul(400), 0, -8), aUv, ts, loopOff);
|
|
69
|
+
const origin = u.uShedModel.mul(vec4(ws.pos, 1)).xyz.toVar("origin");
|
|
70
|
+
const outward = normalize(origin.sub(u.uCenter).add(vec3(1e-4))).toVar("outward");
|
|
71
|
+
const p = origin.add(outward.mul(age).mul(u.uDrift)).add(aRnd.xyz.sub(.5).mul(age).mul(u.uDrift).mul(.35)).toVar("p");
|
|
72
|
+
p.addAssign(u.uUp.mul(age).mul(u.uRise));
|
|
73
|
+
const span = flags.pointerFx ? abs(u.uDrift).mul(1.35).add(abs(u.uRise)).add(u.uWander).toVar("span") : null;
|
|
74
|
+
If(u.uSwirl.notEqual(0), () => {
|
|
75
|
+
const nrm = cross(u.uRight, u.uUp);
|
|
76
|
+
const rel = p.sub(u.uCenter).toVar("rel");
|
|
77
|
+
if (span) span.addAssign(abs(u.uSwirl).mul(TAU).mul(length(rel)));
|
|
78
|
+
const rx = dot(rel, u.uRight);
|
|
79
|
+
const ry = dot(rel, u.uUp);
|
|
80
|
+
const rz = dot(rel, nrm);
|
|
81
|
+
const a = age.mul(u.uSwirl).mul(TAU);
|
|
82
|
+
const ca = cos(a);
|
|
83
|
+
const sa = sin(a);
|
|
84
|
+
p.assign(u.uCenter.add(u.uRight.mul(rx.mul(ca).sub(ry.mul(sa)))).add(u.uUp.mul(rx.mul(sa).add(ry.mul(ca)))).add(nrm.mul(rz)));
|
|
85
|
+
});
|
|
86
|
+
If(u.uWander.notEqual(0), () => {
|
|
87
|
+
const wan = vec2(simplexNoise(vec2(aSeed.mul(17), age.mul(3))), simplexNoise(vec2(age.mul(3), aSeed.mul(23))));
|
|
88
|
+
p.addAssign(u.uRight.mul(wan.x).add(u.uUp.mul(wan.y)).mul(u.uWander));
|
|
89
|
+
});
|
|
90
|
+
if (flags.pointerFx && span) {
|
|
91
|
+
const pMvp = cameraProjectionMatrix.mul(cameraViewMatrix).mul(u.uShedModel).toVar("pMvp");
|
|
92
|
+
const originClip = pMvp.mul(vec4(ws.pos, 1)).toVar("originClip");
|
|
93
|
+
const localAxis = applyTwist(applyTwist(applyTwist(vec3(0, 1, 0), ws.twists[0]), ws.twists[1]), ws.twists[2]);
|
|
94
|
+
const dispAxis = u.uShedModel.mul(vec4(localAxis, 0)).xyz.toVar("dispAxis");
|
|
95
|
+
const opts = {
|
|
96
|
+
loopMotion: flags.loopMotion,
|
|
97
|
+
ripples: flags.pointerRipples
|
|
98
|
+
};
|
|
99
|
+
const weld = pointerField(wave, opts, originClip.xy.div(max(originClip.w, 1e-6)), pMvp, ws.twists, ws.pos, ts, loopOff);
|
|
100
|
+
const attach = select(span.greaterThan(1e-4), float(1).sub(clamp(length(p.sub(origin)).div(span), 0, 1)), float(1)).toVar("attach");
|
|
101
|
+
p.addAssign(dispAxis.mul(weld.disp.mul(attach)));
|
|
102
|
+
If(u.uPartShove.notEqual(0), () => {
|
|
103
|
+
const pClip = cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(p, 1)).toVar("pClip");
|
|
104
|
+
const shove = pointerField(wave, opts, pClip.xy.div(max(pClip.w, 1e-6)), pMvp, ws.twists, ws.pos, ts, loopOff);
|
|
105
|
+
p.addAssign(dispAxis.mul(shove.disp.mul(float(1).sub(attach)).mul(u.uPartShove)));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return p;
|
|
109
|
+
})();
|
|
110
|
+
const outwardDir = Fn(() => {
|
|
111
|
+
const spawn = waveShape(wave, flags, vec3(aUv.y.sub(.5).mul(400), 0, -8), aUv, ts, loopOff);
|
|
112
|
+
const spawnWorld = u.uShedModel.mul(vec4(spawn.pos, 1)).xyz;
|
|
113
|
+
return normalize(spawnWorld.sub(u.uCenter).add(vec3(1e-4)));
|
|
114
|
+
})();
|
|
115
|
+
const tw = sin(age.mul(9).add(aSeed).mul(TAU)).mul(.5).add(.5);
|
|
116
|
+
const vAlpha = varying(fade.mul(mix(float(1), tw, clamp(u.uTwinkle, 0, 1))), "vAlpha");
|
|
117
|
+
const vColor = varying(mix(u.uColor, u.uColor2, aRnd.w), "vColor");
|
|
118
|
+
const vDir = varying(normalize(vec2(dot(outwardDir, u.uRight), dot(outwardDir, u.uUp)).add(vec2(1e-4))), "vDir");
|
|
119
|
+
material.colorNode = Fn(() => {
|
|
120
|
+
const pointCoord = vec2(uv().x, float(1).sub(uv().y)).toVar("pointCoord");
|
|
121
|
+
if (flags.sprite && spriteTexture) {
|
|
122
|
+
const tex = texture(spriteTexture).sample(vec2(pointCoord.x, float(1).sub(pointCoord.y)));
|
|
123
|
+
return vec4(vColor.mul(tex.rgb), tex.a.mul(vAlpha));
|
|
124
|
+
}
|
|
125
|
+
const a = shapeAlpha(u.uShape, pointCoord.sub(.5), vDir).mul(vAlpha);
|
|
126
|
+
return vec4(vColor, a);
|
|
127
|
+
})();
|
|
128
|
+
return material;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { buildParticleMaterial };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=particleMaterial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"particleMaterial.js","names":["tabs"],"sources":["../../../src/renderer/tsl/particleMaterial.ts"],"sourcesContent":["/**\n * The particle field's material in TSL — the port of `particleVertexShader` /\n * `particleFragmentShader` in `../shaders.ts`.\n *\n * The one structural change the backend forces: WebGPU point primitives are fixed at ONE pixel, so\n * a `THREE.Points` sized through `gl_PointSize` cannot work. three's own `PointsNodeMaterial`\n * documents that a size is honoured only when the material is attached to a `Sprite`, which draws\n * the field as instanced quads instead. That costs 4 vertices and 2 triangles per particle where\n * the point list needed 1 vertex, and it changes two things in the shader:\n *\n * - `gl_PointSize` becomes `sizeNode`, which three multiplies by the device pixel ratio itself —\n * so the GLSL's explicit `uPixelRatio` factor must NOT be repeated here or size is squared.\n * - `gl_PointCoord` becomes `uv()`. Their Y axes are opposite (`gl_PointCoord` runs DOWN from the\n * top-left), which the GLSL already had to flip for sprite artwork. Every procedural shape but\n * \"streak\" is symmetric about Y, so getting this wrong would show up only on that one.\n *\n * Everything else — the deterministic life, the shed emitter riding the shared wave deform, and the\n * pointer weld/shove — is the same graph the ribbon uses, reading the OWNING WAVE's uniform\n * registry directly rather than mirroring values across two materials.\n */\nimport { PointsNodeMaterial } from \"three/webgpu\";\nimport {\n Fn,\n float,\n vec2,\n vec3,\n vec4,\n cos,\n sin,\n atan,\n pow,\n abs as tabs,\n exp,\n max,\n clamp,\n mix,\n fract,\n floor,\n length,\n dot,\n cross,\n normalize,\n smoothstep,\n uv,\n varying,\n instancedArray,\n instanceIndex,\n cameraProjectionMatrix,\n cameraViewMatrix,\n select,\n If,\n texture,\n} from \"three/tsl\";\nimport type { Texture } from \"three\";\nimport { RIBBON_Z_CENTER } from \"../WaveGeometry\";\nimport { simplexNoise } from \"./noise\";\nimport { waveShape, applyTwist, type WaveShapeFlags } from \"./waveShape\";\nimport { pointerField } from \"./pointerField\";\nimport type { FloatNode, Vec2Node, Vec3Node } from \"./types\";\nimport type { WaveTslUniforms } from \"./uniforms\";\nimport type { ParticleTslUniforms } from \"./particleUniforms\";\n\nconst TAU = 6.28318530718;\n\nexport interface ParticleMaterialFlags extends WaveShapeFlags {\n pointerFx: boolean;\n pointerRipples: boolean;\n /** Bind user artwork instead of the procedural shapes. */\n sprite: boolean;\n}\n\nexport interface ParticleAttributeArrays {\n aSeed: Float32Array;\n aRnd: Float32Array;\n aUv: Float32Array;\n}\n\n/** Alpha for the procedural shapes, indexed by `uShape` (0 glitter, 1 soft, 2 ring, 3 star, 4 streak). */\nfunction shapeAlpha(shape: FloatNode, pc: Vec2Node, dir: Vec2Node): FloatNode {\n const d = length(pc).toVar(\"pcD\");\n const glitter = smoothstep(0.5, 0.0, d);\n const soft = exp(d.mul(d).mul(-7.0)); // a diffuse gaussian blob (motes / pollen)\n const ring = smoothstep(0.09, 0.0, tabs(d.sub(0.34))); // a hollow band (bubbles)\n const ang = atan(pc.y, pc.x); // star: a 4-point sparkle\n const spike = pow(tabs(cos(ang.mul(2.0))), 6.0);\n const star = smoothstep(1.0, 0.0, d.div(spike.mul(0.5).add(0.14)));\n const along = dot(pc, dir); // streak: an elongated comet along the motion direction\n const perp = dot(pc, vec2(dir.y.negate(), dir.x));\n const streak = smoothstep(0.5, 0.0, length(vec2(along.mul(0.42), perp.mul(2.2))));\n // Rounded to an integer index in the GLSL (`int(uShape + 0.5)`); expressed as a select chain here.\n const s = floor(shape.add(0.5)).toVar(\"shapeIdx\");\n return select(\n s.equal(1),\n soft,\n select(s.equal(2), ring, select(s.equal(3), star, select(s.equal(4), streak, glitter))),\n );\n}\n\n/**\n * Build one field's material.\n *\n * `wave` is the OWNING wave's uniform registry: the shed emitter and the pointer weld read it\n * directly, so the dust and the ribbon are driven by one set of nodes.\n */\nexport function buildParticleMaterial(\n wave: WaveTslUniforms,\n u: ParticleTslUniforms,\n attrs: ParticleAttributeArrays,\n flags: ParticleMaterialFlags,\n spriteTexture: Texture | null,\n): PointsNodeMaterial {\n // instancedArray().element(instanceIndex), NOT instancedBufferAttribute.\n //\n // The latter is the accessor three's own PointsNodeMaterial docs reach for, but on a Sprite every\n // instance reads the same element and the whole field collapses onto one particle — silently,\n // with no warning. Measured on a 64-instance 4px grid: instancedBufferAttribute lit 37 pixels in\n // a 26x4 box; instancedArray lit exactly 1024 in a 94x94 box, which is 64 x 4x4 as intended.\n const aSeed = instancedArray(attrs.aSeed, \"float\").element(instanceIndex) as unknown as FloatNode;\n const aRnd = instancedArray(attrs.aRnd, \"vec4\").element(instanceIndex) as unknown as Vec3Node & {\n w: FloatNode;\n xyz: Vec3Node;\n };\n const aUv = instancedArray(attrs.aUv, \"vec2\").element(instanceIndex) as unknown as Vec2Node;\n\n const material = new PointsNodeMaterial();\n material.transparent = true;\n material.depthTest = false; // always composite OVER the waves...\n material.depthWrite = false; // ...and never occlude anything (additive glints)\n material.blending = 2; // THREE.AdditiveBlending\n material.sizeAttenuation = false; // orthographic: size is constant in device pixels\n\n // Deterministic life: age 0..1 from uTime + a per-particle seed. Advances once per loop period\n // when looping (so the field repeats seamlessly), else once per uLife seconds. Built OUTSIDE the\n // Fn bodies below, because `sizeNode` and the varyings are read by the material's own setup and\n // by the fragment graph — assigning them from inside an Fn would make them depend on which graph\n // three happens to build first.\n const cyc = max(1.0, floor(u.uPartSpeed.add(0.5)));\n const rate = select(\n u.uLoopSeconds.greaterThan(0.0),\n u.uTime.div(u.uLoopSeconds).mul(cyc),\n u.uTime.mul(u.uPartSpeed).div(max(u.uLife, 0.001)),\n );\n const age = fract(rate.add(aSeed));\n const fade = sin(age.mul(Math.PI)); // 0 at birth/death, 1 mid-life\n\n // Point size in LOGICAL pixels: three multiplies by the device pixel ratio itself, so the GLSL's\n // explicit uPixelRatio factor is deliberately absent here.\n const jitter = float(1).add(u.uSizeJitter.mul(aSeed.sub(0.5)).mul(2.0));\n material.sizeNode = max(u.uSize.mul(jitter).mul(fade), 0.0);\n\n // Linear / orbit time for the shed emitter. Only the one selected by loopMotion is read.\n const ts = flags.loopMotion ? float(0) : u.uTime.mul(u.uShedSpeed).add(u.uShedSeed);\n const loopTheta = u.uTime.mul(TAU).div(u.uLoopSeconds).add(u.uShedSeed);\n const loopR = u.uShedSpeed.mul(u.uLoopSeconds).mul(0.159154943092);\n const loopOff = flags.loopMotion ? vec2(cos(loopTheta), sin(loopTheta)).mul(loopR) : vec2(0, 0);\n\n // Spawn on the owning wave's DEFORMED surface at aUv, then peel outward as the particle ages.\n const emit = Fn(() => {\n // Approximate the base hairpin point for this uv (length from uv.y; width centre), then deform\n // it exactly as the wave does. Good enough for dust — the fan / displacement dominate.\n const base = vec3(aUv.y.sub(0.5).mul(400.0), 0.0, RIBBON_Z_CENTER);\n const ws = waveShape(wave, flags, base, aUv, ts, loopOff);\n const origin = u.uShedModel.mul(vec4(ws.pos, 1.0)).xyz.toVar(\"origin\");\n const outward = normalize(origin.sub(u.uCenter).add(vec3(1e-4))).toVar(\"outward\");\n\n const p = origin\n .add(outward.mul(age).mul(u.uDrift))\n .add(aRnd.xyz.sub(0.5).mul(age).mul(u.uDrift).mul(0.35))\n .toVar(\"p\");\n\n // Motion styles, each 0 = off, all riding age so they stay loop-safe.\n p.addAssign(u.uUp.mul(age).mul(u.uRise)); // screen-vertical buoyancy\n\n // How far this mote travels from its birth patch over a WHOLE life. Only the pointer weld\n // reads it, so it is only computed when the pointer field is compiled in.\n const span = flags.pointerFx\n ? tabs(u.uDrift).mul(1.35).add(tabs(u.uRise)).add(u.uWander).toVar(\"span\")\n : null;\n\n If(u.uSwirl.notEqual(0.0), () => {\n const nrm = cross(u.uRight, u.uUp);\n const rel = p.sub(u.uCenter).toVar(\"rel\");\n if (span) span.addAssign(tabs(u.uSwirl).mul(TAU).mul(length(rel)));\n const rx = dot(rel, u.uRight);\n const ry = dot(rel, u.uUp);\n const rz = dot(rel, nrm);\n const a = age.mul(u.uSwirl).mul(TAU);\n const ca = cos(a);\n const sa = sin(a);\n p.assign(\n u.uCenter\n .add(u.uRight.mul(rx.mul(ca).sub(ry.mul(sa))))\n .add(u.uUp.mul(rx.mul(sa).add(ry.mul(ca))))\n .add(nrm.mul(rz)),\n );\n });\n\n If(u.uWander.notEqual(0.0), () => {\n const wan = vec2(\n simplexNoise(vec2(aSeed.mul(17.0), age.mul(3.0))),\n simplexNoise(vec2(age.mul(3.0), aSeed.mul(23.0))),\n );\n p.addAssign(u.uRight.mul(wan.x).add(u.uUp.mul(wan.y)).mul(u.uWander));\n });\n\n if (flags.pointerFx && span) {\n // WELD: the ribbon displaces its surface along its own post-twist up-axis, and a mote sitting\n // ON that surface has to take the same ride, or the cursor's dome lifts the silk out from\n // under its own glitter. Sampled at the SPAWN point, exactly as the ribbon's own displacement\n // is, so the two land in the same place — and so `outward` stays derived from the\n // UNDISPLACED origin, leaving the drift direction unbent by a poke.\n const pMvp = cameraProjectionMatrix.mul(cameraViewMatrix).mul(u.uShedModel).toVar(\"pMvp\");\n const originClip = pMvp.mul(vec4(ws.pos, 1.0)).toVar(\"originClip\");\n const localAxis = applyTwist(\n applyTwist(applyTwist(vec3(0, 1, 0), ws.twists[0]), ws.twists[1]),\n ws.twists[2],\n );\n const dispAxis = u.uShedModel.mul(vec4(localAxis, 0.0)).xyz.toVar(\"dispAxis\");\n const opts = { loopMotion: flags.loopMotion, ripples: flags.pointerRipples };\n const weld = pointerField(\n wave,\n opts,\n originClip.xy.div(max(originClip.w, 1.0e-6)),\n pMvp,\n ws.twists,\n ws.pos,\n ts,\n loopOff,\n );\n // How attached to its birth patch this mote still is: 1 on the surface, 0 once it has\n // travelled a full life's worth away. Measured from DISTANCE rather than age, because dust\n // with no drift / rise / swirl / wander never leaves the surface at all — an age fade would\n // quietly stop that dust from following the ribbon halfway through its life.\n const attach = select(\n span.greaterThan(1.0e-4),\n float(1).sub(clamp(length(p.sub(origin)).div(span), 0, 1)),\n float(1),\n ).toVar(\"attach\");\n p.addAssign(dispAxis.mul(weld.disp.mul(attach)));\n\n // SHOVE: the same field at the mote's OWN screen position, so the cursor also pushes dust\n // that has already left the surface and a click ripple blows through the cloud instead of\n // stopping dead at the ribbon.\n If(u.uPartShove.notEqual(0.0), () => {\n const pClip = cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(p, 1.0)).toVar(\"pClip\");\n const shove = pointerField(\n wave,\n opts,\n pClip.xy.div(max(pClip.w, 1.0e-6)),\n pMvp,\n ws.twists,\n ws.pos,\n ts,\n loopOff,\n );\n p.addAssign(dispAxis.mul(shove.disp.mul(float(1).sub(attach)).mul(u.uPartShove)));\n });\n }\n\n return p;\n });\n\n material.positionNode = emit();\n\n // Vertex-stage values the fragment needs. `outward` is recomputed here rather than threaded out\n // of the emitter: it is a pure function of the spawn point, so the graph is common-subexpression\n // eliminated back into one evaluation.\n //\n // Wrapped in an Fn because waveShape uses `.toVar()` / `.assign()`, which need a stack — calling\n // it at module level fails with \"No stack defined for assign operation\".\n const outwardDir = Fn(() => {\n const spawn = waveShape(\n wave,\n flags,\n vec3(aUv.y.sub(0.5).mul(400.0), 0.0, RIBBON_Z_CENTER),\n aUv,\n ts,\n loopOff,\n );\n const spawnWorld = u.uShedModel.mul(vec4(spawn.pos, 1.0)).xyz;\n return normalize(spawnWorld.sub(u.uCenter).add(vec3(1e-4)));\n })();\n\n const tw = sin(age.mul(9.0).add(aSeed).mul(TAU)).mul(0.5).add(0.5); // loop-safe flicker\n const vAlpha = varying(fade.mul(mix(float(1), tw, clamp(u.uTwinkle, 0, 1))), \"vAlpha\");\n const vColor = varying(mix(u.uColor, u.uColor2, aRnd.w), \"vColor\"); // two-tone dust\n const vDir = varying(\n normalize(vec2(dot(outwardDir, u.uRight), dot(outwardDir, u.uUp)).add(vec2(1e-4))),\n \"vDir\",\n );\n\n material.colorNode = Fn(() => {\n // gl_PointCoord's origin is the TOP-left with y running DOWN; the sprite quad's uv runs UP.\n const pointCoord = vec2(uv().x, float(1).sub(uv().y)).toVar(\"pointCoord\");\n if (flags.sprite && spriteTexture) {\n const tex = texture(spriteTexture).sample(vec2(pointCoord.x, float(1).sub(pointCoord.y)));\n // Tinted by the dust colour so color / color2 keep working: white artwork takes the tint\n // exactly, coloured artwork multiplies it.\n return vec4(vColor.mul(tex.rgb), tex.a.mul(vAlpha));\n }\n const a = shapeAlpha(u.uShape, pointCoord.sub(0.5), vDir).mul(vAlpha);\n return vec4(vColor, a); // AdditiveBlending (src = SrcAlpha) -> adds vColor*a\n })();\n\n return material;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { floatUniform } from "./types.js";
|
|
2
|
+
import { Matrix4, Vector3 } from "three";
|
|
3
|
+
import { uniform } from "three/tsl";
|
|
4
|
+
//#region src/renderer/tsl/particleUniforms.ts
|
|
5
|
+
/**
|
|
6
|
+
* The TSL mirror of the particle material's own uniforms.
|
|
7
|
+
*
|
|
8
|
+
* Only the particle-specific ones live here. The SHAPE and POINTER uniforms are not copied at all
|
|
9
|
+
* on this backend: the graph reads the owning wave's registry directly, so the dust rides the exact
|
|
10
|
+
* same nodes the ribbon does. The GLSL path has to mirror those values across two materials every
|
|
11
|
+
* frame (see `ParticleField.configure`), which is a sync step this backend simply does not have.
|
|
12
|
+
*/
|
|
13
|
+
const vec3Uniform = (x, y, z) => uniform(new Vector3(x, y, z));
|
|
14
|
+
/** Build one field's uniform registry. Defaults mirror the GLSL material's. */
|
|
15
|
+
function makeParticleUniforms() {
|
|
16
|
+
return {
|
|
17
|
+
uTime: floatUniform(0),
|
|
18
|
+
uLoopSeconds: floatUniform(0),
|
|
19
|
+
uLife: floatUniform(6),
|
|
20
|
+
uPartSpeed: floatUniform(1),
|
|
21
|
+
uSize: floatUniform(2),
|
|
22
|
+
uSizeJitter: floatUniform(0),
|
|
23
|
+
uTwinkle: floatUniform(0),
|
|
24
|
+
uColor: vec3Uniform(1, .81, .54),
|
|
25
|
+
uColor2: vec3Uniform(1, .81, .54),
|
|
26
|
+
uCenter: vec3Uniform(0, 0, 0),
|
|
27
|
+
uRight: vec3Uniform(1, 0, 0),
|
|
28
|
+
uUp: vec3Uniform(0, 1, 0),
|
|
29
|
+
uDrift: floatUniform(0),
|
|
30
|
+
uRise: floatUniform(0),
|
|
31
|
+
uSwirl: floatUniform(0),
|
|
32
|
+
uWander: floatUniform(0),
|
|
33
|
+
uShape: floatUniform(0),
|
|
34
|
+
/** The owning wave's matrixWorld: deformed LOCAL -> world. */
|
|
35
|
+
uShedModel: uniform(new Matrix4()),
|
|
36
|
+
uShedSpeed: floatUniform(0),
|
|
37
|
+
uShedSeed: floatUniform(0),
|
|
38
|
+
uPartShove: floatUniform(1)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { makeParticleUniforms };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=particleUniforms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"particleUniforms.js","names":[],"sources":["../../../src/renderer/tsl/particleUniforms.ts"],"sourcesContent":["/**\n * The TSL mirror of the particle material's own uniforms.\n *\n * Only the particle-specific ones live here. The SHAPE and POINTER uniforms are not copied at all\n * on this backend: the graph reads the owning wave's registry directly, so the dust rides the exact\n * same nodes the ribbon does. The GLSL path has to mirror those values across two materials every\n * frame (see `ParticleField.configure`), which is a sync step this backend simply does not have.\n */\nimport { Matrix4, Vector3 } from \"three\";\nimport { uniform } from \"three/tsl\";\nimport { floatUniform, type FloatUniform } from \"./types\";\nimport type { Mat4Node, Vec3Node } from \"./types\";\n\nexport type Vec3Uniform = Vec3Node & { value: Vector3 };\nexport type Mat4Uniform = Mat4Node & { value: Matrix4 };\n\nconst vec3Uniform = (x: number, y: number, z: number): Vec3Uniform =>\n uniform(new Vector3(x, y, z)) as unknown as Vec3Uniform;\n\n/** Build one field's uniform registry. Defaults mirror the GLSL material's. */\nexport function makeParticleUniforms() {\n return {\n uTime: floatUniform(0),\n uLoopSeconds: floatUniform(0),\n uLife: floatUniform(6),\n uPartSpeed: floatUniform(1),\n uSize: floatUniform(2),\n uSizeJitter: floatUniform(0),\n uTwinkle: floatUniform(0),\n uColor: vec3Uniform(1, 0.81, 0.54),\n uColor2: vec3Uniform(1, 0.81, 0.54),\n uCenter: vec3Uniform(0, 0, 0),\n uRight: vec3Uniform(1, 0, 0),\n uUp: vec3Uniform(0, 1, 0),\n uDrift: floatUniform(0),\n uRise: floatUniform(0),\n uSwirl: floatUniform(0),\n uWander: floatUniform(0),\n uShape: floatUniform(0),\n /** The owning wave's matrixWorld: deformed LOCAL -> world. */\n uShedModel: uniform(new Matrix4()) as unknown as Mat4Uniform,\n uShedSpeed: floatUniform(0),\n uShedSeed: floatUniform(0),\n uPartShove: floatUniform(1),\n };\n}\n\nexport type ParticleTslUniforms = ReturnType<typeof makeParticleUniforms> & {\n uPixelRatio?: FloatUniform;\n};\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAM,eAAe,GAAW,GAAW,MACzC,QAAQ,IAAI,QAAQ,GAAG,GAAG,CAAC,CAAC;;AAG9B,SAAgB,uBAAuB;CACrC,OAAO;EACL,OAAO,aAAa,CAAC;EACrB,cAAc,aAAa,CAAC;EAC5B,OAAO,aAAa,CAAC;EACrB,YAAY,aAAa,CAAC;EAC1B,OAAO,aAAa,CAAC;EACrB,aAAa,aAAa,CAAC;EAC3B,UAAU,aAAa,CAAC;EACxB,QAAQ,YAAY,GAAG,KAAM,GAAI;EACjC,SAAS,YAAY,GAAG,KAAM,GAAI;EAClC,SAAS,YAAY,GAAG,GAAG,CAAC;EAC5B,QAAQ,YAAY,GAAG,GAAG,CAAC;EAC3B,KAAK,YAAY,GAAG,GAAG,CAAC;EACxB,QAAQ,aAAa,CAAC;EACtB,OAAO,aAAa,CAAC;EACrB,QAAQ,aAAa,CAAC;EACtB,SAAS,aAAa,CAAC;EACvB,QAAQ,aAAa,CAAC;;EAEtB,YAAY,QAAQ,IAAI,QAAQ,CAAC;EACjC,YAAY,aAAa,CAAC;EAC1B,WAAW,aAAa,CAAC;EACzB,YAAY,aAAa,CAAC;CAC5B;AACF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { simplexNoise } from "./noise.js";
|
|
2
|
+
import { applyTwist } from "./waveShape.js";
|
|
3
|
+
import { If, Loop, clamp, cos, dot, exp, float, length, smoothstep, vec2, vec3, vec4 } from "three/tsl";
|
|
4
|
+
//#region src/renderer/tsl/pointerField.ts
|
|
5
|
+
/**
|
|
6
|
+
* The pointer field in TSL — the port of `pointerFieldChunk` in `../shaders.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the wave material and the particle emitter exactly as it is in the GLSL, so a wave's
|
|
9
|
+
* dust reacts to the cursor through the SAME footprint, falloff and displacement its ribbon does
|
|
10
|
+
* instead of staying pinned to the un-poked surface.
|
|
11
|
+
*
|
|
12
|
+
* `fall` is the screen falloff times presence, which both fragment themes consume; `disp` is the
|
|
13
|
+
* signed displacement along the surface's own up-axis, which the CALLER applies — the wave in its
|
|
14
|
+
* local space, the dust through the wave's world matrix.
|
|
15
|
+
*/
|
|
16
|
+
const RIPPLE_WAVE_SPEED = .85;
|
|
17
|
+
const RIPPLE_SIGMA = .14;
|
|
18
|
+
const RIPPLE_FREQ = 11;
|
|
19
|
+
const RIPPLE_MAX_R = 1.2;
|
|
20
|
+
/**
|
|
21
|
+
* Sample the field for ONE point.
|
|
22
|
+
*
|
|
23
|
+
* `ndc` is that point's screen position; `mvp` the clip transform of the space the twists and
|
|
24
|
+
* `churnPos` live in (the owning wave's local space); `t` / `loopOff` the caller's linear / orbit
|
|
25
|
+
* time — only the one selected by `loopMotion` is read.
|
|
26
|
+
*/
|
|
27
|
+
function pointerField(u, opts, ndc, mvp, twists, churnPos, t, loopOff) {
|
|
28
|
+
const aspect = vec2(u.uPointerAspect, 1);
|
|
29
|
+
const dp = ndc.sub(u.uPointer).mul(aspect).toVar("pfDp");
|
|
30
|
+
If(u.uShapeFlow.greaterThan(0), () => {
|
|
31
|
+
const tangentLocal = applyTwist(applyTwist(applyTwist(vec3(1, 0, 0), twists[0]), twists[1]), twists[2]);
|
|
32
|
+
const tang = mvp.mul(vec4(tangentLocal, 0)).xy.mul(aspect).toVar("pfTang");
|
|
33
|
+
const tl = length(tang).toVar("pfTl");
|
|
34
|
+
If(tl.greaterThan(1e-6), () => {
|
|
35
|
+
tang.divAssign(tl);
|
|
36
|
+
const nrm = vec2(tang.y.negate(), tang.x);
|
|
37
|
+
dp.assign(vec2(dot(dp, tang).div(u.uShapeFlow.mul(2.5).add(1)), dot(dp, nrm)));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const fall = smoothstep(u.uPointerRadius, 0, length(dp)).mul(u.uPointerActive).toVar("pfFall");
|
|
41
|
+
const churnArg = opts.loopMotion ? vec2(churnPos.x.mul(u.uDispFreqX).mul(3), churnPos.z.mul(u.uDispFreqZ).mul(3)).add(loopOff.mul(4)) : vec2(churnPos.x.mul(u.uDispFreqX).mul(3).add(t.mul(4)), churnPos.z.mul(u.uDispFreqZ).mul(3));
|
|
42
|
+
const disp = u.uPointerAgitate.mul(fall).mul(simplexNoise(churnArg)).toVar("pfDisp");
|
|
43
|
+
disp.addAssign(u.uPointerPush.mul(fall));
|
|
44
|
+
const velC = u.uPointerVel.mul(aspect).toVar("pfVel");
|
|
45
|
+
const wakeSpeed = length(velC).toVar("pfSpeed");
|
|
46
|
+
If(u.uPointerWake.notEqual(0).and(wakeSpeed.greaterThan(1e-4)), () => {
|
|
47
|
+
const behind = clamp(dot(dp.negate(), velC).div(wakeSpeed.mul(u.uPointerRadius)), 0, 1);
|
|
48
|
+
disp.subAssign(u.uPointerWake.mul(fall).mul(behind).mul(smoothstep(.05, .6, wakeSpeed)));
|
|
49
|
+
});
|
|
50
|
+
if (opts.ripples) Loop({
|
|
51
|
+
start: 0,
|
|
52
|
+
end: 4,
|
|
53
|
+
type: "int"
|
|
54
|
+
}, ({ i }) => {
|
|
55
|
+
const amp = u.uRippleAmp.el(i).toVar("rAmp");
|
|
56
|
+
If(amp.greaterThan(0), () => {
|
|
57
|
+
const rd = length(ndc.sub(u.uRippleOrigin.el(i)).mul(aspect)).toVar("rD");
|
|
58
|
+
const front = u.uRippleAge.el(i).mul(RIPPLE_WAVE_SPEED).toVar("rFront");
|
|
59
|
+
const band = rd.sub(front).toVar("rBand");
|
|
60
|
+
const packet = exp(band.mul(band).negate().div(2 * RIPPLE_SIGMA * RIPPLE_SIGMA)).mul(cos(band.mul(RIPPLE_FREQ)));
|
|
61
|
+
const reach = float(1).sub(smoothstep(RIPPLE_MAX_R * .7, RIPPLE_MAX_R, front));
|
|
62
|
+
disp.addAssign(u.uPointerRipple.mul(amp).mul(packet).mul(reach));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
fall,
|
|
67
|
+
disp
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { pointerField };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=pointerField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointerField.js","names":[],"sources":["../../../src/renderer/tsl/pointerField.ts"],"sourcesContent":["/**\n * The pointer field in TSL — the port of `pointerFieldChunk` in `../shaders.ts`.\n *\n * Shared by the wave material and the particle emitter exactly as it is in the GLSL, so a wave's\n * dust reacts to the cursor through the SAME footprint, falloff and displacement its ribbon does\n * instead of staying pinned to the un-poked surface.\n *\n * `fall` is the screen falloff times presence, which both fragment themes consume; `disp` is the\n * signed displacement along the surface's own up-axis, which the CALLER applies — the wave in its\n * local space, the dust through the wave's world matrix.\n */\nimport {\n float,\n vec2,\n vec3,\n vec4,\n cos,\n exp,\n clamp,\n dot,\n length,\n smoothstep,\n Loop,\n If,\n} from \"three/tsl\";\nimport { RIPPLE_SLOTS } from \"../interactionGates\";\nimport { simplexNoise } from \"./noise\";\nimport { applyTwist, type Twist } from \"./waveShape\";\nimport type { FloatNode, Vec2Node, Vec3Node, Mat4Node } from \"./types\";\nimport type { WaveTslUniforms } from \"./uniforms\";\n\nconst RIPPLE_WAVE_SPEED = 0.85; // NDC/s the ring crest travels outward\nconst RIPPLE_SIGMA = 0.14; // gaussian half-width of the travelling packet (NDC)\nconst RIPPLE_FREQ = 11.0; // oscillation within the packet (one crest + faint troughs)\nconst RIPPLE_MAX_R = 1.2; // reach where the crest has fully left the frame\n\nexport interface PointerHit {\n /** Screen falloff x presence. */\n fall: FloatNode;\n /** Signed displacement along the surface's own up-axis. */\n disp: FloatNode;\n}\n\nexport interface PointerFieldOpts {\n loopMotion: boolean;\n ripples: boolean;\n}\n\n/**\n * Sample the field for ONE point.\n *\n * `ndc` is that point's screen position; `mvp` the clip transform of the space the twists and\n * `churnPos` live in (the owning wave's local space); `t` / `loopOff` the caller's linear / orbit\n * time — only the one selected by `loopMotion` is read.\n */\nexport function pointerField(\n u: WaveTslUniforms,\n opts: PointerFieldOpts,\n ndc: Vec2Node,\n mvp: Mat4Node,\n twists: [Twist, Twist, Twist],\n churnPos: Vec3Node,\n t: FloatNode,\n loopOff: Vec2Node,\n): PointerHit {\n const aspect = vec2(u.uPointerAspect, 1.0);\n // Screen-space offset from the cursor (aspect-corrected → round in pixels). The DEFAULT metric.\n const dp = ndc.sub(u.uPointer).mul(aspect).toVar(\"pfDp\");\n\n // Ribbon flow: stretch the metric along the strip's own LENGTH axis so the field reaches ALONG\n // the ribbon and stays tight across it. The length axis is local +X carried through the SAME\n // twists as the surface. The camera is orthographic (affine, w = 1), so the axis's screen image\n // is the linear map of the DIRECTION (w = 0): one mat*dir, no perspective divide.\n If(u.uShapeFlow.greaterThan(0.0), () => {\n const tangentLocal = applyTwist(\n applyTwist(applyTwist(vec3(1, 0, 0), twists[0]), twists[1]),\n twists[2],\n );\n const tang = mvp.mul(vec4(tangentLocal, 0.0)).xy.mul(aspect).toVar(\"pfTang\");\n const tl = length(tang).toVar(\"pfTl\");\n If(tl.greaterThan(1.0e-6), () => {\n tang.divAssign(tl);\n const nrm = vec2(tang.y.negate(), tang.x);\n // up to 3.5x reach along the length\n dp.assign(vec2(dot(dp, tang).div(u.uShapeFlow.mul(2.5).add(1.0)), dot(dp, nrm)));\n });\n });\n\n const fall = smoothstep(u.uPointerRadius, 0.0, length(dp)).mul(u.uPointerActive).toVar(\"pfFall\");\n\n // Agitation: a fast churn octave near the cursor (additive — never rewrites the base noise time,\n // which would force restructuring the shared path). Loop-safe under both time variants.\n const churnArg = opts.loopMotion\n ? vec2(churnPos.x.mul(u.uDispFreqX).mul(3.0), churnPos.z.mul(u.uDispFreqZ).mul(3.0)).add(\n loopOff.mul(4.0),\n )\n : vec2(\n churnPos.x.mul(u.uDispFreqX).mul(3.0).add(t.mul(4.0)),\n churnPos.z.mul(u.uDispFreqZ).mul(3.0),\n );\n const disp = u.uPointerAgitate.mul(fall).mul(simplexNoise(churnArg)).toVar(\"pfDisp\");\n\n // Membrane push/pull: a smooth dome that swells toward you (+ repel) or dents away (- attract).\n disp.addAssign(u.uPointerPush.mul(fall));\n\n // Drag-wake: pull the surface just BEHIND the moving cursor into a trailing trough. dp points\n // from cursor to vertex; \"behind\" is how far the vertex sits opposite the velocity, gated by\n // speed so it only forms while dragging and heals when the cursor stops.\n const velC = u.uPointerVel.mul(aspect).toVar(\"pfVel\");\n const wakeSpeed = length(velC).toVar(\"pfSpeed\");\n If(u.uPointerWake.notEqual(0.0).and(wakeSpeed.greaterThan(1.0e-4)), () => {\n const behind = clamp(dot(dp.negate(), velC).div(wakeSpeed.mul(u.uPointerRadius)), 0, 1);\n disp.subAssign(\n u.uPointerWake\n .mul(fall)\n .mul(behind)\n .mul(smoothstep(0.05, 0.6, wakeSpeed)),\n );\n });\n\n if (opts.ripples) {\n Loop({ start: 0, end: RIPPLE_SLOTS, type: \"int\" }, ({ i }) => {\n const amp = u.uRippleAmp.el(i).toVar(\"rAmp\");\n If(amp.greaterThan(0.0), () => {\n const rd = length(ndc.sub(u.uRippleOrigin.el(i)).mul(aspect)).toVar(\"rD\");\n // A wave PACKET whose crest travels outward: a gaussian window centred on the moving front\n // carrying a short oscillation, so the energy radiates instead of throbbing at the click\n // point. The shared envelope fades the packet over its lifetime; reach fades it as the\n // crest leaves frame.\n const front = u.uRippleAge.el(i).mul(RIPPLE_WAVE_SPEED).toVar(\"rFront\");\n const band = rd.sub(front).toVar(\"rBand\");\n const packet = exp(\n band\n .mul(band)\n .negate()\n .div(2.0 * RIPPLE_SIGMA * RIPPLE_SIGMA),\n ).mul(cos(band.mul(RIPPLE_FREQ)));\n const reach = float(1).sub(smoothstep(RIPPLE_MAX_R * 0.7, RIPPLE_MAX_R, front));\n disp.addAssign(u.uPointerRipple.mul(amp).mul(packet).mul(reach));\n });\n });\n }\n\n return { fall, disp };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Break, Fn, If, Loop, cos, dot, float, fract, mat2, mix, screenCoordinate, screenSize, sin, smoothstep, uv, vec2, vec4 } from "three/tsl";
|
|
2
|
+
//#region src/renderer/tsl/post.ts
|
|
3
|
+
/**
|
|
4
|
+
* The base post pass in TSL — the port of `postFragmentShader` in `../shaders.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Angular (spin) blur plus static film grain, applied to the composed scene. Alpha is carried
|
|
7
|
+
* through so a transparent background survives the pass, exactly as in the GLSL.
|
|
8
|
+
*
|
|
9
|
+
* The GLSL runs this as a `ShaderPass` inside an `EffectComposer`; here it is a node graph over the
|
|
10
|
+
* scene `pass()`, driven by `PostProcessing`. The sample count stays a uniform-bounded loop rather
|
|
11
|
+
* than a JS-unrolled one, because `uBlurSamples` changes with quality settings at runtime.
|
|
12
|
+
*/
|
|
13
|
+
/** The GLSL's `random2` — a cheap value hash, deliberately the same constants. */
|
|
14
|
+
const random2 = /*@__PURE__*/ Fn(([st]) => fract(sin(dot(st, vec2(12.9898, 78.233))).mul(43758.5453))).setLayout({
|
|
15
|
+
name: "wave_random2",
|
|
16
|
+
type: "float",
|
|
17
|
+
inputs: [{
|
|
18
|
+
name: "st",
|
|
19
|
+
type: "vec2"
|
|
20
|
+
}]
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Angular (spin) blur: rotate the sample coord around the centre and accumulate — a tangential
|
|
24
|
+
* smear that grows toward the edges.
|
|
25
|
+
*
|
|
26
|
+
* The GLSL caps the loop at a literal 64 and breaks on `uBlurSamples`; that shape is kept because
|
|
27
|
+
* the bound must be a compile-time constant on either backend.
|
|
28
|
+
*/
|
|
29
|
+
function blurAngular(sample, at, angle, samples) {
|
|
30
|
+
const total = vec4(0).toVar("blurTotal");
|
|
31
|
+
const dist = float(1).div(samples).toVar("blurStep");
|
|
32
|
+
const dir = vec2(cos(angle.mul(dist)), sin(angle.mul(dist))).toVar();
|
|
33
|
+
const rot = mat2(dir.x, dir.y, dir.y.negate(), dir.x).toVar();
|
|
34
|
+
const coord = at.sub(.5).toVar("blurCoord");
|
|
35
|
+
Loop({
|
|
36
|
+
start: 0,
|
|
37
|
+
end: 64,
|
|
38
|
+
type: "int"
|
|
39
|
+
}, ({ i }) => {
|
|
40
|
+
If(float(i).greaterThanEqual(samples), () => {
|
|
41
|
+
Break();
|
|
42
|
+
});
|
|
43
|
+
total.addAssign(sample(coord.add(.5)));
|
|
44
|
+
coord.assign(rot.mul(coord));
|
|
45
|
+
});
|
|
46
|
+
return total.mul(dist);
|
|
47
|
+
}
|
|
48
|
+
/** Build the base post graph over a scene sampler. */
|
|
49
|
+
function buildBasePost(sample, u) {
|
|
50
|
+
return Fn(() => {
|
|
51
|
+
const vUv = uv();
|
|
52
|
+
const sceneColor = sample(vUv).toVar("sceneColor");
|
|
53
|
+
const color = mix(blurAngular(sample, vUv, u.uBlurAmount, u.uBlurSamples).toVar("blurColor"), sceneColor, smoothstep(0, .7, vUv.y).sub(smoothstep(.2, 1, vUv.y))).toVar("postColor");
|
|
54
|
+
const fragCoord = vec2(screenCoordinate.x, screenSize.y.sub(screenCoordinate.y));
|
|
55
|
+
const g = mix(u.uGrainAmount, u.uGrainAmount.negate(), random2(fragCoord.mul(.01)));
|
|
56
|
+
color.rgb.addAssign(g.mul(4 / 255));
|
|
57
|
+
return color;
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { buildBasePost };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=post.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post.js","names":["tvec2"],"sources":["../../../src/renderer/tsl/post.ts"],"sourcesContent":["/**\n * The base post pass in TSL — the port of `postFragmentShader` in `../shaders.ts`.\n *\n * Angular (spin) blur plus static film grain, applied to the composed scene. Alpha is carried\n * through so a transparent background survives the pass, exactly as in the GLSL.\n *\n * The GLSL runs this as a `ShaderPass` inside an `EffectComposer`; here it is a node graph over the\n * scene `pass()`, driven by `PostProcessing`. The sample count stays a uniform-bounded loop rather\n * than a JS-unrolled one, because `uBlurSamples` changes with quality settings at runtime.\n */\nimport {\n Fn,\n Loop,\n If,\n Break,\n float,\n vec2,\n vec4,\n mat2,\n cos,\n sin,\n mix,\n fract,\n dot,\n smoothstep,\n screenCoordinate,\n screenSize,\n uv,\n vec2 as tvec2,\n} from \"three/tsl\";\nimport type { FloatUniform, Vec2Node, Vec4Node } from \"./types\";\n\n/** The GLSL's `random2` — a cheap value hash, deliberately the same constants. */\nconst random2 = /*@__PURE__*/ Fn(([st]: [Vec2Node]) =>\n fract(sin(dot(st, vec2(12.9898, 78.233))).mul(43758.5453)),\n).setLayout({\n name: \"wave_random2\",\n type: \"float\",\n inputs: [{ name: \"st\", type: \"vec2\" }],\n});\n\n/** How the caller exposes the scene texture: a function of uv, so the pass source stays pluggable. */\nexport type SceneSampler = (at: Vec2Node) => Vec4Node;\n\nexport interface PostUniforms {\n uBlurAmount: FloatUniform;\n uBlurSamples: FloatUniform;\n uGrainAmount: FloatUniform;\n}\n\n/**\n * Angular (spin) blur: rotate the sample coord around the centre and accumulate — a tangential\n * smear that grows toward the edges.\n *\n * The GLSL caps the loop at a literal 64 and breaks on `uBlurSamples`; that shape is kept because\n * the bound must be a compile-time constant on either backend.\n */\nfunction blurAngular(\n sample: SceneSampler,\n at: Vec2Node,\n angle: FloatUniform,\n samples: FloatUniform,\n) {\n const total = vec4(0).toVar(\"blurTotal\");\n const dist = float(1).div(samples).toVar(\"blurStep\");\n const dir = vec2(cos(angle.mul(dist)), sin(angle.mul(dist))).toVar();\n // The GLSL writes `coord * rot` (ROW-vector order), which is what sets the spin direction.\n //\n // Note the arguments below are in the same order as the GLSL's `mat2(dir.x, dir.y, -dir.y, dir.x)`\n // even though this multiplies the other way round (`rot.mul(coord)`). That is not an oversight:\n // TSL's `mat2(a, b, c, d)` fills ROW-major where GLSL's fills COLUMN-major, so the two\n // conventions cancel. Getting this backwards reverses the smear direction — verified by\n // `parity:math`, and worth ~6 mae on a blurred preset.\n const rot = mat2(dir.x, dir.y, dir.y.negate(), dir.x).toVar();\n const coord = at.sub(0.5).toVar(\"blurCoord\");\n Loop({ start: 0, end: 64, type: \"int\" }, ({ i }) => {\n If(float(i).greaterThanEqual(samples), () => {\n Break();\n });\n total.addAssign(sample(coord.add(0.5)));\n coord.assign(rot.mul(coord));\n });\n return total.mul(dist);\n}\n\n/** Build the base post graph over a scene sampler. */\nexport function buildBasePost(sample: SceneSampler, u: PostUniforms): Vec4Node {\n return Fn(() => {\n const vUv = uv();\n const sceneColor = sample(vUv).toVar(\"sceneColor\");\n const blurColor = blurAngular(sample, vUv, u.uBlurAmount, u.uBlurSamples).toVar(\"blurColor\");\n // blurPower: keep a sharp band weighted to the middle, blurring toward top & bottom.\n const blurPower = smoothstep(0.0, 0.7, vUv.y).sub(smoothstep(0.2, 1.0, vUv.y));\n const color = mix(blurColor, sceneColor, blurPower).toVar(\"postColor\");\n // Static film grain: keyed off the fragment coordinate only (no uTime), so it doesn't flicker.\n //\n // `screenCoordinate` follows the WebGPU convention (origin TOP-left) and flips Y on the WebGL\n // backend so both agree — but the GLSL this ports keys the hash off `gl_FragCoord`, whose\n // origin is BOTTOM-left. Left unflipped the hash samples a mirrored coordinate and produces a\n // completely different grain pattern: visually similar, but speckle across every pixel of a\n // parity diff.\n const fragCoord = tvec2(screenCoordinate.x, screenSize.y.sub(screenCoordinate.y));\n const g = mix(u.uGrainAmount, u.uGrainAmount.negate(), random2(fragCoord.mul(0.01)));\n color.rgb.addAssign(g.mul(4.0 / 255.0));\n return color; // alpha preserved → transparent background works\n })();\n}\n"],"mappings":";;;;;;;;;;;;;AAiCA,MAAM,UAAwB,kBAAI,CAAC,QACjC,MAAM,IAAI,IAAI,IAAI,KAAK,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAC3D,CAAC,CAAC,UAAU;CACV,MAAM;CACN,MAAM;CACN,QAAQ,CAAC;EAAE,MAAM;EAAM,MAAM;CAAO,CAAC;AACvC,CAAC;;;;;;;;AAkBD,SAAS,YACP,QACA,IACA,OACA,SACA;CACA,MAAM,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,WAAW;CACvC,MAAM,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,MAAM,UAAU;CACnD,MAAM,MAAM,KAAK,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM;CAQnE,MAAM,MAAM,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM;CAC5D,MAAM,QAAQ,GAAG,IAAI,EAAG,CAAC,CAAC,MAAM,WAAW;CAC3C,KAAK;EAAE,OAAO;EAAG,KAAK;EAAI,MAAM;CAAM,IAAI,EAAE,QAAQ;EAClD,GAAG,MAAM,CAAC,CAAC,CAAC,iBAAiB,OAAO,SAAS;GAC3C,MAAM;EACR,CAAC;EACD,MAAM,UAAU,OAAO,MAAM,IAAI,EAAG,CAAC,CAAC;EACtC,MAAM,OAAO,IAAI,IAAI,KAAK,CAAC;CAC7B,CAAC;CACD,OAAO,MAAM,IAAI,IAAI;AACvB;;AAGA,SAAgB,cAAc,QAAsB,GAA2B;CAC7E,OAAO,SAAS;EACd,MAAM,MAAM,GAAG;EACf,MAAM,aAAa,OAAO,GAAG,CAAC,CAAC,MAAM,YAAY;EAIjD,MAAM,QAAQ,IAHI,YAAY,QAAQ,KAAK,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,MAAM,WAGtD,GAAG,YADX,WAAW,GAAK,IAAK,IAAI,CAAC,CAAC,CAAC,IAAI,WAAW,IAAK,GAAK,IAAI,CAAC,CAC3B,CAAC,CAAC,CAAC,MAAM,WAAW;EAQrE,MAAM,YAAYA,KAAM,iBAAiB,GAAG,WAAW,EAAE,IAAI,iBAAiB,CAAC,CAAC;EAChF,MAAM,IAAI,IAAI,EAAE,cAAc,EAAE,aAAa,OAAO,GAAG,QAAQ,UAAU,IAAI,GAAI,CAAC,CAAC;EACnF,MAAM,IAAI,UAAU,EAAE,IAAI,IAAM,GAAK,CAAC;EACtC,OAAO;CACT,CAAC,CAAC,CAAC;AACL"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { buildBasePost } from "./post.js";
|
|
2
|
+
import { dither, fragCoord, halftone, halftoneCmyk, heatmap, innerLight, paperTexture } from "./postEffects.js";
|
|
3
|
+
import { convertToTexture, renderOutput, screenCoordinate } from "three/tsl";
|
|
4
|
+
import { bloom } from "three/addons/tsl/display/BloomNode.js";
|
|
5
|
+
//#region src/renderer/tsl/postChain.ts
|
|
6
|
+
/**
|
|
7
|
+
* Assembles the post chain in the SAME order the WebGL `EffectComposer` runs it.
|
|
8
|
+
*
|
|
9
|
+
* That order is not arbitrary. `applyPost()` inserts bloom and then innerLight at index 1, so
|
|
10
|
+
* innerLight ends up ahead of bloom; both therefore act on the raw, pre-tone-map scene. The
|
|
11
|
+
* remaining effects are appended AFTER `OutputPass`, so they operate on display-space colour —
|
|
12
|
+
* dithering a linear buffer would crush the steps in the shadows. Reproducing that means placing
|
|
13
|
+
* `renderOutput()` ourselves partway down the chain instead of letting `RenderPipeline` apply it at
|
|
14
|
+
* the end, which is what `outputColorTransform = false` is for.
|
|
15
|
+
*
|
|
16
|
+
* scene → innerLight → +bloom → blur/grain → renderOutput → halftone → heatmap → CMYK → paper → dither
|
|
17
|
+
*
|
|
18
|
+
* Stages that sample an OFFSET coordinate need their input backed by a render target, since a
|
|
19
|
+
* composed node is only defined at the current fragment. `convertToTexture()` materialises those.
|
|
20
|
+
*/
|
|
21
|
+
const asStage = (node) => {
|
|
22
|
+
return convertToTexture(node);
|
|
23
|
+
};
|
|
24
|
+
function buildPostChain(scenePass, u, flags) {
|
|
25
|
+
const coord = fragCoord(screenCoordinate.xy);
|
|
26
|
+
let stage = scenePass;
|
|
27
|
+
if (flags.innerLight) {
|
|
28
|
+
const input = asStage(stage);
|
|
29
|
+
stage = innerLight((at) => input.sample(at), u);
|
|
30
|
+
}
|
|
31
|
+
if (flags.bloom) {
|
|
32
|
+
const src = convertToTexture(stage);
|
|
33
|
+
stage = src.add(bloom(src, u.uBloomStrength.mul(3), u.uBloomRadius, u.uBloomThreshold));
|
|
34
|
+
}
|
|
35
|
+
{
|
|
36
|
+
const input = asStage(stage);
|
|
37
|
+
stage = buildBasePost((at) => input.sample(at), u);
|
|
38
|
+
}
|
|
39
|
+
stage = renderOutput(stage);
|
|
40
|
+
if (flags.halftone) {
|
|
41
|
+
const input = asStage(stage);
|
|
42
|
+
stage = halftone((at) => input.sample(at), u, coord);
|
|
43
|
+
}
|
|
44
|
+
if (flags.heatmap) stage = heatmap(stage, u.uHeatmap);
|
|
45
|
+
if (flags.halftoneCmyk) stage = halftoneCmyk(stage, u.uHalftoneCmyk, u.uHalftoneCmykCell, coord);
|
|
46
|
+
if (flags.paperTexture) stage = paperTexture(stage, u.uPaper, u.uPaperScale, coord);
|
|
47
|
+
if (flags.dither) {
|
|
48
|
+
const input = asStage(stage);
|
|
49
|
+
stage = dither((at) => input.sample(at), u, coord);
|
|
50
|
+
}
|
|
51
|
+
return stage;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { buildPostChain };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=postChain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postChain.js","names":[],"sources":["../../../src/renderer/tsl/postChain.ts"],"sourcesContent":["/**\n * Assembles the post chain in the SAME order the WebGL `EffectComposer` runs it.\n *\n * That order is not arbitrary. `applyPost()` inserts bloom and then innerLight at index 1, so\n * innerLight ends up ahead of bloom; both therefore act on the raw, pre-tone-map scene. The\n * remaining effects are appended AFTER `OutputPass`, so they operate on display-space colour —\n * dithering a linear buffer would crush the steps in the shadows. Reproducing that means placing\n * `renderOutput()` ourselves partway down the chain instead of letting `RenderPipeline` apply it at\n * the end, which is what `outputColorTransform = false` is for.\n *\n * scene → innerLight → +bloom → blur/grain → renderOutput → halftone → heatmap → CMYK → paper → dither\n *\n * Stages that sample an OFFSET coordinate need their input backed by a render target, since a\n * composed node is only defined at the current fragment. `convertToTexture()` materialises those.\n */\nimport { bloom } from \"three/addons/tsl/display/BloomNode.js\";\nimport { convertToTexture, renderOutput, screenCoordinate } from \"three/tsl\";\nimport { buildBasePost, type PostUniforms } from \"./post\";\nimport {\n innerLight,\n halftone,\n heatmap,\n halftoneCmyk,\n paperTexture,\n dither,\n fragCoord,\n type InnerLightUniforms,\n type HalftoneUniforms,\n type DitherUniforms,\n} from \"./postEffects\";\nimport type { FloatUniform, Vec2Node, Vec4Node } from \"./types\";\n\n/** Which effects this chain includes. Changing the set rebuilds the graph, as the WebGL path\n * inserts and removes passes. */\nexport interface PostFlags {\n bloom: boolean;\n innerLight: boolean;\n halftone: boolean;\n heatmap: boolean;\n halftoneCmyk: boolean;\n paperTexture: boolean;\n dither: boolean;\n}\n\nexport interface PostChainUniforms\n extends PostUniforms, InnerLightUniforms, HalftoneUniforms, DitherUniforms {\n uBloomStrength: FloatUniform;\n uBloomRadius: FloatUniform;\n uBloomThreshold: FloatUniform;\n uHeatmap: FloatUniform;\n uHalftoneCmyk: FloatUniform;\n uHalftoneCmykCell: FloatUniform;\n uPaper: FloatUniform;\n uPaperScale: FloatUniform;\n}\n\n/** A node that can be sampled at an arbitrary uv. */\ntype TextureStage = { sample: (at: Vec2Node) => Vec4Node };\n\nconst asStage = (node: unknown): TextureStage => {\n const tex = convertToTexture(node as never) as unknown as TextureStage;\n return tex;\n};\n\nexport function buildPostChain(\n scenePass: unknown,\n u: PostChainUniforms,\n flags: PostFlags,\n): Vec4Node {\n // gl_FragCoord's bottom-left origin, which every screen-space effect below was written against.\n const coord = fragCoord(screenCoordinate.xy as unknown as Vec2Node);\n\n // --- scene zone: acts on linear, pre-tone-map colour ---\n let stage: unknown = scenePass;\n\n if (flags.innerLight) {\n // Bind the input BEFORE reassigning `stage`. TSL evaluates an `Fn` body when the shader is\n // built, not when the function is called, so a closure reading `stage` lazily would resolve to\n // the innerLight node itself — a self-referential graph that recurses until the stack blows.\n const input = asStage(stage);\n stage = innerLight((at) => input.sample(at), u);\n }\n if (flags.bloom) {\n // BloomNode returns the bloom CONTRIBUTION, not the composite — UnrealBloomPass adds it to the\n // base image too, so the sum matches. The input is materialised because BloomNode reads it\n // through its own downsample chain.\n const src = convertToTexture(stage as never);\n // x3 on the strength: UnrealBloomPass composites `3.0 * bloomStrength * sum` — its own comment\n // calls the constant \"backwards compatibility with previous alpha-based intensity\" — while\n // three's TSL BloomNode composites `sum * strength` with no such factor. Passing the authored\n // strength straight through therefore renders bloom at a third of the intensity every existing\n // preset was tuned against.\n stage = src.add(\n bloom(\n src,\n u.uBloomStrength.mul(3) as never,\n u.uBloomRadius as never,\n u.uBloomThreshold as never,\n ),\n );\n }\n\n // --- blur + grain (the always-on base pass) ---\n {\n const input = asStage(stage);\n stage = buildBasePost((at) => input.sample(at), u);\n }\n\n // --- OutputPass: tone mapping + sRGB. Everything after this sees display-space colour. ---\n stage = renderOutput(stage as never);\n\n // --- finish zone ---\n if (flags.halftone) {\n const input = asStage(stage);\n stage = halftone((at) => input.sample(at), u, coord);\n }\n if (flags.heatmap) stage = heatmap(stage as never, u.uHeatmap);\n if (flags.halftoneCmyk) {\n stage = halftoneCmyk(stage as never, u.uHalftoneCmyk, u.uHalftoneCmykCell, coord);\n }\n if (flags.paperTexture) {\n stage = paperTexture(stage as never, u.uPaper, u.uPaperScale, coord);\n }\n if (flags.dither) {\n const input = asStage(stage);\n stage = dither((at) => input.sample(at), u, coord);\n }\n\n return stage as Vec4Node;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2DA,MAAM,WAAW,SAAgC;CAE/C,OADY,iBAAiB,IACpB;AACX;AAEA,SAAgB,eACd,WACA,GACA,OACU;CAEV,MAAM,QAAQ,UAAU,iBAAiB,EAAyB;CAGlE,IAAI,QAAiB;CAErB,IAAI,MAAM,YAAY;EAIpB,MAAM,QAAQ,QAAQ,KAAK;EAC3B,QAAQ,YAAY,OAAO,MAAM,OAAO,EAAE,GAAG,CAAC;CAChD;CACA,IAAI,MAAM,OAAO;EAIf,MAAM,MAAM,iBAAiB,KAAc;EAM3C,QAAQ,IAAI,IACV,MACE,KACA,EAAE,eAAe,IAAI,CAAC,GACtB,EAAE,cACF,EAAE,eACJ,CACF;CACF;CAGA;EACE,MAAM,QAAQ,QAAQ,KAAK;EAC3B,QAAQ,eAAe,OAAO,MAAM,OAAO,EAAE,GAAG,CAAC;CACnD;CAGA,QAAQ,aAAa,KAAc;CAGnC,IAAI,MAAM,UAAU;EAClB,MAAM,QAAQ,QAAQ,KAAK;EAC3B,QAAQ,UAAU,OAAO,MAAM,OAAO,EAAE,GAAG,GAAG,KAAK;CACrD;CACA,IAAI,MAAM,SAAS,QAAQ,QAAQ,OAAgB,EAAE,QAAQ;CAC7D,IAAI,MAAM,cACR,QAAQ,aAAa,OAAgB,EAAE,eAAe,EAAE,mBAAmB,KAAK;CAElF,IAAI,MAAM,cACR,QAAQ,aAAa,OAAgB,EAAE,QAAQ,EAAE,aAAa,KAAK;CAErE,IAAI,MAAM,QAAQ;EAChB,MAAM,QAAQ,QAAQ,KAAK;EAC3B,QAAQ,QAAQ,OAAO,MAAM,OAAO,EAAE,GAAG,GAAG,KAAK;CACnD;CAEA,OAAO;AACT"}
|