playcanvas
Version:
PlayCanvas WebGL game engine
263 lines (260 loc) • 11.9 kB
JavaScript
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 };