@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
196 lines (188 loc) • 6.89 kB
JavaScript
import { assertUniformName } from "./uniforms.js";
//#region src/lib/core/shader.ts
/**
* Fallback uniform field used when no custom uniforms are provided.
*/
var DEFAULT_UNIFORM_FIELD = "motiongpu_unused: vec4f,";
/**
* Builds WGSL struct fields for user uniforms.
*/
function buildUniformStruct(layout) {
if (layout.entries.length === 0) return DEFAULT_UNIFORM_FIELD;
return layout.entries.map((entry) => {
assertUniformName(entry.name);
return `${entry.name}: ${entry.type},`;
}).join("\n ");
}
/**
* Builds a numeric expression that references one uniform value to keep bindings alive.
*/
function getKeepAliveExpression(layout) {
if (layout.entries.length === 0) return "motiongpuUniforms.motiongpu_unused.x";
const [firstEntry] = layout.entries;
if (!firstEntry) return "motiongpuUniforms.motiongpu_unused.x";
if (firstEntry.type === "f32") return `motiongpuUniforms.${firstEntry.name}`;
if (firstEntry.type === "mat4x4f") return `motiongpuUniforms.${firstEntry.name}[0].x`;
return `motiongpuUniforms.${firstEntry.name}.x`;
}
/**
* Builds texture sampler/texture binding declarations.
*/
function buildTextureBindings(textureKeys) {
if (textureKeys.length === 0) return "";
const declarations = [];
for (let index = 0; index < textureKeys.length; index += 1) {
const key = textureKeys[index];
if (key === void 0) continue;
assertUniformName(key);
const binding = 2 + index * 2;
declarations.push(` var ${key}Sampler: sampler;`);
declarations.push(` var ${key}: texture_2d<f32>;`);
}
return declarations.join("\n");
}
/**
* Builds read-only storage buffer bindings for fragment shader.
*/
function buildFragmentStorageBufferBindings(storageBufferKeys, definitions) {
if (storageBufferKeys.length === 0) return "";
const declarations = [];
for (let index = 0; index < storageBufferKeys.length; index += 1) {
const key = storageBufferKeys[index];
if (key === void 0) continue;
const definition = definitions[key];
if (!definition) continue;
declarations.push(` var<storage, read> ${key}: ${definition.type};`);
}
return declarations.join("\n");
}
/**
* Optionally returns helper WGSL for linear-to-sRGB conversion.
*/
function buildColorTransformHelpers(enableSrgbTransform) {
if (!enableSrgbTransform) return "";
return `
fn motiongpuLinearToSrgb(linearColor: vec3f) -> vec3f {
let cutoff = vec3f(0.0031308);
let lower = linearColor * 12.92;
let higher = vec3f(1.055) * pow(linearColor, vec3f(1.0 / 2.4)) - vec3f(0.055);
return select(lower, higher, linearColor > cutoff);
}
`;
}
/**
* Builds fragment output code with optional color-space conversion.
*/
function buildFragmentOutput(keepAliveExpression, enableSrgbTransform) {
if (enableSrgbTransform) return `
let fragColor = frag(in.uv);
let motiongpuKeepAlive = ${keepAliveExpression};
let motiongpuLinear = vec4f(fragColor.rgb + motiongpuKeepAlive * 0.0, fragColor.a);
let motiongpuSrgb = motiongpuLinearToSrgb(max(motiongpuLinear.rgb, vec3f(0.0)));
return vec4f(motiongpuSrgb, motiongpuLinear.a);
`;
return `
let fragColor = frag(in.uv);
let motiongpuKeepAlive = ${keepAliveExpression};
return vec4f(fragColor.rgb + motiongpuKeepAlive * 0.0, fragColor.a);
`;
}
function countLines(source, end = source.length) {
let lineCount = 1;
for (let index = 0; index < end; index += 1) if (source.charCodeAt(index) === 10) lineCount += 1;
return lineCount;
}
/**
* Assembles complete WGSL shader source used by the fullscreen renderer pipeline.
*
* @param fragmentWgsl - User fragment shader code containing `frag(uv: vec2f) -> vec4f`.
* @param uniformLayout - Resolved uniform layout.
* @param textureKeys - Sorted texture keys.
* @param options - Shader build options.
* @returns Complete WGSL source for vertex + fragment stages.
*/
function buildShaderSource(fragmentWgsl, uniformLayout, textureKeys = [], options) {
const uniformFields = buildUniformStruct(uniformLayout);
const keepAliveExpression = getKeepAliveExpression(uniformLayout);
const textureBindings = buildTextureBindings(textureKeys);
const enableSrgbTransform = options?.convertLinearToSrgb ?? false;
const colorTransformHelpers = buildColorTransformHelpers(enableSrgbTransform);
const fragmentOutput = buildFragmentOutput(keepAliveExpression, enableSrgbTransform);
const storageBufferBindings = buildFragmentStorageBufferBindings(options?.storageBufferKeys ?? [], options?.storageBufferDefinitions ?? {});
return `
struct MotionGPUFrame {
time: f32,
delta: f32,
resolution: vec2f,
};
struct MotionGPUUniforms {
${uniformFields}
};
var<uniform> motiongpuFrame: MotionGPUFrame;
var<uniform> motiongpuUniforms: MotionGPUUniforms;
${textureBindings}
${storageBufferBindings ? "\n" + storageBufferBindings : ""}
${colorTransformHelpers}
struct MotionGPUVertexOut {
position: vec4f,
uv: vec2f,
};
fn motiongpuVertex( index: u32) -> MotionGPUVertexOut {
var positions = array<vec2f, 3>(
vec2f(-1.0, -3.0),
vec2f(-1.0, 1.0),
vec2f(3.0, 1.0)
);
let position = positions[index];
var out: MotionGPUVertexOut;
out.position = vec4f(position, 0.0, 1.0);
out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
return out;
}
${fragmentWgsl}
fn motiongpuFragment(in: MotionGPUVertexOut) -> vec4f {
${fragmentOutput}
}
`;
}
/**
* Assembles complete WGSL shader source with material-source line mapping metadata.
*/
function buildShaderSourceWithMap(fragmentWgsl, uniformLayout, textureKeys = [], options) {
const code = buildShaderSource(fragmentWgsl, uniformLayout, textureKeys, options);
const fragmentStartIndex = code.indexOf(fragmentWgsl);
const lineCount = countLines(code);
const lineMap = new Array(lineCount + 1).fill(null);
if (fragmentStartIndex === -1) return {
code,
lineMap
};
const fragmentStartLine = countLines(code, fragmentStartIndex);
const fragmentLineCount = countLines(fragmentWgsl);
for (let line = 0; line < fragmentLineCount; line += 1) {
const generatedLine = fragmentStartLine + line;
lineMap[generatedLine] = options?.fragmentLineMap?.[line + 1] ?? {
kind: "fragment",
line: line + 1
};
}
return {
code,
lineMap
};
}
/**
* Converts source location metadata to user-facing diagnostics label.
*/
function formatShaderSourceLocation(location) {
if (!location) return null;
if (location.kind === "fragment") return `fragment line ${location.line}`;
if (location.kind === "include") return `include <${location.include}> line ${location.line}`;
if (location.kind === "compute") return `compute line ${location.line}`;
return `define "${location.define}" line ${location.line}`;
}
//#endregion
export { buildShaderSource, buildShaderSourceWithMap, formatShaderSourceLocation };
//# sourceMappingURL=shader.js.map