reze-engine 0.24.1 → 0.25.1

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.
Files changed (40) hide show
  1. package/README.md +5 -1
  2. package/dist/engine.d.ts +127 -1
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +422 -34
  5. package/dist/gpu-profile.d.ts +19 -0
  6. package/dist/gpu-profile.d.ts.map +1 -0
  7. package/dist/gpu-profile.js +120 -0
  8. package/dist/graph/slots.d.ts.map +1 -1
  9. package/dist/graph/slots.js +10 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/physics/profile.d.ts +18 -0
  13. package/dist/physics/profile.d.ts.map +1 -0
  14. package/dist/physics/profile.js +44 -0
  15. package/dist/shaders/materials/common.d.ts +1 -1
  16. package/dist/shaders/materials/common.d.ts.map +1 -1
  17. package/dist/shaders/materials/common.js +143 -141
  18. package/dist/shaders/passes/composite.d.ts +23 -1
  19. package/dist/shaders/passes/composite.d.ts.map +1 -1
  20. package/dist/shaders/passes/composite.js +62 -16
  21. package/dist/shaders/passes/depth-prepass.d.ts +2 -0
  22. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -0
  23. package/dist/shaders/passes/depth-prepass.js +56 -0
  24. package/dist/shaders/passes/ground.d.ts +1 -1
  25. package/dist/shaders/passes/ground.d.ts.map +1 -1
  26. package/dist/shaders/passes/ground.js +8 -0
  27. package/package.json +1 -1
  28. package/src/engine.ts +459 -34
  29. package/src/graph/slots.ts +11 -2
  30. package/src/index.ts +2 -0
  31. package/src/shaders/materials/common.ts +207 -205
  32. package/src/shaders/passes/composite.ts +89 -16
  33. package/src/shaders/passes/depth-prepass.ts +57 -0
  34. package/src/shaders/passes/ground.ts +8 -0
  35. package/dist/physics-debug.d.ts +0 -30
  36. package/dist/physics-debug.d.ts.map +0 -1
  37. package/dist/physics-debug.js +0 -526
  38. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  39. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  40. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,19 @@
