UNPKG

@luma.gl/shadertools

Version:

Shader module system for luma.gl

269 lines (235 loc) 7.94 kB
import {VERTEX_SHADER, FRAGMENT_SHADER} from './constants'; import {resolveModules} from './resolve-modules'; import {getPlatformShaderDefines, getVersionDefines} from './platform-defines'; import injectShader, {DECLARATION_INJECT_MARKER} from './inject-shader'; import transpileShader from './transpile-shader'; import {assert} from '../utils'; const INJECT_SHADER_DECLARATIONS = `\n\n${DECLARATION_INJECT_MARKER}\n\n`; const SHADER_TYPE = { [VERTEX_SHADER]: 'vertex', [FRAGMENT_SHADER]: 'fragment' }; // Precision prologue to inject before functions are injected in shader // TODO - extract any existing prologue in the fragment source and move it up... const FRAGMENT_SHADER_PROLOGUE = `\ precision highp float; `; // Inject a list of modules export function assembleShaders(gl, opts) { const {vs, fs} = opts; const modules = resolveModules(opts.modules || []); return { gl, vs: assembleShader(gl, Object.assign({}, opts, {source: vs, type: VERTEX_SHADER, modules})), fs: assembleShader(gl, Object.assign({}, opts, {source: fs, type: FRAGMENT_SHADER, modules})), getUniforms: assembleGetUniforms(modules) }; } // Pulls together complete source code for either a vertex or a fragment shader // adding prologues, requested module chunks, and any final injections. function assembleShader( gl, { id, source, type, modules, defines = {}, hookFunctions = [], inject = {}, transpileToGLSL100 = false, prologue = true, log } ) { assert(typeof source === 'string', 'shader source must be a string'); const isVertex = type === VERTEX_SHADER; const sourceLines = source.split('\n'); let glslVersion = 100; let versionLine = ''; let coreSource = source; // Extract any version directive string from source. // TODO : keep all pre-processor statements at the begining of the shader. if (sourceLines[0].indexOf('#version ') === 0) { glslVersion = 300; // TODO - regexp that matches actual version number versionLine = sourceLines[0]; coreSource = sourceLines.slice(1).join('\n'); } else { versionLine = `#version ${glslVersion}`; } // Combine Module and Application Defines const allDefines = {}; modules.forEach(module => { Object.assign(allDefines, module.getDefines()); }); Object.assign(allDefines, defines); // Add platform defines (use these to work around platform-specific bugs and limitations) // Add common defines (GLSL version compatibility, feature detection) // Add precision declaration for fragment shaders let assembledSource = prologue ? `\ ${versionLine} ${getShaderName({id, source, type})} ${getShaderType({type})} ${getPlatformShaderDefines(gl)} ${getVersionDefines(gl, glslVersion, !isVertex)} ${getApplicationDefines(allDefines)} ${isVertex ? '' : FRAGMENT_SHADER_PROLOGUE} ` : `${versionLine} `; const hookFunctionMap = normalizeHookFunctions(hookFunctions); // Add source of dependent modules in resolved order 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 = key.match(/^(v|f)s:(#)?([\w-]+)$/); 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 { // Regex injection mainInjections[key] = [injection]; } } for (const module of modules) { if (log) { module.checkDeprecations(coreSource, log); } const moduleSource = module.getModuleSource(type, glslVersion); // Add the module source, and a #define that declares it presence assembledSource += moduleSource; const injections = module.injections[type]; for (const key in injections) { const match = key.match(/^(v|f)s:#([\w-]+)$/); 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]); } } } // For injectShader assembledSource += INJECT_SHADER_DECLARATIONS; assembledSource = injectShader(assembledSource, type, declInjections); assembledSource += getHookFunctions(hookFunctionMap[type], hookInjections); // Add the version directive and actual source of this shader assembledSource += coreSource; // Apply any requested shader injections assembledSource = injectShader(assembledSource, type, mainInjections); assembledSource = transpileShader( assembledSource, transpileToGLSL100 ? 100 : glslVersion, isVertex ); return assembledSource; } // Returns a combined `getUniforms` covering the options for all the modules, // the created function will pass on options to the inidividual `getUniforms` // function of each shader module and combine the results into one object that // can be passed to setUniforms. function assembleGetUniforms(modules) { return function getUniforms(opts) { const uniforms = {}; for (const module of modules) { // `modules` is already sorted by dependency level. This guarantees that // modules have access to the uniforms that are generated by their dependencies. const moduleUniforms = module.getUniforms(opts, uniforms); Object.assign(uniforms, moduleUniforms); } return uniforms; }; } function getShaderType({type}) { return ` #define SHADER_TYPE_${SHADER_TYPE[type].toUpperCase()} `; } // Generate "glslify-compatible" SHADER_NAME defines // These are understood by the GLSL error parsing function // If id is provided and no SHADER_NAME constant is present in source, create one function getShaderName({id, source, type}) { const injectShaderName = id && typeof id === 'string' && source.indexOf('SHADER_NAME') === -1; return injectShaderName ? ` #define SHADER_NAME ${id}_${SHADER_TYPE[type]} ` : ''; } // Generates application defines from an object function getApplicationDefines(defines = {}) { let count = 0; let sourceText = ''; for (const define in defines) { if (count === 0) { sourceText += '\n// APPLICATION DEFINES\n'; } count++; const value = defines[define]; if (value || Number.isFinite(value)) { sourceText += `#define ${define.toUpperCase()} ${defines[define]}\n`; } } if (count === 0) { sourceText += '\n'; } return sourceText; } function getHookFunctions(hookFunctions, hookInjections) { let result = ''; for (const hookName in hookFunctions) { const hookFunction = hookFunctions[hookName]; result += `void ${hookFunction.signature} {\n`; 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}\n`; } } if (hookFunction.footer) { result += ` ${hookFunction.footer}`; } result += '}\n'; } return result; } function normalizeHookFunctions(hookFunctions) { const result = { vs: {}, fs: {} }; hookFunctions.forEach(hook => { let opts; if (typeof hook !== 'string') { opts = hook; hook = opts.hook; } else { opts = {}; } hook = hook.trim(); const [stage, signature] = hook.split(':'); const name = hook.replace(/\(.+/, ''); result[stage][name] = Object.assign(opts, {signature}); }); return result; }