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 CustomShaderMode from "./CustomShaderMode.js";
import UniformType from "./UniformType.js";
import TextureManager from "./TextureManager.js";
import CustomShaderTranslucencyMode from "./CustomShaderTranslucencyMode.js";
/**
* An object describing a uniform, its type, and an initial value
*
* @typedef {object} UniformSpecifier
* @property {UniformType} type The Glsl type of the uniform.
* @property {boolean|number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4|TextureUniform} value The initial value of the uniform
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* A set of variables parsed from the user-defined shader code. These can be
* used for optimizations when generating the overall shader. Though they are
* represented as JS objects, the intended use is like a set, so only the
* existence of keys matter. The values will always be true
if
* defined. This data structure is used because:
*
variableSet.hasOwnProperty("position")
are straightforwardvsInput.attributes
struct.
* @property {VariableSet} featureIdSet A set of all unique feature ID sets used in the vertex shader via the vsInput.featureIds
struct.
* @property {VariableSet} metadataSet A set of all unique metadata properties used in the vertex shader via the vsInput.metadata
struct.
* @private
*/
/**
* Variable sets parsed from the user-defined fragment shader text.
* @typedef {object} FragmentVariableSets
* @property {VariableSet} attributeSet A set of all unique attributes used in the fragment shader via the fsInput.attributes
struct
* @property {VariableSet} featureIdSet A set of all unique feature ID sets used in the fragment shader via the fsInput.featureIds
struct.
* @property {VariableSet} metadataSet A set of all unique metadata properties used in the fragment shader via the fsInput.metadata
struct.
* @property {VariableSet} materialSet A set of all material variables such as diffuse, specular or alpha that are used in the fragment shader via the material
struct.
* @private
*/
/**
* A user defined GLSL shader used with {@link Model} as well
* as {@link Cesium3DTileset}.
* * If texture uniforms are used, additional resource management must be done: *
*update
function must be called each frame. When a
* custom shader is passed to a {@link Model} or a
* {@link Cesium3DTileset}, this step is handled automaticaly
* * See the {@link https://github.com/CesiumGS/cesium/tree/main/Documentation/CustomShaderGuide|Custom Shader Guide} for more detailed documentation. *
* * @param {object} options An object with the following options * @param {CustomShaderMode} [options.mode=CustomShaderMode.MODIFY_MATERIAL] The custom shader mode, which determines how the custom shader code is inserted into the fragment shader. * @param {LightingModel} [options.lightingModel] The lighting model (e.g. PBR or unlit). If present, this overrides the default lighting for the model. * @param {CustomShaderTranslucencyMode} [options.translucencyMode=CustomShaderTranslucencyMode.INHERIT] The translucency mode, which determines how the custom shader will be applied. If the value is CustomShaderTransulcencyMode.OPAQUE or CustomShaderTransulcencyMode.TRANSLUCENT, the custom shader will override settings from the model's material. If the value is CustomShaderTransulcencyMode.INHERIT, the custom shader will render as either opaque or translucent depending on the primitive's material settings. * @param {ObjectvertexShaderText
. This
* is used only for optimizations in {@link CustomShaderPipelineStage}.
* @type {VertexVariableSets}
* @private
*/
this.usedVariablesVertex = {
attributeSet: {},
featureIdSet: {},
metadataSet: {},
};
/**
* A collection of variables used in fragmentShaderText
. This
* is used only for optimizations in {@link CustomShaderPipelineStage}.
* @type {FragmentVariableSets}
* @private
*/
this.usedVariablesFragment = {
attributeSet: {},
featureIdSet: {},
metadataSet: {},
materialSet: {},
};
findUsedVariables(this);
validateBuiltinVariables(this);
}
function buildUniformMap(customShader) {
const uniforms = customShader.uniforms;
const uniformMap = {};
for (const uniformName in uniforms) {
if (uniforms.hasOwnProperty(uniformName)) {
const uniform = uniforms[uniformName];
const type = uniform.type;
//>>includeStart('debug', pragmas.debug);
if (type === UniformType.SAMPLER_CUBE) {
throw new DeveloperError(
"CustomShader does not support samplerCube uniforms"
);
}
//>>includeEnd('debug');
if (type === UniformType.SAMPLER_2D) {
customShader._textureManager.loadTexture2D(uniformName, uniform.value);
uniformMap[uniformName] = createUniformTexture2DFunction(
customShader,
uniformName
);
} else {
uniformMap[uniformName] = createUniformFunction(
customShader,
uniformName
);
}
}
}
return uniformMap;
}
function createUniformTexture2DFunction(customShader, uniformName) {
return function () {
return defaultValue(
customShader._textureManager.getTexture(uniformName),
customShader._defaultTexture
);
};
}
function createUniformFunction(customShader, uniformName) {
return function () {
return customShader.uniforms[uniformName].value;
};
}
function getVariables(shaderText, regex, outputSet) {
let match;
while ((match = regex.exec(shaderText)) !== null) {
const variableName = match[1];
// Using a dictionary like a set. The value doesn't
// matter, as this will only be used for queries such as
// if (set.hasOwnProperty(variableName)) { ... }
outputSet[variableName] = true;
}
}
function findUsedVariables(customShader) {
const attributeRegex = /[vf]sInput\.attributes\.(\w+)/g;
const featureIdRegex = /[vf]sInput\.featureIds\.(\w+)/g;
const metadataRegex = /[vf]sInput\.metadata.(\w+)/g;
let attributeSet;
const vertexShaderText = customShader.vertexShaderText;
if (defined(vertexShaderText)) {
attributeSet = customShader.usedVariablesVertex.attributeSet;
getVariables(vertexShaderText, attributeRegex, attributeSet);
attributeSet = customShader.usedVariablesVertex.featureIdSet;
getVariables(vertexShaderText, featureIdRegex, attributeSet);
attributeSet = customShader.usedVariablesVertex.metadataSet;
getVariables(vertexShaderText, metadataRegex, attributeSet);
}
const fragmentShaderText = customShader.fragmentShaderText;
if (defined(fragmentShaderText)) {
attributeSet = customShader.usedVariablesFragment.attributeSet;
getVariables(fragmentShaderText, attributeRegex, attributeSet);
attributeSet = customShader.usedVariablesFragment.featureIdSet;
getVariables(fragmentShaderText, featureIdRegex, attributeSet);
attributeSet = customShader.usedVariablesFragment.metadataSet;
getVariables(fragmentShaderText, metadataRegex, attributeSet);
const materialRegex = /material\.(\w+)/g;
const materialSet = customShader.usedVariablesFragment.materialSet;
getVariables(fragmentShaderText, materialRegex, materialSet);
}
}
function expandCoordinateAbbreviations(variableName) {
const modelCoordinatesRegex = /^.*MC$/;
const worldCoordinatesRegex = /^.*WC$/;
const eyeCoordinatesRegex = /^.*EC$/;
if (modelCoordinatesRegex.test(variableName)) {
return `${variableName} (model coordinates)`;
}
if (worldCoordinatesRegex.test(variableName)) {
return `${variableName} (Cartesian world coordinates)`;
}
if (eyeCoordinatesRegex.test(variableName)) {
return `${variableName} (eye coordinates)`;
}
return variableName;
}
function validateVariableUsage(
variableSet,
incorrectVariable,
correctVariable,
vertexOrFragment
) {
if (variableSet.hasOwnProperty(incorrectVariable)) {
const message = `${expandCoordinateAbbreviations(
incorrectVariable
)} is not available in the ${vertexOrFragment} shader. Did you mean ${expandCoordinateAbbreviations(
correctVariable
)} instead?`;
throw new DeveloperError(message);
}
}
function validateBuiltinVariables(customShader) {
const attributesVS = customShader.usedVariablesVertex.attributeSet;
// names without MC/WC/EC are ambiguous
validateVariableUsage(attributesVS, "position", "positionMC", "vertex");
validateVariableUsage(attributesVS, "normal", "normalMC", "vertex");
validateVariableUsage(attributesVS, "tangent", "tangentMC", "vertex");
validateVariableUsage(attributesVS, "bitangent", "bitangentMC", "vertex");
// world and eye coordinate positions are only available in the fragment shader.
validateVariableUsage(attributesVS, "positionWC", "positionMC", "vertex");
validateVariableUsage(attributesVS, "positionEC", "positionMC", "vertex");
// normal, tangent and bitangent are in model coordinates in the vertex shader
validateVariableUsage(attributesVS, "normalEC", "normalMC", "vertex");
validateVariableUsage(attributesVS, "tangentEC", "tangentMC", "vertex");
validateVariableUsage(attributesVS, "bitangentEC", "bitangentMC", "vertex");
const attributesFS = customShader.usedVariablesFragment.attributeSet;
// names without MC/WC/EC are ambiguous
validateVariableUsage(attributesFS, "position", "positionEC", "fragment");
validateVariableUsage(attributesFS, "normal", "normalEC", "fragment");
validateVariableUsage(attributesFS, "tangent", "tangentEC", "fragment");
validateVariableUsage(attributesFS, "bitangent", "bitangentEC", "fragment");
// normal, tangent, and bitangent are in eye coordinates in the fragment
// shader.
validateVariableUsage(attributesFS, "normalMC", "normalEC", "fragment");
validateVariableUsage(attributesFS, "tangentMC", "tangentEC", "fragment");
validateVariableUsage(attributesFS, "bitangentMC", "bitangentEC", "fragment");
}
/**
* Update the value of a uniform declared in the shader
* @param {string} uniformName The GLSL name of the uniform. This must match one of the uniforms declared in the constructor
* @param {boolean|number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4|string|Resource} value The new value of the uniform.
*/
CustomShader.prototype.setUniform = function (uniformName, value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("uniformName", uniformName);
Check.defined("value", value);
if (!defined(this.uniforms[uniformName])) {
throw new DeveloperError(
`Uniform ${uniformName} must be declared in the CustomShader constructor.`
);
}
//>>includeEnd('debug');
const uniform = this.uniforms[uniformName];
if (uniform.type === UniformType.SAMPLER_2D) {
// Textures are loaded asynchronously
this._textureManager.loadTexture2D(uniformName, value);
} else if (defined(value.clone)) {
// clone Cartesian and Matrix types.
uniform.value = value.clone(uniform.value);
} else {
uniform.value = value;
}
};
CustomShader.prototype.update = function (frameState) {
this._defaultTexture = frameState.context.defaultTexture;
this._textureManager.update(frameState);
};
/**
* Returns true if this object was destroyed; otherwise, false.
* isDestroyed
will result in a {@link DeveloperError} exception.
*
* @returns {boolean} True if this object was destroyed; otherwise, false.
*
* @see CustomShader#destroy
* @private
*/
CustomShader.prototype.isDestroyed = function () {
return false;
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined
) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* customShader = customShader && customShader.destroy();
*
* @see CustomShader#isDestroyed
* @private
*/
CustomShader.prototype.destroy = function () {
this._textureManager = this._textureManager && this._textureManager.destroy();
destroyObject(this);
};
export default CustomShader;