1
+ export declare class GpuTimestamp {
2
+ enabled: boolean;
3
+ readonly supported: boolean;
4
+ private device;
5
+ private querySet;
6
+ private resolveBuffer;
7
+ private readbackBuffer;
8
+ private nextSlot;
9
+ private frameLabels;
10
+ private inFlight;
11
+ private pendingLabels;
12
+ constructor(device: GPUDevice);
13
+ private ensureCreated;
14
+ beginFrame(): void;
15
+ wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
16
+ endFrame(encoder: GPUCommandEncoder): void;
17
+ afterSubmit(): void;
18
+ }
19
+ //# sourceMappingURL=gpu-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
@@ -0,0 +1,120 @@
1
+ // WebGPU timestamp-query profiling. Records pass durations into the same
2
+ // profiler module as CPU phases (so the snapshot table mixes both with a
3
+ // "gpu." prefix). Read-only — no effect on rendering output.
4
+ //
5
+ // Pattern (per spec):
6
+ // 1. Each render pass declares timestampWrites { begin, end } slot indices.
7
+ // 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
8
+ // into a QUERY_RESOLVE buffer.
9
+ // 3. We copy the resolve buffer into a MAP_READ readback buffer.
10
+ // 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
11
+ // and record them.
12
+ //
13
+ // One readback buffer with an in-flight gate: if mapAsync hasn't resolved
14
+ // from a previous frame, we skip the copy this frame (no contention, just
15
+ // one frame of dropped sampling).
16
+ import { profiler } from "./physics/profile";
17
+ const MAX_PASSES_PER_FRAME = 64;
18
+ export class GpuTimestamp {
19
+ constructor(device) {
20
+ this.enabled = false;
21
+ this.querySet = null;
22
+ this.resolveBuffer = null;
23
+ this.readbackBuffer = null;
24
+ this.nextSlot = 0;
25
+ this.frameLabels = [];
26
+ this.inFlight = false;
27
+ // Set inside endFrame() when a copy was queued this frame; consumed by
28
+ // afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
29
+ // calling it before submit puts the buffer into "mapping pending" state,
30
+ // which makes the copy in the encoder illegal.
31
+ this.pendingLabels = null;
32
+ this.device = device;
33
+ this.supported = device.features.has("timestamp-query");
34
+ }
35
+ ensureCreated() {
36
+ if (this.querySet || !this.supported)
37
+ return;
38
+ const slots = MAX_PASSES_PER_FRAME * 2;
39
+ this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
40
+ this.resolveBuffer = this.device.createBuffer({
41
+ label: "gpu-timestamp-resolve",
42
+ size: slots * 8,
43
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
44
+ });
45
+ this.readbackBuffer = this.device.createBuffer({
46
+ label: "gpu-timestamp-readback",
47
+ size: slots * 8,
48
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
49
+ });
50
+ }
51
+ // Called once at the top of each render() before any pass wrap.
52
+ beginFrame() {
53
+ if (!this.enabled || !this.supported)
54
+ return;
55
+ this.ensureCreated();
56
+ this.nextSlot = 0;
57
+ this.frameLabels = [];
58
+ }
59
+ // Wrap a render-pass descriptor with timestampWrites for `label`. If
60
+ // profiling is off / unsupported / out of slots, returns desc unchanged.
61
+ // The same label called multiple times (e.g. each bloom mip) is fine —
62
+ // the profiler accumulates by label.
63
+ wrap(desc, label) {
64
+ if (!this.enabled || !this.querySet)
65
+ return desc;
66
+ if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
67
+ return desc;
68
+ const begin = this.nextSlot;
69
+ const end = this.nextSlot + 1;
70
+ this.nextSlot += 2;
71
+ this.frameLabels.push(label);
72
+ return {
73
+ ...desc,
74
+ timestampWrites: {
75
+ querySet: this.querySet,
76
+ beginningOfPassWriteIndex: begin,
77
+ endOfPassWriteIndex: end,
78
+ },
79
+ };
80
+ }
81
+ // Call on the encoder after all passes, BEFORE encoder.finish(). Adds
82
+ // resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
83
+ // afterSubmit() — calling it before submit transitions the readback buffer
84
+ // into "mapping pending" state, which makes the queued copy illegal.
85
+ endFrame(encoder) {
86
+ if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
87
+ return;
88
+ const count = this.frameLabels.length * 2;
89
+ encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
90
+ if (this.inFlight)
91
+ return;
92
+ encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
93
+ this.pendingLabels = this.frameLabels.slice();
94
+ }
95
+ // Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
96
+ // this frame; the .then() reads durations and unmaps.
97
+ afterSubmit() {
98
+ const labels = this.pendingLabels;
99
+ if (!labels)
100
+ return;
101
+ this.pendingLabels = null;
102
+ this.inFlight = true;
103
+ const readback = this.readbackBuffer;
104
+ void readback.mapAsync(GPUMapMode.READ).then(() => {
105
+ const arr = new BigInt64Array(readback.getMappedRange());
106
+ for (let i = 0; i < labels.length; i++) {
107
+ const start = arr[i * 2];
108
+ const end = arr[i * 2 + 1];
109
+ // Per spec, timestamps are nanoseconds.
110
+ const ms = Number(end - start) / 1e6;
111
+ if (ms >= 0 && ms < 1000)
112
+ profiler.record("gpu." + labels[i], ms);
113
+ }
114
+ readback.unmap();
115
+ this.inFlight = false;
116
+ }, () => {
117
+ this.inFlight = false;
118
+ });
119
+ }
120
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAiD5D,eAAO,MAAM,mBAAmB,gHAG/B,CAAA;AAuDD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,OAAO,GAC5B,MAAM,CAYR"}
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAiD5D,eAAO,MAAM,mBAAmB,gHAG/B,CAAA;AAgED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,OAAO,GAC5B,MAAM,CAYR"}
@@ -67,14 +67,22 @@ function prelude(renderClass, alphaMode) {
67
67
  ? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
68
68
  : " if (alpha < 0.001) { discard; }";
69
69
  const gate = renderClass === "eye" ? EYE_REAR_GATE : "";
70
+ // Double-sided shading, winding-independent: a normal pointing away from the
71
+ // camera means we're seeing the surface's other side — flip it. Only genuinely
72
+ // camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
73
+ // untouched), unlike a front_facing test, which PMX's clockwise winding
74
+ // inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
75
+ // shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
76
+ // by design, so its visible fragments are back faces with intended normals.
77
+ const flip = renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);";
70
78
  return `@fragment fn fs(input: VertexOutput) -> FSOut {
71
79
  let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
72
80
  // MMD alpha semantics: material alpha × texture alpha.
73
81
  let alpha = material.alpha * tex_s.a;
74
82
  ${discard}
75
83
 
76
- let n = safe_normal(input.normal);
77
- let v = normalize(camera.viewPos - input.worldPos);
84
+ var n = safe_normal(input.normal);
85
+ let v = normalize(camera.viewPos - input.worldPos);${flip}
78
86
  ${gate}
79
87
  let l = -light.lights[0].direction.xyz;
80
88
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, } from "./engine";
1
+ export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, type BackgroundEffectParamValue, type BackgroundEffectResult, } from "./engine";
2
2
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
3
3
  export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, type CompileResult, type StyleSlot, } from "./graph/compile";
4
4
  export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,GAC5B,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,18 @@
1
+ declare class PhysicsProfiler {
2
+ enabled: boolean;
3
+ private sums;
4
+ private counts;
5
+ begin(): number;
6
+ end(label: string, t0: number): void;
7
+ record(label: string, ms: number): void;
8
+ snapshot(): Record<string, {
9
+ avgMs: number;
10
+ calls: number;
11
+ totalMs: number;
12
+ }>;
13
+ reset(): void;
14
+ }
15
+ export declare const profiler: PhysicsProfiler;
16
+ export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
17
+ export {};
18
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
@@ -0,0 +1,44 @@
1
+ // Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
2
+ // begin()/end() short-circuit so production has zero overhead. Toggle via
3
+ // `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
4
+ class PhysicsProfiler {
5
+ constructor() {
6
+ this.enabled = false;
7
+ this.sums = Object.create(null);
8
+ this.counts = Object.create(null);
9
+ }
10
+ begin() {
11
+ return this.enabled ? performance.now() : 0;
12
+ }
13
+ end(label, t0) {
14
+ if (!this.enabled)
15
+ return;
16
+ const dt = performance.now() - t0;
17
+ this.sums[label] = (this.sums[label] ?? 0) + dt;
18
+ this.counts[label] = (this.counts[label] ?? 0) + 1;
19
+ }
20
+ // Direct accumulator for durations not measured via begin()/end() — used by
21
+ // GPU timestamp readback, which arrives async after the frame has ended.
22
+ record(label, ms) {
23
+ if (!this.enabled)
24
+ return;
25
+ this.sums[label] = (this.sums[label] ?? 0) + ms;
26
+ this.counts[label] = (this.counts[label] ?? 0) + 1;
27
+ }
28
+ // Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
29
+ // window cost — divide by elapsed wall time to get a "% of frame budget".
30
+ snapshot() {
31
+ const out = {};
32
+ for (const k in this.sums) {
33
+ const total = this.sums[k];
34
+ const calls = this.counts[k];
35
+ out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
36
+ }
37
+ return out;
38
+ }
39
+ reset() {
40
+ this.sums = Object.create(null);
41
+ this.counts = Object.create(null);
42
+ }
43
+ }
44
+ export const profiler = new PhysicsProfiler();
@@ -1,5 +1,5 @@
1
1
  export declare const COMMON_BINDINGS_WGSL = "\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\n// Per-material uniforms. Every material binds this layout even if it ignores fields;\n// the engine keeps one bind group layout across all material pipelines. The PMX\n// classic-material fields (ambient/specular/shininess) are carried for graph nodes that\n// want them; most graphs read only diffuseColor + alpha.\nstruct MaterialUniforms {\n diffuseColor: vec3f, // PMX diffuse rgb \u2014 the material_diffuse node reads this\n alpha: f32, // 0 \u2192 discard; <1 \u2192 transparent draw call\n ambient: vec3f, // PMX ambient rgb\n shininess: f32, // PMX specular power\n specular: vec3f, // PMX specular rgb\n sphereMode: f32, // 0 none \u00B7 1 multiply (sph) \u00B7 2 add (spa)\n // Skeleton index of the \u982D (head) bone, or -1. Lets the eye shader gate\n // the post-alpha-eye stencil by camera-vs-face hemisphere.\n headBoneIndex: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural\n // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of\n // worldPos so the pattern rides with the surface \u2014 otherwise the mesh swims through a\n // world-static noise field under any skinning deformation or root (\u30BB\u30F3\u30BF\u30FC) motion.\n // At rest skinMats are identity so restPos == worldPos, which is why existing noise-\n // scale constants stay valid without retuning.\n @location(3) restPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n// Reserved for future sphere/toon graph nodes; graphs that don't read them get the\n// 1\u00D71 white fallback bound here.\n@group(2) @binding(2) var toonTexture: texture_2d<f32>;\n@group(2) @binding(3) var sphereTexture: texture_2d<f32>;\n\n// Four-bone blended normals can cancel to ~zero on physics-driven parts\n// (opposing bone rotations at 50/50 weights) \u2014 normalize(0) is 0/0 = NaN,\n// which poisons the whole shading stack and flashes through bloom. Fall\n// back to up for degenerate normals instead.\nfn safe_normal(nIn: vec3f) -> vec3f {\n let l2 = dot(nIn, nIn);\n if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }\n return nIn * inverseSqrt(l2);\n}\n\n";
2
- export declare const SAMPLE_SHADOW_WGSL = "\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 2048.0;\n let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);\n let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);\n let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);\n let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);\n let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);\n let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);\n let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);\n let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);\n let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);\n return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);\n}\n\n";
2
+ export declare const SAMPLE_SHADOW_WGSL = "\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);\n let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);\n let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);\n let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);\n let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);\n let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);\n let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);\n let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);\n let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);\n return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);\n}\n\n";
3
3
  export declare const COMMON_VS_WGSL = "\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = skinnedNrm;\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n output.restPos = position;\n return output;\n}\n\n";
4
4
  export declare const COMMON_FS_OUT_WGSL = "\n\nstruct FSOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n};\n\n";
5
5
  export declare const COMMON_MATERIAL_PRELUDE_WGSL: string;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oBAAoB,8pGA+EhC,CAAC;AAOF,eAAO,MAAM,kBAAkB,4xCAsB9B,CAAC;AAQF,eAAO,MAAM,cAAc,s+BA8B1B,CAAC;AAsBF,eAAO,MAAM,kBAAkB,0FAO9B,CAAC;AAMF,eAAO,MAAM,4BAA4B,QACwC,CAAA"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oBAAoB,8pGA+EhC,CAAC;AASF,eAAO,MAAM,kBAAkB,4xCAsB9B,CAAC;AAQF,eAAO,MAAM,cAAc,s+BA8B1B,CAAC;AAsBF,eAAO,MAAM,kBAAkB,0FAO9B,CAAC;AAMF,eAAO,MAAM,4BAA4B,QACwC,CAAA"}
