@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
220 lines (219 loc) • 7.45 kB
JavaScript
import { storageTextureSampleScalarType } from "../core/compute-shader.js";
import { assertUniformName } from "../core/uniforms.js";
import { preprocessMaterialFragment } from "../core/material-preprocess.js";
//#region src/lib/passes/PingPongShaderPass.ts
var FRAGMENT_FUNCTION_SIGNATURE_PATTERN = /\bfn\s+frag\s*\(\s*([^)]*?)\s*\)\s*->\s*([A-Za-z_][A-Za-z0-9_<>\s]*)\s*(?:\{|$)/m;
var FRAGMENT_FUNCTION_NAME_PATTERN = /\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
function normalizeSignaturePart(value) {
return value.replace(/\s+/g, " ").trim();
}
function listFunctionNames(fragment) {
const names = /* @__PURE__ */ new Set();
for (const match of fragment.matchAll(FRAGMENT_FUNCTION_NAME_PATTERN)) {
const name = match[1];
if (name) names.add(name);
}
return Array.from(names);
}
function assertFragmentContract(fragment) {
if (typeof fragment !== "string" || fragment.trim().length === 0) throw new Error("PingPongShaderPass fragment must be a non-empty WGSL string.");
const signature = fragment.match(FRAGMENT_FUNCTION_SIGNATURE_PATTERN);
if (!signature) {
const discoveredFunctions = listFunctionNames(fragment).slice(0, 4);
const discoveredLabel = discoveredFunctions.length > 0 ? `Found: ${discoveredFunctions.map((name) => `\`${name}(...)\``).join(", ")}.` : "No WGSL function declarations were found.";
throw new Error(`PingPongShaderPass fragment contract mismatch: missing entrypoint \`fn frag(uv: vec2f) -> vec4f\`. ${discoveredLabel}`);
}
const params = normalizeSignaturePart(signature[1] ?? "");
const returnType = normalizeSignaturePart(signature[2] ?? "");
if (params !== "uv: vec2f") throw new Error(`PingPongShaderPass fragment contract mismatch for \`frag\`: expected parameter list \`(uv: vec2f)\`, received \`(${params || "..."})\`.`);
if (returnType !== "vec4f") throw new Error(`PingPongShaderPass fragment contract mismatch for \`frag\`: expected return type \`vec4f\`, received \`${returnType}\`.`);
}
function assertPositiveFinite(name, value) {
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a finite number greater than 0`);
}
function assertIterations(count) {
if (!Number.isFinite(count) || count < 1 || !Number.isInteger(count)) throw new Error(`PingPongShaderPass iterations must be a positive integer >= 1, got ${count}`);
return count;
}
function assertFloatSampledFormat(format) {
if (storageTextureSampleScalarType(format) !== "f32") throw new Error(`PingPongShaderPass format "${format}" must be float-sampled so fragment shaders can read the previous state.`);
return format;
}
function cloneColor(color) {
return [
color[0],
color[1],
color[2],
color[3]
];
}
function resolveDimension(explicitValue, canvasDimension, scale) {
if (explicitValue !== void 0) {
assertPositiveFinite("PingPongShaderPass dimension", explicitValue);
return Math.max(1, Math.floor(explicitValue));
}
return Math.max(1, Math.floor(canvasDimension * scale));
}
/**
* Fragment-shader feedback pass for iterative GPU simulations.
*
* The renderer owns two render textures, alternates read/write direction per
* iteration, then exposes the latest texture view through the declared material
* texture slot.
*/
var PingPongShaderPass = class {
/**
* Enables/disables this pass without removing it from graph.
*/
enabled;
/**
* Discriminant flag for render graph to identify fragment feedback passes.
*/
isPingPongShader = true;
fragment;
fragmentLineMap;
target;
width;
height;
scale;
format;
filter;
addressModeU;
addressModeV;
iterations;
clearColor;
totalIterations = 0;
resetPending = true;
constructor(options) {
assertUniformName(options.target);
const preprocessed = preprocessMaterialFragment({
fragment: options.fragment,
...options.defines !== void 0 ? { defines: options.defines } : {},
...options.includes !== void 0 ? { includes: options.includes } : {}
});
assertFragmentContract(preprocessed.fragment);
this.fragment = preprocessed.fragment;
this.fragmentLineMap = preprocessed.lineMap;
this.target = options.target;
this.width = options.width;
this.height = options.height;
this.scale = options.scale ?? 1;
assertPositiveFinite("PingPongShaderPass scale", this.scale);
if (this.width !== void 0) assertPositiveFinite("PingPongShaderPass width", this.width);
if (this.height !== void 0) assertPositiveFinite("PingPongShaderPass height", this.height);
this.format = assertFloatSampledFormat(options.format ?? "rgba16float");
this.filter = options.filter ?? "linear";
this.addressModeU = options.addressModeU ?? "clamp-to-edge";
this.addressModeV = options.addressModeV ?? "clamp-to-edge";
this.iterations = assertIterations(options.iterations ?? 1);
this.clearColor = cloneColor(options.clearColor ?? [
0,
0,
0,
0
]);
this.enabled = options.enabled ?? true;
}
/**
* Replaces fragment source and updates define/include preprocessing.
*/
setFragment(fragment, options) {
const preprocessed = preprocessMaterialFragment({
fragment,
...options?.defines !== void 0 ? { defines: options.defines } : {},
...options?.includes !== void 0 ? { includes: options.includes } : {}
});
assertFragmentContract(preprocessed.fragment);
this.fragment = preprocessed.fragment;
this.fragmentLineMap = preprocessed.lineMap;
}
/**
* Updates iteration count.
*/
setIterations(count) {
this.iterations = assertIterations(count);
}
/**
* Updates explicit dimensions. Passing `undefined` returns that axis to scale-based sizing.
*/
setDimensions(width, height) {
if (width !== void 0) assertPositiveFinite("PingPongShaderPass width", width);
if (height !== void 0) assertPositiveFinite("PingPongShaderPass height", height);
this.width = width;
this.height = height;
this.reset();
}
/**
* Updates canvas-relative scale used for implicit dimensions.
*/
setScale(scale) {
assertPositiveFinite("PingPongShaderPass scale", scale);
this.scale = scale;
this.reset();
}
/**
* Requests both ping-pong textures to be cleared before the next iteration.
*/
reset(clearColor) {
if (clearColor) this.clearColor = cloneColor(clearColor);
this.totalIterations = 0;
this.resetPending = true;
}
/**
* Returns and clears the pending reset color for renderer use.
*/
consumeResetColor() {
if (!this.resetPending) return null;
this.resetPending = false;
return cloneColor(this.clearColor);
}
/**
* Returns the texture key holding the latest result.
*/
getCurrentOutput() {
return this.totalIterations % 2 === 0 ? `${this.target}A` : `${this.target}B`;
}
/**
* Advances the iteration accumulator by the current iteration count.
*/
advanceFrame() {
this.totalIterations += this.iterations;
}
resolveSize(canvasSize) {
return {
width: resolveDimension(this.width, canvasSize.width, this.scale),
height: resolveDimension(this.height, canvasSize.height, this.scale)
};
}
getTarget() {
return this.target;
}
getFragment() {
return this.fragment;
}
getFragmentLineMap() {
return [...this.fragmentLineMap];
}
getIterations() {
return this.iterations;
}
getFormat() {
return this.format;
}
getFilter() {
return this.filter;
}
getAddressModeU() {
return this.addressModeU;
}
getAddressModeV() {
return this.addressModeV;
}
getClearColor() {
return cloneColor(this.clearColor);
}
dispose() {}
};
//#endregion
export { PingPongShaderPass };
//# sourceMappingURL=PingPongShaderPass.js.map