@wave3d/core 0.7.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 +78 -4
- package/dist/config/model.js +12 -2
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/presets.js +35 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +146 -12
- package/dist/renderer/WaveRenderer.js +218 -56
- 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 -28
- package/dist/renderer/particleField.js +173 -16
- 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/shaders.js +178 -86
- package/dist/renderer/shaders.js.map +1 -1
- 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 +2219 -1929
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"color.js","names":[],"sources":["../../../src/renderer/tsl/color.ts"],"sourcesContent":["/**\n * Colour: the palette/gradient sampler and the colour grade, in TSL.\n *\n * The port of the `colorFns` GLSL chunk in `../shaders.ts`, shared by both fragment themes exactly\n * as it is there. Uniform-count loops become `Loop` + `Break` rather than JS-level unrolling,\n * because the counts (`uColorCount`, `uMeshPointCount`) are live uniforms the studio changes without\n * rebuilding the material — unrolling them would force a graph rebuild on every stop added.\n */\nimport {\n Fn,\n Loop,\n If,\n Break,\n float,\n vec2,\n vec3,\n cos,\n sin,\n atan,\n dot,\n cross,\n mix,\n max,\n clamp,\n fract,\n length,\n pow,\n radians,\n select,\n} from \"three/tsl\";\nimport { simplexNoise } from \"./noise\";\nimport { MAX_COLORS, MAX_MESH_POINTS } from \"../../config/model\";\nimport type { FloatNode, Vec2Node, Vec3Node } from \"./types\";\nimport type { WaveTslUniforms } from \"./uniforms\";\n\n// The GLSL writes the literal 3.14159265359; it and Math.PI round to the same float32, so the\n// shader sees an identical value either way.\nconst PI = Math.PI;\n\n/** `(v - 0.5) * a + 0.5` */\nconst contrastFn = (v: Vec3Node, a: FloatNode): Vec3Node => v.sub(0.5).mul(a).add(0.5);\n\n/** Pull `color` toward its luminance by `factor`. */\nconst desaturate = (color: Vec3Node, factor: FloatNode): Vec3Node =>\n mix(color, vec3(dot(vec3(0.299, 0.587, 0.114), color)), factor);\n\n/** Rotate hue by `shift` RADIANS about the grey axis (the GLSL takes radians here too). */\nexport const hueShift = (color: Vec3Node, shift: FloatNode): Vec3Node => {\n const g = vec3(0.57735);\n const proj = g.mul(dot(g, color));\n const u = color.sub(proj);\n const w = cross(g, u);\n return u\n .mul(cos(shift))\n .add(w.mul(sin(shift)))\n .add(proj);\n};\n\n/**\n * Interpolate the gradient stops by their positions (`uColorPos` sorted ascending).\n *\n * Faithful to the GLSL, which does NOT break once it finds the bracketing stop — it keeps\n * overwriting for every qualifying `i`, so the LAST qualifying stop wins. That is what makes\n * out-of-order or coincident stops behave the way the studio's presets expect.\n */\nfunction grad(u: WaveTslUniforms, t: FloatNode): Vec3Node {\n const uu = clamp(t, 0, 1).toVar();\n const col = u.uColors.el(0).toVar(\"gradCol\");\n Loop({ start: 0, end: MAX_COLORS - 1, type: \"int\" }, ({ i }) => {\n If(float(i).greaterThanEqual(float(u.uColorCount).sub(1)), () => {\n Break();\n });\n const p0 = u.uColorPos.el(i).toVar();\n const p1 = u.uColorPos.el(float(i).add(1)).toVar();\n If(uu.greaterThanEqual(p0), () => {\n const k = clamp(uu.sub(p0).div(max(p1.sub(p0), 1e-5)), 0, 1);\n col.assign(mix(u.uColors.el(i), u.uColors.el(float(i).add(1)), k));\n });\n });\n return col;\n}\n\n/**\n * iOS-style 2D colour field: each control point contributes an inverse-distance weight, and\n * normalising the sum fills the whole surface without dark seams.\n */\nfunction meshGradient(u: WaveTslUniforms, uvIn: Vec2Node): Vec3Node {\n const colorSum = vec3(0).toVar(\"meshColorSum\");\n const weightSum = float(0).toVar(\"meshWeightSum\");\n const exponent = mix(4.8, 1.35, clamp(u.uMeshSoftness, 0, 1)).toVar();\n Loop({ start: 0, end: MAX_MESH_POINTS, type: \"int\" }, ({ i }) => {\n If(float(i).greaterThanEqual(float(u.uMeshPointCount)), () => {\n Break();\n });\n const influence = max(u.uMeshPointInfluence.el(i), 0.05);\n const d = length(uvIn.sub(u.uMeshPointPos.el(i))).div(influence);\n const weight = float(1).div(pow(max(d, 0.012), exponent).add(0.002));\n colorSum.addAssign(u.uMeshPointColor.el(i).mul(weight));\n weightSum.addAssign(weight);\n });\n return colorSum.div(max(weightSum, 0.0001));\n}\n\n/**\n * Map a surface uv to the 0–1 gradient coordinate per gradient type. `uGradShift` adds a\n * low-frequency simplex warp so the colour varies in 2D — a 2D palette feel rather than flat bands.\n */\nfunction gradCoord(u: WaveTslUniforms, uvIn: Vec2Node): FloatNode {\n const warp = u.uGradShift.mul(simplexNoise(uvIn.mul(1.6).add(4.0))).toVar();\n const radial = clamp(length(uvIn.sub(0.5)).mul(2.0).add(warp), 0, 1);\n const conic = fract(\n atan(uvIn.y.sub(0.5), uvIn.x.sub(0.5))\n .div(2.0 * PI)\n .add(0.5)\n .add(warp),\n );\n const dir = vec2(sin(u.uGradAngle), cos(u.uGradAngle));\n const linear = clamp(dot(uvIn.sub(0.5), dir).add(0.5).add(warp), 0, 1);\n return select(u.uGradType.equal(1), radial, select(u.uGradType.equal(2), conic, linear));\n}\n\n/**\n * One base-colour sample for the whole surface: rotate/scale/offset the raw-palette uv, then pick\n * the mesh field / baked 2D texture / procedural stops by mode. The raw palette is sampled by\n * (uv.x, uv.y) directly; the stops-generated texture goes through `gradCoord` so its angle, type\n * and warp still apply.\n */\nexport function waveBaseColor(u: WaveTslUniforms, uvIn: Vec2Node): Vec3Node {\n const gc = gradCoord(u, uvIn).toVar();\n const mediaCos = cos(u.uPaletteRotation);\n const mediaSin = sin(u.uPaletteRotation);\n const rel = uvIn.sub(0.5).toVar();\n const rotated = vec2(\n mediaCos.mul(rel.x).add(mediaSin.mul(rel.y)),\n mediaSin.negate().mul(rel.x).add(mediaCos.mul(rel.y)),\n );\n const mediaUv = rotated.mul(u.uPaletteScale).add(0.5).add(u.uPaletteOffset);\n const puv = select(\n u.uPaletteRaw.greaterThan(0.5),\n clamp(mediaUv, vec2(0), vec2(1)),\n vec2(gc, clamp(uvIn.y, 0, 1)),\n );\n const sampled = u.uPalette.sample(puv).rgb;\n const proceduralOrTexture = select(u.uUsePalette.greaterThan(0.5), sampled, grad(u, gc));\n return select(u.uGradType.equal(3), meshGradient(u, uvIn), proceduralOrTexture);\n}\n\n/** The shared colour grade: contrast → desaturate → hue rotate (uHueShift is in DEGREES). */\nexport function applyColorGrade(u: WaveTslUniforms, c: Vec3Node): Vec3Node {\n const contrasted = contrastFn(c, u.uContrast);\n const desaturated = desaturate(contrasted, float(1).sub(u.uSaturation));\n return hueShift(desaturated, radians(u.uHueShift));\n}\n\n/** Striation helpers shared with the solid theme. */\nexport const parabola = /*@__PURE__*/ Fn(([x, k]: [FloatNode, FloatNode]) =>\n pow(x.mul(4.0).mul(float(1).sub(x)), k),\n).setLayout({\n name: \"wave_parabola\",\n type: \"float\",\n inputs: [\n { name: \"x\", type: \"float\" },\n { name: \"k\", type: \"float\" },\n ],\n});\n\n/** `mapLinear(v, a, b, c, d)` — the GLSL helper of the same name. */\nexport const mapLinear = (v: FloatNode, a: number, b: number, c: number, d: number): FloatNode =>\n float(c).add(v.sub(a).mul((d - c) / (b - a)));\n"],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { asFloat, asUVec2 } from "./types.js";
|
|
2
|
+
import { Fn, cos, dot, float, floatBitsToUint, floor, max, sin, step, uint, uintBitsToFloat, vec2, vec3 } from "three/tsl";
|
|
3
|
+
//#region src/renderer/tsl/noise.ts
|
|
4
|
+
/**
|
|
5
|
+
* The simplex noise the wave is built from, in TSL.
|
|
6
|
+
*
|
|
7
|
+
* A direct port of the `simplex2d` GLSL chunk in `../shaders.ts` — an xxHash-seeded unit-vector
|
|
8
|
+
* gradient feeding a Gustavson simplex. Keep the two in lockstep: every wave's silhouette comes out
|
|
9
|
+
* of this function, so a divergence here shows up as a different shape on every preset, not as a
|
|
10
|
+
* subtle shading difference. `parity/` is what catches that.
|
|
11
|
+
*
|
|
12
|
+
* The hash needs integer bit ops on the float's raw bits. Those exist in TSL as `floatBitsToUint` /
|
|
13
|
+
* `uintBitsToFloat` and compile to both WGSL (`bitcast<u32>`) and GLSL ES 3.00 (`floatBitsToUint`),
|
|
14
|
+
* so the same graph serves the WebGPU and WebGL backends.
|
|
15
|
+
*/
|
|
16
|
+
/** xxHash32 over the two float lanes, returned as a float in [0,1). */
|
|
17
|
+
const xxhash = /*@__PURE__*/ Fn(([x]) => {
|
|
18
|
+
const t = asUVec2(floatBitsToUint(x)).toVar();
|
|
19
|
+
const h = uint(3266489917).mul(t.x).add(uint(374761401)).toVar();
|
|
20
|
+
h.assign(h.shiftLeft(uint(17)).bitOr(h.shiftRight(uint(15))).mul(uint(668265263)));
|
|
21
|
+
h.addAssign(uint(3266489917).mul(t.y));
|
|
22
|
+
h.assign(h.shiftLeft(uint(17)).bitOr(h.shiftRight(uint(15))).mul(uint(668265263)));
|
|
23
|
+
h.assign(h.bitXor(h.shiftRight(uint(15))));
|
|
24
|
+
h.mulAssign(uint(2246822519));
|
|
25
|
+
h.assign(h.bitXor(h.shiftRight(uint(13))));
|
|
26
|
+
h.mulAssign(uint(3266489917));
|
|
27
|
+
h.assign(h.bitXor(h.shiftRight(uint(16))));
|
|
28
|
+
return asFloat(uintBitsToFloat(h.shiftRight(uint(9)).bitOr(uint(1065353216)))).sub(1);
|
|
29
|
+
}).setLayout({
|
|
30
|
+
name: "wave_xxhash",
|
|
31
|
+
type: "float",
|
|
32
|
+
inputs: [{
|
|
33
|
+
name: "x",
|
|
34
|
+
type: "vec2"
|
|
35
|
+
}]
|
|
36
|
+
});
|
|
37
|
+
/** A unit vector whose angle is the hashed cell — the simplex gradient. */
|
|
38
|
+
const gradHash = /*@__PURE__*/ Fn(([x]) => {
|
|
39
|
+
const k = xxhash(x).mul(6.283185307);
|
|
40
|
+
return vec2(cos(k), sin(k));
|
|
41
|
+
}).setLayout({
|
|
42
|
+
name: "wave_gradHash",
|
|
43
|
+
type: "vec2",
|
|
44
|
+
inputs: [{
|
|
45
|
+
name: "x",
|
|
46
|
+
type: "vec2"
|
|
47
|
+
}]
|
|
48
|
+
});
|
|
49
|
+
const K1 = .366025404;
|
|
50
|
+
const K2 = .211324865;
|
|
51
|
+
/** 2D simplex noise, output roughly in [-1,1]. The GLSL twin is `simplexNoise` in `../shaders.ts`. */
|
|
52
|
+
const simplexNoise = /*@__PURE__*/ Fn(([p]) => {
|
|
53
|
+
const i = floor(p.add(p.x.add(p.y).mul(K1))).toVar();
|
|
54
|
+
const a = p.sub(i).add(i.x.add(i.y).mul(K2)).toVar();
|
|
55
|
+
const m = step(a.y, a.x);
|
|
56
|
+
const o = vec2(m, float(1).sub(m)).toVar();
|
|
57
|
+
const b = a.sub(o).add(K2).toVar();
|
|
58
|
+
const c = a.sub(1).add(2 * K2).toVar();
|
|
59
|
+
const h = max(float(.5).sub(vec3(dot(a, a), dot(b, b), dot(c, c))), 0).toVar();
|
|
60
|
+
return dot(h.mul(h).mul(h).mul(vec3(dot(a, gradHash(i)), dot(b, gradHash(i.add(o))), dot(c, gradHash(i.add(1))))), vec3(32.99));
|
|
61
|
+
}).setLayout({
|
|
62
|
+
name: "wave_simplexNoise",
|
|
63
|
+
type: "float",
|
|
64
|
+
inputs: [{
|
|
65
|
+
name: "p",
|
|
66
|
+
type: "vec2"
|
|
67
|
+
}]
|
|
68
|
+
});
|
|
69
|
+
/** The cheap value hash behind the optional grain overlay (`grainHash` in `../shaders.ts`). */
|
|
70
|
+
const grainHash = /*@__PURE__*/ Fn(([p]) => {
|
|
71
|
+
return sin(dot(p, vec2(12.9898, 78.233))).mul(43758.5453).fract();
|
|
72
|
+
}).setLayout({
|
|
73
|
+
name: "wave_grainHash",
|
|
74
|
+
type: "float",
|
|
75
|
+
inputs: [{
|
|
76
|
+
name: "p",
|
|
77
|
+
type: "vec2"
|
|
78
|
+
}]
|
|
79
|
+
});
|
|
80
|
+
//#endregion
|
|
81
|
+
export { grainHash, simplexNoise };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=noise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noise.js","names":[],"sources":["../../../src/renderer/tsl/noise.ts"],"sourcesContent":["/**\n * The simplex noise the wave is built from, in TSL.\n *\n * A direct port of the `simplex2d` GLSL chunk in `../shaders.ts` — an xxHash-seeded unit-vector\n * gradient feeding a Gustavson simplex. Keep the two in lockstep: every wave's silhouette comes out\n * of this function, so a divergence here shows up as a different shape on every preset, not as a\n * subtle shading difference. `parity/` is what catches that.\n *\n * The hash needs integer bit ops on the float's raw bits. Those exist in TSL as `floatBitsToUint` /\n * `uintBitsToFloat` and compile to both WGSL (`bitcast<u32>`) and GLSL ES 3.00 (`floatBitsToUint`),\n * so the same graph serves the WebGPU and WebGL backends.\n */\nimport { asFloat, asUVec2, type Vec2Node } from \"./types\";\nimport {\n Fn,\n float,\n uint,\n uintBitsToFloat,\n floatBitsToUint,\n vec2,\n vec3,\n cos,\n sin,\n floor,\n step,\n max,\n dot,\n} from \"three/tsl\";\n\n/** xxHash32 over the two float lanes, returned as a float in [0,1). */\nconst xxhash = /*@__PURE__*/ Fn(([x]: [Vec2Node]) => {\n const t = asUVec2(floatBitsToUint(x)).toVar();\n // Every step below is a u32 op; both targets wrap on overflow, which is what the hash relies on.\n const h = uint(0xc2b2ae3d).mul(t.x).add(uint(0x165667b9)).toVar();\n h.assign(\n h\n .shiftLeft(uint(17))\n .bitOr(h.shiftRight(uint(15)))\n .mul(uint(0x27d4eb2f)),\n );\n h.addAssign(uint(0xc2b2ae3d).mul(t.y));\n h.assign(\n h\n .shiftLeft(uint(17))\n .bitOr(h.shiftRight(uint(15)))\n .mul(uint(0x27d4eb2f)),\n );\n h.assign(h.bitXor(h.shiftRight(uint(15))));\n h.mulAssign(uint(0x85ebca77));\n h.assign(h.bitXor(h.shiftRight(uint(13))));\n h.mulAssign(uint(0xc2b2ae3d));\n h.assign(h.bitXor(h.shiftRight(uint(16))));\n // Take the top mantissa bits into the exponent of 1.0 → a float in [1,2), then shift to [0,1).\n return asFloat(uintBitsToFloat(h.shiftRight(uint(9)).bitOr(uint(0x3f800000)))).sub(1.0);\n}).setLayout({\n name: \"wave_xxhash\",\n type: \"float\",\n inputs: [{ name: \"x\", type: \"vec2\" }],\n});\n\n/** A unit vector whose angle is the hashed cell — the simplex gradient. */\nconst gradHash = /*@__PURE__*/ Fn(([x]: [Vec2Node]) => {\n const k = xxhash(x).mul(6.283185307);\n return vec2(cos(k), sin(k));\n}).setLayout({\n name: \"wave_gradHash\",\n type: \"vec2\",\n inputs: [{ name: \"x\", type: \"vec2\" }],\n});\n\nconst K1 = 0.366025404; // (sqrt(3)-1)/2\nconst K2 = 0.211324865; // (3-sqrt(3))/6\n\n/** 2D simplex noise, output roughly in [-1,1]. The GLSL twin is `simplexNoise` in `../shaders.ts`. */\nexport const simplexNoise = /*@__PURE__*/ Fn(([p]: [Vec2Node]) => {\n const i = floor(p.add(p.x.add(p.y).mul(K1))).toVar();\n const a = p.sub(i).add(i.x.add(i.y).mul(K2)).toVar();\n const m = step(a.y, a.x);\n const o = vec2(m, float(1.0).sub(m)).toVar();\n const b = a.sub(o).add(K2).toVar();\n const c = a\n .sub(1.0)\n .add(2.0 * K2)\n .toVar();\n const h = max(float(0.5).sub(vec3(dot(a, a), dot(b, b), dot(c, c))), 0.0).toVar();\n const n = h\n .mul(h)\n .mul(h)\n .mul(vec3(dot(a, gradHash(i)), dot(b, gradHash(i.add(o))), dot(c, gradHash(i.add(1.0)))));\n return dot(n, vec3(32.99)); // analytic normalisation (= 2916*sqrt(2)/125)\n}).setLayout({\n name: \"wave_simplexNoise\",\n type: \"float\",\n inputs: [{ name: \"p\", type: \"vec2\" }],\n});\n\n/** The cheap value hash behind the optional grain overlay (`grainHash` in `../shaders.ts`). */\nexport const grainHash = /*@__PURE__*/ Fn(([p]: [Vec2Node]) => {\n return sin(dot(p, vec2(12.9898, 78.233)))\n .mul(43758.5453)\n .fract();\n}).setLayout({\n name: \"wave_grainHash\",\n type: \"float\",\n inputs: [{ name: \"p\", type: \"vec2\" }],\n});\n"],"mappings":"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"}
|
|
@@ -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"}
|