@@ -18,148 +18,150 @@
18
18
  // group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
19
19
  // group(1): per-model skinning
20
20
  // group(2): per-material (diffuse texture + material uniforms)
21
- export const COMMON_BINDINGS_WGSL = /* wgsl */ `
22
-
23
- struct CameraUniforms {
24
- view: mat4x4f,
25
- projection: mat4x4f,
26
- viewPos: vec3f,
27
- _padding: f32,
28
- };
29
-
30
- struct Light {
31
- direction: vec4f,
32
- color: vec4f,
33
- };
34
-
35
- struct LightUniforms {
36
- ambientColor: vec4f,
37
- lights: array<Light, 4>,
38
- };
39
-
40
- // Per-material uniforms. Every material binds this layout even if it ignores fields;
41
- // the engine keeps one bind group layout across all material pipelines. The PMX
42
- // classic-material fields (ambient/specular/shininess) are carried for graph nodes that
43
- // want them; most graphs read only diffuseColor + alpha.
44
- struct MaterialUniforms {
45
- diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
46
- alpha: f32, // 0 → discard; <1 → transparent draw call
47
- ambient: vec3f, // PMX ambient rgb
48
- shininess: f32, // PMX specular power
49
- specular: vec3f, // PMX specular rgb
50
- sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
51
- // Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
52
- // the post-alpha-eye stencil by camera-vs-face hemisphere.
53
- headBoneIndex: f32,
54
- _pad0: f32,
55
- _pad1: f32,
56
- _pad2: f32,
57
- };
58
-
59
- struct VertexOutput {
60
- @builtin(position) position: vec4f,
61
- @location(0) normal: vec3f,
62
- @location(1) uv: vec2f,
63
- @location(2) worldPos: vec3f,
64
- // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
65
- // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
66
- // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
67
- // world-static noise field under any skinning deformation or root (センター) motion.
68
- // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
69
- // scale constants stay valid without retuning.
70
- @location(3) restPos: vec3f,
71
- };
72
-
73
- struct LightVP { viewProj: mat4x4f, };
74
-
75
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
76
- @group(0) @binding(1) var<uniform> light: LightUniforms;
77
- @group(0) @binding(2) var diffuseSampler: sampler;
78
- @group(0) @binding(3) var shadowMap: texture_depth_2d;
79
- @group(0) @binding(4) var shadowSampler: sampler_comparison;
80
- @group(0) @binding(5) var<uniform> lightVP: LightVP;
81
- // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
82
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
83
- @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
84
- @group(2) @binding(1) var<uniform> material: MaterialUniforms;
85
- // Reserved for future sphere/toon graph nodes; graphs that don't read them get the
86
- // 1×1 white fallback bound here.
87
- @group(2) @binding(2) var toonTexture: texture_2d<f32>;
88
- @group(2) @binding(3) var sphereTexture: texture_2d<f32>;
89
-
90
- // Four-bone blended normals can cancel to ~zero on physics-driven parts
91
- // (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
92
- // which poisons the whole shading stack and flashes through bloom. Fall
93
- // back to up for degenerate normals instead.
94
- fn safe_normal(nIn: vec3f) -> vec3f {
95
- let l2 = dot(nIn, nIn);
96
- if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
97
- return nIn * inverseSqrt(l2);
98
- }
99
-
21
+ export const COMMON_BINDINGS_WGSL = /* wgsl */ `
22
+
23
+ struct CameraUniforms {
24
+ view: mat4x4f,
25
+ projection: mat4x4f,
26
+ viewPos: vec3f,
27
+ _padding: f32,
28
+ };
29
+
30
+ struct Light {
31
+ direction: vec4f,
32
+ color: vec4f,
33
+ };
34
+
35
+ struct LightUniforms {
36
+ ambientColor: vec4f,
37
+ lights: array<Light, 4>,
38
+ };
39
+
40
+ // Per-material uniforms. Every material binds this layout even if it ignores fields;
41
+ // the engine keeps one bind group layout across all material pipelines. The PMX
42
+ // classic-material fields (ambient/specular/shininess) are carried for graph nodes that
43
+ // want them; most graphs read only diffuseColor + alpha.
44
+ struct MaterialUniforms {
45
+ diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
46
+ alpha: f32, // 0 → discard; <1 → transparent draw call
47
+ ambient: vec3f, // PMX ambient rgb
48
+ shininess: f32, // PMX specular power
49
+ specular: vec3f, // PMX specular rgb
50
+ sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
51
+ // Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
52
+ // the post-alpha-eye stencil by camera-vs-face hemisphere.
53
+ headBoneIndex: f32,
54
+ _pad0: f32,
55
+ _pad1: f32,
56
+ _pad2: f32,
57
+ };
58
+
59
+ struct VertexOutput {
60
+ @builtin(position) position: vec4f,
61
+ @location(0) normal: vec3f,
62
+ @location(1) uv: vec2f,
63
+ @location(2) worldPos: vec3f,
64
+ // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
65
+ // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
66
+ // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
67
+ // world-static noise field under any skinning deformation or root (センター) motion.
68
+ // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
69
+ // scale constants stay valid without retuning.
70
+ @location(3) restPos: vec3f,
71
+ };
72
+
73
+ struct LightVP { viewProj: mat4x4f, };
74
+
75
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
76
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
77
+ @group(0) @binding(2) var diffuseSampler: sampler;
78
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
79
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
80
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
81
+ // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
82
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
83
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
84
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
85
+ // Reserved for future sphere/toon graph nodes; graphs that don't read them get the
86
+ // 1×1 white fallback bound here.
87
+ @group(2) @binding(2) var toonTexture: texture_2d<f32>;
88
+ @group(2) @binding(3) var sphereTexture: texture_2d<f32>;
89
+
90
+ // Four-bone blended normals can cancel to ~zero on physics-driven parts
91
+ // (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
92
+ // which poisons the whole shading stack and flashes through bloom. Fall
93
+ // back to up for degenerate normals instead.
94
+ fn safe_normal(nIn: vec3f) -> vec3f {
95
+ let l2 = dot(nIn, nIn);
96
+ if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
97
+ return nIn * inverseSqrt(l2);
98
+ }
99
+
100
100
  `;
101
101
  // ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
102
- // 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
103
- // doesn't unroll nested shadow loops reliably, and the early out on back-facing
104
- // fragments saves 9 texture taps per skipped pixel.
105
- export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
106
-
107
- fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
108
- if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
109
- let biasedPos = worldPos + n * 0.08;
110
- let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
111
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
112
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
113
- let cmpZ = ndc.z - 0.001;
114
- let ts = 1.0 / 2048.0;
115
- let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
116
- let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
117
- let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
118
- let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
119
- let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
120
- let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
121
- let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
122
- let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
123
- let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
124
- return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
125
- }
126
-
102
+ // 4096-map (MUST match Engine.SHADOW_MAP_SIZE), normal-bias 0.08, depth-bias
103
+ // 0.001. Unrolled — Safari's Metal backend doesn't unroll nested shadow loops
104
+ // reliably. The texel size was stale at 1/2048 after the map grew to 4096:
105
+ // PCF taps landed TWO texels apart, quantizing self-shadow edges into
106
+ // texel-sized gray/black squares that crawled with the animation.
107
+ export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
108
+
109
+ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
110
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
111
+ let biasedPos = worldPos + n * 0.08;
112
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
113
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
114
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
115
+ let cmpZ = ndc.z - 0.001;
116
+ let ts = 1.0 / 4096.0;
117
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
118
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
119
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
120
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
121
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
122
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
123
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
124
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
125
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
126
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
127
+ }
128
+
127
129
  `;
