cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
619 lines (544 loc) • 20.9 kB
JavaScript
import combine from "../../Core/combine.js";
import defined from "../../Core/defined.js";
import oneTimeWarning from "../../Core/oneTimeWarning.js";
import ShaderDestination from "../../Renderer/ShaderDestination.js";
import Pass from "../../Renderer/Pass.js";
import CustomShaderStageVS from "../../Shaders/ModelExperimental/CustomShaderStageVS.js";
import CustomShaderStageFS from "../../Shaders/ModelExperimental/CustomShaderStageFS.js";
import AlphaMode from "../AlphaMode.js";
import CustomShaderMode from "./CustomShaderMode.js";
import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js";
import MetadataPipelineStage from "./MetadataPipelineStage.js";
import ModelExperimentalUtility from "./ModelExperimentalUtility.js";
/**
* The custom shader pipeline stage takes GLSL callbacks from the
* {@link CustomShader} and inserts them into the overall shader code for the
* {@link ModelExperimental}. The input to the callback is a struct with many
* properties that depend on the attributes of the primitive. This shader code
* is automatically generated by this stage.
*
* @namespace CustomShaderPipelineStage
*
* @private
*/
const CustomShaderPipelineStage = {};
CustomShaderPipelineStage.name = "CustomShaderPipelineStage"; // Helps with debugging
CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_VS = "AttributesVS";
CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_FS = "AttributesFS";
CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES = "Attributes";
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT = "VertexInput";
CustomShaderPipelineStage.STRUCT_NAME_VERTEX_INPUT = "VertexInput";
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT = "FragmentInput";
CustomShaderPipelineStage.STRUCT_NAME_FRAGMENT_INPUT = "FragmentInput";
CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_VS =
"initializeInputStructVS";
CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_VS =
"void initializeInputStruct(out VertexInput vsInput, ProcessedAttributes attributes)";
CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_FS =
"initializeInputStructFS";
CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_FS =
"void initializeInputStruct(out FragmentInput fsInput, ProcessedAttributes attributes)";
/**
* Process a primitive. This modifies the following parts of the render
* resources:
* <ul>
* <li>Modifies the shader to include the custom shader code to the vertex and fragment shaders</li>
* <li>Modifies the shader to include automatically-generated structs that serve as input to the custom shader callbacks </li>
* <li>Modifies the shader to include any additional user-defined uniforms</li>
* <li>Modifies the shader to include any additional user-defined varyings</li>
* <li>Adds any user-defined uniforms to the uniform map</li>
* <li>If the user specified a lighting model, the settings are overridden in the render resources</li>
* </ul>
* <p>
* This pipeline stage is designed to fail gracefully where possible. If the
* primitive does not have the right attributes to satisfy the shader code,
* defaults will be inferred (when reasonable to do so). If not, the custom
* shader will be disabled.
* <p>
*
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
* @param {ModelComponents.Primitive} primitive The primitive to be rendered
* @param {FrameState} frameState The frame state.
* @private
*/
CustomShaderPipelineStage.process = function (
renderResources,
primitive,
frameState
) {
const shaderBuilder = renderResources.shaderBuilder;
const customShader = renderResources.model.customShader;
// Check the lighting model and translucent options first, as sometimes
// these are used even if there is no vertex or fragment shader text.
// if present, the lighting model overrides the material's lighting model.
if (defined(customShader.lightingModel)) {
renderResources.lightingOptions.lightingModel = customShader.lightingModel;
}
const alphaOptions = renderResources.alphaOptions;
if (customShader.isTranslucent) {
alphaOptions.pass = Pass.TRANSLUCENT;
alphaOptions.alphaMode = AlphaMode.BLEND;
} else {
// Use the default pass (either OPAQUE or 3D_TILES), regardless of whether
// the material pipeline stage used translucent. The default is configured
// in AlphaPipelineStage
alphaOptions.pass = undefined;
alphaOptions.alphaMode = AlphaMode.OPAQUE;
}
// Generate lines of code for the shader, but don't add them to the shader
// yet.
const generatedCode = generateShaderLines(customShader, primitive);
// In some corner cases, the primitive may not be compatible with the
// shader. In this case, skip the custom shader.
if (!generatedCode.customShaderEnabled) {
return;
}
addLinesToShader(shaderBuilder, customShader, generatedCode);
// the input to the fragment shader may include a low-precision ECEF position
if (generatedCode.shouldComputePositionWC) {
shaderBuilder.addDefine(
"COMPUTE_POSITION_WC",
undefined,
ShaderDestination.BOTH
);
}
if (defined(customShader.vertexShaderText)) {
shaderBuilder.addDefine(
"HAS_CUSTOM_VERTEX_SHADER",
undefined,
ShaderDestination.VERTEX
);
}
if (defined(customShader.fragmentShaderText)) {
shaderBuilder.addDefine(
"HAS_CUSTOM_FRAGMENT_SHADER",
undefined,
ShaderDestination.FRAGMENT
);
// add defines like CUSTOM_SHADER_MODIFY_MATERIAL
const shaderModeDefine = CustomShaderMode.getDefineName(customShader.mode);
shaderBuilder.addDefine(
shaderModeDefine,
undefined,
ShaderDestination.FRAGMENT
);
}
const uniforms = customShader.uniforms;
for (const uniformName in uniforms) {
if (uniforms.hasOwnProperty(uniformName)) {
const uniform = uniforms[uniformName];
shaderBuilder.addUniform(uniform.type, uniformName);
}
}
const varyings = customShader.varyings;
for (const varyingName in varyings) {
if (varyings.hasOwnProperty(varyingName)) {
const varyingType = varyings[varyingName];
shaderBuilder.addVarying(varyingType, varyingName);
}
}
renderResources.uniformMap = combine(
renderResources.uniformMap,
customShader.uniformMap
);
};
function getAttributesByName(attributes) {
const names = {};
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
const attributeInfo = ModelExperimentalUtility.getAttributeInfo(attribute);
names[attributeInfo.variableName] = attributeInfo;
}
return names;
}
// GLSL types of standard attribute types when uniquely defined
const attributeTypeLUT = {
position: "vec3",
normal: "vec3",
tangent: "vec3",
bitangent: "vec3",
texCoord: "vec2",
color: "vec4",
joints: "ivec4",
weights: "vec4",
};
// Corresponding attribute values
const attributeDefaultValueLUT = {
position: "vec3(0.0)",
normal: "vec3(0.0, 0.0, 1.0)",
tangent: "vec3(1.0, 0.0, 0.0)",
bitangent: "vec3(0.0, 1.0, 0.0)",
texCoord: "vec2(0.0)",
color: "vec4(1.0)",
joints: "ivec4(0)",
weights: "vec4(0.0)",
};
function inferAttributeDefaults(attributeName) {
// remove trailing set indices. E.g. "texCoord_0" -> "texCoord"
let trimmed = attributeName.replace(/_[0-9]+$/, "");
// also remove the MC/EC since they will have the same default value
trimmed = trimmed.replace(/(MC|EC)$/, "");
const glslType = attributeTypeLUT[trimmed];
const value = attributeDefaultValueLUT[trimmed];
// - _CUSTOM_ATTRIBUTE has an unknown type.
if (!defined(glslType)) {
return undefined;
}
return {
attributeField: [glslType, attributeName],
value: value,
};
}
function generateVertexShaderLines(
customShader,
attributesByName,
vertexLines
) {
const categories = partitionAttributes(
attributesByName,
customShader.usedVariablesVertex.attributeSet,
false
);
const addToShader = categories.addToShader;
const needsDefault = categories.missingAttributes;
let variableName;
let vertexInitialization;
const attributeFields = [];
const initializationLines = [];
for (variableName in addToShader) {
if (addToShader.hasOwnProperty(variableName)) {
const attributeInfo = addToShader[variableName];
const attributeField = [attributeInfo.glslType, variableName];
attributeFields.push(attributeField);
// Initializing attribute structs are just a matter of copying the
// attribute or varying: E.g.:
// " vsInput.attributes.position = a_position;"
vertexInitialization = `vsInput.attributes.${variableName} = attributes.${variableName};`;
initializationLines.push(vertexInitialization);
}
}
for (let i = 0; i < needsDefault.length; i++) {
variableName = needsDefault[i];
const attributeDefaults = inferAttributeDefaults(variableName);
if (!defined(attributeDefaults)) {
CustomShaderPipelineStage._oneTimeWarning(
"CustomShaderPipelineStage.incompatiblePrimitiveVS",
`Primitive is missing attribute ${variableName}, disabling custom vertex shader`
);
// This primitive isn't compatible with the shader. Return early
// to skip the vertex shader
return;
}
attributeFields.push(attributeDefaults.attributeField);
vertexInitialization = `vsInput.attributes.${variableName} = ${attributeDefaults.value};`;
initializationLines.push(vertexInitialization);
}
vertexLines.enabled = true;
vertexLines.attributeFields = attributeFields;
vertexLines.initializationLines = initializationLines;
}
function generatePositionBuiltins(customShader) {
const attributeFields = [];
const initializationLines = [];
const usedVariables = customShader.usedVariablesFragment.attributeSet;
// Model space position is the same position as in the glTF accessor,
// this is already added to the shader with other attributes.
// World coordinates in ECEF coordinates. Note that this is
// low precision (32-bit floats) on the GPU.
if (usedVariables.hasOwnProperty("positionWC")) {
attributeFields.push(["vec3", "positionWC"]);
initializationLines.push(
"fsInput.attributes.positionWC = attributes.positionWC;"
);
}
// position in eye coordinates
if (usedVariables.hasOwnProperty("positionEC")) {
attributeFields.push(["vec3", "positionEC"]);
initializationLines.push(
"fsInput.attributes.positionEC = attributes.positionEC;"
);
}
return {
attributeFields: attributeFields,
initializationLines: initializationLines,
};
}
function generateFragmentShaderLines(
customShader,
attributesByName,
fragmentLines
) {
const categories = partitionAttributes(
attributesByName,
customShader.usedVariablesFragment.attributeSet,
true
);
const addToShader = categories.addToShader;
const needsDefault = categories.missingAttributes;
let variableName;
let fragmentInitialization;
const attributeFields = [];
const initializationLines = [];
for (variableName in addToShader) {
if (addToShader.hasOwnProperty(variableName)) {
const attributeInfo = addToShader[variableName];
const attributeField = [attributeInfo.glslType, variableName];
attributeFields.push(attributeField);
// Initializing attribute structs are just a matter of copying the
// value from the processed attributes
// " fsInput.attributes.positionMC = attributes.positionMC;"
fragmentInitialization = `fsInput.attributes.${variableName} = attributes.${variableName};`;
initializationLines.push(fragmentInitialization);
}
}
for (let i = 0; i < needsDefault.length; i++) {
variableName = needsDefault[i];
const attributeDefaults = inferAttributeDefaults(variableName);
if (!defined(attributeDefaults)) {
CustomShaderPipelineStage._oneTimeWarning(
"CustomShaderPipelineStage.incompatiblePrimitiveFS",
`Primitive is missing attribute ${variableName}, disabling custom fragment shader.`
);
// This primitive isn't compatible with the shader. Return early
// so the fragment shader is skipped
return;
}
attributeFields.push(attributeDefaults.attributeField);
fragmentInitialization = `fsInput.attributes.${variableName} = ${attributeDefaults.value};`;
initializationLines.push(fragmentInitialization);
}
// Built-ins for positions in various coordinate systems.
const positionBuiltins = generatePositionBuiltins(customShader);
fragmentLines.enabled = true;
fragmentLines.attributeFields = attributeFields.concat(
positionBuiltins.attributeFields
);
fragmentLines.initializationLines = positionBuiltins.initializationLines.concat(
initializationLines
);
}
// These attributes are derived from positionMC, and are handled separately
// from other attributes
const builtinAttributes = {
positionWC: true,
positionEC: true,
};
function partitionAttributes(
primitiveAttributes,
shaderAttributeSet,
isFragmentShader
) {
// shaderAttributes = set of all attributes used in the shader
// primitiveAttributes = set of all the primitive's attributes
// partition into three categories:
// - addToShader = shaderAttributes intersect primitiveAttributes
// - missingAttributes = shaderAttributes - primitiveAttributes - builtinAttributes
// - unneededAttributes = (primitiveAttributes - shaderAttributes) U builtinAttributes
//
// addToShader are attributes that should be added to the shader.
// missingAttributes are attributes for which we need to provide a default value
// unneededAttributes are other attributes that can be skipped.
let renamed;
let attributeName;
const addToShader = {};
for (attributeName in primitiveAttributes) {
if (primitiveAttributes.hasOwnProperty(attributeName)) {
const attribute = primitiveAttributes[attributeName];
// normals and tangents are in model coordinates in the attributes but
// in eye coordinates in the fragment shader.
renamed = attributeName;
if (isFragmentShader && attributeName === "normalMC") {
renamed = "normalEC";
} else if (isFragmentShader && attributeName === "tangentMC") {
renamed = "tangentEC";
}
if (shaderAttributeSet.hasOwnProperty(renamed)) {
addToShader[renamed] = attribute;
}
}
}
const missingAttributes = [];
for (attributeName in shaderAttributeSet) {
if (shaderAttributeSet.hasOwnProperty(attributeName)) {
if (builtinAttributes.hasOwnProperty(attributeName)) {
// Builtins are handled separately from attributes, so skip them here
continue;
}
// normals and tangents are in model coordinates in the attributes but
// in eye coordinates in the fragment shader.
renamed = attributeName;
if (isFragmentShader && attributeName === "normalEC") {
renamed = "normalMC";
} else if (isFragmentShader && attributeName === "tangentEC") {
renamed = "tangentMC";
}
if (!primitiveAttributes.hasOwnProperty(renamed)) {
missingAttributes.push(attributeName);
}
}
}
return {
addToShader: addToShader,
missingAttributes: missingAttributes,
};
}
function generateShaderLines(customShader, primitive) {
// Assume shader code is disabled unless proven otherwise
const vertexLines = {
enabled: false,
};
const fragmentLines = {
enabled: false,
};
// Attempt to generate vertex and fragment shader lines before adding any
// code to the shader.
const attributesByName = getAttributesByName(primitive.attributes);
if (defined(customShader.vertexShaderText)) {
generateVertexShaderLines(customShader, attributesByName, vertexLines);
}
if (defined(customShader.fragmentShaderText)) {
generateFragmentShaderLines(customShader, attributesByName, fragmentLines);
}
// positionWC must be computed in the vertex shader
// for use in the fragmentShader. However, this can be skipped if:
// - positionWC isn't used in the fragment shader
// - or the fragment shader is disabled
const attributeSetFS = customShader.usedVariablesFragment.attributeSet;
const shouldComputePositionWC =
attributeSetFS.hasOwnProperty("positionWC") && fragmentLines.enabled;
// Return any generated shader code along with some flags to indicate which
// defines should be added.
return {
vertexLines: vertexLines,
fragmentLines: fragmentLines,
vertexLinesEnabled: vertexLines.enabled,
fragmentLinesEnabled: fragmentLines.enabled,
customShaderEnabled: vertexLines.enabled || fragmentLines.enabled,
shouldComputePositionWC: shouldComputePositionWC,
};
}
function addVertexLinesToShader(shaderBuilder, vertexLines) {
// Vertex Lines ---------------------------------------------------------
let i;
let structId = CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_VS;
shaderBuilder.addStruct(
structId,
CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
ShaderDestination.VERTEX
);
const attributeFields = vertexLines.attributeFields;
for (i = 0; i < attributeFields.length; i++) {
const field = attributeFields[i];
const glslType = field[0];
const variableName = field[1];
shaderBuilder.addStructField(structId, glslType, variableName);
}
// This could be hard-coded, but the symmetry with other structs makes unit
// tests more convenient
structId = CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT;
shaderBuilder.addStruct(
structId,
CustomShaderPipelineStage.STRUCT_NAME_VERTEX_INPUT,
ShaderDestination.VERTEX
);
shaderBuilder.addStructField(
structId,
CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
"attributes"
);
// Add FeatureIds struct from the Feature ID stage
shaderBuilder.addStructField(
structId,
FeatureIdPipelineStage.STRUCT_NAME_FEATURE_IDS,
"featureIds"
);
// Add Metadata struct from the metadata stage
shaderBuilder.addStructField(
structId,
MetadataPipelineStage.STRUCT_NAME_METADATA,
"metadata"
);
const functionId =
CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_VS;
shaderBuilder.addFunction(
functionId,
CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_VS,
ShaderDestination.VERTEX
);
const initializationLines = vertexLines.initializationLines;
shaderBuilder.addFunctionLines(functionId, initializationLines);
}
function addFragmentLinesToShader(shaderBuilder, fragmentLines) {
let i;
let structId = CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_FS;
shaderBuilder.addStruct(
structId,
CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
ShaderDestination.FRAGMENT
);
let field;
let glslType;
let variableName;
const attributeFields = fragmentLines.attributeFields;
for (i = 0; i < attributeFields.length; i++) {
field = attributeFields[i];
glslType = field[0];
variableName = field[1];
shaderBuilder.addStructField(structId, glslType, variableName);
}
structId = CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT;
shaderBuilder.addStruct(
structId,
CustomShaderPipelineStage.STRUCT_NAME_FRAGMENT_INPUT,
ShaderDestination.FRAGMENT
);
shaderBuilder.addStructField(
structId,
CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
"attributes"
);
// Add FeatureIds struct from the Feature ID stage
shaderBuilder.addStructField(
structId,
FeatureIdPipelineStage.STRUCT_NAME_FEATURE_IDS,
"featureIds"
);
// Add Metadata struct from the metadata stage
shaderBuilder.addStructField(
structId,
MetadataPipelineStage.STRUCT_NAME_METADATA,
"metadata"
);
const functionId =
CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_FS;
shaderBuilder.addFunction(
functionId,
CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_FS,
ShaderDestination.FRAGMENT
);
const initializationLines = fragmentLines.initializationLines;
shaderBuilder.addFunctionLines(functionId, initializationLines);
}
function addLinesToShader(shaderBuilder, customShader, generatedCode) {
const vertexLines = generatedCode.vertexLines;
if (vertexLines.enabled) {
addVertexLinesToShader(shaderBuilder, vertexLines);
shaderBuilder.addVertexLines([
"#line 0",
customShader.vertexShaderText,
CustomShaderStageVS,
]);
}
const fragmentLines = generatedCode.fragmentLines;
if (fragmentLines.enabled) {
addFragmentLinesToShader(shaderBuilder, fragmentLines);
shaderBuilder.addFragmentLines([
"#line 0",
customShader.fragmentShaderText,
CustomShaderStageFS,
]);
}
}
// exposed for testing.
CustomShaderPipelineStage._oneTimeWarning = oneTimeWarning;
export default CustomShaderPipelineStage;