UNPKG

@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
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