import Check from "../Core/Check.js"; import clone from "../Core/clone.js"; import defined from "../Core/defined.js"; import defaultValue from "../Core/defaultValue.js"; import DeveloperError from "../Core/DeveloperError.js"; import ShaderDestination from "./ShaderDestination.js"; import ShaderProgram from "./ShaderProgram.js"; import ShaderSource from "./ShaderSource.js"; import ShaderStruct from "./ShaderStruct.js"; import ShaderFunction from "./ShaderFunction.js"; /** * An object that makes it easier to build the text of a {@link ShaderProgram}. This tracks GLSL code for both the vertex shader and the fragment shader. *
* For vertex shaders, the shader builder tracks a list of #defines
,
* a list of attributes, a list of uniforms, and a list of shader lines. It also
* tracks the location of each attribute so the caller can easily build the {@link VertexArray}
*
* For fragment shaders, the shader builder tracks a list of #defines
,
* a list of attributes, a list of uniforms, and a list of shader lines.
*
#define
macro to one or both of the shaders. These lines
* will appear at the top of the final shader source.
*
* @param {string} identifier An identifier for the macro. Identifiers must use uppercase letters with underscores to be consistent with Cesium's style guide.
* @param {string} [value] The value of the macro. If undefined, the define will not include a value. The value will be converted to GLSL code via toString()
* @param {ShaderDestination} [destination=ShaderDestination.BOTH] Whether the define appears in the vertex shader, the fragment shader, or both.
*
* @example
* // creates the line "#define ENABLE_LIGHTING" in both shaders
* shaderBuilder.addDefine("ENABLE_LIGHTING");
* // creates the line "#define PI 3.141592" in the fragment shader
* shaderBuilder.addDefine("PI", 3.141593, ShaderDestination.FRAGMENT);
*/
ShaderBuilder.prototype.addDefine = function (identifier, value, destination) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
destination = defaultValue(destination, ShaderDestination.BOTH);
// The ShaderSource created in build() will add the #define part
let line = identifier;
if (defined(value)) {
line += ` ${value.toString()}`;
}
if (ShaderDestination.includesVertexShader(destination)) {
this._vertexShaderParts.defineLines.push(line);
}
if (ShaderDestination.includesFragmentShader(destination)) {
this._fragmentShaderParts.defineLines.push(line);
}
};
/**
* Add a new dynamically-generated struct to the shader
* @param {string} structId A unique ID to identify this struct in {@link ShaderBuilder#addStructField}
* @param {string} structName The name of the struct as it will appear in the shader.
* @param {ShaderDestination} destination Whether the struct will appear in the vertex shader, the fragment shader, or both.
*
* @example
* // generates the following struct in the fragment shader
* // struct TestStruct
* // {
* // };
* shaderBuilder.addStruct("testStructId", "TestStruct", ShaderDestination.FRAGMENT);
*/
ShaderBuilder.prototype.addStruct = function (
structId,
structName,
destination
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("structId", structId);
Check.typeOf.string("structName", structName);
Check.typeOf.number("destination", destination);
//>>includeEnd('debug');
this._structs[structId] = new ShaderStruct(structName);
if (ShaderDestination.includesVertexShader(destination)) {
this._vertexShaderParts.structIds.push(structId);
}
if (ShaderDestination.includesFragmentShader(destination)) {
this._fragmentShaderParts.structIds.push(structId);
}
};
/**
* Add a field to a dynamically-generated struct.
* @param {string} structId The ID of the struct. This must be created first with {@link ShaderBuilder#addStruct}
* @param {string} type The GLSL type of the field
* @param {string} identifier The identifier of the field.
*
* @example
* // generates the following struct in the fragment shader
* // struct TestStruct
* // {
* // float minimum;
* // float maximum;
* // };
* shaderBuilder.addStruct("testStructId", "TestStruct", ShaderDestination.FRAGMENT);
* shaderBuilder.addStructField("testStructId", "float", "maximum");
* shaderBuilder.addStructField("testStructId", "float", "minimum");
*/
ShaderBuilder.prototype.addStructField = function (structId, type, identifier) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("structId", structId);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
this._structs[structId].addField(type, identifier);
};
/**
* Add a new dynamically-generated function to the shader.
* @param {string} functionName The name of the function. This will be used to identify the function in {@link ShaderBuilder#addFunctionLines}.
* @param {string} signature The full signature of the function as it will appear in the shader. Do not include the curly braces.
* @param {ShaderDestination} destination Whether the struct will appear in the vertex shader, the fragment shader, or both.
* @example
* // generates the following function in the vertex shader
* // vec3 testFunction(float parameter)
* // {
* // }
* shaderBuilder.addStruct("testFunction", "vec3 testFunction(float parameter)", ShaderDestination.VERTEX);
*/
ShaderBuilder.prototype.addFunction = function (
functionName,
signature,
destination
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("functionName", functionName);
Check.typeOf.string("signature", signature);
Check.typeOf.number("destination", destination);
//>>includeEnd('debug');
this._functions[functionName] = new ShaderFunction(signature);
if (ShaderDestination.includesVertexShader(destination)) {
this._vertexShaderParts.functionIds.push(functionName);
}
if (ShaderDestination.includesFragmentShader(destination)) {
this._fragmentShaderParts.functionIds.push(functionName);
}
};
/**
* Add lines to a dynamically-generated function
* @param {string} functionName The name of the function. This must be created beforehand using {@link ShaderBuilder#addFunction}
* @param {string|string[]} lines One or more lines of GLSL code to add to the function body. Do not include any preceding or ending whitespace, but do include the semicolon for each line.
*
* @example
* // generates the following function in the vertex shader
* // vec3 testFunction(float parameter)
* // {
* // float signed = 2.0 * parameter - 1.0;
* // return vec3(signed, 0.0, 0.0);
* // }
* shaderBuilder.addStruct("testFunction", "vec3 testFunction(float parameter)", ShaderDestination.VERTEX);
* shaderBuilder.addFunctionLines("testFunction", [
* "float signed = 2.0 * parameter - 1.0;",
* "return vec3(parameter);"
* ]);
*/
ShaderBuilder.prototype.addFunctionLines = function (functionName, lines) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("functionName", functionName);
if (typeof lines !== "string" && !Array.isArray(lines)) {
throw new DeveloperError(
`Expected lines to be a string or an array of strings, actual value was ${lines}`
);
}
//>>includeEnd('debug');
this._functions[functionName].addLines(lines);
};
/**
* Add a uniform declaration to one or both of the shaders. These lines
* will appear grouped near the top of the final shader source.
*
* @param {string} type The GLSL type of the uniform.
* @param {string} identifier An identifier for the uniform. Identifiers must begin with u_
to be consistent with Cesium's style guide.
* @param {ShaderDestination} [destination=ShaderDestination.BOTH] Whether the uniform appears in the vertex shader, the fragment shader, or both.
*
* @example
* // creates the line "uniform vec3 u_resolution;"
* shaderBuilder.addUniform("vec3", "u_resolution", ShaderDestination.FRAGMENT);
* // creates the line "uniform float u_time;" in both shaders
* shaderBuilder.addUniform("float", "u_time", ShaderDestination.BOTH);
*/
ShaderBuilder.prototype.addUniform = function (type, identifier, destination) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
destination = defaultValue(destination, ShaderDestination.BOTH);
const line = `uniform ${type} ${identifier};`;
if (ShaderDestination.includesVertexShader(destination)) {
this._vertexShaderParts.uniformLines.push(line);
}
if (ShaderDestination.includesFragmentShader(destination)) {
this._fragmentShaderParts.uniformLines.push(line);
}
};
/**
* Add a position attribute declaration to the vertex shader. These lines
* will appear grouped near the top of the final shader source.
* * Some WebGL implementations require attribute 0 to be enabled, so this is * reserved for the position attribute. For all other attributes, see * {@link ShaderBuilder#addAttribute} *
* * @param {string} type The GLSL type of the attribute * @param {string} identifier An identifier for the attribute. Identifiers must begin witha_
to be consistent with Cesium's style guide.
* @return {number} The integer location of the attribute. This location can be used when creating attributes for a {@link VertexArray}. This will always be 0.
*
* @example
* // creates the line "in vec3 a_position;"
* shaderBuilder.setPositionAttribute("vec3", "a_position");
*/
ShaderBuilder.prototype.setPositionAttribute = function (type, identifier) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
if (defined(this._positionAttributeLine)) {
throw new DeveloperError(
"setPositionAttribute() must be called exactly once for the attribute used for gl_Position. For other attributes, use addAttribute()"
);
}
//>>includeEnd('debug');
this._positionAttributeLine = `in ${type} ${identifier};`;
// Some WebGL implementations require attribute 0 to always be active, so
// this builder assumes the position will always go in location 0
this._attributeLocations[identifier] = 0;
return 0;
};
/**
* Add an attribute declaration to the vertex shader. These lines
* will appear grouped near the top of the final shader source.
* * Some WebGL implementations require attribute 0 to be enabled, so this is * reserved for the position attribute. See {@link ShaderBuilder#setPositionAttribute} *
* * @param {string} type The GLSL type of the attribute * @param {string} identifier An identifier for the attribute. Identifiers must begin witha_
to be consistent with Cesium's style guide.
* @return {number} The integer location of the attribute. This location can be used when creating attributes for a {@link VertexArray}
*
* @example
* // creates the line "in vec2 a_texCoord0;" in the vertex shader
* shaderBuilder.addAttribute("vec2", "a_texCoord0");
*/
ShaderBuilder.prototype.addAttribute = function (type, identifier) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
const line = `in ${type} ${identifier};`;
this._attributeLines.push(line);
const location = this._nextAttributeLocation;
this._attributeLocations[identifier] = location;
// Most attributes only require a single attribute location, but matrices
// require more.
this._nextAttributeLocation += getAttributeLocationCount(type);
return location;
};
/**
* Add a varying declaration to both the vertex and fragment shaders.
*
* @param {string} type The GLSL type of the varying
* @param {string} identifier An identifier for the varying. Identifiers must begin with v_
to be consistent with Cesium's style guide.
*
* @example
* // creates the line "in vec3 v_color;" in the vertex shader
* // creates the line "out vec3 v_color;" in the fragment shader
* shaderBuilder.addVarying("vec3", "v_color");
*/
ShaderBuilder.prototype.addVarying = function (type, identifier) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("type", type);
Check.typeOf.string("identifier", identifier);
//>>includeEnd('debug');
const line = `${type} ${identifier};`;
this._vertexShaderParts.varyingLines.push(`out ${line}`);
this._fragmentShaderParts.varyingLines.push(`in ${line}`);
};
/**
* Appends lines of GLSL code to the vertex shader
*
* @param {string|string[]} lines One or more lines to add to the end of the vertex shader source
*
* @example
* shaderBuilder.addVertexLines([
* "void main()",
* "{",
* " v_color = a_color;",
* " gl_Position = vec4(a_position, 1.0);",
* "}"
* ]);
*/
ShaderBuilder.prototype.addVertexLines = function (lines) {
//>>includeStart('debug', pragmas.debug);
if (typeof lines !== "string" && !Array.isArray(lines)) {
throw new DeveloperError(
`Expected lines to be a string or an array of strings, actual value was ${lines}`
);
}
//>>includeEnd('debug');
const vertexLines = this._vertexShaderParts.shaderLines;
if (Array.isArray(lines)) {
vertexLines.push.apply(vertexLines, lines);
} else {
// Single string case
vertexLines.push(lines);
}
};
/**
* Appends lines of GLSL code to the fragment shader
*
* @param {string[]} lines The lines to add to the end of the fragment shader source
*
* @example
* shaderBuilder.addFragmentLines([
* "void main()",
* "{",
* " #ifdef SOLID_COLOR",
* " out_FragColor = vec4(u_color, 1.0);",
* " #else",
* " out_FragColor = vec4(v_color, 1.0);",
* " #endif",
* "}"
* ]);
*/
ShaderBuilder.prototype.addFragmentLines = function (lines) {
//>>includeStart('debug', pragmas.debug);
if (typeof lines !== "string" && !Array.isArray(lines)) {
throw new DeveloperError(
`Expected lines to be a string or an array of strings, actual value was ${lines}`
);
}
//>>includeEnd('debug');
const fragmentLines = this._fragmentShaderParts.shaderLines;
if (Array.isArray(lines)) {
fragmentLines.push.apply(fragmentLines, lines);
} else {
// Single string case
fragmentLines.push(lines);
}
};
/**
* Builds the {@link ShaderProgram} from the pieces added by the other methods.
* Call this one time at the end of modifying the shader through the other
* methods in this class.
*
* @param {Context} context The context to use for creating the shader.
* @return {ShaderProgram} A shader program to use for rendering.
*
* @example
* const shaderProgram = shaderBuilder.buildShaderProgram(context);
*/
ShaderBuilder.prototype.buildShaderProgram = function (context) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("context", context);
//>>includeEnd('debug');
const positionAttribute = defined(this._positionAttributeLine)
? [this._positionAttributeLine]
: [];
const structLines = generateStructLines(this);
const functionLines = generateFunctionLines(this);
// Lines are joined here so the ShaderSource
// generates a single #line 0 directive
const vertexLines = positionAttribute
.concat(
this._attributeLines,
this._vertexShaderParts.uniformLines,
this._vertexShaderParts.varyingLines,
structLines.vertexLines,
functionLines.vertexLines,
this._vertexShaderParts.shaderLines
)
.join("\n");
const vertexShaderSource = new ShaderSource({
defines: this._vertexShaderParts.defineLines,
sources: [vertexLines],
});
const fragmentLines = this._fragmentShaderParts.uniformLines
.concat(
this._fragmentShaderParts.varyingLines,
structLines.fragmentLines,
functionLines.fragmentLines,
this._fragmentShaderParts.shaderLines
)
.join("\n");
const fragmentShaderSource = new ShaderSource({
defines: this._fragmentShaderParts.defineLines,
sources: [fragmentLines],
});
return ShaderProgram.fromCache({
context: context,
vertexShaderSource: vertexShaderSource,
fragmentShaderSource: fragmentShaderSource,
attributeLocations: this._attributeLocations,
});
};
ShaderBuilder.prototype.clone = function () {
return clone(this, true);
};
function generateStructLines(shaderBuilder) {
const vertexLines = [];
const fragmentLines = [];
let i;
let structIds = shaderBuilder._vertexShaderParts.structIds;
let structId;
let struct;
let structLines;
for (i = 0; i < structIds.length; i++) {
structId = structIds[i];
struct = shaderBuilder._structs[structId];
structLines = struct.generateGlslLines();
vertexLines.push.apply(vertexLines, structLines);
}
structIds = shaderBuilder._fragmentShaderParts.structIds;
for (i = 0; i < structIds.length; i++) {
structId = structIds[i];
struct = shaderBuilder._structs[structId];
structLines = struct.generateGlslLines();
fragmentLines.push.apply(fragmentLines, structLines);
}
return {
vertexLines: vertexLines,
fragmentLines: fragmentLines,
};
}
function getAttributeLocationCount(glslType) {
switch (glslType) {
case "mat2":
return 2;
case "mat3":
return 3;
case "mat4":
return 4;
default:
return 1;
}
}
function generateFunctionLines(shaderBuilder) {
const vertexLines = [];
const fragmentLines = [];
let i;
let functionIds = shaderBuilder._vertexShaderParts.functionIds;
let functionId;
let func;
let functionLines;
for (i = 0; i < functionIds.length; i++) {
functionId = functionIds[i];
func = shaderBuilder._functions[functionId];
functionLines = func.generateGlslLines();
vertexLines.push.apply(vertexLines, functionLines);
}
functionIds = shaderBuilder._fragmentShaderParts.functionIds;
for (i = 0; i < functionIds.length; i++) {
functionId = functionIds[i];
func = shaderBuilder._functions[functionId];
functionLines = func.generateGlslLines();
fragmentLines.push.apply(fragmentLines, functionLines);
}
return {
vertexLines: vertexLines,
fragmentLines: fragmentLines,
};
}
export default ShaderBuilder;