UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

165 lines (164 loc) 6.43 kB
import { assertUniformName } from "./uniforms.js"; //#region src/lib/core/material-preprocess.ts var INCLUDE_DIRECTIVE_PATTERN = /^\s*#include\s+<([A-Za-z_][A-Za-z0-9_]*)>\s*$/; var DEFINE_VECTOR_LENGTHS = { vec2f: 2, vec3f: 3, vec4f: 4 }; function normalizeTypedDefine(name, define) { const value = define.value; if (define.type === "bool") { if (typeof value !== "boolean") throw new Error(`Invalid define value for "${name}". bool define requires boolean value.`); return { type: "bool", value }; } if (define.type === "vec2f" || define.type === "vec3f" || define.type === "vec4f") { const expectedLength = DEFINE_VECTOR_LENGTHS[define.type]; if (!Array.isArray(value) || value.length !== expectedLength || !value.every((entry) => typeof entry === "number" && Number.isFinite(entry))) throw new Error(`Invalid define value for "${name}". ${define.type} define requires a tuple with ${expectedLength} finite numbers.`); return { type: define.type, value: Object.freeze([...value]) }; } if (define.type !== "f32" && define.type !== "i32" && define.type !== "u32") throw new Error(`Invalid define value for "${name}". Unsupported define type.`); if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Invalid define value for "${name}". Numeric define must be finite.`); if ((define.type === "i32" || define.type === "u32") && !Number.isInteger(value)) throw new Error(`Invalid define value for "${name}". ${define.type} define requires integer.`); if (define.type === "u32" && value < 0) throw new Error(`Invalid define value for "${name}". u32 define must be >= 0.`); return { type: define.type, value }; } function toF32Literal(value) { return Number.isInteger(value) ? `${value}.0` : `${value}`; } function toVectorDefineLine(key, type, value) { return `const ${key}: ${type} = ${type}(${value.map(toF32Literal).join(", ")});`; } /** * Validates and normalizes define entries. */ function normalizeDefines(defines) { const resolved = {}; for (const [name, value] of Object.entries(defines ?? {})) { assertUniformName(name); if (typeof value === "boolean") { resolved[name] = value; continue; } if (typeof value === "number") { if (!Number.isFinite(value)) throw new Error(`Invalid define value for "${name}". Define numbers must be finite.`); resolved[name] = value; continue; } const normalized = normalizeTypedDefine(name, value); resolved[name] = Object.freeze(normalized); } return resolved; } /** * Validates include map identifiers and source chunks. */ function normalizeIncludes(includes) { const resolved = {}; for (const [name, source] of Object.entries(includes ?? {})) { assertUniformName(name); if (typeof source !== "string" || source.trim().length === 0) throw new Error(`Invalid include "${name}". Include source must be a non-empty WGSL string.`); resolved[name] = source; } return resolved; } /** * Converts one define declaration to WGSL `const`. */ function toDefineLine(key, value) { if (typeof value === "boolean") return `const ${key}: bool = ${value ? "true" : "false"};`; if (typeof value === "number") return `const ${key}: f32 = ${toF32Literal(value)};`; switch (value.type) { case "bool": return `const ${key}: bool = ${value.value ? "true" : "false"};`; case "f32": return `const ${key}: f32 = ${toF32Literal(value.value)};`; case "i32": return `const ${key}: i32 = ${value.value};`; case "u32": return `const ${key}: u32 = ${value.value}u;`; case "vec2f": case "vec3f": case "vec4f": return toVectorDefineLine(key, value.type, value.value); default: throw new Error(`Invalid define value for "${key}". Unsupported define type.`); } } function expandChunk(source, kind, includeName, includes, stack, expandedIncludes) { const sourceLines = source.split("\n"); const lines = []; const mapEntries = []; for (let index = 0; index < sourceLines.length; index += 1) { const sourceLine = sourceLines[index]; if (sourceLine === void 0) continue; const includeMatch = sourceLine.match(INCLUDE_DIRECTIVE_PATTERN); if (!includeMatch) { lines.push(sourceLine); mapEntries.push({ kind, line: index + 1, ...kind === "include" && includeName ? { include: includeName } : {} }); continue; } const includeKey = includeMatch[1]; if (!includeKey) throw new Error("Invalid include directive in fragment shader."); assertUniformName(includeKey); const includeSource = includes[includeKey]; if (!includeSource) throw new Error(`Unknown include "${includeKey}" referenced in fragment shader.`); if (stack.includes(includeKey)) throw new Error(`Circular include detected for "${includeKey}". Include stack: ${[...stack, includeKey].join(" -> ")}.`); if (expandedIncludes.has(includeKey)) continue; expandedIncludes.add(includeKey); const nested = expandChunk(includeSource, "include", includeKey, includes, [...stack, includeKey], expandedIncludes); lines.push(...nested.lines); mapEntries.push(...nested.mapEntries); } return { lines, mapEntries }; } /** * Preprocesses material fragment with deterministic define/include expansion and line mapping. */ function preprocessMaterialFragment(input) { const normalizedDefines = normalizeDefines(input.defines); const normalizedIncludes = normalizeIncludes(input.includes); const fragmentExpanded = expandChunk(input.fragment, "fragment", void 0, normalizedIncludes, [], /* @__PURE__ */ new Set()); const defineEntries = Object.entries(normalizedDefines).sort(([a], [b]) => a.localeCompare(b)); const lines = []; const defineLines = []; const mapEntries = []; for (let index = 0; index < defineEntries.length; index += 1) { const entry = defineEntries[index]; if (!entry) continue; const [name, value] = entry; const defineLine = toDefineLine(name, value); lines.push(defineLine); defineLines.push(defineLine); mapEntries.push({ kind: "define", line: index + 1, define: name }); } if (defineEntries.length > 0) { lines.push(""); mapEntries.push(null); } lines.push(...fragmentExpanded.lines); mapEntries.push(...fragmentExpanded.mapEntries); const lineMap = [null, ...mapEntries]; return { fragment: lines.join("\n"), lineMap, defineBlockSource: defineLines.join("\n") }; } //#endregion export { normalizeDefines, normalizeIncludes, preprocessMaterialFragment, toDefineLine }; //# sourceMappingURL=material-preprocess.js.map