128
130
  // ─── Skinning vertex shader ─────────────────────────────────────────
129
131
  // Four-bone linear blend skinning. Renormalizes weights when they don't sum to 1
130
132
  // (PMX models occasionally ship with unnormalized weights on extras like hair tips).
131
133
  // VS normalize on the outgoing normal is skipped — interpolation denormalizes it
132
134
  // anyway and every fragment shader does `normalize(input.normal)` as its first line.
133
- export const COMMON_VS_WGSL = /* wgsl */ `
134
-
135
- @vertex fn vs(
136
- @location(0) position: vec3f,
137
- @location(1) normal: vec3f,
138
- @location(2) uv: vec2f,
139
- @location(3) joints0: vec4<u32>,
140
- @location(4) weights0: vec4<f32>
141
- ) -> VertexOutput {
142
- var output: VertexOutput;
143
- let pos4 = vec4f(position, 1.0);
144
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
145
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
146
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
147
- var skinnedPos = vec4f(0.0);
148
- var skinnedNrm = vec3f(0.0);
149
- for (var i = 0u; i < 4u; i++) {
150
- let m = skinMats[joints0[i]];
151
- let w = nw[i];
152
- skinnedPos += (m * pos4) * w;
153
- skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
154
- }
155
- output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
156
- output.normal = skinnedNrm;
157
- output.uv = uv;
158
- output.worldPos = skinnedPos.xyz;
159
- output.restPos = position;
160
- return output;
161
- }
162
-
135
+ export const COMMON_VS_WGSL = /* wgsl */ `
136
+
137
+ @vertex fn vs(
138
+ @location(0) position: vec3f,
139
+ @location(1) normal: vec3f,
140
+ @location(2) uv: vec2f,
141
+ @location(3) joints0: vec4<u32>,
142
+ @location(4) weights0: vec4<f32>
143
+ ) -> VertexOutput {
144
+ var output: VertexOutput;
145
+ let pos4 = vec4f(position, 1.0);
146
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
147
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
148
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
149
+ var skinnedPos = vec4f(0.0);
150
+ var skinnedNrm = vec3f(0.0);
151
+ for (var i = 0u; i < 4u; i++) {
152
+ let m = skinMats[joints0[i]];
153
+ let w = nw[i];
154
+ skinnedPos += (m * pos4) * w;
155
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
156
+ }
157
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
158
+ output.normal = skinnedNrm;
159
+ output.uv = uv;
160
+ output.worldPos = skinnedPos.xyz;
161
+ output.restPos = position;
162
+ return output;
163
+ }
164
+
163
165
  `;
164
166
  // ─── FS output struct ───────────────────────────────────────────────
165
167
  // Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
@@ -180,13 +182,13 @@ export const COMMON_VS_WGSL = /* wgsl */ `
180
182
  // src-alpha blending enabled:
