import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import modernizeShader from "../Renderer/modernizeShader.js"; import CzmBuiltins from "../Shaders/Builtin/CzmBuiltins.js"; import AutomaticUniforms from "./AutomaticUniforms.js"; function removeComments(source) { // remove inline comments source = source.replace(/\/\/.*/g, ""); // remove multiline comment block return source.replace(/\/\*\*[\s\S]*?\*\//gm, function (match) { // preserve the number of lines in the comment block so the line numbers will be correct when debugging shaders var numberOfLines = match.match(/\n/gm).length; var replacement = ""; for (var lineNumber = 0; lineNumber < numberOfLines; ++lineNumber) { replacement += "\n"; } return replacement; }); } function getDependencyNode(name, glslSource, nodes) { var dependencyNode; // check if already loaded for (var i = 0; i < nodes.length; ++i) { if (nodes[i].name === name) { dependencyNode = nodes[i]; } } if (!defined(dependencyNode)) { // strip doc comments so we don't accidentally try to determine a dependency for something found // in a comment glslSource = removeComments(glslSource); // create new node dependencyNode = { name: name, glslSource: glslSource, dependsOn: [], requiredBy: [], evaluated: false, }; nodes.push(dependencyNode); } return dependencyNode; } function generateDependencies(currentNode, dependencyNodes) { if (currentNode.evaluated) { return; } currentNode.evaluated = true; // identify all dependencies that are referenced from this glsl source code var czmMatches = currentNode.glslSource.match(/\bczm_[a-zA-Z0-9_]*/g); if (defined(czmMatches) && czmMatches !== null) { // remove duplicates czmMatches = czmMatches.filter(function (elem, pos) { return czmMatches.indexOf(elem) === pos; }); czmMatches.forEach(function (element) { if ( element !== currentNode.name && ShaderSource._czmBuiltinsAndUniforms.hasOwnProperty(element) ) { var referencedNode = getDependencyNode( element, ShaderSource._czmBuiltinsAndUniforms[element], dependencyNodes ); currentNode.dependsOn.push(referencedNode); referencedNode.requiredBy.push(currentNode); // recursive call to find any dependencies of the new node generateDependencies(referencedNode, dependencyNodes); } }); } } function sortDependencies(dependencyNodes) { var nodesWithoutIncomingEdges = []; var allNodes = []; while (dependencyNodes.length > 0) { var node = dependencyNodes.pop(); allNodes.push(node); if (node.requiredBy.length === 0) { nodesWithoutIncomingEdges.push(node); } } while (nodesWithoutIncomingEdges.length > 0) { var currentNode = nodesWithoutIncomingEdges.shift(); dependencyNodes.push(currentNode); for (var i = 0; i < currentNode.dependsOn.length; ++i) { // remove the edge from the graph var referencedNode = currentNode.dependsOn[i]; var index = referencedNode.requiredBy.indexOf(currentNode); referencedNode.requiredBy.splice(index, 1); // if referenced node has no more incoming edges, add to list if (referencedNode.requiredBy.length === 0) { nodesWithoutIncomingEdges.push(referencedNode); } } } // if there are any nodes left with incoming edges, then there was a circular dependency somewhere in the graph var badNodes = []; for (var j = 0; j < allNodes.length; ++j) { if (allNodes[j].requiredBy.length !== 0) { badNodes.push(allNodes[j]); } } //>>includeStart('debug', pragmas.debug); if (badNodes.length !== 0) { var message = "A circular dependency was found in the following built-in functions/structs/constants: \n"; for (var k = 0; k < badNodes.length; ++k) { message = message + badNodes[k].name + "\n"; } throw new DeveloperError(message); } //>>includeEnd('debug'); } function getBuiltinsAndAutomaticUniforms(shaderSource) { // generate a dependency graph for builtin functions var dependencyNodes = []; var root = getDependencyNode("main", shaderSource, dependencyNodes); generateDependencies(root, dependencyNodes); sortDependencies(dependencyNodes); // Concatenate the source code for the function dependencies. // Iterate in reverse so that dependent items are declared before they are used. var builtinsSource = ""; for (var i = dependencyNodes.length - 1; i >= 0; --i) { builtinsSource = builtinsSource + dependencyNodes[i].glslSource + "\n"; } return builtinsSource.replace(root.glslSource, ""); } function combineShader(shaderSource, isFragmentShader, context) { var i; var length; // Combine shader sources, generally for pseudo-polymorphism, e.g., czm_getMaterial. var combinedSources = ""; var sources = shaderSource.sources; if (defined(sources)) { for (i = 0, length = sources.length; i < length; ++i) { // #line needs to be on its own line. combinedSources += "\n#line 0\n" + sources[i]; } } combinedSources = removeComments(combinedSources); // Extract existing shader version from sources var version; combinedSources = combinedSources.replace(/#version\s+(.*?)\n/gm, function ( match, group1 ) { //>>includeStart('debug', pragmas.debug); if (defined(version) && version !== group1) { throw new DeveloperError( "inconsistent versions found: " + version + " and " + group1 ); } //>>includeEnd('debug'); // Extract #version to put at the top version = group1; // Replace original #version directive with a new line so the line numbers // are not off by one. There can be only one #version directive // and it must appear at the top of the source, only preceded by // whitespace and comments. return "\n"; }); // Extract shader extensions from sources var extensions = []; combinedSources = combinedSources.replace(/#extension.*\n/gm, function ( match ) { // Extract extension to put at the top extensions.push(match); // Replace original #extension directive with a new line so the line numbers // are not off by one. return "\n"; }); // Remove precision qualifier combinedSources = combinedSources.replace( /precision\s(lowp|mediump|highp)\s(float|int);/, "" ); // Replace main() for picked if desired. var pickColorQualifier = shaderSource.pickColorQualifier; if (defined(pickColorQualifier)) { combinedSources = ShaderSource.createPickFragmentShaderSource( combinedSources, pickColorQualifier ); } // combine into single string var result = ""; // #version must be first // defaults to #version 100 if not specified if (defined(version)) { result = "#version " + version + "\n"; } var extensionsLength = extensions.length; for (i = 0; i < extensionsLength; i++) { result += extensions[i]; } if (isFragmentShader) { // If high precision isn't support replace occurrences of highp with mediump // The highp keyword is not always available on older mobile devices // See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#In_WebGL_1_highp_float_support_is_optional_in_fragment_shaders result += "\ #ifdef GL_FRAGMENT_PRECISION_HIGH\n\ precision highp float;\n\ #else\n\ precision mediump float;\n\ #define highp mediump\n\ #endif\n\n"; } // Prepend #defines for uber-shaders var defines = shaderSource.defines; if (defined(defines)) { for (i = 0, length = defines.length; i < length; ++i) { var define = defines[i]; if (define.length !== 0) { result += "#define " + define + "\n"; } } } // GLSLModernizer inserts its own layout qualifiers // at this position in the source if (context.webgl2) { result += "#define OUTPUT_DECLARATION\n\n"; } // Define a constant for the OES_texture_float_linear extension since WebGL does not. if (context.textureFloatLinear) { result += "#define OES_texture_float_linear\n\n"; } // Define a constant for the OES_texture_float extension since WebGL does not. if (context.floatingPointTexture) { result += "#define OES_texture_float\n\n"; } // append built-ins if (shaderSource.includeBuiltIns) { result += getBuiltinsAndAutomaticUniforms(combinedSources); } // reset line number result += "\n#line 0\n"; // append actual source result += combinedSources; // modernize the source if (context.webgl2) { result = modernizeShader(result, isFragmentShader, true); } return result; } /** * An object containing various inputs that will be combined to form a final GLSL shader string. * * @param {Object} [options] Object with the following properties: * @param {String[]} [options.sources] An array of strings to combine containing GLSL code for the shader. * @param {String[]} [options.defines] An array of strings containing GLSL identifiers to #define. * @param {String} [options.pickColorQualifier] The GLSL qualifier, uniform or varying, for the input czm_pickColor. When defined, a pick fragment shader is generated. * @param {Boolean} [options.includeBuiltIns=true] If true, referenced built-in functions will be included with the combined shader. Set to false if this shader will become a source in another shader, to avoid duplicating functions. * * @exception {DeveloperError} options.pickColorQualifier must be 'uniform' or 'varying'. * * @example * // 1. Prepend #defines to a shader * var source = new Cesium.ShaderSource({ * defines : ['WHITE'], * sources : ['void main() { \n#ifdef WHITE\n gl_FragColor = vec4(1.0); \n#else\n gl_FragColor = vec4(0.0); \n#endif\n }'] * }); * * // 2. Modify a fragment shader for picking * var source = new Cesium.ShaderSource({ * sources : ['void main() { gl_FragColor = vec4(1.0); }'], * pickColorQualifier : 'uniform' * }); * * @private */ function ShaderSource(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); var pickColorQualifier = options.pickColorQualifier; //>>includeStart('debug', pragmas.debug); if ( defined(pickColorQualifier) && pickColorQualifier !== "uniform" && pickColorQualifier !== "varying" ) { throw new DeveloperError( "options.pickColorQualifier must be 'uniform' or 'varying'." ); } //>>includeEnd('debug'); this.defines = defined(options.defines) ? options.defines.slice(0) : []; this.sources = defined(options.sources) ? options.sources.slice(0) : []; this.pickColorQualifier = pickColorQualifier; this.includeBuiltIns = defaultValue(options.includeBuiltIns, true); } ShaderSource.prototype.clone = function () { return new ShaderSource({ sources: this.sources, defines: this.defines, pickColorQualifier: this.pickColorQualifier, includeBuiltIns: this.includeBuiltIns, }); }; ShaderSource.replaceMain = function (source, renamedMain) { renamedMain = "void " + renamedMain + "()"; return source.replace(/void\s+main\s*\(\s*(?:void)?\s*\)/g, renamedMain); }; /** * Create a single string containing the full, combined vertex shader with all dependencies and defines. * * @param {Context} context The current rendering context * * @returns {String} The combined shader string. */ ShaderSource.prototype.createCombinedVertexShader = function (context) { return combineShader(this, false, context); }; /** * Create a single string containing the full, combined fragment shader with all dependencies and defines. * * @param {Context} context The current rendering context * * @returns {String} The combined shader string. */ ShaderSource.prototype.createCombinedFragmentShader = function (context) { return combineShader(this, true, context); }; /** * For ShaderProgram testing * @private */ ShaderSource._czmBuiltinsAndUniforms = {}; // combine automatic uniforms and Cesium built-ins for (var builtinName in CzmBuiltins) { if (CzmBuiltins.hasOwnProperty(builtinName)) { ShaderSource._czmBuiltinsAndUniforms[builtinName] = CzmBuiltins[builtinName]; } } for (var uniformName in AutomaticUniforms) { if (AutomaticUniforms.hasOwnProperty(uniformName)) { var uniform = AutomaticUniforms[uniformName]; if (typeof uniform.getDeclaration === "function") { ShaderSource._czmBuiltinsAndUniforms[ uniformName ] = uniform.getDeclaration(uniformName); } } } ShaderSource.createPickVertexShaderSource = function (vertexShaderSource) { var renamedVS = ShaderSource.replaceMain(vertexShaderSource, "czm_old_main"); var pickMain = "attribute vec4 pickColor; \n" + "varying vec4 czm_pickColor; \n" + "void main() \n" + "{ \n" + " czm_old_main(); \n" + " czm_pickColor = pickColor; \n" + "}"; return renamedVS + "\n" + pickMain; }; ShaderSource.createPickFragmentShaderSource = function ( fragmentShaderSource, pickColorQualifier ) { var renamedFS = ShaderSource.replaceMain( fragmentShaderSource, "czm_old_main" ); var pickMain = pickColorQualifier + " vec4 czm_pickColor; \n" + "void main() \n" + "{ \n" + " czm_old_main(); \n" + " if (gl_FragColor.a == 0.0) { \n" + " discard; \n" + " } \n" + " gl_FragColor = czm_pickColor; \n" + "}"; return renamedFS + "\n" + pickMain; }; ShaderSource.findVarying = function (shaderSource, names) { var sources = shaderSource.sources; var namesLength = names.length; for (var i = 0; i < namesLength; ++i) { var name = names[i]; var sourcesLength = sources.length; for (var j = 0; j < sourcesLength; ++j) { if (sources[j].indexOf(name) !== -1) { return name; } } } return undefined; }; var normalVaryingNames = ["v_normalEC", "v_normal"]; ShaderSource.findNormalVarying = function (shaderSource) { return ShaderSource.findVarying(shaderSource, normalVaryingNames); }; var positionVaryingNames = ["v_positionEC"]; ShaderSource.findPositionVarying = function (shaderSource) { return ShaderSource.findVarying(shaderSource, positionVaryingNames); }; export default ShaderSource;