UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

364 lines (361 loc) 15 kB
import { Debug } from '../../../core/debug.js'; import { TRACEID_SHADER_COMPILE } from '../../../core/constants.js'; import { now } from '../../../core/time.js'; import { WebglShaderInput } from './webgl-shader-input.js'; import { semanticToLocation, SHADERTAG_MATERIAL } from '../constants.js'; import { DeviceCache } from '../device-cache.js'; import { DebugGraphics } from '../debug-graphics.js'; /** * @import { Shader } from '../shader.js' * @import { WebglGraphicsDevice } from './webgl-graphics-device.js' */ var _totalCompileTime = 0; var _vertexShaderBuiltins = new Set([ 'gl_VertexID', 'gl_InstanceID', 'gl_DrawID', 'gl_BaseVertex', 'gl_BaseInstance' ]); // class used to hold compiled WebGL vertex or fragment shaders in the device cache class CompiledShaderCache { // destroy all created shaders when the device is destroyed destroy(device) { this.map.forEach((shader)=>{ device.gl.deleteShader(shader); }); } // just empty the cache when the context is lost loseContext(device) { this.map.clear(); } constructor(){ // maps shader source to a compiled WebGL shader this.map = new Map(); } } var _vertexShaderCache = new DeviceCache(); var _fragmentShaderCache = new DeviceCache(); /** * A WebGL implementation of the Shader. * * @ignore */ class WebglShader { /** * Free the WebGL resources associated with a shader. * * @param {Shader} shader - The shader to free. */ destroy(shader) { if (this.glProgram) { shader.device.gl.deleteProgram(this.glProgram); this.glProgram = null; } } init() { this.uniforms = []; this.samplers = []; this.attributes = []; this.glProgram = null; this.glVertexShader = null; this.glFragmentShader = null; } /** * Dispose the shader when the context has been lost. */ loseContext() { this.init(); } /** * Restore shader after the context has been obtained. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {Shader} shader - The shader to restore. */ restoreContext(device, shader) { this.compile(device, shader); this.link(device, shader); } /** * Compile shader programs. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {Shader} shader - The shader to compile. */ compile(device, shader) { var definition = shader.definition; this.glVertexShader = this._compileShaderSource(device, definition.vshader, true); this.glFragmentShader = this._compileShaderSource(device, definition.fshader, false); } /** * Link shader programs. This is called at a later stage, to allow many shaders to compile in parallel. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {Shader} shader - The shader to compile. */ link(device, shader) { // if the shader was already linked if (this.glProgram) { return; } // if the device is lost, silently ignore var gl = device.gl; if (gl.isContextLost()) { return; } var startTime = 0; Debug.call(()=>{ this.compileDuration = 0; startTime = now(); }); var glProgram = gl.createProgram(); this.glProgram = glProgram; gl.attachShader(glProgram, this.glVertexShader); gl.attachShader(glProgram, this.glFragmentShader); var definition = shader.definition; var attrs = definition.attributes; if (definition.useTransformFeedback) { // Collect all "out_" attributes and use them for output var outNames = []; for(var attr in attrs){ if (attrs.hasOwnProperty(attr)) { outNames.push("out_" + attr); } } gl.transformFeedbackVaryings(glProgram, outNames, gl.INTERLEAVED_ATTRIBS); } // map all vertex input attributes to fixed locations var locations = {}; for(var attr1 in attrs){ if (attrs.hasOwnProperty(attr1)) { var semantic = attrs[attr1]; var loc = semanticToLocation[semantic]; Debug.assert(!locations.hasOwnProperty(loc), "WARNING: Two attributes are mapped to the same location in a shader: " + locations[loc] + " and " + attr1); locations[loc] = attr1; gl.bindAttribLocation(glProgram, loc, attr1); } } gl.linkProgram(glProgram); Debug.call(()=>{ this.compileDuration = now() - startTime; }); device._shaderStats.linked++; if (definition.tag === SHADERTAG_MATERIAL) { device._shaderStats.materialShaders++; } } /** * Compiles an individual shader. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {string} src - The shader source code. * @param {boolean} isVertexShader - True if the shader is a vertex shader, false if it is a * fragment shader. * @returns {WebGLShader|null} The compiled shader, or null if the device is lost. * @private */ _compileShaderSource(device, src, isVertexShader) { var gl = device.gl; // if the device is lost, silently ignore if (gl.isContextLost()) { return null; } // device cache for current device, containing cache of compiled shaders var shaderDeviceCache = isVertexShader ? _vertexShaderCache : _fragmentShaderCache; var shaderCache = shaderDeviceCache.get(device, ()=>{ return new CompiledShaderCache(); }); // try to get compiled shader from the cache var glShader = shaderCache.map.get(src); if (!glShader) { var startTime = now(); device.fire('shader:compile:start', { timestamp: startTime, target: device }); glShader = gl.createShader(isVertexShader ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER); gl.shaderSource(glShader, src); gl.compileShader(glShader); shaderCache.map.set(src, glShader); var endTime = now(); device.fire('shader:compile:end', { timestamp: endTime, target: device }); device._shaderStats.compileTime += endTime - startTime; if (isVertexShader) { device._shaderStats.vsCompiled++; } else { device._shaderStats.fsCompiled++; } } return glShader; } /** * Link the shader, and extract its attributes and uniform information. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {Shader} shader - The shader to query. * @returns {boolean} True if the shader was successfully queried and false otherwise. */ finalize(device, shader) { // if the device is lost, silently ignore var gl = device.gl; if (gl.isContextLost()) { return true; } var glProgram = this.glProgram; var definition = shader.definition; var startTime = now(); device.fire('shader:link:start', { timestamp: startTime, target: device }); // this is the main thead blocking part of the shader compilation, time it var linkStartTime = 0; Debug.call(()=>{ linkStartTime = now(); }); // check the link status of a shader - this is a blocking operation waiting for the shader // to finish compiling and linking var linkStatus = gl.getProgramParameter(glProgram, gl.LINK_STATUS); if (!linkStatus) { var _gl_getExtension, _gl_getExtension1; // Check for compilation errors if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, 'vertex')) { return false; } if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, 'fragment')) { return false; } var message = "Failed to link shader program. Error: " + gl.getProgramInfoLog(glProgram); // log translated shaders definition.translatedFrag = (_gl_getExtension = gl.getExtension('WEBGL_debug_shaders')) == null ? void 0 : _gl_getExtension.getTranslatedShaderSource(this.glFragmentShader); definition.translatedVert = (_gl_getExtension1 = gl.getExtension('WEBGL_debug_shaders')) == null ? void 0 : _gl_getExtension1.getTranslatedShaderSource(this.glVertexShader); console.error(message, definition); return false; } // Query the program for each vertex buffer input (GLSL 'attribute') var numAttributes = gl.getProgramParameter(glProgram, gl.ACTIVE_ATTRIBUTES); shader.attributes.clear(); for(var i = 0; i < numAttributes; i++){ var info = gl.getActiveAttrib(glProgram, i); var location = gl.getAttribLocation(glProgram, info.name); // a built-in attributes for which we do not need to provide any data if (_vertexShaderBuiltins.has(info.name)) { continue; } // Check attributes are correctly linked up if (definition.attributes[info.name] === undefined) { console.error('Vertex shader attribute "' + info.name + '" is not mapped to a semantic in shader definition, shader [' + shader.label + "]", shader); shader.failed = true; } else { shader.attributes.set(location, info.name); } } // Query the program for each shader state (GLSL 'uniform') var samplerTypes = device._samplerTypes; var numUniforms = gl.getProgramParameter(glProgram, gl.ACTIVE_UNIFORMS); for(var i1 = 0; i1 < numUniforms; i1++){ var info1 = gl.getActiveUniform(glProgram, i1); var location1 = gl.getUniformLocation(glProgram, info1.name); var shaderInput = new WebglShaderInput(device, info1.name, device.pcUniformType[info1.type], location1); if (samplerTypes.has(info1.type)) { this.samplers.push(shaderInput); } else { this.uniforms.push(shaderInput); } } shader.ready = true; var endTime = now(); device.fire('shader:link:end', { timestamp: endTime, target: device }); device._shaderStats.compileTime += endTime - startTime; Debug.call(()=>{ var duration = now() - linkStartTime; this.compileDuration += duration; _totalCompileTime += this.compileDuration; Debug.trace(TRACEID_SHADER_COMPILE, "[id: " + shader.id + "] " + shader.name + ": " + this.compileDuration.toFixed(1) + "ms, TOTAL: " + _totalCompileTime.toFixed(1) + "ms"); }); return true; } /** * Check the compilation status of a shader. * * @param {WebglGraphicsDevice} device - The graphics device. * @param {Shader} shader - The shader to query. * @param {WebGLShader} glShader - The WebGL shader. * @param {string} source - The shader source code. * @param {string} shaderType - The shader type. Can be 'vertex' or 'fragment'. * @returns {boolean} True if the shader compiled successfully, false otherwise. * @private */ _isCompiled(device, shader, glShader, source, shaderType) { var gl = device.gl; if (!gl.getShaderParameter(glShader, gl.COMPILE_STATUS)) { var infoLog = gl.getShaderInfoLog(glShader); var [code, error] = this._processError(source, infoLog); var message = "Failed to compile " + shaderType + " shader:\n\n" + infoLog + "\n" + code + " while rendering " + DebugGraphics.toString(); error.shader = shader; console.error(message, error); return false; } return true; } /** * Check the linking status of a shader. * * @param {WebglGraphicsDevice} device - The graphics device. * @returns {boolean} True if the shader is already linked, false otherwise. Note that unless the * device supports the KHR_parallel_shader_compile extension, this will always return true. */ isLinked(device) { var { extParallelShaderCompile } = device; if (extParallelShaderCompile) { return device.gl.getProgramParameter(this.glProgram, extParallelShaderCompile.COMPLETION_STATUS_KHR); } return true; } /** * Truncate the WebGL shader compilation log to just include the error line plus the 5 lines * before and after it. * * @param {string} src - The shader source code. * @param {string} infoLog - The info log returned from WebGL on a failed shader compilation. * @returns {Array} An array where the first element is the 10 lines of code around the first * detected error, and the second element an object storing the error message, line number and * complete shader source. * @private */ _processError(src, infoLog) { var error = {}; var code = ''; if (src) { var lines = src.split('\n'); var from = 0; var to = lines.length; // if error is in the code, only show nearby lines instead of whole shader code if (infoLog && infoLog.startsWith('ERROR:')) { var match = infoLog.match(/^ERROR:\s(\d+):(\d+):\s*(.+)/); if (match) { error.message = match[3]; error.line = parseInt(match[2], 10); from = Math.max(0, error.line - 6); to = Math.min(lines.length, error.line + 5); } } // Chrome reports shader errors on lines indexed from 1 for(var i = from; i < to; i++){ var linePrefix = i + 1 === error.line ? '> ' : ' '; code += "" + linePrefix + (i + 1) + ": " + lines[i] + "\n"; } error.source = src; } return [ code, error ]; } constructor(shader){ this.compileDuration = 0; this.init(); // kick off vertex and fragment shader compilation this.compile(shader.device, shader); // kick off linking, as this is non-blocking too this.link(shader.device, shader); // add it to a device list of all shaders shader.device.shaders.push(shader); } } export { WebglShader };