UNPKG

@luma.gl/shadertools

Version:

Shader module system for luma.gl

1,407 lines (1,381 loc) 286 kB
(function webpackUniversalModuleDefinition(root, factory) { if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if (typeof define === 'function' && define.amd) define([], factory); else if (typeof exports === 'object') exports['luma'] = factory(); else root['luma'] = factory();})(globalThis, function () { "use strict"; var __exports__ = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // external-global-plugin:@luma.gl/core var require_core = __commonJS({ "external-global-plugin:@luma.gl/core"(exports, module) { module.exports = globalThis.luma; } }); // bundle.ts var bundle_exports = {}; __export(bundle_exports, { ShaderAssembler: () => ShaderAssembler, _getDependencyGraph: () => getDependencyGraph, _resolveModules: () => resolveModules, assembleGLSLShaderPair: () => assembleGLSLShaderPair, capitalize: () => capitalize, checkShaderModuleDeprecations: () => checkShaderModuleDeprecations, combineInjects: () => combineInjects, convertToVec4: () => convertToVec4, dirlight: () => dirlight, floatColors: () => floatColors, fp32: () => fp32, fp64: () => fp64, fp64LowPart: () => fp64LowPart, fp64arithmetic: () => fp64arithmetic, fp64ify: () => fp64ify, fp64ifyMatrix4: () => fp64ifyMatrix4, fromHalfFloat: () => fromHalfFloat, generateShaderForModule: () => generateShaderForModule, getGLSLUniformBlocks: () => getGLSLUniformBlocks, getPassthroughFS: () => getPassthroughFS, getQualifierDetails: () => getQualifierDetails, getShaderInfo: () => getShaderInfo, getShaderModuleDependencies: () => getShaderModuleDependencies, getShaderModuleSource: () => getShaderModuleSource, getShaderModuleUniformBlockFields: () => getShaderModuleUniformBlockFields, getShaderModuleUniformBlockName: () => getShaderModuleUniformBlockName, getShaderModuleUniformLayoutValidationResult: () => getShaderModuleUniformLayoutValidationResult, getShaderModuleUniforms: () => getShaderModuleUniforms, gouraudMaterial: () => gouraudMaterial, ibl: () => ibl, initializeShaderModule: () => initializeShaderModule, initializeShaderModules: () => initializeShaderModules, lambertMaterial: () => lambertMaterial, lighting: () => lighting, normalizeByteColor3: () => normalizeByteColor3, normalizeByteColor4: () => normalizeByteColor4, pbrMaterial: () => pbrMaterial, pbrScene: () => pbrScene, phongMaterial: () => phongMaterial, picking: () => picking, preprocess: () => preprocess, random: () => random, resolveUseByteColors: () => resolveUseByteColors, skin: () => skin, toHalfFloat: () => toHalfFloat, typeToChannelCount: () => typeToChannelCount, typeToChannelSuffix: () => typeToChannelSuffix, validateShaderModuleUniformLayout: () => validateShaderModuleUniformLayout, warnIfGLSLUniformBlocksAreNotStd140: () => warnIfGLSLUniformBlocksAreNotStd140 }); __reExport(bundle_exports, __toESM(require_core(), 1)); // src/lib/utils/assert.ts function assert(condition, message) { if (!condition) { const error = new Error(message || "shadertools: assertion failed."); Error.captureStackTrace?.(error, assert); throw error; } } // src/lib/filters/prop-types.ts var DEFAULT_PROP_VALIDATORS = { number: { type: "number", validate(value, propType) { return Number.isFinite(value) && typeof propType === "object" && (propType.max === void 0 || value <= propType.max) && (propType.min === void 0 || value >= propType.min); } }, array: { type: "array", validate(value, propType) { return Array.isArray(value) || ArrayBuffer.isView(value); } } }; function makePropValidators(propTypes) { const propValidators = {}; for (const [name, propType] of Object.entries(propTypes)) { propValidators[name] = makePropValidator(propType); } return propValidators; } function getValidatedProperties(properties, propValidators, errorMessage) { const validated = {}; for (const [key, propsValidator] of Object.entries(propValidators)) { if (properties && key in properties && !propsValidator.private) { if (propsValidator.validate) { assert( propsValidator.validate(properties[key], propsValidator), `${errorMessage}: invalid ${key}` ); } validated[key] = properties[key]; } else { validated[key] = propsValidator.value; } } return validated; } function makePropValidator(propType) { let type = getTypeOf(propType); if (type !== "object") { return { value: propType, ...DEFAULT_PROP_VALIDATORS[type], type }; } if (typeof propType === "object") { if (!propType) { return { type: "object", value: null }; } if (propType.type !== void 0) { return { ...propType, ...DEFAULT_PROP_VALIDATORS[propType.type], type: propType.type }; } if (propType.value === void 0) { return { type: "object", value: propType }; } type = getTypeOf(propType.value); return { ...propType, ...DEFAULT_PROP_VALIDATORS[type], type }; } throw new Error("props"); } function getTypeOf(value) { if (Array.isArray(value) || ArrayBuffer.isView(value)) { return "array"; } return typeof value; } // src/module-injectors.ts var MODULE_INJECTORS_VS = ( /* glsl */ `#ifdef MODULE_LOGDEPTH logdepth_adjustPosition(gl_Position); #endif ` ); var MODULE_INJECTORS_FS = ( /* glsl */ `#ifdef MODULE_MATERIAL fragColor = material_filterColor(fragColor); #endif #ifdef MODULE_LIGHTING fragColor = lighting_filterColor(fragColor); #endif #ifdef MODULE_FOG fragColor = fog_filterColor(fragColor); #endif #ifdef MODULE_PICKING fragColor = picking_filterHighlightColor(fragColor); fragColor = picking_filterPickingColor(fragColor); #endif #ifdef MODULE_LOGDEPTH logdepth_setFragDepth(); #endif ` ); // src/lib/shader-assembly/shader-injections.ts var MODULE_INJECTORS = { vertex: MODULE_INJECTORS_VS, fragment: MODULE_INJECTORS_FS }; var REGEX_START_OF_MAIN = /void\s+main\s*\([^)]*\)\s*\{\n?/; var REGEX_END_OF_MAIN = /}\n?[^{}]*$/; var fragments = []; var DECLARATION_INJECT_MARKER = "__LUMA_INJECT_DECLARATIONS__"; function normalizeInjections(injections) { const result = { vertex: {}, fragment: {} }; for (const hook in injections) { let injection = injections[hook]; const stage = getHookStage(hook); if (typeof injection === "string") { injection = { order: 0, injection }; } result[stage][hook] = injection; } return result; } function getHookStage(hook) { const type = hook.slice(0, 2); switch (type) { case "vs": return "vertex"; case "fs": return "fragment"; default: throw new Error(type); } } function injectShader(source4, stage, inject, injectStandardStubs = false) { const isVertex = stage === "vertex"; for (const key in inject) { const fragmentData = inject[key]; fragmentData.sort((a, b) => a.order - b.order); fragments.length = fragmentData.length; for (let i = 0, len = fragmentData.length; i < len; ++i) { fragments[i] = fragmentData[i].injection; } const fragmentString = `${fragments.join("\n")} `; switch (key) { case "vs:#decl": if (isVertex) { source4 = source4.replace(DECLARATION_INJECT_MARKER, fragmentString); } break; case "vs:#main-start": if (isVertex) { source4 = source4.replace(REGEX_START_OF_MAIN, (match) => match + fragmentString); } break; case "vs:#main-end": if (isVertex) { source4 = source4.replace(REGEX_END_OF_MAIN, (match) => fragmentString + match); } break; case "fs:#decl": if (!isVertex) { source4 = source4.replace(DECLARATION_INJECT_MARKER, fragmentString); } break; case "fs:#main-start": if (!isVertex) { source4 = source4.replace(REGEX_START_OF_MAIN, (match) => match + fragmentString); } break; case "fs:#main-end": if (!isVertex) { source4 = source4.replace(REGEX_END_OF_MAIN, (match) => fragmentString + match); } break; default: source4 = source4.replace(key, (match) => match + fragmentString); } } source4 = source4.replace(DECLARATION_INJECT_MARKER, ""); if (injectStandardStubs) { source4 = source4.replace(/\}\s*$/, (match) => match + MODULE_INJECTORS[stage]); } return source4; } function combineInjects(injects) { const result = {}; assert(Array.isArray(injects) && injects.length > 1); injects.forEach((inject) => { for (const key in inject) { result[key] = result[key] ? `${result[key]} ${inject[key]}` : inject[key]; } }); return result; } // src/lib/shader-module/shader-module.ts function initializeShaderModules(modules) { modules.map((module) => initializeShaderModule(module)); } function initializeShaderModule(module) { if (module.instance) { return; } initializeShaderModules(module.dependencies || []); const { propTypes = {}, deprecations = [], // defines = {}, inject = {} } = module; const instance = { normalizedInjections: normalizeInjections(inject), parsedDeprecations: parseDeprecationDefinitions(deprecations) }; if (propTypes) { instance.propValidators = makePropValidators(propTypes); } module.instance = instance; let defaultProps = {}; if (propTypes) { defaultProps = Object.entries(propTypes).reduce( (obj, [key, propType]) => { const value = propType?.value; if (value) { obj[key] = value; } return obj; }, {} ); } module.defaultUniforms = { ...module.defaultUniforms, ...defaultProps }; } function getShaderModuleUniforms(module, props, oldUniforms) { initializeShaderModule(module); const uniforms = oldUniforms || { ...module.defaultUniforms }; if (props && module.getUniforms) { return module.getUniforms(props, uniforms); } return getValidatedProperties(props, module.instance?.propValidators, module.name); } function checkShaderModuleDeprecations(shaderModule, shaderSource, log2) { shaderModule.deprecations?.forEach((def) => { if (def.regex?.test(shaderSource)) { if (def.deprecated) { log2.deprecated(def.old, def.new)(); } else { log2.removed(def.old, def.new)(); } } }); } function parseDeprecationDefinitions(deprecations) { deprecations.forEach((def) => { switch (def.type) { case "function": def.regex = new RegExp(`\\b${def.old}\\(`); break; default: def.regex = new RegExp(`${def.type} ${def.old};`); } }); return deprecations; } // src/lib/shader-module/shader-module-dependencies.ts function getShaderModuleDependencies(modules) { initializeShaderModules(modules); const moduleMap = {}; const moduleDepth = {}; getDependencyGraph({ modules, level: 0, moduleMap, moduleDepth }); const dependencies = Object.keys(moduleDepth).sort((a, b) => moduleDepth[b] - moduleDepth[a]).map((name) => moduleMap[name]); initializeShaderModules(dependencies); return dependencies; } function getDependencyGraph(options) { const { modules, level, moduleMap, moduleDepth } = options; if (level >= 5) { throw new Error("Possible loop in shader dependency graph"); } for (const module of modules) { moduleMap[module.name] = module; if (moduleDepth[module.name] === void 0 || moduleDepth[module.name] < level) { moduleDepth[module.name] = level; } } for (const module of modules) { if (module.dependencies) { getDependencyGraph({ modules: module.dependencies, level: level + 1, moduleMap, moduleDepth }); } } } function getShaderDependencies(modules) { initializeShaderModules(modules); const moduleMap = {}; const moduleDepth = {}; getDependencyGraph({ modules, level: 0, moduleMap, moduleDepth }); modules = Object.keys(moduleDepth).sort((a, b) => moduleDepth[b] - moduleDepth[a]).map((name) => moduleMap[name]); initializeShaderModules(modules); return modules; } function resolveModules(modules) { return getShaderDependencies(modules); } // src/lib/shader-module/shader-module-uniform-layout.ts var GLSL_UNIFORM_BLOCK_FIELD_REGEXP = /^(?:uniform\s+)?(?:(?:lowp|mediump|highp)\s+)?[A-Za-z0-9_]+(?:<[^>]+>)?\s+([A-Za-z0-9_]+)(?:\s*\[[^\]]+\])?\s*;/; var GLSL_UNIFORM_BLOCK_REGEXP = /((?:layout\s*\([^)]*\)\s*)*)uniform\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}\s*([A-Za-z_][A-Za-z0-9_]*)?\s*;/g; function getShaderModuleUniformBlockName(module) { return `${module.name}Uniforms`; } function getShaderModuleUniformBlockFields(module, stage) { const shaderSource = stage === "wgsl" ? module.source : stage === "vertex" ? module.vs : module.fs; if (!shaderSource) { return null; } const uniformBlockName = getShaderModuleUniformBlockName(module); return extractShaderUniformBlockFieldNames( shaderSource, stage === "wgsl" ? "wgsl" : "glsl", uniformBlockName ); } function getShaderModuleUniformLayoutValidationResult(module, stage) { const expectedUniformNames = Object.keys(module.uniformTypes || {}); if (!expectedUniformNames.length) { return null; } const actualUniformNames = getShaderModuleUniformBlockFields(module, stage); if (!actualUniformNames) { return null; } return { moduleName: module.name, uniformBlockName: getShaderModuleUniformBlockName(module), stage, expectedUniformNames, actualUniformNames, matches: areStringArraysEqual(expectedUniformNames, actualUniformNames) }; } function validateShaderModuleUniformLayout(module, stage, options = {}) { const validationResult = getShaderModuleUniformLayoutValidationResult(module, stage); if (!validationResult || validationResult.matches) { return validationResult; } const message = formatShaderModuleUniformLayoutError(validationResult); options.log?.error?.(message, validationResult)(); if (options.throwOnError !== false) { assert(false, message); } return validationResult; } function getGLSLUniformBlocks(shaderSource) { const blocks = []; const uncommentedSource = stripShaderComments(shaderSource); for (const sourceMatch of uncommentedSource.matchAll(GLSL_UNIFORM_BLOCK_REGEXP)) { const layoutQualifier = sourceMatch[1]?.trim() || null; blocks.push({ blockName: sourceMatch[2], body: sourceMatch[3], instanceName: sourceMatch[4] || null, layoutQualifier, hasLayoutQualifier: Boolean(layoutQualifier), isStd140: Boolean( layoutQualifier && /\blayout\s*\([^)]*\bstd140\b[^)]*\)/.exec(layoutQualifier) ) }); } return blocks; } function warnIfGLSLUniformBlocksAreNotStd140(shaderSource, stage, log2, context) { const nonStd140Blocks = getGLSLUniformBlocks(shaderSource).filter((block) => !block.isStd140); const seenBlockNames = /* @__PURE__ */ new Set(); for (const block of nonStd140Blocks) { if (seenBlockNames.has(block.blockName)) { continue; } seenBlockNames.add(block.blockName); const shaderLabel = context?.label ? `${context.label} ` : ""; const actualLayout = block.hasLayoutQualifier ? `declares ${normalizeWhitespace(block.layoutQualifier)} instead of layout(std140)` : "does not declare layout(std140)"; const message = `${shaderLabel}${stage} shader uniform block ${block.blockName} ${actualLayout}. luma.gl host-side shader block packing assumes explicit layout(std140) for GLSL uniform blocks. Add \`layout(std140)\` to the block declaration.`; log2?.warn?.(message, block)(); } return nonStd140Blocks; } function extractShaderUniformBlockFieldNames(shaderSource, language, uniformBlockName) { const sourceBody = language === "wgsl" ? extractWGSLStructBody(shaderSource, uniformBlockName) : extractGLSLUniformBlockBody(shaderSource, uniformBlockName); if (!sourceBody) { return null; } const fieldNames = []; for (const sourceLine of sourceBody.split("\n")) { const line = sourceLine.replace(/\/\/.*$/, "").trim(); if (!line || line.startsWith("#")) { continue; } const fieldMatch = language === "wgsl" ? line.match(/^([A-Za-z0-9_]+)\s*:/) : line.match(GLSL_UNIFORM_BLOCK_FIELD_REGEXP); if (fieldMatch) { fieldNames.push(fieldMatch[1]); } } return fieldNames; } function extractWGSLStructBody(shaderSource, uniformBlockName) { const structMatch = new RegExp(`\\bstruct\\s+${uniformBlockName}\\b`, "m").exec(shaderSource); if (!structMatch) { return null; } const openBraceIndex = shaderSource.indexOf("{", structMatch.index); if (openBraceIndex < 0) { return null; } let braceDepth = 0; for (let index = openBraceIndex; index < shaderSource.length; index++) { const character = shaderSource[index]; if (character === "{") { braceDepth++; continue; } if (character !== "}") { continue; } braceDepth--; if (braceDepth === 0) { return shaderSource.slice(openBraceIndex + 1, index); } } return null; } function extractGLSLUniformBlockBody(shaderSource, uniformBlockName) { const block = getGLSLUniformBlocks(shaderSource).find( (candidate) => candidate.blockName === uniformBlockName ); return block?.body || null; } function areStringArraysEqual(leftValues, rightValues) { if (leftValues.length !== rightValues.length) { return false; } for (let valueIndex = 0; valueIndex < leftValues.length; valueIndex++) { if (leftValues[valueIndex] !== rightValues[valueIndex]) { return false; } } return true; } function formatShaderModuleUniformLayoutError(validationResult) { const { expectedUniformNames, actualUniformNames } = validationResult; const missingUniformNames = expectedUniformNames.filter( (uniformName) => !actualUniformNames.includes(uniformName) ); const unexpectedUniformNames = actualUniformNames.filter( (uniformName) => !expectedUniformNames.includes(uniformName) ); const mismatchDetails = [ `Expected ${expectedUniformNames.length} fields, found ${actualUniformNames.length}.` ]; const firstMismatchDescription = getFirstUniformMismatchDescription( expectedUniformNames, actualUniformNames ); if (firstMismatchDescription) { mismatchDetails.push(firstMismatchDescription); } if (missingUniformNames.length) { mismatchDetails.push( `Missing from shader block (${missingUniformNames.length}): ${formatUniformNameList( missingUniformNames )}.` ); } if (unexpectedUniformNames.length) { mismatchDetails.push( `Unexpected in shader block (${unexpectedUniformNames.length}): ${formatUniformNameList( unexpectedUniformNames )}.` ); } if (expectedUniformNames.length <= 12 && actualUniformNames.length <= 12 && (missingUniformNames.length || unexpectedUniformNames.length)) { mismatchDetails.push(`Expected: ${expectedUniformNames.join(", ")}.`); mismatchDetails.push(`Actual: ${actualUniformNames.join(", ")}.`); } return `${validationResult.moduleName}: ${validationResult.stage} shader uniform block ${validationResult.uniformBlockName} does not match module.uniformTypes. ${mismatchDetails.join(" ")}`; } function stripShaderComments(shaderSource) { return shaderSource.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, ""); } function normalizeWhitespace(value) { return value.replace(/\s+/g, " ").trim(); } function getFirstUniformMismatchDescription(expectedUniformNames, actualUniformNames) { const minimumLength = Math.min(expectedUniformNames.length, actualUniformNames.length); for (let index = 0; index < minimumLength; index++) { if (expectedUniformNames[index] !== actualUniformNames[index]) { return `First mismatch at field ${index + 1}: expected ${expectedUniformNames[index]}, found ${actualUniformNames[index]}.`; } } if (expectedUniformNames.length > actualUniformNames.length) { return `Shader block ends after field ${actualUniformNames.length}; expected next field ${expectedUniformNames[actualUniformNames.length]}.`; } if (actualUniformNames.length > expectedUniformNames.length) { return `Shader block has extra field ${actualUniformNames.length}: ${actualUniformNames[expectedUniformNames.length]}.`; } return null; } function formatUniformNameList(uniformNames, maxNames = 8) { if (uniformNames.length <= maxNames) { return uniformNames.join(", "); } const remainingCount = uniformNames.length - maxNames; return `${uniformNames.slice(0, maxNames).join(", ")}, ... (${remainingCount} more)`; } // src/lib/shader-assembly/platform-defines.ts function getPlatformShaderDefines(platformInfo) { switch (platformInfo?.gpu.toLowerCase()) { case "apple": return ( /* glsl */ `#define APPLE_GPU // Apple optimizes away the calculation necessary for emulated fp64 #define LUMA_FP64_CODE_ELIMINATION_WORKAROUND 1 #define LUMA_FP32_TAN_PRECISION_WORKAROUND 1 // Intel GPU doesn't have full 32 bits precision in same cases, causes overflow #define LUMA_FP64_HIGH_BITS_OVERFLOW_WORKAROUND 1 ` ); case "nvidia": return ( /* glsl */ `#define NVIDIA_GPU // Nvidia optimizes away the calculation necessary for emulated fp64 #define LUMA_FP64_CODE_ELIMINATION_WORKAROUND 1 ` ); case "intel": return ( /* glsl */ `#define INTEL_GPU // Intel optimizes away the calculation necessary for emulated fp64 #define LUMA_FP64_CODE_ELIMINATION_WORKAROUND 1 // Intel's built-in 'tan' function doesn't have acceptable precision #define LUMA_FP32_TAN_PRECISION_WORKAROUND 1 // Intel GPU doesn't have full 32 bits precision in same cases, causes overflow #define LUMA_FP64_HIGH_BITS_OVERFLOW_WORKAROUND 1 ` ); case "amd": return ( /* glsl */ `#define AMD_GPU ` ); default: return ( /* glsl */ `#define DEFAULT_GPU // Prevent driver from optimizing away the calculation necessary for emulated fp64 #define LUMA_FP64_CODE_ELIMINATION_WORKAROUND 1 // Headless Chrome's software shader 'tan' function doesn't have acceptable precision #define LUMA_FP32_TAN_PRECISION_WORKAROUND 1 // If the GPU doesn't have full 32 bits precision, will causes overflow #define LUMA_FP64_HIGH_BITS_OVERFLOW_WORKAROUND 1 ` ); } } // src/lib/shader-transpiler/transpile-glsl-shader.ts function transpileGLSLShader(source4, stage) { const sourceGLSLVersion = Number(source4.match(/^#version[ \t]+(\d+)/m)?.[1] || 100); if (sourceGLSLVersion !== 300) { throw new Error("luma.gl v9 only supports GLSL 3.00 shader sources"); } switch (stage) { case "vertex": source4 = convertShader(source4, ES300_VERTEX_REPLACEMENTS); return source4; case "fragment": source4 = convertShader(source4, ES300_FRAGMENT_REPLACEMENTS); return source4; default: throw new Error(stage); } } var ES300_REPLACEMENTS = [ // Fix poorly formatted version directive [/^(#version[ \t]+(100|300[ \t]+es))?[ \t]*\n/, "#version 300 es\n"], // The individual `texture...()` functions were replaced with `texture()` overloads [/\btexture(2D|2DProj|Cube)Lod(EXT)?\(/g, "textureLod("], [/\btexture(2D|2DProj|Cube)(EXT)?\(/g, "texture("] ]; var ES300_VERTEX_REPLACEMENTS = [ ...ES300_REPLACEMENTS, // `attribute` keyword replaced with `in` [makeVariableTextRegExp("attribute"), "in $1"], // `varying` keyword replaced with `out` [makeVariableTextRegExp("varying"), "out $1"] ]; var ES300_FRAGMENT_REPLACEMENTS = [ ...ES300_REPLACEMENTS, // `varying` keyword replaced with `in` [makeVariableTextRegExp("varying"), "in $1"] ]; function convertShader(source4, replacements) { for (const [pattern, replacement] of replacements) { source4 = source4.replace(pattern, replacement); } return source4; } function makeVariableTextRegExp(qualifier) { return new RegExp(`\\b${qualifier}[ \\t]+(\\w+[ \\t]+\\w+(\\[\\w+\\])?;)`, "g"); } // src/lib/shader-assembly/shader-hooks.ts function getShaderHooks(hookFunctions, hookInjections) { let result = ""; for (const hookName in hookFunctions) { const hookFunction = hookFunctions[hookName]; result += `void ${hookFunction.signature} { `; if (hookFunction.header) { result += ` ${hookFunction.header}`; } if (hookInjections[hookName]) { const injections = hookInjections[hookName]; injections.sort((a, b) => a.order - b.order); for (const injection of injections) { result += ` ${injection.injection} `; } } if (hookFunction.footer) { result += ` ${hookFunction.footer}`; } result += "}\n"; } return result; } function normalizeShaderHooks(hookFunctions) { const result = { vertex: {}, fragment: {} }; for (const hookFunction of hookFunctions) { let opts; let hook; if (typeof hookFunction !== "string") { opts = hookFunction; hook = opts.hook; } else { opts = {}; hook = hookFunction; } hook = hook.trim(); const [shaderStage, signature] = hook.split(":"); const name = hook.replace(/\(.+/, ""); const normalizedHook = Object.assign(opts, { signature }); switch (shaderStage) { case "vs": result.vertex[name] = normalizedHook; break; case "fs": result.fragment[name] = normalizedHook; break; default: throw new Error(shaderStage); } } return result; } // src/lib/glsl-utils/get-shader-info.ts function getShaderInfo(source4, defaultName) { return { name: getShaderName(source4, defaultName), language: "glsl", version: getShaderVersion(source4) }; } function getShaderName(shader, defaultName = "unnamed") { const SHADER_NAME_REGEXP = /#define[^\S\r\n]*SHADER_NAME[^\S\r\n]*([A-Za-z0-9_-]+)\s*/; const match = SHADER_NAME_REGEXP.exec(shader); return match ? match[1] : defaultName; } function getShaderVersion(source4) { let version = 100; const words = source4.match(/[^\s]+/g); if (words && words.length >= 2 && words[0] === "#version") { const parsedVersion = parseInt(words[1], 10); if (Number.isFinite(parsedVersion)) { version = parsedVersion; } } if (version !== 100 && version !== 300) { throw new Error(`Invalid GLSL version ${version}`); } return version; } // src/lib/shader-assembly/wgsl-binding-scan.ts var WGSL_BINDABLE_VARIABLE_PATTERN = "(?:var<\\s*(uniform|storage(?:\\s*,\\s*[A-Za-z_][A-Za-z0-9_]*)?)\\s*>|var)\\s+([A-Za-z_][A-Za-z0-9_]*)"; var WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN = "\\s*"; var MODULE_WGSL_BINDING_DECLARATION_REGEXES = [ new RegExp( `@binding\\(\\s*(auto|\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@binding\\(\\s*(auto|\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ) ]; var WGSL_BINDING_DECLARATION_REGEXES = [ new RegExp( `@binding\\(\\s*(auto|\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@binding\\(\\s*(auto|\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ) ]; var WGSL_EXPLICIT_BINDING_DECLARATION_REGEXES = [ new RegExp( `@binding\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}@binding\\(\\s*(\\d+)\\s*\\)${WGSL_BINDING_DECLARATION_SEPARATOR_PATTERN}${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ) ]; var WGSL_AUTO_BINDING_DECLARATION_REGEXES = [ new RegExp( `@binding\\(\\s*(auto)\\s*\\)\\s*@group\\(\\s*(\\d+)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)\\s*@binding\\(\\s*(auto)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@binding\\(\\s*(auto)\\s*\\)\\s*@group\\(\\s*(\\d+)\\s*\\)(?:[\\s\\n\\r]*@[A-Za-z_][^\\n\\r]*)*[\\s\\n\\r]*${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)\\s*@binding\\(\\s*(auto)\\s*\\)(?:[\\s\\n\\r]*@[A-Za-z_][^\\n\\r]*)*[\\s\\n\\r]*${WGSL_BINDABLE_VARIABLE_PATTERN}`, "g" ) ]; function maskWGSLComments(source4) { const maskedCharacters = source4.split(""); let index = 0; let blockCommentDepth = 0; let inLineComment = false; let inString = false; let isEscaped = false; while (index < source4.length) { const character = source4[index]; const nextCharacter = source4[index + 1]; if (inString) { if (isEscaped) { isEscaped = false; } else if (character === "\\") { isEscaped = true; } else if (character === '"') { inString = false; } index++; continue; } if (inLineComment) { if (character === "\n" || character === "\r") { inLineComment = false; } else { maskedCharacters[index] = " "; } index++; continue; } if (blockCommentDepth > 0) { if (character === "/" && nextCharacter === "*") { maskedCharacters[index] = " "; maskedCharacters[index + 1] = " "; blockCommentDepth++; index += 2; continue; } if (character === "*" && nextCharacter === "/") { maskedCharacters[index] = " "; maskedCharacters[index + 1] = " "; blockCommentDepth--; index += 2; continue; } if (character !== "\n" && character !== "\r") { maskedCharacters[index] = " "; } index++; continue; } if (character === '"') { inString = true; index++; continue; } if (character === "/" && nextCharacter === "/") { maskedCharacters[index] = " "; maskedCharacters[index + 1] = " "; inLineComment = true; index += 2; continue; } if (character === "/" && nextCharacter === "*") { maskedCharacters[index] = " "; maskedCharacters[index + 1] = " "; blockCommentDepth = 1; index += 2; continue; } index++; } return maskedCharacters.join(""); } function getWGSLBindingDeclarationMatches(source4, regexes) { const maskedSource = maskWGSLComments(source4); const matches = []; for (const regex of regexes) { regex.lastIndex = 0; let match; match = regex.exec(maskedSource); while (match) { const isBindingFirst = regex === regexes[0]; const index = match.index; const length = match[0].length; matches.push({ match: source4.slice(index, index + length), index, length, bindingToken: match[isBindingFirst ? 1 : 2], groupToken: match[isBindingFirst ? 2 : 1], accessDeclaration: match[3]?.trim(), name: match[4] }); match = regex.exec(maskedSource); } } return matches.sort((left, right) => left.index - right.index); } function replaceWGSLBindingDeclarationMatches(source4, regexes, replacer) { const matches = getWGSLBindingDeclarationMatches(source4, regexes); if (!matches.length) { return source4; } let relocatedSource = ""; let lastIndex = 0; for (const match of matches) { relocatedSource += source4.slice(lastIndex, match.index); relocatedSource += replacer(match); lastIndex = match.index + match.length; } relocatedSource += source4.slice(lastIndex); return relocatedSource; } function hasWGSLAutoBinding(source4) { return /@binding\(\s*auto\s*\)/.test(maskWGSLComments(source4)); } function getFirstWGSLAutoBindingDeclarationMatch(source4, regexes) { const autoBindingRegexes = regexes === MODULE_WGSL_BINDING_DECLARATION_REGEXES || regexes === WGSL_BINDING_DECLARATION_REGEXES ? WGSL_AUTO_BINDING_DECLARATION_REGEXES : regexes; return getWGSLBindingDeclarationMatches(source4, autoBindingRegexes).find( (declarationMatch) => declarationMatch.bindingToken === "auto" ); } // src/lib/shader-assembly/wgsl-binding-debug.ts var WGSL_BINDING_DEBUG_REGEXES = [ new RegExp( `@binding\\(\\s*(\\d+)\\s*\\)\\s*@group\\(\\s*(\\d+)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}\\s*:\\s*([^;]+);`, "g" ), new RegExp( `@group\\(\\s*(\\d+)\\s*\\)\\s*@binding\\(\\s*(\\d+)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}\\s*:\\s*([^;]+);`, "g" ) ]; function getShaderBindingDebugRowsFromWGSL(source4, bindingAssignments = []) { const maskedSource = maskWGSLComments(source4); const assignmentMap = /* @__PURE__ */ new Map(); for (const bindingAssignment of bindingAssignments) { assignmentMap.set( getBindingAssignmentKey( bindingAssignment.name, bindingAssignment.group, bindingAssignment.location ), bindingAssignment.moduleName ); } const rows = []; for (const regex of WGSL_BINDING_DEBUG_REGEXES) { regex.lastIndex = 0; let match; match = regex.exec(maskedSource); while (match) { const isBindingFirst = regex === WGSL_BINDING_DEBUG_REGEXES[0]; const binding = Number(match[isBindingFirst ? 1 : 2]); const group = Number(match[isBindingFirst ? 2 : 1]); const accessDeclaration = match[3]?.trim(); const name = match[4]; const resourceType = match[5].trim(); const moduleName = assignmentMap.get(getBindingAssignmentKey(name, group, binding)); rows.push( normalizeShaderBindingDebugRow({ name, group, binding, owner: moduleName ? "module" : "application", moduleName, accessDeclaration, resourceType }) ); match = regex.exec(maskedSource); } } return rows.sort((left, right) => { if (left.group !== right.group) { return left.group - right.group; } if (left.binding !== right.binding) { return left.binding - right.binding; } return left.name.localeCompare(right.name); }); } function normalizeShaderBindingDebugRow(row) { const baseRow = { name: row.name, group: row.group, binding: row.binding, owner: row.owner, kind: "unknown", moduleName: row.moduleName, resourceType: row.resourceType }; if (row.accessDeclaration) { const access = row.accessDeclaration.split(",").map((value) => value.trim()); if (access[0] === "uniform") { return { ...baseRow, kind: "uniform", access: "uniform" }; } if (access[0] === "storage") { const storageAccess = access[1] || "read_write"; return { ...baseRow, kind: storageAccess === "read" ? "read-only-storage" : "storage", access: storageAccess }; } } if (row.resourceType === "sampler" || row.resourceType === "sampler_comparison") { return { ...baseRow, kind: "sampler", samplerKind: row.resourceType === "sampler_comparison" ? "comparison" : "filtering" }; } if (row.resourceType.startsWith("texture_storage_")) { return { ...baseRow, kind: "storage-texture", access: getStorageTextureAccess(row.resourceType), viewDimension: getTextureViewDimension(row.resourceType) }; } if (row.resourceType.startsWith("texture_")) { return { ...baseRow, kind: "texture", viewDimension: getTextureViewDimension(row.resourceType), sampleType: getTextureSampleType(row.resourceType), multisampled: row.resourceType.startsWith("texture_multisampled_") }; } return baseRow; } function getBindingAssignmentKey(name, group, binding) { return `${group}:${binding}:${name}`; } function getTextureViewDimension(resourceType) { if (resourceType.includes("cube_array")) { return "cube-array"; } if (resourceType.includes("2d_array")) { return "2d-array"; } if (resourceType.includes("cube")) { return "cube"; } if (resourceType.includes("3d")) { return "3d"; } if (resourceType.includes("2d")) { return "2d"; } if (resourceType.includes("1d")) { return "1d"; } return void 0; } function getTextureSampleType(resourceType) { if (resourceType.startsWith("texture_depth_")) { return "depth"; } if (resourceType.includes("<i32>")) { return "sint"; } if (resourceType.includes("<u32>")) { return "uint"; } if (resourceType.includes("<f32>")) { return "float"; } return void 0; } function getStorageTextureAccess(resourceType) { const match = /,\s*([A-Za-z_][A-Za-z0-9_]*)\s*>$/.exec(resourceType); return match?.[1]; } // src/lib/shader-assembly/assemble-shaders.ts var INJECT_SHADER_DECLARATIONS = ` ${DECLARATION_INJECT_MARKER} `; var RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT = 100; var FRAGMENT_SHADER_PROLOGUE = ( /* glsl */ `precision highp float; ` ); function assembleWGSLShader(options) { const modules = getShaderModuleDependencies(options.modules || []); const { source: source4, bindingAssignments } = assembleShaderWGSL(options.platformInfo, { ...options, source: options.source, stage: "vertex", modules }); return { source: source4, getUniforms: assembleGetUniforms(modules), bindingAssignments, bindingTable: getShaderBindingDebugRowsFromWGSL(source4, bindingAssignments) }; } function assembleGLSLShaderPair(options) { const { vs: vs4, fs: fs5 } = options; const modules = getShaderModuleDependencies(options.modules || []); return { vs: assembleShaderGLSL(options.platformInfo, { ...options, source: vs4, stage: "vertex", modules }), fs: assembleShaderGLSL(options.platformInfo, { ...options, // @ts-expect-error source: fs5, stage: "fragment", modules }), getUniforms: assembleGetUniforms(modules) }; } function assembleShaderWGSL(platformInfo, options) { const { // id, source: source4, stage, modules, // defines = {}, hookFunctions = [], inject = {}, log: log2 } = options; assert(typeof source4 === "string", "shader source must be a string"); const coreSource = source4; let assembledSource = ""; const hookFunctionMap = normalizeShaderHooks(hookFunctions); const hookInjections = {}; const declInjections = {}; const mainInjections = {}; for (const key in inject) { const injection = typeof inject[key] === "string" ? { injection: inject[key], order: 0 } : inject[key]; const match = /^(v|f)s:(#)?([\w-]+)$/.exec(key); if (match) { const hash = match[2]; const name = match[3]; if (hash) { if (name === "decl") { declInjections[key] = [injection]; } else { mainInjections[key] = [injection]; } } else { hookInjections[key] = [injection]; } } else { mainInjections[key] = [injection]; } } const modulesToInject = modules; const applicationRelocation = relocateWGSLApplicationBindings(coreSource); const usedBindingsByGroup = getUsedBindingsByGroupFromApplicationWGSL( applicationRelocation.source ); const reservedBindingKeysByGroup = reserveRegisteredModuleBindings( modulesToInject, options._bindingRegistry, usedBindingsByGroup ); const bindingAssignments = []; for (const module of modulesToInject) { if (log2) { checkShaderModuleDeprecations(module, coreSource, log2); } const relocation = relocateWGSLModuleBindings( getShaderModuleSource(module, "wgsl", log2), module, { usedBindingsByGroup, bindingRegistry: options._bindingRegistry, reservedBindingKeysByGroup } ); bindingAssignments.push(...relocation.bindingAssignments); const moduleSource = relocation.source; assembledSource += moduleSource; const injections = module.injections?.[stage] || {}; for (const key in injections) { const match = /^(v|f)s:#([\w-]+)$/.exec(key); if (match) { const name = match[2]; const injectionType = name === "decl" ? declInjections : mainInjections; injectionType[key] = injectionType[key] || []; injectionType[key].push(injections[key]); } else { hookInjections[key] = hookInjections[key] || []; hookInjections[key].push(injections[key]); } } } assembledSource += INJECT_SHADER_DECLARATIONS; assembledSource = injectShader(assembledSource, stage, declInjections); assembledSource += getShaderHooks(hookFunctionMap[stage], hookInjections); assembledSource += formatWGSLBindingAssignmentComments(bindingAssignments); assembledSource += applicationRelocation.source; assembledSource = injectShader(assembledSource, stage, mainInjections); assertNoUnresolvedAutoBindings(assembledSource); return { source: assembledSource, bindingAssignments }; } function assembleShaderGLSL(platformInfo, options) { const { source: source4, stage, language = "glsl", modules, defines = {}, hookFunctions = [], inject = {}, prologue = true, log: log2 } = options; assert(typeof source4 === "string", "shader source must be a string"); const sourceVersion = language === "glsl" ? getShaderInfo(source4).version : -1; const targetVersion = platformInfo.shaderLanguageVersion; const sourceVersionDirective = sourceVersion === 100 ? "#version 100" : "#version 300 es"; const sourceLines = source4.split("\n"); const coreSource = sourceLines.slice(1).join("\n"); const allDefines = {}; modules.forEach((module) => { Object.assign(allDefines, module.defines); }); Object.assign(allDefines, defines); let assembledSource = ""; switch (language) { case "wgsl": break; case "glsl": assembledSource = prologue ? `${sourceVersionDirective} // ----- PROLOGUE ------------------------- ${`#define SHADER_TYPE_${stage.toUpperCase()}`} ${getPlatformShaderDefines(platformInfo)} ${stage === "fragment" ? FRAGMENT_SHADER_PROLOGUE : ""} // ----- APPLICATION DEFINES ------------------------- ${getApplicationDefines(allDefines)} ` : `${sourceVersionDirective} `; break; } const hookFunctionMap = normalizeShaderHooks(hookFunctions); const hookInjections = {}; const declInjections = {}; const mainInjections = {}; for (const key in inject) { const injection = typeof inject[key] === "string" ? { injection: inject[key], order: 0 } : inject[key]; const match = /^(v|f)s:(#)?([\w-]+)$/.exec(key); if (match) { const hash = match[2]; const name = match[3]; if (hash) { if (name === "decl") { declInjections[key] = [injection]; } else { mainInjections[key] = [injection]; } } else { hookInjections[key] = [injection]; } } else { mainInjections[key] = [injection]; } } for (const module of modules) { if (log2) { checkShaderModuleDeprecations(module, coreSource, log2); } const moduleSource = getShaderModuleSource(module, stage, log2); assembledSource += moduleSource; const injections = module.instance?.normalizedInjections[stage] || {}; for (const key in injections) { const match = /^(v|f)s:#([\w-]+)$/.exec(key); if (match) { const name = match[2]; const injectionType = name === "decl" ? declInjections : mainInjections; injectionType[key] = injectionType[key] || []; injectionType[key].push(injections[key]); } else { hookInjections[key] = hookInjections[key] || []; hookInjections[key].push(injections[key]); } } } assembledSource += "// ----- MAIN SHADER SOURCE -------------------------"; assembledSource += INJECT_SHADER_DECLARATIONS; assembledSource = injectShader(assembledSource, stage, declInjections); assembledSource += getShaderHooks(hookFunctionMap[stage], hookInjections); assembledSource += coreSource; assembledSource = injectShader(assembledSource, stage, mainInjections); if (language === "glsl" && sourceVersion !== targetVersion) { assembledSource = transpileGLSLShader(assembledSource, stage); } if (language === "glsl") { warnIfGLSLUniformBlocksAreNotStd140(assembledSource, stage, log2); } return assembledSource.trim(); } function assembleGetUniforms(modules) { return function getUniforms4(opts) { const uniforms = {}; for (const module of modules) { const moduleUniforms = module.getUniforms?.(opts, uniforms); Object.assign(uniforms, moduleUniforms); } return uniforms; }; } function getApplicationDefines(defines = {}) { let sourceText = ""; for (const define in defines) { const value = defines[define]; if (value || Number.isFinite(value)) { sourceText += `#define ${define.toUpperCase()} ${defines[define]} `; } } return sourceText; } function getShaderModuleSource(module, stage, log2) { let moduleSource; switch (stage) { case "vertex": moduleSource = module.vs