reze-engine 0.24.1 → 0.25.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 +49 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +297 -29
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -0
- package/dist/shaders/passes/composite.d.ts +23 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +62 -16
- package/dist/shaders/passes/ground.d.ts +1 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +8 -0
- package/package.json +1 -1
- package/src/engine.ts +327 -30
- package/src/index.ts +2 -0
- package/src/shaders/passes/composite.ts +89 -16
- package/src/shaders/passes/ground.ts +8 -0
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
|
@@ -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
|
+
}
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,2 +1,24 @@
|
|
|
1
|
-
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;yEAayE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AA8HD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAWlF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Composite: HDR scene + bloom pyramid → Filmic tone map → gamma → swapchain.
|
|
2
2
|
// Bloom tint/intensity applied at combine (EEVEE treats them as combine-stage params, not prefilter).
|
|
3
|
-
|
|
3
|
+
//
|
|
4
|
+
// The shader is a TEMPLATE: buildCompositeShader() emits either the base pass or
|
|
5
|
+
// a variant with a user background effect injected (setBackgroundEffect). The
|
|
6
|
+
// effect is background mode 3, a sibling of the 360 equirect (mode 2) — it reuses
|
|
7
|
+
// the same per-pixel view-ray reconstruction and composites in display space
|
|
8
|
+
// under the scene, so it never affects lighting, bloom, or tonemapping.
|
|
9
|
+
const COMPOSITE_HEAD = /* wgsl */ `
|
|
4
10
|
// Pipeline-override constant: the engine creates two composite pipelines, one
|
|
5
11
|
// with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.
|
|
6
12
|
// The 'if (APPLY_GAMMA)' below is resolved at pipeline-compile time — the
|
|
@@ -11,7 +17,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
11
17
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
12
18
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
13
19
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
14
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
20
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 7>;
|
|
15
21
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
16
22
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
17
23
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -28,10 +34,12 @@ override APPLY_GAMMA: bool = true;
|
|
|
28
34
|
@group(0) @binding(5) var filmicLut: texture_2d<f32>;
|
|
29
35
|
// viewU[0] = (exposure, invGamma, _, _); viewU[1] = (tint.rgb, intensity)
|
|
30
36
|
// viewU[2] = (background.rgb, mode) — display-space sRGB, composited UNDER the
|
|
31
|
-
// scene post-tonemap. mode: 0 transparent (DOM shows),
|
|
32
|
-
// 2 = 360 equirect skybox sampled by view ray.
|
|
37
|
+
// scene post-tonemap. BASE-layer mode: 0 transparent (DOM shows),
|
|
38
|
+
// 1 solid color, 2 = 360 equirect skybox sampled by view ray. A user
|
|
39
|
+
// WGSL effect is a separate LAYER over the base (viewU[6].y flag).
|
|
33
40
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
34
|
-
// viewU[5] = (camera forward, _) — refreshed per frame while
|
|
41
|
+
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
42
|
+
// viewU[6] = (time seconds, effect on/off, canvas width, canvas height).
|
|
35
43
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
36
44
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
37
45
|
|
|
@@ -49,6 +57,10 @@ fn filmic(x: f32) -> f32 {
|
|
|
49
57
|
return textureSampleLevel(filmicLut, bloomSamp, vec2f(u, 0.5), 0.0).r;
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
/** Canvas size in pixels — for user background effects (aspect correction). */
|
|
61
|
+
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
62
|
+
`;
|
|
63
|
+
const COMPOSITE_BODY = /* wgsl */ `
|
|
52
64
|
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
53
65
|
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
54
66
|
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
@@ -75,21 +87,55 @@ fn filmic(x: f32) -> f32 {
|
|
|
75
87
|
if (APPLY_GAMMA) {
|
|
76
88
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
77
89
|
}
|
|
78
|
-
// Composite over the background in display space (premultiplied out).
|
|
90
|
+
// Composite over the background in display space (premultiplied out). The
|
|
91
|
+
// background is TWO layers: a base (transparent / solid color / 360 equirect)
|
|
92
|
+
// and an optional user WGSL effect over-composited onto it.
|
|
79
93
|
let bg = viewU[2];
|
|
80
|
-
var bgRgb = bg.rgb;
|
|
81
94
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
95
|
+
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
96
|
+
let fxOn = viewU[6].y > 0.5;
|
|
97
|
+
if (bg.w > 1.5 || fxOn) {
|
|
98
|
+
// The equirect and any effect both need this pixel's world-space view ray,
|
|
99
|
+
// rebuilt from the camera basis. The dome sits at infinity (no parallax) —
|
|
100
|
+
// PhotoDome-style, display-only.
|
|
86
101
|
let ndc = vec2f(fragCoord.x / fullSz.x * 2.0 - 1.0, 1.0 - fragCoord.y / fullSz.y * 2.0);
|
|
87
102
|
let dir = normalize(viewU[5].xyz + ndc.x * viewU[3].w * viewU[3].xyz + ndc.y * viewU[4].w * viewU[4].xyz);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
if (bg.w > 1.5) {
|
|
104
|
+
// LH world (+Z forward): longitude = atan2(x, z), Babylon-PhotoDome convention.
|
|
105
|
+
let su = 0.5 + atan2(dir.x, dir.z) * 0.15915494309; // 1/(2π)
|
|
106
|
+
let sv = 0.5 - asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618; // 1/π
|
|
107
|
+
bgPm = textureSampleLevel(bgEquirect, bloomSamp, vec2f(su, sv), 0.0).rgb;
|
|
108
|
+
}
|
|
109
|
+
BG_EFFECT_CALL
|
|
92
110
|
}
|
|
93
|
-
return vec4f(disp * alpha +
|
|
111
|
+
return vec4f(disp * alpha + bgPm * (1.0 - alpha), alpha + bgA * (1.0 - alpha));
|
|
94
112
|
}
|
|
95
113
|
`;
|
|
114
|
+
// Base variant: no effect installed, the flag is never set — the ray block only
|
|
115
|
+
// runs for the equirect, and there is nothing to add. (`dir` may go unused when
|
|
116
|
+
// this compiles with mode<2 shaders; WGSL is fine with an unused let.)
|
|
117
|
+
const NO_EFFECT_CALL = `_ = dir;`;
|
|
118
|
+
// uv flipped to bottom-left origin (shadertoy convention); clamped so a stray
|
|
119
|
+
// effect can't push negatives/NaN into the premultiplied composite. Standard
|
|
120
|
+
// OVER onto the base layer.
|
|
121
|
+
const EFFECT_CALL = /* wgsl */ `
|
|
122
|
+
if (fxOn) {
|
|
123
|
+
let bgUv = vec2f(fragCoord.x / fullSz.x, 1.0 - fragCoord.y / fullSz.y);
|
|
124
|
+
let fx = clamp(background(dir, bgUv, viewU[6].x), vec4f(0.0), vec4f(1.0));
|
|
125
|
+
bgPm = fx.rgb * fx.a + bgPm * (1.0 - fx.a);
|
|
126
|
+
bgA = fx.a + bgA * (1.0 - fx.a);
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
export function buildCompositeShader(effect) {
|
|
130
|
+
if (!effect)
|
|
131
|
+
return COMPOSITE_HEAD + COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL);
|
|
132
|
+
return (COMPOSITE_HEAD +
|
|
133
|
+
"\n// ── user background effect (setBackgroundEffect) ──\n" +
|
|
134
|
+
effect.paramsDecl +
|
|
135
|
+
"\n" +
|
|
136
|
+
effect.wgsl +
|
|
137
|
+
"\n" +
|
|
138
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()));
|
|
139
|
+
}
|
|
140
|
+
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
141
|
+
export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const GROUND_SHADOW_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };\nstruct Light { direction: vec4f, color: vec4f, };\nstruct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };\nstruct GroundShadowMat {\n diffuseColor: vec3f, fadeStart: f32,\n fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,\n gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,\n gridLineColor: vec3f, _pad2: f32,\n};\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var shadowMap: texture_depth_2d;\n@group(0) @binding(3) var shadowSampler: sampler_comparison;\n@group(0) @binding(4) var<uniform> material: GroundShadowMat;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n\nfn hash2(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));\n return fract((p3.x + p3.y) * p3.z);\n}\nfn valueNoise(p: vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),\n mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);\n}\nfn fbmNoise(p: vec2f) -> f32 {\n var v = 0.0;\n var a = 0.5;\n var pp = p;\n for (var i = 0; i < 4; i++) {\n v += a * valueNoise(pp);\n pp *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nstruct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {\n var o: VO; o.worldPos = position; o.normal = normal;\n o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;\n}\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(i: VO) -> FSOut {\n let n = normalize(i.normal);\n let centerDist = length(i.worldPos.xz);\n let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));\n\n let lclip = lightVP.viewProj * vec4f(i.worldPos, 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 suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));\n let st = material.pcfTexel;\n let compareZ = ndc.z - 0.0035;\n var vis = 0.0;\n for (var y = -2; y <= 2; y++) {\n for (var x = -2; x <= 2; x++) {\n vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);\n }\n }\n vis *= 0.04;\n\n // Frosted/matte micro-texture\n let noiseVal = fbmNoise(i.worldPos.xz * 3.0);\n let noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;\n\n // Grid lines \u2014 anti-aliased via screen-space derivatives\n let gp = i.worldPos.xz / material.gridSpacing;\n let gridFrac = abs(fract(gp - 0.5) - 0.5);\n let gridDeriv = fwidth(gp);\n let halfLine = material.gridLineWidth * 0.5;\n let gridLine = 1.0 - min(\n smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),\n smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)\n );\n let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);\n let dark = (1.0 - vis) * material.shadowStrength;\n var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);\n baseColor *= noiseTint;\n let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);\n // Whole-ground opacity fades the SURFACE (color, grid) but the shadow stays \u2014\n // as opacity drops, the received shadow becomes a translucent dark layer\n // (Blender's Shadow Catcher), so models still feel grounded on a photo or\n // 360 backdrop. At opacity 1 this reduces exactly to the plain surface.\n let surfA = edgeFade * material.opacity;\n let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity);\n let outA = surfA + catchA;\n var out: FSOut;\n out.color = vec4f(finalColor * surfA, outA);\n // mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =\n // outA turns the aux blend into alpha-over, so the drawable alpha goes\n // from outA at the center to 0 at the radial edge \u2014 letting the page\n // background show through under the premultiplied canvas alphaMode.\n out.mask = vec4f(0.0, 1.0, 0.0, outA);\n return out;\n}\n";
|
|
1
|
+
export declare const GROUND_SHADOW_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };\nstruct Light { direction: vec4f, color: vec4f, };\nstruct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };\nstruct GroundShadowMat {\n diffuseColor: vec3f, fadeStart: f32,\n fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,\n gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,\n gridLineColor: vec3f, _pad2: f32,\n};\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var shadowMap: texture_depth_2d;\n@group(0) @binding(3) var shadowSampler: sampler_comparison;\n@group(0) @binding(4) var<uniform> material: GroundShadowMat;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n\nfn hash2(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));\n return fract((p3.x + p3.y) * p3.z);\n}\nfn valueNoise(p: vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),\n mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);\n}\nfn fbmNoise(p: vec2f) -> f32 {\n var v = 0.0;\n var a = 0.5;\n var pp = p;\n for (var i = 0; i < 4; i++) {\n v += a * valueNoise(pp);\n pp *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nstruct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {\n var o: VO; o.worldPos = position; o.normal = normal;\n o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;\n}\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(i: VO) -> FSOut {\n let n = normalize(i.normal);\n let centerDist = length(i.worldPos.xz);\n let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));\n\n let lclip = lightVP.viewProj * vec4f(i.worldPos, 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 suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));\n let st = material.pcfTexel;\n let compareZ = ndc.z - 0.0035;\n var vis = 0.0;\n for (var y = -2; y <= 2; y++) {\n for (var x = -2; x <= 2; x++) {\n vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);\n }\n }\n vis *= 0.04;\n // Outside the light's frustum there IS no shadow information \u2014 the clamped\n // border samples above compare against unrelated depths and read \"shadowed\",\n // darkening the whole far plane with a visible band at the frustum edge\n // (masked by the opaque surface normally, glaring in green-screen mode where\n // only the shadow-catcher term renders). Fade to fully lit near the border.\n let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);\n let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;\n vis = mix(1.0, vis, frustum);\n\n // Frosted/matte micro-texture\n let noiseVal = fbmNoise(i.worldPos.xz * 3.0);\n let noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;\n\n // Grid lines \u2014 anti-aliased via screen-space derivatives\n let gp = i.worldPos.xz / material.gridSpacing;\n let gridFrac = abs(fract(gp - 0.5) - 0.5);\n let gridDeriv = fwidth(gp);\n let halfLine = material.gridLineWidth * 0.5;\n let gridLine = 1.0 - min(\n smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),\n smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)\n );\n let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);\n let dark = (1.0 - vis) * material.shadowStrength;\n var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);\n baseColor *= noiseTint;\n let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);\n // Whole-ground opacity fades the SURFACE (color, grid) but the shadow stays \u2014\n // as opacity drops, the received shadow becomes a translucent dark layer\n // (Blender's Shadow Catcher), so models still feel grounded on a photo or\n // 360 backdrop. At opacity 1 this reduces exactly to the plain surface.\n let surfA = edgeFade * material.opacity;\n let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity);\n let outA = surfA + catchA;\n var out: FSOut;\n out.color = vec4f(finalColor * surfA, outA);\n // mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =\n // outA turns the aux blend into alpha-over, so the drawable alpha goes\n // from outA at the center to 0 at the radial edge \u2014 letting the page\n // background show through under the premultiplied canvas alphaMode.\n out.mask = vec4f(0.0, 1.0, 0.0, outA);\n return out;\n}\n";
|
|
2
2
|
//# sourceMappingURL=ground.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB,2kKA6GrC,CAAA"}
|
|
@@ -66,6 +66,14 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
vis *= 0.04;
|
|
69
|
+
// Outside the light's frustum there IS no shadow information — the clamped
|
|
70
|
+
// border samples above compare against unrelated depths and read "shadowed",
|
|
71
|
+
// darkening the whole far plane with a visible band at the frustum edge
|
|
72
|
+
// (masked by the opaque surface normally, glaring in green-screen mode where
|
|
73
|
+
// only the shadow-catcher term renders). Fade to fully lit near the border.
|
|
74
|
+
let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
|
|
75
|
+
let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
|
|
76
|
+
vis = mix(1.0, vis, frustum);
|
|
69
77
|
|
|
70
78
|
// Frosted/matte micro-texture
|
|
71
79
|
let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
|