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