reze-engine 0.54.16 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +210 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1063 -11
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -0
- package/dist/overlay.d.ts +200 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +806 -0
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/pmx-document.d.ts +218 -0
- package/dist/pmx-document.d.ts.map +1 -0
- package/dist/pmx-document.js +685 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/overlay.d.ts +3 -0
- package/dist/shaders/passes/overlay.d.ts.map +1 -0
- package/dist/shaders/passes/overlay.js +164 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/wireframe.d.ts +2 -0
- package/dist/shaders/passes/wireframe.d.ts.map +1 -0
- package/dist/shaders/passes/wireframe.js +107 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/package.json +1 -1
- package/src/engine.ts +1147 -13
- package/src/index.ts +50 -0
- package/src/model.ts +9 -0
- package/src/overlay.ts +1000 -0
- package/src/pmx-document.ts +882 -0
- package/src/shaders/passes/overlay.ts +166 -0
- package/src/shaders/passes/wireframe.ts +108 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sceneFsOutWgsl } from "./scene-contract";
|
|
2
|
+
/**
|
|
3
|
+
* The field layer, drawn INTO the scene.
|
|
4
|
+
*
|
|
5
|
+
* Field effects render into their own targets at their own resolution — that
|
|
6
|
+
* part was always right. What was wrong was where the result landed: the
|
|
7
|
+
* composite sampled it AFTER tone mapping, which is why a field effect could
|
|
8
|
+
* never bloom, could never light anything, and tone mapped differently from the
|
|
9
|
+
* particle and ribbon mounts drawing the same kind of glow.
|
|
10
|
+
*
|
|
11
|
+
* Two draws inside the scene pass — background before any geometry, foreground
|
|
12
|
+
* after all of it — so the bloom pyramid sees field effects and one tone map
|
|
13
|
+
* covers the whole frame.
|
|
14
|
+
*
|
|
15
|
+
* NO DEPTH, neither written nor tested. A foreground is handed the scene's
|
|
16
|
+
* depth as a parameter and does its own occlusion; testing it here would
|
|
17
|
+
* occlude it a second time, and a background has nothing to test against.
|
|
18
|
+
*
|
|
19
|
+
* COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
|
|
20
|
+
* layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
|
|
21
|
+
* unpremultiply from dividing colour by nothing — but N additive effects can
|
|
22
|
+
* sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
|
|
23
|
+
* A coverage above 1 would darken what the layer is drawn over by more than
|
|
24
|
+
* fully covering it.
|
|
25
|
+
*/
|
|
26
|
+
export function fieldBlitShaderWgsl() {
|
|
27
|
+
return /* wgsl */ `
|
|
28
|
+
@group(0) @binding(0) var _rzLayer: texture_2d<f32>;
|
|
29
|
+
@group(0) @binding(1) var _rzLayerSamp: sampler;
|
|
30
|
+
// (bloom, _, fullW, fullH). The size is the SCENE target's, not the layer's:
|
|
31
|
+
// the layer is often half resolution, and normalised uv is what makes that
|
|
32
|
+
// invisible.
|
|
33
|
+
@group(0) @binding(2) var<uniform> _rzBlitU: vec4f;
|
|
34
|
+
|
|
35
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
36
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
37
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
38
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${sceneFsOutWgsl()}
|
|
42
|
+
@fragment fn fs(@builtin(position) fragCoord: vec4f) -> FSOut {
|
|
43
|
+
var out: FSOut;
|
|
44
|
+
// Bilinear, because the layer is usually half resolution and the whole point
|
|
45
|
+
// of that choice is that low-frequency effects upsample invisibly.
|
|
46
|
+
let uv = fragCoord.xy / max(_rzBlitU.zw, vec2f(1.0));
|
|
47
|
+
let c = textureSampleLevel(_rzLayer, _rzLayerSamp, uv, 0.0);
|
|
48
|
+
let cov = clamp(c.a, 0.0, 1.0);
|
|
49
|
+
// PREMULTIPLIED, and blitted with a premultiplied-over blend — the field
|
|
50
|
+
// targets have been premultiplied since N effects had to compose into them
|
|
51
|
+
// associatively. Handing this to the material blend would scale rgb by alpha
|
|
52
|
+
// a second time and every layer would come out dark.
|
|
53
|
+
out.color = vec4f(max(c.rgb, vec3f(0.0)), cov);
|
|
54
|
+
// The aux target: bloom mask, then coverage. THIS is what finally makes
|
|
55
|
+
// // @bloom mean something on a field effect — the flag has been parsed and
|
|
56
|
+
// ignored for every mount but particles and ribbons, which is the kind of
|
|
57
|
+
// author-surface lie the install-time guards exist to kill.
|
|
58
|
+
//
|
|
59
|
+
// The mask is written UNWEIGHTED and the blend weights it by src.a — the
|
|
60
|
+
// convention the materials already follow (they write vec4f(1, 1, 0, alpha)).
|
|
61
|
+
out.mask = vec4f(_rzBlitU.x, 1.0, 0.0, cov);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
|
|
3
|
+
* for a repeating texture). Mips are the point as much as the speed: procedural
|
|
4
|
+
* noise has no mip chain, so at grazing angles it shimmered.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildGroundNoiseMips(): Uint8Array[];
|
|
7
|
+
//# sourceMappingURL=ground-noise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ground-noise.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground-noise.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAsCnD"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Bakes the ground's frosted micro-texture into a seamless, mipmapped tile.
|
|
2
|
+
//
|
|
3
|
+
// The shader used to evaluate four octaves of value noise per pixel — sixteen
|
|
4
|
+
// hash rounds — and the ground covers the entire screen as soon as the camera
|
|
5
|
+
// tilts toward the horizon. This produces the same character of pattern once, at
|
|
6
|
+
// load, so the fragment shader pays one filtered fetch instead.
|
|
7
|
+
//
|
|
8
|
+
// SEAMLESS BY CONSTRUCTION. The original hash was not periodic, so a finite
|
|
9
|
+
// crop of it could never tile. Here every octave's lattice wraps at a whole
|
|
10
|
+
// number of cells across the tile, which makes the result repeat exactly at the
|
|
11
|
+
// edges. The pattern is therefore not pixel-identical to the analytic version —
|
|
12
|
+
// it is the same kind of noise, not the same noise. At the default strength of
|
|
13
|
+
// 0.05 it modulates brightness by ±2.5%, where the repeat is not findable by eye.
|
|
14
|
+
import { GROUND_NOISE_RES, GROUND_NOISE_TILE_WORLD } from "./ground";
|
|
15
|
+
/** Lattice cells across the tile at octave 0. The analytic version sampled fbm
|
|
16
|
+
* at worldXZ × 3, so a tile spanning 8 world units covers 24 base cells — the
|
|
17
|
+
* same spatial frequency the surface had before. */
|
|
18
|
+
const BASE_CELLS = GROUND_NOISE_TILE_WORLD * 3;
|
|
19
|
+
const OCTAVES = 4;
|
|
20
|
+
/** Deterministic lattice hash. Integer coordinates are wrapped by the caller,
|
|
21
|
+
* which is what makes the tile seamless. */
|
|
22
|
+
function hash(ix, iy) {
|
|
23
|
+
let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
|
|
24
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
25
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
26
|
+
}
|
|
27
|
+
/** Value noise on a lattice that repeats every `period` cells. */
|
|
28
|
+
function periodicValueNoise(x, y, period) {
|
|
29
|
+
const ix = Math.floor(x);
|
|
30
|
+
const iy = Math.floor(y);
|
|
31
|
+
const fx = x - ix;
|
|
32
|
+
const fy = y - iy;
|
|
33
|
+
// Smoothstep weights — same curve the WGSL used.
|
|
34
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
35
|
+
const uy = fy * fy * (3 - 2 * fy);
|
|
36
|
+
const x0 = ((ix % period) + period) % period;
|
|
37
|
+
const y0 = ((iy % period) + period) % period;
|
|
38
|
+
const x1 = (x0 + 1) % period;
|
|
39
|
+
const y1 = (y0 + 1) % period;
|
|
40
|
+
const a = hash(x0, y0);
|
|
41
|
+
const b = hash(x1, y0);
|
|
42
|
+
const c = hash(x0, y1);
|
|
43
|
+
const d = hash(x1, y1);
|
|
44
|
+
return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
|
|
48
|
+
* for a repeating texture). Mips are the point as much as the speed: procedural
|
|
49
|
+
* noise has no mip chain, so at grazing angles it shimmered.
|
|
50
|
+
*/
|
|
51
|
+
export function buildGroundNoiseMips() {
|
|
52
|
+
const levels = [];
|
|
53
|
+
const res = GROUND_NOISE_RES;
|
|
54
|
+
const base = new Uint8Array(res * res);
|
|
55
|
+
for (let py = 0; py < res; py++) {
|
|
56
|
+
for (let px = 0; px < res; px++) {
|
|
57
|
+
// Texel centre, in units of octave-0 lattice cells.
|
|
58
|
+
const x = ((px + 0.5) / res) * BASE_CELLS;
|
|
59
|
+
const y = ((py + 0.5) / res) * BASE_CELLS;
|
|
60
|
+
let v = 0;
|
|
61
|
+
let amp = 0.5;
|
|
62
|
+
let freq = 1;
|
|
63
|
+
for (let o = 0; o < OCTAVES; o++) {
|
|
64
|
+
v += amp * periodicValueNoise(x * freq, y * freq, BASE_CELLS * freq);
|
|
65
|
+
freq *= 2;
|
|
66
|
+
amp *= 0.5;
|
|
67
|
+
}
|
|
68
|
+
base[py * res + px] = Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
levels.push(base);
|
|
72
|
+
let src = base;
|
|
73
|
+
let size = res;
|
|
74
|
+
while (size > 1) {
|
|
75
|
+
const half = size >> 1;
|
|
76
|
+
const dst = new Uint8Array(half * half);
|
|
77
|
+
for (let y = 0; y < half; y++) {
|
|
78
|
+
for (let x = 0; x < half; x++) {
|
|
79
|
+
const s = y * 2 * size + x * 2;
|
|
80
|
+
dst[y * half + x] = (src[s] + src[s + 1] + src[s + size] + src[s + size + 1] + 2) >> 2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
levels.push(dst);
|
|
84
|
+
src = dst;
|
|
85
|
+
size = half;
|
|
86
|
+
}
|
|
87
|
+
return levels;
|
|
88
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const OVERLAY_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };\n// 16 bytes exactly. Scalars rather than a vec3f tail: a vec3f aligns to 16 and\n// would push the struct past the buffer the pipeline is handed.\nstruct OverlayUniforms { viewport: vec2f, dashPeriodPx: f32, _pad0: f32 };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> overlay: OverlayUniforms;\n\nstruct VSOut {\n @builtin(position) pos: vec4f,\n @location(0) @interpolate(flat) color: vec4f,\n}\n\nfn qrot(q: vec4f, v: vec3f) -> vec3f {\n let t = 2.0 * cross(q.xyz, v);\n return v + q.w * t + cross(q.xyz, t);\n}\n\n// Placement mode, carried per VERTEX because it belongs to the shape rather than\n// to the copy.\n//\n// 0 world \u2014 rotate, scale, translate. The shapes that ARE geometry:\n// rigidbody volumes, joint crosses, plain lines.\n// 3 facing \u2014 a flat filled shape in the camera's plane, radius scale.x.\n// The bone marker's solid centre.\n// 5 solid \u2014 placed in world like mode 0, but filled: the rigidbody\n// volumes, which read as volumes rather than as a cage of lines.\n// 4 annulus \u2014 the marker's ring: world radius scale.x, and a stroke that is\n// a PIXEL width like every other line, so the two links leave it\n// as a continuation of the same stroke rather than a step. The\n// inner and outer rims are offset radially in screen space,\n// which is what lets a small circle carry a thick stroke \u2014 the\n// stroked polyline that preceded it tore into a sunburst as soon\n// as the width passed the segment length.\n// 2 link \u2014 the taper from a bone to its child: along the bone, splayed\n// across it in whichever direction the camera can actually see,\n// so the two lines never collapse into one.\nfn placeWorld(p: vec3f, capSign: f32, iRot: vec4f, iPos: vec4f, iScale: vec4f, mode: f32) -> vec3f {\n let right = vec3f(camera.view[0].x, camera.view[1].x, camera.view[2].x);\n let up = vec3f(camera.view[0].y, camera.view[1].y, camera.view[2].y);\n\n if (mode > 2.5) {\n return iPos.xyz + (right * p.x + up * p.y) * iScale.x;\n }\n if (mode > 1.5) {\n let boneDir = normalize(qrot(iRot, vec3f(0.0, 1.0, 0.0)));\n let camFwd = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);\n var splay = cross(boneDir, camFwd);\n let sl = length(splay);\n // Looking straight down the bone there is no visible across, and any\n // perpendicular does \u2014 the taper is a dot on screen either way.\n splay = select(right, splay / max(sl, 1e-6), sl > 1e-4);\n return iPos.xyz + splay * (p.x * iScale.x) + boneDir * (p.y * iScale.y);\n }\n return qrot(iRot, p * iScale.xyz + vec3f(0.0, capSign * iPos.w, 0.0)) + iPos.xyz;\n}\n\n@vertex fn vs(\n @location(0) pos: vec3f,\n @location(1) dir: vec3f,\n @location(2) caps: vec2f,\n @location(3) side: f32,\n @location(4) t: f32,\n @location(5) mode: f32,\n @location(6) iRot: vec4f,\n @location(7) iPos: vec4f,\n @location(8) iScale: vec4f,\n @location(9) iColor: vec4f,\n) -> VSOut {\n let vp = camera.projection * camera.view;\n let half = 0.5 * overlay.viewport;\n\n var out: VSOut;\n out.color = iColor;\n\n // A solid volume is placed in world, like a stroked shape, but filled.\n if (mode > 4.5) {\n out.pos = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, 0.0), 1.0);\n return out;\n }\n\n // The marker's ring: a circle of world radius, stroked to a pixel width.\n if (mode > 3.5) {\n let centre = vp * vec4f(iPos.xyz, 1.0);\n let w = max(abs(centre.w), 1e-6);\n let pxPerWorld = camera.projection[1].y * half.y / w;\n // side is -1 on the inner rim and +1 on the outer.\n let radiusPx = iScale.x * pxPerWorld + side * abs(iScale.w) * 0.5;\n let radial = normalize(pos.xy);\n out.pos = vec4f(centre.xy + ((radial * radiusPx) / half) * centre.w, centre.z, centre.w);\n return out;\n }\n\n let c0 = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, mode), 1.0);\n\n // A filled vertex is already where it belongs.\n if (mode > 2.5) {\n out.pos = c0;\n return out;\n }\n\n let c1 = vp * vec4f(placeWorld(pos + dir, caps.y, iRot, iPos, iScale, mode), 1.0);\n let w0 = max(abs(c0.w), 1e-6);\n let w1 = max(abs(c1.w), 1e-6);\n let s0 = (c0.xy / w0) * half;\n let s1 = (c1.xy / w1) * half;\n let delta = s1 - s0;\n let segLenPx = max(length(delta), 1e-6);\n let tangent = delta / segLenPx;\n let normalPx = vec2f(-tangent.y, tangent.x);\n let offsetPx = normalPx * side * abs(iScale.w) * 0.5;\n out.pos = vec4f(c0.xy + (offsetPx / half) * c0.w, c0.z, c0.w);\n return out;\n}\n\n@fragment fn fs(in: VSOut) -> @location(0) vec4f {\n return vec4f(in.color.rgb * in.color.a, in.color.a);\n}\n";
|
|
2
|
+
export declare const OVERLAY_COMPOSITE_SHADER_WGSL = "\n@group(0) @binding(0) var overlayTex: texture_2d<f32>;\n\n@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n let x = f32((vi & 1u) << 2u) - 1.0;\n let y = f32((vi & 2u) << 1u) - 1.0;\n return vec4f(x, y, 0.0, 1.0);\n}\n\n@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {\n return textureLoad(overlayTex, vec2<i32>(p.xy), 0);\n}\n";
|
|
3
|
+
//# sourceMappingURL=overlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/overlay.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,mBAAmB,81JAuH/B,CAAA;AAID,eAAO,MAAM,6BAA6B,kYAYzC,CAAA"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Editor overlays — bones, rigidbodies and joints — as one instanced pass.
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of geometry, because thick lines and thick rings want different
|
|
4
|
+
// answers.
|
|
5
|
+
//
|
|
6
|
+
// STROKED shapes (mode 0, 2) expand each segment into a screen-space quad, so
|
|
7
|
+
// their width is a pixel count and holds at any zoom. That works while a segment
|
|
8
|
+
// is longer on screen than the stroke is wide; past that, neighbouring quads
|
|
9
|
+
// meet at a corner nothing mitres and a circle drawn this way turns into a
|
|
10
|
+
// sunburst. So the one shape that must be both small and thick — the bone
|
|
11
|
+
// marker — is not stroked at all.
|
|
12
|
+
//
|
|
13
|
+
// FILLED shapes (mode 3) are triangles already: the marker's ring is an annulus
|
|
14
|
+
// and its centre is a disc, both facing the camera. No joins exist to get wrong,
|
|
15
|
+
// and the ring can be as heavy as it likes.
|
|
16
|
+
//
|
|
17
|
+
// The layer is 4x multisampled and composited over the finished frame rather
|
|
18
|
+
// than drawn into it: the swapchain is single-sample and an MSAA attachment
|
|
19
|
+
// cannot load from one. So fragments here are PREMULTIPLIED — rgb already
|
|
20
|
+
// carries alpha — and the two blends compose correctly, once against a
|
|
21
|
+
// transparent layer and once onto the frame.
|
|
22
|
+
//
|
|
23
|
+
// The capsule is why `caps` exists. A PMX capsule is a radius and a cylinder
|
|
24
|
+
// length, so scaling a unit capsule non-uniformly would stretch its hemispheres
|
|
25
|
+
// into ellipsoids. Instead the unit shape carries both caps at the origin and
|
|
26
|
+
// each vertex names which cap it belongs to (-1, 0, +1); the VS pushes it along
|
|
27
|
+
// local Y by that sign times `extent`. A segment can straddle the two — the
|
|
28
|
+
// cylinder's side lines run from +1 to -1 — so `caps` holds the sign at BOTH
|
|
29
|
+
// ends, not one.
|
|
30
|
+
export const OVERLAY_SHADER_WGSL = /* wgsl */ `
|
|
31
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
32
|
+
// 16 bytes exactly. Scalars rather than a vec3f tail: a vec3f aligns to 16 and
|
|
33
|
+
// would push the struct past the buffer the pipeline is handed.
|
|
34
|
+
struct OverlayUniforms { viewport: vec2f, dashPeriodPx: f32, _pad0: f32 };
|
|
35
|
+
|
|
36
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
37
|
+
@group(0) @binding(1) var<uniform> overlay: OverlayUniforms;
|
|
38
|
+
|
|
39
|
+
struct VSOut {
|
|
40
|
+
@builtin(position) pos: vec4f,
|
|
41
|
+
@location(0) @interpolate(flat) color: vec4f,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn qrot(q: vec4f, v: vec3f) -> vec3f {
|
|
45
|
+
let t = 2.0 * cross(q.xyz, v);
|
|
46
|
+
return v + q.w * t + cross(q.xyz, t);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Placement mode, carried per VERTEX because it belongs to the shape rather than
|
|
50
|
+
// to the copy.
|
|
51
|
+
//
|
|
52
|
+
// 0 world — rotate, scale, translate. The shapes that ARE geometry:
|
|
53
|
+
// rigidbody volumes, joint crosses, plain lines.
|
|
54
|
+
// 3 facing — a flat filled shape in the camera's plane, radius scale.x.
|
|
55
|
+
// The bone marker's solid centre.
|
|
56
|
+
// 5 solid — placed in world like mode 0, but filled: the rigidbody
|
|
57
|
+
// volumes, which read as volumes rather than as a cage of lines.
|
|
58
|
+
// 4 annulus — the marker's ring: world radius scale.x, and a stroke that is
|
|
59
|
+
// a PIXEL width like every other line, so the two links leave it
|
|
60
|
+
// as a continuation of the same stroke rather than a step. The
|
|
61
|
+
// inner and outer rims are offset radially in screen space,
|
|
62
|
+
// which is what lets a small circle carry a thick stroke — the
|
|
63
|
+
// stroked polyline that preceded it tore into a sunburst as soon
|
|
64
|
+
// as the width passed the segment length.
|
|
65
|
+
// 2 link — the taper from a bone to its child: along the bone, splayed
|
|
66
|
+
// across it in whichever direction the camera can actually see,
|
|
67
|
+
// so the two lines never collapse into one.
|
|
68
|
+
fn placeWorld(p: vec3f, capSign: f32, iRot: vec4f, iPos: vec4f, iScale: vec4f, mode: f32) -> vec3f {
|
|
69
|
+
let right = vec3f(camera.view[0].x, camera.view[1].x, camera.view[2].x);
|
|
70
|
+
let up = vec3f(camera.view[0].y, camera.view[1].y, camera.view[2].y);
|
|
71
|
+
|
|
72
|
+
if (mode > 2.5) {
|
|
73
|
+
return iPos.xyz + (right * p.x + up * p.y) * iScale.x;
|
|
74
|
+
}
|
|
75
|
+
if (mode > 1.5) {
|
|
76
|
+
let boneDir = normalize(qrot(iRot, vec3f(0.0, 1.0, 0.0)));
|
|
77
|
+
let camFwd = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);
|
|
78
|
+
var splay = cross(boneDir, camFwd);
|
|
79
|
+
let sl = length(splay);
|
|
80
|
+
// Looking straight down the bone there is no visible across, and any
|
|
81
|
+
// perpendicular does — the taper is a dot on screen either way.
|
|
82
|
+
splay = select(right, splay / max(sl, 1e-6), sl > 1e-4);
|
|
83
|
+
return iPos.xyz + splay * (p.x * iScale.x) + boneDir * (p.y * iScale.y);
|
|
84
|
+
}
|
|
85
|
+
return qrot(iRot, p * iScale.xyz + vec3f(0.0, capSign * iPos.w, 0.0)) + iPos.xyz;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@vertex fn vs(
|
|
89
|
+
@location(0) pos: vec3f,
|
|
90
|
+
@location(1) dir: vec3f,
|
|
91
|
+
@location(2) caps: vec2f,
|
|
92
|
+
@location(3) side: f32,
|
|
93
|
+
@location(4) t: f32,
|
|
94
|
+
@location(5) mode: f32,
|
|
95
|
+
@location(6) iRot: vec4f,
|
|
96
|
+
@location(7) iPos: vec4f,
|
|
97
|
+
@location(8) iScale: vec4f,
|
|
98
|
+
@location(9) iColor: vec4f,
|
|
99
|
+
) -> VSOut {
|
|
100
|
+
let vp = camera.projection * camera.view;
|
|
101
|
+
let half = 0.5 * overlay.viewport;
|
|
102
|
+
|
|
103
|
+
var out: VSOut;
|
|
104
|
+
out.color = iColor;
|
|
105
|
+
|
|
106
|
+
// A solid volume is placed in world, like a stroked shape, but filled.
|
|
107
|
+
if (mode > 4.5) {
|
|
108
|
+
out.pos = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, 0.0), 1.0);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The marker's ring: a circle of world radius, stroked to a pixel width.
|
|
113
|
+
if (mode > 3.5) {
|
|
114
|
+
let centre = vp * vec4f(iPos.xyz, 1.0);
|
|
115
|
+
let w = max(abs(centre.w), 1e-6);
|
|
116
|
+
let pxPerWorld = camera.projection[1].y * half.y / w;
|
|
117
|
+
// side is -1 on the inner rim and +1 on the outer.
|
|
118
|
+
let radiusPx = iScale.x * pxPerWorld + side * abs(iScale.w) * 0.5;
|
|
119
|
+
let radial = normalize(pos.xy);
|
|
120
|
+
out.pos = vec4f(centre.xy + ((radial * radiusPx) / half) * centre.w, centre.z, centre.w);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let c0 = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, mode), 1.0);
|
|
125
|
+
|
|
126
|
+
// A filled vertex is already where it belongs.
|
|
127
|
+
if (mode > 2.5) {
|
|
128
|
+
out.pos = c0;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let c1 = vp * vec4f(placeWorld(pos + dir, caps.y, iRot, iPos, iScale, mode), 1.0);
|
|
133
|
+
let w0 = max(abs(c0.w), 1e-6);
|
|
134
|
+
let w1 = max(abs(c1.w), 1e-6);
|
|
135
|
+
let s0 = (c0.xy / w0) * half;
|
|
136
|
+
let s1 = (c1.xy / w1) * half;
|
|
137
|
+
let delta = s1 - s0;
|
|
138
|
+
let segLenPx = max(length(delta), 1e-6);
|
|
139
|
+
let tangent = delta / segLenPx;
|
|
140
|
+
let normalPx = vec2f(-tangent.y, tangent.x);
|
|
141
|
+
let offsetPx = normalPx * side * abs(iScale.w) * 0.5;
|
|
142
|
+
out.pos = vec4f(c0.xy + (offsetPx / half) * c0.w, c0.z, c0.w);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
147
|
+
return vec4f(in.color.rgb * in.color.a, in.color.a);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
// Lays the resolved overlay layer over the finished frame. Its input is
|
|
151
|
+
// premultiplied, so the blend is a straight `src + dst * (1 - src.a)`.
|
|
152
|
+
export const OVERLAY_COMPOSITE_SHADER_WGSL = /* wgsl */ `
|
|
153
|
+
@group(0) @binding(0) var overlayTex: texture_2d<f32>;
|
|
154
|
+
|
|
155
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
156
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
157
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
158
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
162
|
+
return textureLoad(overlayTex, vec2<i32>(p.xy), 0);
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CastLayout } from "./particles";
|
|
2
|
+
export declare const SIM_MAX = 1024;
|
|
3
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
4
|
+
export declare const SIM_FORMAT: GPUTextureFormat;
|
|
5
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
6
|
+
export declare function parseSimSize(wgsl: string, max: number): number;
|
|
7
|
+
/** Whether this effect drives a grid at all. */
|
|
8
|
+
export declare function simEntryPoint(wgsl: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
11
|
+
*
|
|
12
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
13
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
14
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
15
|
+
*/
|
|
16
|
+
export declare function simReadApi(group: number, tex: number, samp: number, size: number): string;
|
|
17
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
18
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
19
|
+
export declare function simClockApi(timeExpr: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The step shader: one invocation per texel, once per frame.
|
|
22
|
+
*
|
|
23
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
24
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
25
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
26
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
27
|
+
*
|
|
28
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
29
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
30
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
31
|
+
* what turns smoke into a staircase.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildSimShader(wgsl: string, size: number, _cast: CastLayout): string;
|
|
34
|
+
//# sourceMappingURL=sim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/sim.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAgC7C,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BzF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAwDpF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { audioApi } from "../audio-api";
|
|
2
|
+
import { scoreApi } from "../score-api";
|
|
3
|
+
import { EFFECT_SCENE_API } from "./composite";
|
|
4
|
+
// A persistent grid an effect can step and read: the one thing an effect could
|
|
5
|
+
// not have before, which is MEMORY.
|
|
6
|
+
//
|
|
7
|
+
// Every other mount is a pure function of position and time. A field shader
|
|
8
|
+
// recomputes its noise from scratch every frame; a trail reads history the
|
|
9
|
+
// engine happens to keep. Neither can build on what it produced last frame, and
|
|
10
|
+
// a whole class of effect is nothing but that:
|
|
11
|
+
//
|
|
12
|
+
// Fog that a dancer walks through does not "look displaced near her" — it IS
|
|
13
|
+
// displaced, and stays displaced, and creeps back over seconds. Water does not
|
|
14
|
+
// look rippled where a foot landed; a ripple leaves and propagates and
|
|
15
|
+
// reflects off the far edge long after the foot is gone. Both are the same
|
|
16
|
+
// shape of thing — a grid, stepped by its own previous value.
|
|
17
|
+
//
|
|
18
|
+
// It is also the only way to get the LOOK of a fluid, not merely its motion.
|
|
19
|
+
// Advection compounds: each frame's field is the last one pushed sideways, and a
|
|
20
|
+
// few hundred frames of that stretches and folds a smooth field into filaments
|
|
21
|
+
// and vortex sheets finer than the grid storing them. No noise function
|
|
22
|
+
// produces that, at any octave count, because the structure is not a function of
|
|
23
|
+
// position at all — it is a function of history.
|
|
24
|
+
//
|
|
25
|
+
// The engine owns the ping-pong, the dispatch and the sampling. The author owns
|
|
26
|
+
// the KERNEL, which is the part that differs: advection for smoke, the wave
|
|
27
|
+
// equation for water, a decaying stamp for footprints. That split is the same
|
|
28
|
+
// one the particle mounts make, and for the same reason — the loop is identical
|
|
29
|
+
// in every such effect and the physics never is.
|
|
30
|
+
// A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
|
|
31
|
+
// actually bounds this is the STEP: one invocation per texel per frame, each
|
|
32
|
+
// taking a dozen samples, so doubling the side quadruples the work.
|
|
33
|
+
export const SIM_MAX = 1024;
|
|
34
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
35
|
+
export const SIM_FORMAT = "rgba16float";
|
|
36
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
37
|
+
export function parseSimSize(wgsl, max) {
|
|
38
|
+
const m = /^\s*\/\/\s*@sim\s+(\d+)\s*$/m.exec(wgsl);
|
|
39
|
+
if (!m)
|
|
40
|
+
return 0;
|
|
41
|
+
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
42
|
+
// 4096 gets the most the engine will give and a scene that still runs.
|
|
43
|
+
return Math.max(8, Math.min(max, parseInt(m[1], 10)));
|
|
44
|
+
}
|
|
45
|
+
/** Whether this effect drives a grid at all. */
|
|
46
|
+
export function simEntryPoint(wgsl) {
|
|
47
|
+
return /\bfn\s+simStep\s*\(/.test(wgsl);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
51
|
+
*
|
|
52
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
53
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
54
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
55
|
+
*/
|
|
56
|
+
export function simReadApi(group, tex, samp, size) {
|
|
57
|
+
return /* wgsl */ `
|
|
58
|
+
const RZ_SIM_SIZE: f32 = ${size > 0 ? size : 1}.0;
|
|
59
|
+
@group(${group}) @binding(${tex}) var _rzSimTex: texture_2d<f32>;
|
|
60
|
+
@group(${group}) @binding(${samp}) var _rzSimSamp: sampler;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
|
|
64
|
+
* the world is the effect's own business — the engine deliberately does not
|
|
65
|
+
* impose a mapping, because a fog laid over a stage and a ripple field around a
|
|
66
|
+
* character want different ones and both are two lines of arithmetic.
|
|
67
|
+
*/
|
|
68
|
+
fn rzSim(uv: vec2f) -> vec4f {
|
|
69
|
+
return textureSampleLevel(_rzSimTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
70
|
+
}
|
|
71
|
+
fn rzSimSize() -> f32 { return RZ_SIM_SIZE; }
|
|
72
|
+
fn rzSimTexel() -> f32 { return 1.0 / RZ_SIM_SIZE; }
|
|
73
|
+
|
|
74
|
+
// ── The step-only half, defined here too and never called here ──
|
|
75
|
+
//
|
|
76
|
+
// One effect file is spliced into EVERY module it has a mount in, so a file
|
|
77
|
+
// with a grid compiles its simStep inside the field and particle shaders as
|
|
78
|
+
// well — where it is dead code that nothing calls, but still code that has to
|
|
79
|
+
// resolve. Leaving these out is a compile error on a function the author was
|
|
80
|
+
// right to write. rzSimPrev reads the current grid rather than a previous one,
|
|
81
|
+
// which is the honest answer outside the step: there is no previous frame here.
|
|
82
|
+
fn rzSimPrev(uv: vec2f) -> vec4f { return rzSim(uv); }
|
|
83
|
+
fn rzSimFrame() -> i32 { return 1; }
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
87
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
88
|
+
export function simClockApi(timeExpr) {
|
|
89
|
+
return /* wgsl */ `
|
|
90
|
+
fn rzTime() -> f32 { return ${timeExpr}; }
|
|
91
|
+
fn rzDt() -> f32 { return 0.0; }
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
const SIM_UNIFORMS = /* wgsl */ `
|
|
95
|
+
struct SimU {
|
|
96
|
+
time: f32,
|
|
97
|
+
dt: f32,
|
|
98
|
+
size: f32,
|
|
99
|
+
frame: f32,
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
/**
|
|
103
|
+
* The step shader: one invocation per texel, once per frame.
|
|
104
|
+
*
|
|
105
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
106
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
107
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
108
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
109
|
+
*
|
|
110
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
111
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
112
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
113
|
+
* what turns smoke into a staircase.
|
|
114
|
+
*/
|
|
115
|
+
export function buildSimShader(wgsl, size, _cast) {
|
|
116
|
+
return (SIM_UNIFORMS +
|
|
117
|
+
/* wgsl */ `
|
|
118
|
+
@group(0) @binding(0) var<uniform> su: SimU;
|
|
119
|
+
@group(0) @binding(1) var _rzSimPrevTex: texture_2d<f32>;
|
|
120
|
+
@group(0) @binding(2) var _rzSimSamp: sampler;
|
|
121
|
+
@group(0) @binding(3) var _rzSimOut: texture_storage_2d<${SIM_FORMAT}, write>;
|
|
122
|
+
@group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
|
|
123
|
+
// The same view uniform the composite reads, so the scene API below is the real
|
|
124
|
+
// thing here rather than a stub — a kernel can ask where a bone is, and a
|
|
125
|
+
// foreground spliced into this module resolves its camera calls.
|
|
126
|
+
@group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
127
|
+
|
|
128
|
+
fn rzTime() -> f32 { return su.time; }
|
|
129
|
+
fn rzDt() -> f32 { return su.dt; }
|
|
130
|
+
/** Texels per side. */
|
|
131
|
+
fn rzSimSize() -> f32 { return su.size; }
|
|
132
|
+
/** One texel, in uv — the step a neighbour lookup takes. */
|
|
133
|
+
fn rzSimTexel() -> f32 { return 1.0 / su.size; }
|
|
134
|
+
/**
|
|
135
|
+
* Steps since this effect was installed. Zero on the very first one, which is
|
|
136
|
+
* the only chance to seed a grid: everything after it builds on what is there.
|
|
137
|
+
*/
|
|
138
|
+
fn rzSimFrame() -> i32 { return i32(su.frame); }
|
|
139
|
+
|
|
140
|
+
/** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
|
|
141
|
+
fn rzSimPrev(uv: vec2f) -> vec4f {
|
|
142
|
+
return textureSampleLevel(_rzSimPrevTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
143
|
+
}
|
|
144
|
+
` +
|
|
145
|
+
EFFECT_SCENE_API +
|
|
146
|
+
audioApi(0, 5) +
|
|
147
|
+
scoreApi(0, 7) +
|
|
148
|
+
// rzSim itself, so a kernel may read the grid it is writing — through the
|
|
149
|
+
// PREVIOUS frame's texture, which is the only version of it that exists
|
|
150
|
+
// while the current one is still being written.
|
|
151
|
+
/* wgsl */ `
|
|
152
|
+
fn rzSim(uv: vec2f) -> vec4f { return rzSimPrev(uv); }
|
|
153
|
+
` +
|
|
154
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
155
|
+
wgsl +
|
|
156
|
+
/* wgsl */ `
|
|
157
|
+
|
|
158
|
+
@compute @workgroup_size(8, 8)
|
|
159
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
160
|
+
let n = u32(${size});
|
|
161
|
+
if (gid.x >= n || gid.y >= n) { return; }
|
|
162
|
+
let xy = vec2i(i32(gid.x), i32(gid.y));
|
|
163
|
+
// The texel CENTRE, so a kernel that reads its own uv back through rzSimPrev
|
|
164
|
+
// lands on itself rather than a quarter-texel off.
|
|
165
|
+
let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
|
|
166
|
+
textureStore(_rzSimOut, xy, simStep(uv, textureLoad(_rzSimPrevTex, xy, 0), su.dt));
|
|
167
|
+
}
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const WIREFRAME_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };\nstruct WireUniforms { color: vec4f, viewport: vec2f, thicknessPx: f32, _pad: f32 };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> wire: WireUniforms;\n\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n// The model's own vertex buffer: pos(3), normal(3), uv(2) \u2014 8 floats a vertex.\n@group(1) @binding(1) var<storage, read> vertices: array<f32>;\n// uint16x4 per vertex, so two u32 each.\n@group(1) @binding(2) var<storage, read> joints: array<u32>;\n// unorm8x4 per vertex, one u32 each.\n@group(1) @binding(3) var<storage, read> weights: array<u32>;\n// Pairs of vertex indices, one pair an edge.\n@group(1) @binding(4) var<storage, read> edges: array<u32>;\n\nfn skinnedPos(i: u32) -> vec3f {\n let o = i * 8u;\n let rest = vec4f(vertices[o], vertices[o + 1u], vertices[o + 2u], 1.0);\n\n let j0 = joints[i * 2u];\n let j1 = joints[i * 2u + 1u];\n let packed = weights[i];\n var w = vec4f(\n f32(packed & 0xffu),\n f32((packed >> 8u) & 0xffu),\n f32((packed >> 16u) & 0xffu),\n f32((packed >> 24u) & 0xffu),\n ) / 255.0;\n let sum = w.x + w.y + w.z + w.w;\n w = select(vec4f(1.0, 0.0, 0.0, 0.0), w / max(sum, 1e-4), sum > 0.0001);\n\n var acc = vec4f(0.0);\n acc += (skinMats[j0 & 0xffffu] * rest) * w.x;\n acc += (skinMats[j0 >> 16u] * rest) * w.y;\n acc += (skinMats[j1 & 0xffffu] * rest) * w.z;\n acc += (skinMats[j1 >> 16u] * rest) * w.w;\n return acc.xyz;\n}\n\n// Six vertices an edge: (a,-1) (a,+1) (b,-1) (a,+1) (b,+1) (b,-1).\n@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4f {\n let a = edges[ii * 2u];\n let b = edges[ii * 2u + 1u];\n let vp = camera.projection * camera.view;\n let ca = vp * vec4f(skinnedPos(a), 1.0);\n let cb = vp * vec4f(skinnedPos(b), 1.0);\n\n let atB = vi == 2u || vi == 4u || vi == 5u;\n let side = select(-1.0, 1.0, vi == 1u || vi == 3u || vi == 4u);\n let own = select(ca, cb, atB);\n let other = select(cb, ca, atB);\n\n let half = 0.5 * wire.viewport;\n let s0 = (own.xy / max(abs(own.w), 1e-6)) * half;\n let s1 = (other.xy / max(abs(other.w), 1e-6)) * half;\n let d = s1 - s0;\n // A degenerate edge has no direction on screen; any perpendicular will do,\n // and it is a dot either way.\n let tangent = select(vec2f(1.0, 0.0), normalize(d), length(d) > 1e-6);\n let normalPx = vec2f(-tangent.y, tangent.x);\n let offsetPx = normalPx * side * wire.thicknessPx * 0.5;\n return vec4f(own.xy + (offsetPx / half) * own.w, own.z, own.w);\n}\n\n// The depth prepass: the solid mesh through the ordinary vertex stream, writing\n// depth and nothing else, so the far side of a body is hidden behind the near.\n@vertex fn vsDepth(\n @location(0) position: vec3f,\n @location(1) joints0: vec4<u32>,\n @location(2) weights0: vec4<f32>,\n) -> @builtin(position) vec4f {\n let rest = vec4f(position, 1.0);\n let sum = weights0.x + weights0.y + weights0.z + weights0.w;\n let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 / max(sum, 1e-4), sum > 0.0001);\n var acc = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) {\n acc += (skinMats[joints0[i]] * rest) * w[i];\n }\n return camera.projection * camera.view * vec4f(acc.xyz, 1.0);\n}\n\n@fragment fn fs() -> @location(0) vec4f {\n // Premultiplied, like every other fragment in this layer.\n return vec4f(wire.color.rgb * wire.color.a, wire.color.a);\n}\n";
|
|
2
|
+
//# sourceMappingURL=wireframe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wireframe.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/wireframe.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,qBAAqB,u8GAuFjC,CAAA"}
|