UNPKG

@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
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(`@group(0) @binding(${binding}) var ${key}Sampler: sampler;`); declarations.push(`@group(0) @binding(${binding + 1}) 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(`@group(1) @binding(${index}) 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} }; @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame; @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms; ${textureBindings} ${storageBufferBindings ? "\n" + storageBufferBindings : ""} ${colorTransformHelpers} struct MotionGPUVertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f, }; @vertex fn motiongpuVertex(@builtin(vertex_index) 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} @fragment fn motiongpuFragment(in: MotionGPUVertexOut) -> @location(0) 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