UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

263 lines (260 loc) 11.9 kB
import { Debug } from '../../core/debug.js'; import { hashCode } from '../../core/hash.js'; import { version, revision } from '../../core/core.js'; import { Shader } from '../../platform/graphics/shader.js'; import { SHADER_DEPTH, SHADER_PICK, SHADER_PREPASS, SHADER_FORWARD, SHADER_SHADOW } from '../constants.js'; import { ShaderPass } from '../shader-pass.js'; import { StandardMaterialOptions } from '../materials/standard-material-options.js'; import { CameraShaderParams } from '../camera-shader-params.js'; /** * @import { ShaderGenerator } from './programs/shader-generator.js' */ /** * A class responsible for creation and caching of required shaders. * There is a two level cache. The first level generates the shader based on the provided options. * The second level processes this generated shader using processing options - in most cases * modifies it to support uniform buffers. * * @ignore */ class ProgramLibrary { destroy() { this.clearCache(); } register(name, generator) { if (!this._generators.has(name)) { this._generators.set(name, generator); } } unregister(name) { if (this._generators.has(name)) { this._generators.delete(name); } } isRegistered(name) { return this._generators.has(name); } /** * Returns a generated shader definition for the specified options. They key is used to cache the * shader definition. * * @param {ShaderGenerator} generator - The generator to use. * @param {string} name - The unique name of the shader generator. * @param {number} key - A unique key representing the shader options. * @param {object} options - The shader options. * @returns {object} - The shader definition. */ generateShaderDefinition(generator, name, key, options) { var def = this.definitionsCache.get(key); if (!def) { var _options_litOptions, _options_litOptions1; var lights; if ((_options_litOptions = options.litOptions) == null ? undefined : _options_litOptions.lights) { lights = options.litOptions.lights; options.litOptions.lights = lights.map((l)=>{ // TODO: refactor this to avoid creating a clone of the light. var lcopy = l.clone ? l.clone() : l; lcopy.key = l.key; return lcopy; }); } this.storeNewProgram(name, options); if ((_options_litOptions1 = options.litOptions) == null ? undefined : _options_litOptions1.lights) { options.litOptions.lights = lights; } if (this._precached) { Debug.log("ProgramLibrary#getProgram: Cache miss for shader " + name + " key " + key + " after shaders precaching"); } var device = this._device; def = generator.createShaderDefinition(device, options); var _def_name; def.name = (_def_name = def.name) != null ? _def_name : options.pass ? name + "-pass:" + options.pass : name; this.definitionsCache.set(key, def); } return def; } getCachedShader(key) { return this.processedCache.get(key); } setCachedShader(key, shader) { this.processedCache.set(key, shader); } getProgram(name, options, processingOptions, userMaterialId) { var generator = this._generators.get(name); if (!generator) { Debug.warn("ProgramLibrary#getProgram: No program library functions registered for: " + name); return null; } // we have a key for shader source code generation, a key for its further processing to work with // uniform buffers, and a final key to get the processed shader from the cache var generationKeyString = generator.generateKey(options); var generationKey = hashCode(generationKeyString); var processingKeyString = processingOptions.generateKey(this._device); var processingKey = hashCode(processingKeyString); var totalKey = generationKey + "#" + processingKey; // do we have final processed shader var processedShader = this.getCachedShader(totalKey); if (!processedShader) { // get generated shader var generatedShaderDef = this.generateShaderDefinition(generator, name, generationKey, options); Debug.assert(generatedShaderDef); // use shader pass name if known var passName = ''; var shaderPassInfo; if (options.pass !== undefined) { shaderPassInfo = ShaderPass.get(this._device).getByIndex(options.pass); passName = "-" + shaderPassInfo.name; } // fire an event to allow the shader to be modified by the user. Note that any modifications are applied // to all materials using the same generated shader, as the cache key is not modified. this._device.fire('shader:generate', { userMaterialId, shaderPassInfo, definition: generatedShaderDef }); // create a shader definition for the shader that will include the processingOptions var shaderDefinition = { name: "" + generatedShaderDef.name + passName + "-proc", attributes: generatedShaderDef.attributes, vshader: generatedShaderDef.vshader, vincludes: generatedShaderDef.vincludes, fincludes: generatedShaderDef.fincludes, fshader: generatedShaderDef.fshader, processingOptions: processingOptions, shaderLanguage: generatedShaderDef.shaderLanguage, meshUniformBufferFormat: generatedShaderDef.meshUniformBufferFormat, meshBindGroupFormat: generatedShaderDef.meshBindGroupFormat }; // add new shader to the processed cache processedShader = new Shader(this._device, shaderDefinition); // keep the keys in the debug mode Debug.call(()=>{ processedShader._generationKey = generationKeyString; processedShader._processingKey = processingKeyString; }); this.setCachedShader(totalKey, processedShader); } return processedShader; } storeNewProgram(name, options) { var opt = {}; if (name === 'standard') { // For standard material saving all default values is overkill, so we store only diff var defaultMat = this._getDefaultStdMatOptions(options.pass); for(var p in options){ if (options.hasOwnProperty(p) && defaultMat[p] !== options[p] || p === 'pass') { opt[p] = options[p]; } } // Note: this was added in #4792 and it does not filter out the default values, like the loop above for(var p1 in options.litOptions){ opt[p1] = options.litOptions[p1]; } } else { // Other shaders have only dozen params opt = options; } this._programsCollection.push(JSON.stringify({ name: name, options: opt })); } // run pc.getProgramLibrary(device).dumpPrograms(); from browser console to build shader options script dumpPrograms() { var text = 'let device = pc.app ? pc.app.graphicsDevice : pc.Application.getApplication().graphicsDevice;\n'; text += 'let shaders = ['; if (this._programsCollection[0]) { text += "\n " + this._programsCollection[0]; } for(var i = 1; i < this._programsCollection.length; ++i){ text += ",\n " + this._programsCollection[i]; } text += '\n];\n'; text += 'pc.getProgramLibrary(device).precompile(shaders);\n'; text += 'if (pc.version != "' + version + '" || pc.revision != "' + revision + '")\n'; text += '\tconsole.warn(\"precompile-shaders.js: engine version mismatch, rebuild shaders lib with current engine\");'; var element = document.createElement('a'); element.setAttribute('href', "data:text/plain;charset=utf-8," + encodeURIComponent(text)); element.setAttribute('download', 'precompile-shaders.js'); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } clearCache() { this._isClearingCache = true; this.processedCache.forEach((shader)=>{ shader.destroy(); }); this.processedCache.clear(); this._isClearingCache = false; } /** * Remove shader from the cache. This function does not destroy it, that is the responsibility * of the caller. * * @param {Shader} shader - The shader to be removed. */ removeFromCache(shader) { // don't delete by one when clearing whole cache if (this._isClearingCache) { return; } this.processedCache.forEach((cachedShader, key)=>{ if (shader === cachedShader) { this.processedCache.delete(key); } }); } _getDefaultStdMatOptions(pass) { var shaderPassInfo = ShaderPass.get(this._device).getByIndex(pass); return pass === SHADER_DEPTH || pass === SHADER_PICK || pass === SHADER_PREPASS || shaderPassInfo.isShadow ? this._defaultStdMatOptionMin : this._defaultStdMatOption; } precompile(cache) { if (cache) { var shaders = new Array(cache.length); for(var i = 0; i < cache.length; i++){ // default options for the standard materials are not stored, and so they are inserted // back into the loaded options if (cache[i].name === 'standard') { var opt = cache[i].options; var defaultMat = this._getDefaultStdMatOptions(opt.pass); for(var p in defaultMat){ if (defaultMat.hasOwnProperty(p) && opt[p] === undefined) { opt[p] = defaultMat[p]; } } } shaders[i] = this.getProgram(cache[i].name, cache[i].options); } } this._precached = true; } constructor(device, standardMaterial){ /** * A cache of shaders processed using processing options. * * @type {Map<string, Shader>} */ this.processedCache = new Map(); /** * A cache of shader definitions before processing. * * @type {Map<number, object>} */ this.definitionsCache = new Map(); /** * Named shader generators. * * @type {Map<string, ShaderGenerator>} */ this._generators = new Map(); this._device = device; this._isClearingCache = false; this._precached = false; // Unique non-cached programs collection to dump and update game shaders cache this._programsCollection = []; this._defaultStdMatOption = new StandardMaterialOptions(); this._defaultStdMatOptionMin = new StandardMaterialOptions(); var defaultCameraShaderParams = new CameraShaderParams(); standardMaterial.shaderOptBuilder.updateRef(this._defaultStdMatOption, {}, defaultCameraShaderParams, standardMaterial, null, [], SHADER_FORWARD, null); standardMaterial.shaderOptBuilder.updateMinRef(this._defaultStdMatOptionMin, {}, standardMaterial, null, SHADER_SHADOW, null); device.on('destroy:shader', (shader)=>{ this.removeFromCache(shader); }); } } export { ProgramLibrary };