181
183
  // out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
182
184
  // out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
183
- export const COMMON_FS_OUT_WGSL = /* wgsl */ `
184
-
185
- struct FSOut {
186
- @location(0) color: vec4f,
187
- @location(1) mask: vec4f,
188
- };
189
-
185
+ export const COMMON_FS_OUT_WGSL = /* wgsl */ `
186
+
187
+ struct FSOut {
188
+ @location(0) color: vec4f,
189
+ @location(1) mask: vec4f,
190
+ };
191
+
190
192
  `;
191
193
  // ─── Convenience: full shared prelude ───────────────────────────────
192
194
  // Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
@@ -1,2 +1,24 @@
1
- export declare const COMPOSITE_SHADER_WGSL = "\n// Pipeline-override constant: the engine creates two composite pipelines, one\n// with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.\n// The 'if (APPLY_GAMMA)' below is resolved at pipeline-compile time \u2014 the\n// dead branch is dropped by the shader compiler (no runtime branch, no pow\n// invocation on Safari's Metal backend in the common case).\noverride APPLY_GAMMA: bool = true;\n\n@group(0) @binding(0) var hdrTex: texture_2d<f32>;\n@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)\n@group(0) @binding(2) var bloomSamp: sampler;\n@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 6>;\n// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).\n// .g = accumulated canvas alpha (what hdr.a carried before the HDR format\n// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then\n// re-premultiply the tonemapped color for output so the premultiplied canvas\n// alphaMode composites the WebGPU surface over the page background correctly.\n@group(0) @binding(4) var maskTex: texture_2d<f32>;\n// Filmic tone curve baked to a WIDTH\u00D71 r16float LUT (bakeFilmicLut on the CPU side).\n// Domain: log2(linear) mapped to [0,13]. Replaces the old array<f32,14> that was indexed\n// by a runtime u32 (a Metal-backend smell \u2014 dynamic local-array indexing lowers to a\n// per-invocation copy/switch) and interpolated piecewise-linearly (C0, so segment slope\n// discontinuities showed as Mach bands in smooth skin/shadow gradients). The LUT is a\n// monotone-cubic (Fritsch\u2013Carlson) fit through the same 14 anchors \u2014 same values, C1\n// continuity kills the banding \u2014 sampled with hardware linear filtering.\n@group(0) @binding(5) var filmicLut: texture_2d<f32>;\n// viewU[0] = (exposure, invGamma, _, _); viewU[1] = (tint.rgb, intensity)\n// viewU[2] = (background.rgb, mode) \u2014 display-space sRGB, composited UNDER the\n// scene post-tonemap. mode: 0 transparent (DOM shows), 1 solid color,\n// 2 = 360 equirect skybox sampled by view ray.\n// viewU[3] = (camera right, tanHalfFov\u00B7aspect); viewU[4] = (camera up, tanHalfFov);\n// viewU[5] = (camera forward, _) \u2014 refreshed per frame while the skybox is active.\n// invGamma = 1/gamma precomputed on CPU \u2014 avoids a per-pixel divide.\n@group(0) @binding(6) var bgEquirect: texture_2d<f32>;\n\n// Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).\nconst FILMIC_LUT_W: f32 = 256.0;\n\nfn filmic(x: f32) -> f32 {\n // Reference checkpoints (Blender 3.6 Filmic MHC, sobotka/filmic-blender\n // look_medium-high-contrast.spi1d): linear 0.18 \u2192 ~0.395, linear 1.0 \u2192 ~0.83.\n // NOTE: version-pinned to Blender 3.6 \u2014 4.x defaults to AgX, not Filmic.\n let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);\n // Map t\u2208[0,13] to the texel-center of baked sample j = t\u00B7(W-1)/13.\n let u = (t * (FILMIC_LUT_W - 1.0) / 13.0 + 0.5) / FILMIC_LUT_W;\n // textureSampleLevel (explicit LOD, no derivatives) is legal in non-uniform flow.\n return textureSampleLevel(filmicLut, bloomSamp, vec2f(u, 0.5), 0.0).r;\n}\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) fragCoord: vec4f) -> @location(0) vec4f {\n let coord = vec2<i32>(fragCoord.xy);\n let hdr = textureLoad(hdrTex, coord, 0);\n let alpha = textureLoad(maskTex, coord, 0).g;\n let a = max(alpha, 1e-6);\n let straight = hdr.rgb / a;\n let fullSz = vec2f(textureDimensions(hdrTex));\n // Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.\n // fragCoord.xy is already at pixel center (e.g. 0.5, 0.5 for first pixel).\n let bloomUv = fragCoord.xy / max(fullSz, vec2f(1.0));\n let tint = viewU[1].xyz;\n let intensity = viewU[1].w;\n let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;\n let combined = straight + bloom;\n let exposed = combined * exp2(viewU[0].x);\n let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));\n var disp = max(tm, vec3f(0.0));\n if (APPLY_GAMMA) {\n disp = pow(disp, vec3f(viewU[0].y));\n }\n // Composite over the background in display space (premultiplied out).\n let bg = viewU[2];\n var bgRgb = bg.rgb;\n var bgA = select(0.0, 1.0, bg.w > 0.5);\n if (bg.w > 1.5) {\n // 360 equirect: rebuild this pixel's world-space view ray from the camera\n // basis, then latitude/longitude-map into the panorama. The dome sits at\n // infinity (no parallax) \u2014 PhotoDome-style, display-only.\n let ndc = vec2f(fragCoord.x / fullSz.x * 2.0 - 1.0, 1.0 - fragCoord.y / fullSz.y * 2.0);\n let dir = normalize(viewU[5].xyz + ndc.x * viewU[3].w * viewU[3].xyz + ndc.y * viewU[4].w * viewU[4].xyz);\n // LH world (+Z forward): longitude = atan2(x, z), Babylon-PhotoDome convention.\n let su = 0.5 + atan2(dir.x, dir.z) * 0.15915494309; // 1/(2\u03C0)\n let sv = 0.5 - asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618; // 1/\u03C0\n bgRgb = textureSampleLevel(bgEquirect, bloomSamp, vec2f(su, sv), 0.0).rgb;\n }\n return vec4f(disp * alpha + bgRgb * bgA * (1.0 - alpha), alpha + bgA * (1.0 - alpha));\n}\n";
1
+ /** What a user background effect must define, documented once:
2
+ *
3
+ * fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
4
+ *
5
+ * - `ray` — normalized world-space view direction of this pixel (left-handed,
6
+ * +Z forward; identical to what the 360 skybox samples by).
7
+ * - `uv` — 0..1 across the canvas, origin bottom-left (shadertoy-style).
8
+ * - `time` — seconds since the effect was applied.
9
+ * - `bgResolution()` — canvas size in pixels, for aspect correction.
10
+ * - declared params arrive as `params.<name>` (f32 or vec3f).
11
+ * Return display-space sRGB + alpha, 0..1. The effect is a LAYER: it is
12
+ * over-composited onto the base background (solid color / 360 equirect /
13
+ * transparent) and sits behind the scene — alpha 0 lets the base show
14
+ * through, so e.g. a starfield returns stars with a transparent sky. */
15
+ export type CompositeEffectSource = {
16
+ /** User WGSL defining `background(...)` (plus any helpers it wants). */
17
+ wgsl: string;
18
+ /** Codegen'd `struct BgParams {...}` + binding decl; empty when no params. */
19
+ paramsDecl: string;
20
+ };
21
+ export declare function buildCompositeShader(effect?: CompositeEffectSource | null): string;
22
+ /** Kept for compatibility with existing imports (the base, no-effect shader). */
23
+ export declare const COMPOSITE_SHADER_WGSL: string;
2
24
  //# sourceMappingURL=composite.d.ts.map