import Check from "../Core/Check.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; import RuntimeError from "../Core/RuntimeError.js"; import AutomaticUniforms from "./AutomaticUniforms.js"; import ContextLimits from "./ContextLimits.js"; import createUniform from "./createUniform.js"; import createUniformArray from "./createUniformArray.js"; let nextShaderProgramId = 0; /** * @private */ function ShaderProgram(options) { let vertexShaderText = options.vertexShaderText; let fragmentShaderText = options.fragmentShaderText; if (typeof spector !== "undefined") { // The #line statements common in Cesium shaders interfere with the ability of the // SpectorJS to show errors on the correct line. So remove them when SpectorJS // is active. vertexShaderText = vertexShaderText.replace(/^#line/gm, "//#line"); fragmentShaderText = fragmentShaderText.replace(/^#line/gm, "//#line"); } const modifiedFS = handleUniformPrecisionMismatches( vertexShaderText, fragmentShaderText ); this._gl = options.gl; this._logShaderCompilation = options.logShaderCompilation; this._debugShaders = options.debugShaders; this._attributeLocations = options.attributeLocations; this._program = undefined; this._numberOfVertexAttributes = undefined; this._vertexAttributes = undefined; this._uniformsByName = undefined; this._uniforms = undefined; this._automaticUniforms = undefined; this._manualUniforms = undefined; this._duplicateUniformNames = modifiedFS.duplicateUniformNames; this._cachedShader = undefined; // Used by ShaderCache /** * @private */ this.maximumTextureUnitIndex = undefined; this._vertexShaderSource = options.vertexShaderSource; this._vertexShaderText = options.vertexShaderText; this._fragmentShaderSource = options.fragmentShaderSource; this._fragmentShaderText = modifiedFS.fragmentShaderText; /** * @private */ this.id = nextShaderProgramId++; } ShaderProgram.fromCache = function (options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); //>>includeStart('debug', pragmas.debug); Check.defined("options.context", options.context); //>>includeEnd('debug'); return options.context.shaderCache.getShaderProgram(options); }; ShaderProgram.replaceCache = function (options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); //>>includeStart('debug', pragmas.debug); Check.defined("options.context", options.context); //>>includeEnd('debug'); return options.context.shaderCache.replaceShaderProgram(options); }; Object.defineProperties(ShaderProgram.prototype, { /** * GLSL source for the shader program's vertex shader. * @memberof ShaderProgram.prototype * * @type {ShaderSource} * @readonly */ vertexShaderSource: { get: function () { return this._vertexShaderSource; }, }, /** * GLSL source for the shader program's fragment shader. * @memberof ShaderProgram.prototype * * @type {ShaderSource} * @readonly */ fragmentShaderSource: { get: function () { return this._fragmentShaderSource; }, }, vertexAttributes: { get: function () { initialize(this); return this._vertexAttributes; }, }, numberOfVertexAttributes: { get: function () { initialize(this); return this._numberOfVertexAttributes; }, }, allUniforms: { get: function () { initialize(this); return this._uniformsByName; }, }, }); function extractUniforms(shaderText) { const uniformNames = []; const uniformLines = shaderText.match(/uniform.*?(?![^{]*})(?=[=\[;])/g); if (defined(uniformLines)) { const len = uniformLines.length; for (let i = 0; i < len; i++) { const line = uniformLines[i].trim(); const name = line.slice(line.lastIndexOf(" ") + 1); uniformNames.push(name); } } return uniformNames; } function handleUniformPrecisionMismatches( vertexShaderText, fragmentShaderText ) { // If a uniform exists in both the vertex and fragment shader but with different precision qualifiers, // give the fragment shader uniform a different name. This fixes shader compilation errors on devices // that only support mediump in the fragment shader. const duplicateUniformNames = {}; if (!ContextLimits.highpFloatSupported || !ContextLimits.highpIntSupported) { let i, j; let uniformName; let duplicateName; const vertexShaderUniforms = extractUniforms(vertexShaderText); const fragmentShaderUniforms = extractUniforms(fragmentShaderText); const vertexUniformsCount = vertexShaderUniforms.length; const fragmentUniformsCount = fragmentShaderUniforms.length; for (i = 0; i < vertexUniformsCount; i++) { for (j = 0; j < fragmentUniformsCount; j++) { if (vertexShaderUniforms[i] === fragmentShaderUniforms[j]) { uniformName = vertexShaderUniforms[i]; duplicateName = `czm_mediump_${uniformName}`; // Update fragmentShaderText with renamed uniforms const re = new RegExp(`${uniformName}\\b`, "g"); fragmentShaderText = fragmentShaderText.replace(re, duplicateName); duplicateUniformNames[duplicateName] = uniformName; } } } } return { fragmentShaderText: fragmentShaderText, duplicateUniformNames: duplicateUniformNames, }; } const consolePrefix = "[Cesium WebGL] "; function createAndLinkProgram(gl, shader) { const vsSource = shader._vertexShaderText; const fsSource = shader._fragmentShaderText; const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vsSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fsSource); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); const attributeLocations = shader._attributeLocations; if (defined(attributeLocations)) { for (const attribute in attributeLocations) { if (attributeLocations.hasOwnProperty(attribute)) { gl.bindAttribLocation( program, attributeLocations[attribute], attribute ); } } } gl.linkProgram(program); let log; if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const debugShaders = shader._debugShaders; // For performance, only check compile errors if there is a linker error. if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { log = gl.getShaderInfoLog(fragmentShader); console.error(`${consolePrefix}Fragment shader compile log: ${log}`); if (defined(debugShaders)) { const fragmentSourceTranslation = debugShaders.getTranslatedShaderSource( fragmentShader ); if (fragmentSourceTranslation !== "") { console.error( `${consolePrefix}Translated fragment shader source:\n${fragmentSourceTranslation}` ); } else { console.error(`${consolePrefix}Fragment shader translation failed.`); } } gl.deleteProgram(program); throw new RuntimeError( `Fragment shader failed to compile. Compile log: ${log}` ); } if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { log = gl.getShaderInfoLog(vertexShader); console.error(`${consolePrefix}Vertex shader compile log: ${log}`); if (defined(debugShaders)) { const vertexSourceTranslation = debugShaders.getTranslatedShaderSource( vertexShader ); if (vertexSourceTranslation !== "") { console.error( `${consolePrefix}Translated vertex shader source:\n${vertexSourceTranslation}` ); } else { console.error(`${consolePrefix}Vertex shader translation failed.`); } } gl.deleteProgram(program); throw new RuntimeError( `Vertex shader failed to compile. Compile log: ${log}` ); } log = gl.getProgramInfoLog(program); console.error(`${consolePrefix}Shader program link log: ${log}`); if (defined(debugShaders)) { console.error( `${consolePrefix}Translated vertex shader source:\n${debugShaders.getTranslatedShaderSource( vertexShader )}` ); console.error( `${consolePrefix}Translated fragment shader source:\n${debugShaders.getTranslatedShaderSource( fragmentShader )}` ); } gl.deleteProgram(program); throw new RuntimeError(`Program failed to link. Link log: ${log}`); } const logShaderCompilation = shader._logShaderCompilation; if (logShaderCompilation) { log = gl.getShaderInfoLog(vertexShader); if (defined(log) && log.length > 0) { console.log(`${consolePrefix}Vertex shader compile log: ${log}`); } } if (logShaderCompilation) { log = gl.getShaderInfoLog(fragmentShader); if (defined(log) && log.length > 0) { console.log(`${consolePrefix}Fragment shader compile log: ${log}`); } } if (logShaderCompilation) { log = gl.getProgramInfoLog(program); if (defined(log) && log.length > 0) { console.log(`${consolePrefix}Shader program link log: ${log}`); } } return program; } function findVertexAttributes(gl, program, numberOfAttributes) { const attributes = {}; for (let i = 0; i < numberOfAttributes; ++i) { const attr = gl.getActiveAttrib(program, i); const location = gl.getAttribLocation(program, attr.name); attributes[attr.name] = { name: attr.name, type: attr.type, index: location, }; } return attributes; } function findUniforms(gl, program) { const uniformsByName = {}; const uniforms = []; const samplerUniforms = []; const numberOfUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); for (let i = 0; i < numberOfUniforms; ++i) { const activeUniform = gl.getActiveUniform(program, i); const suffix = "[0]"; const uniformName = activeUniform.name.indexOf( suffix, activeUniform.name.length - suffix.length ) !== -1 ? activeUniform.name.slice(0, activeUniform.name.length - 3) : activeUniform.name; // Ignore GLSL built-in uniforms returned in Firefox. if (uniformName.indexOf("gl_") !== 0) { if (activeUniform.name.indexOf("[") < 0) { // Single uniform const location = gl.getUniformLocation(program, uniformName); // IE 11.0.9 needs this check since getUniformLocation can return null // if the uniform is not active (e.g., it is optimized out). Looks like // getActiveUniform() above returns uniforms that are not actually active. if (location !== null) { const uniform = createUniform( gl, activeUniform, uniformName, location ); uniformsByName[uniformName] = uniform; uniforms.push(uniform); if (uniform._setSampler) { samplerUniforms.push(uniform); } } } else { // Uniform array let uniformArray; let locations; let value; let loc; // On some platforms - Nexus 4 in Firefox for one - an array of sampler2D ends up being represented // as separate uniforms, one for each array element. Check for and handle that case. const indexOfBracket = uniformName.indexOf("["); if (indexOfBracket >= 0) { // We're assuming the array elements show up in numerical order - it seems to be true. uniformArray = uniformsByName[uniformName.slice(0, indexOfBracket)]; // Nexus 4 with Android 4.3 needs this check, because it reports a uniform // with the strange name webgl_3467e0265d05c3c1[1] in our globe surface shader. if (!defined(uniformArray)) { continue; } locations = uniformArray._locations; // On the Nexus 4 in Chrome, we get one uniform per sampler, just like in Firefox, // but the size is not 1 like it is in Firefox. So if we push locations here, // we'll end up adding too many locations. if (locations.length <= 1) { value = uniformArray.value; loc = gl.getUniformLocation(program, uniformName); // Workaround for IE 11.0.9. See above. if (loc !== null) { locations.push(loc); value.push(gl.getUniform(program, loc)); } } } else { locations = []; for (let j = 0; j < activeUniform.size; ++j) { loc = gl.getUniformLocation(program, `${uniformName}[${j}]`); // Workaround for IE 11.0.9. See above. if (loc !== null) { locations.push(loc); } } uniformArray = createUniformArray( gl, activeUniform, uniformName, locations ); uniformsByName[uniformName] = uniformArray; uniforms.push(uniformArray); if (uniformArray._setSampler) { samplerUniforms.push(uniformArray); } } } } } return { uniformsByName: uniformsByName, uniforms: uniforms, samplerUniforms: samplerUniforms, }; } function partitionUniforms(shader, uniforms) { const automaticUniforms = []; const manualUniforms = []; for (const uniform in uniforms) { if (uniforms.hasOwnProperty(uniform)) { const uniformObject = uniforms[uniform]; let uniformName = uniform; // if it's a duplicate uniform, use its original name so it is updated correctly const duplicateUniform = shader._duplicateUniformNames[uniformName]; if (defined(duplicateUniform)) { uniformObject.name = duplicateUniform; uniformName = duplicateUniform; } const automaticUniform = AutomaticUniforms[uniformName]; if (defined(automaticUniform)) { automaticUniforms.push({ uniform: uniformObject, automaticUniform: automaticUniform, }); } else { manualUniforms.push(uniformObject); } } } return { automaticUniforms: automaticUniforms, manualUniforms: manualUniforms, }; } function setSamplerUniforms(gl, program, samplerUniforms) { gl.useProgram(program); let textureUnitIndex = 0; const length = samplerUniforms.length; for (let i = 0; i < length; ++i) { textureUnitIndex = samplerUniforms[i]._setSampler(textureUnitIndex); } gl.useProgram(null); return textureUnitIndex; } function initialize(shader) { if (defined(shader._program)) { return; } reinitialize(shader); } function reinitialize(shader) { const oldProgram = shader._program; const gl = shader._gl; const program = createAndLinkProgram(gl, shader, shader._debugShaders); const numberOfVertexAttributes = gl.getProgramParameter( program, gl.ACTIVE_ATTRIBUTES ); const uniforms = findUniforms(gl, program); const partitionedUniforms = partitionUniforms( shader, uniforms.uniformsByName ); shader._program = program; shader._numberOfVertexAttributes = numberOfVertexAttributes; shader._vertexAttributes = findVertexAttributes( gl, program, numberOfVertexAttributes ); shader._uniformsByName = uniforms.uniformsByName; shader._uniforms = uniforms.uniforms; shader._automaticUniforms = partitionedUniforms.automaticUniforms; shader._manualUniforms = partitionedUniforms.manualUniforms; shader.maximumTextureUnitIndex = setSamplerUniforms( gl, program, uniforms.samplerUniforms ); if (oldProgram) { shader._gl.deleteProgram(oldProgram); } // If SpectorJS is active, add the hook to make the shader editor work. // https://github.com/BabylonJS/Spector.js/blob/master/documentation/extension.md#shader-editor if (typeof spector !== "undefined") { shader._program.__SPECTOR_rebuildProgram = function ( vertexSourceCode, // The new vertex shader source fragmentSourceCode, // The new fragment shader source onCompiled, // Callback triggered by your engine when the compilation is successful. It needs to send back the new linked program. onError // Callback triggered by your engine in case of error. It needs to send the WebGL error to allow the editor to display the error in the gutter. ) { const originalVS = shader._vertexShaderText; const originalFS = shader._fragmentShaderText; // SpectorJS likes to replace `!=` with `! =` for unknown reasons, // and that causes glsl compile failures. So fix that up. const regex = / ! = /g; shader._vertexShaderText = vertexSourceCode.replace(regex, " != "); shader._fragmentShaderText = fragmentSourceCode.replace(regex, " != "); try { reinitialize(shader); onCompiled(shader._program); } catch (e) { shader._vertexShaderText = originalVS; shader._fragmentShaderText = originalFS; // Only pass on the WebGL error: const errorMatcher = /(?:Compile|Link) error: ([^]*)/; const match = errorMatcher.exec(e.message); if (match) { onError(match[1]); } else { onError(e.message); } } }; } } ShaderProgram.prototype._bind = function () { initialize(this); this._gl.useProgram(this._program); }; ShaderProgram.prototype._setUniforms = function ( uniformMap, uniformState, validate ) { let len; let i; if (defined(uniformMap)) { const manualUniforms = this._manualUniforms; len = manualUniforms.length; for (i = 0; i < len; ++i) { const mu = manualUniforms[i]; mu.value = uniformMap[mu.name](); } } const automaticUniforms = this._automaticUniforms; len = automaticUniforms.length; for (i = 0; i < len; ++i) { const au = automaticUniforms[i]; au.uniform.value = au.automaticUniform.getValue(uniformState); } /////////////////////////////////////////////////////////////////// // It appears that assigning the uniform values above and then setting them here // (which makes the GL calls) is faster than removing this loop and making // the GL calls above. I suspect this is because each GL call pollutes the // L2 cache making our JavaScript and the browser/driver ping-pong cache lines. const uniforms = this._uniforms; len = uniforms.length; for (i = 0; i < len; ++i) { uniforms[i].set(); } if (validate) { const gl = this._gl; const program = this._program; gl.validateProgram(program); //>>includeStart('debug', pragmas.debug); if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) { throw new DeveloperError( `Program validation failed. Program info log: ${gl.getProgramInfoLog( program )}` ); } //>>includeEnd('debug'); } }; ShaderProgram.prototype.isDestroyed = function () { return false; }; ShaderProgram.prototype.destroy = function () { this._cachedShader.cache.releaseShaderProgram(this); return undefined; }; ShaderProgram.prototype.finalDestroy = function () { this._gl.deleteProgram(this._program); return destroyObject(this); }; export default ShaderProgram;