import FileLoader from '../Loaders/FileLoader';
import { Program } from '../Program';
/**
* A class to manage programs/shaders
*
* @category Shaders
*/
class ProgramLibrary {
/**
* Get the program with the given name
*
* @param {string} name Program's name
* @return {Program} A Program instance, otherwise null is the program doesn't exist
*/
static get(name) {
let program = ProgramLibrary.programs[name];
if (!program) {
program = new Program();
ProgramLibrary.programs[name] = program;
}
return program;
}
/**
* Load a new program
*
* @param {string} name Program's name
* @param {string} vertexShaderFile Path to the vertex shader file
* @param {string} fragmentShaderFile Path to the fragment shader file
* @param {Array.<string>=} defines An array with defines data
* @return {?Program} A Program instance
*/
static async loadFromFile(name, vertexShaderFile, fragmentShaderFile, defines) {
// Get/Create program
const program = ProgramLibrary.get(name);
// Chunck variables
const chunkPatterns = /include\[([^\]]*)\]/g;
const folderExtractorRegex = /(.*)[/\\]/;
const fragmentFolderPath = vertexShaderFile.match(folderExtractorRegex)[1] || '';
const vertexFolderPath = vertexShaderFile.match(folderExtractorRegex)[1] || '';
// Prepare cache
ProgramLibrary.cache[name] = {
data: null,
ready: false,
sources: [],
};
// Callback processing chunk
const callback = async (type, data) => {
const newProgram = ProgramLibrary.programs[name];
const sources = newProgram.getSources();
const chunks = data.match(chunkPatterns);
// Analyse if the file ask external chunks
if (chunks) {
const waitingChunks = [];
for (let i = 0; i < chunks.length; i += 1) {
const chunk = chunks[i];
const chunkPath = chunk.substring(chunk.lastIndexOf('[') + 1, chunk.lastIndexOf(']'));
if (!ProgramLibrary.chunks[chunkPath]) {
ProgramLibrary.chunks[chunkPath] = { data: '', ready: false };
const folder = type === ProgramLibrary.Target.Vertex ? vertexFolderPath : fragmentFolderPath;
waitingChunks.push(FileLoader.load(`${folder}/${chunkPath}`, chunkPath));
}
}
await Promise.all(waitingChunks).then((results) => {
results.forEach((response) => {
ProgramLibrary.chunks[response.name].data = response.data;
ProgramLibrary.chunks[response.name].ready = true;
});
});
// Try to update
ProgramLibrary.tryUpdateWaitingPrograms();
// Everything is in memory? We can fill the program directly
const result = ProgramLibrary.replaceChunks(data);
ProgramLibrary.fillProgram(sources, newProgram, name, type, result || '');
} else {
ProgramLibrary.fillProgram(sources, newProgram, name, type, data);
}
};
// Load vertex file
const vertexResponse = await FileLoader.load(`${vertexShaderFile}`);
const vertexData = ProgramLibrary.addDefines(vertexResponse.data, defines || []);
ProgramLibrary.cache[name].sources[0] = vertexData;
await callback(ProgramLibrary.Target.Vertex, vertexData);
// Load fragment file
const fragmentResponse = await FileLoader.load(`${fragmentShaderFile}`);
const fragmentData = ProgramLibrary.addDefines(fragmentResponse.data, defines || []);
ProgramLibrary.cache[name].sources[1] = fragmentData;
await callback(ProgramLibrary.Target.Fragment, fragmentData);
return program;
}
/**
* Shortcut to fill program with sources and clear the cache.
* @private
* @param {Array.<string>} sources Vertex and fragment sources.
* @param {Program} program A Program instance.
* @param {string} name Program's name.
* @param {ProgramLibrary.Target} type Type of data.
* @param {string} data Data to add to the program.
*/
static fillProgram(sources, program, name, type, data) {
if (type === ProgramLibrary.Target.Vertex) {
program.loadFromData(data, sources[1]);
} else {
program.loadFromData(sources[0], data);
}
if (program.isReady()) {
delete ProgramLibrary.cache[name];
}
}
/**
* Update waiting programs
*
* @private
*/
static tryUpdateWaitingPrograms() {
for (const i in ProgramLibrary.programs) {
if (!ProgramLibrary.programs[i].isReady()) {
const sources = ProgramLibrary.programs[i].getSources();
for (let j = 0; j < 2; j += 1) {
if (!sources[j]) {
const source = ProgramLibrary.replaceChunks(ProgramLibrary.cache[i].sources[j]);
if (source) {
ProgramLibrary.fillProgram(sources, ProgramLibrary.programs[i], i, (j === 0) ? ProgramLibrary.Target.Vertex : ProgramLibrary.Target.Fragment, source);
}
}
}
}
}
}
/**
* Fill programs with chunks data
*
* @private
* @param {string} data Data to process
* @return {?string} A string if everything is ok, otherwise null
*/
static replaceChunks(data) {
const chunkPattern = /include\[([^\]]*)\]/;
let result = data;
while (chunkPattern.test(result)) {
const chunk = result.match(chunkPattern);
const chunkName = chunk[0].substring(chunk[0].lastIndexOf('[') + 1, chunk[0].lastIndexOf(']'));
// We need to wait all chunks to continue.
if (!ProgramLibrary.chunks[chunkName] || !ProgramLibrary.chunks[chunkName].ready) {
return null;
}
result = result.replace(chunk[0], ProgramLibrary.chunks[chunkName].data || '');
}
return result;
}
/**
* Add defines to the program
*
* @private
* @param {string} source Source data
* @param {Array.<string>} defines An array with defines to add to the source parameter
* @return {string} The new string
*/
static addDefines(source, defines) {
if (!defines) {
return source;
}
let defineString = '';
for (let i = 0; i < defines.length; i += 1) {
defineString += `#define ${defines[i]}\n`;
}
return defineString + source;
}
}
/**
* Put data in cache due to asynchrone loading
*
* @type {Array.<{data: null, ready: boolean, sources: Array<string>}>}
*/
ProgramLibrary.cache = [];
/**
* Chunks in cache
*
* @type {Array.<{data: string, ready: boolean}>}
*/
ProgramLibrary.chunks = [];
/**
* Shaders
*
* @type {Array.<Program>}
*/
ProgramLibrary.programs = [];
/**
* Target
*
* @enum {number}
*/
ProgramLibrary.Target = { Vertex: 0, Fragment: 1 };
export default ProgramLibrary;
Source