@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
658 lines (589 loc) • 25 kB
JavaScript
import Matrix3 from "../../Core/Matrix3.js";
import defined from "../../Core/defined.js";
import ShaderDestination from "../../Renderer/ShaderDestination.js";
import MetadataStageFS from "../../Shaders/Model/MetadataStageFS.js";
import MetadataStageVS from "../../Shaders/Model/MetadataStageVS.js";
import MetadataType from "../MetadataType.js";
import ModelUtility from "./ModelUtility.js";
/**
* The metadata pipeline stage processes metadata properties from
* EXT_structural_metadata and inserts them into a struct in the shader.
* This struct will be used by {@link CustomShaderPipelineStage} to allow the
* user to access metadata using {@link CustomShader}
*
* @namespace MetadataPipelineStage
*
* @private
*/
const MetadataPipelineStage = {
name: "MetadataPipelineStage",
STRUCT_ID_METADATA_VS: "MetadataVS",
STRUCT_ID_METADATA_FS: "MetadataFS",
STRUCT_NAME_METADATA: "Metadata",
STRUCT_ID_METADATA_CLASS_VS: "MetadataClassVS",
STRUCT_ID_METADATA_CLASS_FS: "MetadataClassFS",
STRUCT_NAME_METADATA_CLASS: "MetadataClass",
STRUCT_ID_METADATA_STATISTICS_VS: "MetadataStatisticsVS",
STRUCT_ID_METADATA_STATISTICS_FS: "MetadataStatisticsFS",
STRUCT_NAME_METADATA_STATISTICS: "MetadataStatistics",
FUNCTION_ID_INITIALIZE_METADATA_VS: "initializeMetadataVS",
FUNCTION_ID_INITIALIZE_METADATA_FS: "initializeMetadataFS",
FUNCTION_SIGNATURE_INITIALIZE_METADATA:
"void initializeMetadata(out Metadata metadata, out MetadataClass metadataClass, out MetadataStatistics metadataStatistics, ProcessedAttributes attributes)",
FUNCTION_ID_SET_METADATA_VARYINGS: "setMetadataVaryings",
FUNCTION_SIGNATURE_SET_METADATA_VARYINGS: "void setMetadataVaryings()",
// Metadata class and statistics fields:
// - some must be renamed to avoid reserved words
// - some always have float/vec values, even for integer/ivec property types
METADATA_CLASS_FIELDS: [
{ specName: "noData", shaderName: "noData" },
{ specName: "default", shaderName: "defaultValue" },
{ specName: "min", shaderName: "minValue" },
{ specName: "max", shaderName: "maxValue" },
],
METADATA_STATISTICS_FIELDS: [
{ specName: "min", shaderName: "minValue" },
{ specName: "max", shaderName: "maxValue" },
{ specName: "mean", shaderName: "mean", type: "float" },
{ specName: "median", shaderName: "median" },
{
specName: "standardDeviation",
shaderName: "standardDeviation",
type: "float",
},
{ specName: "variance", shaderName: "variance", type: "float" },
{ specName: "sum", shaderName: "sum" },
],
};
/**
* Process a primitive. This modifies the following parts of the render
* resources:
* <ul>
* <li>Adds a Metadata struct to the shader</li>
* <li>If the primitive has structural metadata, properties are added to the Metadata struct</li>
* <li>dynamic functions are added to the shader to initialize the metadata properties</li>
* <li>Adds uniforms for property textures to the uniform map as needed</li>
* <li>Adds uniforms for offset/scale to the uniform map as needed</li>
* </ul>
* @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
*/
MetadataPipelineStage.process = function (
renderResources,
primitive,
frameState,
) {
const { shaderBuilder, model } = renderResources;
const { structuralMetadata = {}, content } = model;
const statistics = content?.tileset.metadataExtension?.statistics;
const propertyAttributesInfo = getPropertyAttributesInfo(
structuralMetadata.propertyAttributes,
primitive,
statistics,
);
const propertyTexturesInfo = getPropertyTexturesInfo(
structuralMetadata.propertyTextures,
statistics,
);
// Declare <type>MetadataClass and <type>MetadataStatistics structs as needed
const allPropertyInfos = propertyAttributesInfo.concat(propertyTexturesInfo);
declareMetadataTypeStructs(shaderBuilder, allPropertyInfos);
// Always declare the Metadata, MetadataClass, and MetadataStatistics structs
// and the initializeMetadata() function, even if not used
declareStructsAndFunctions(shaderBuilder);
shaderBuilder.addVertexLines(MetadataStageVS);
shaderBuilder.addFragmentLines(MetadataStageFS);
for (let i = 0; i < propertyAttributesInfo.length; i++) {
const info = propertyAttributesInfo[i];
processPropertyAttributeProperty(renderResources, info);
}
for (let i = 0; i < propertyTexturesInfo.length; i++) {
const info = propertyTexturesInfo[i];
processPropertyTextureProperty(renderResources, info);
}
};
/**
* Collect info about all properties of all propertyAttributes, and
* return as a flattened Array
* @param {PropertyAttribute[]} propertyAttributes The PropertyAttributes with properties to be described
* @param {ModelComponents.Primitive} primitive The primitive to be rendered
* @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
* @returns {Object[]} An array of objects containing information about each PropertyAttributeProperty
* @private
*/
function getPropertyAttributesInfo(propertyAttributes, primitive, statistics) {
if (!defined(propertyAttributes)) {
return [];
}
return propertyAttributes.flatMap((propertyAttribute) =>
getPropertyAttributeInfo(propertyAttribute, primitive, statistics),
);
}
/**
* Collect info about the properties of a single PropertyAttribute
* @param {PropertyAttribute} propertyAttribute The PropertyAttribute with properties to be described
* @param {ModelComponents.Primitive} primitive The primitive to be rendered
* @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
* @returns {Object[]} An array of objects containing information about each PropertyAttributeProperty
* @private
*/
function getPropertyAttributeInfo(propertyAttribute, primitive, statistics) {
const { getAttributeByName, getAttributeInfo, sanitizeGlslIdentifier } =
ModelUtility;
const classId = propertyAttribute.class.id;
const classStatistics = statistics?.classes[classId];
const propertiesArray = Object.entries(propertyAttribute.properties);
const infoArray = new Array(propertiesArray.length);
for (let i = 0; i < propertiesArray.length; i++) {
const [propertyId, property] = propertiesArray[i];
const modelAttribute = getAttributeByName(primitive, property.attribute);
const { glslType, variableName } = getAttributeInfo(modelAttribute);
infoArray[i] = {
metadataVariable: sanitizeGlslIdentifier(propertyId),
property,
type: property.classProperty.type,
glslType,
variableName,
propertyStatistics: classStatistics?.properties[propertyId],
shaderDestination: ShaderDestination.BOTH,
};
}
return infoArray;
}
/**
* Collect info about all properties of all propertyTextures, and
* return as a flattened Array
* @param {PropertyTexture[]} propertyTextures The PropertyTextures with properties to be described
* @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
* @returns {Object[]} An array of objects containing information about each PropertyTextureProperty
* @private
*/
function getPropertyTexturesInfo(propertyTextures, statistics) {
if (!defined(propertyTextures)) {
return [];
}
return propertyTextures.flatMap((propertyTexture) =>
getPropertyTextureInfo(propertyTexture, statistics),
);
}
/**
* Collect info about the properties of a single PropertyTexture
* @param {PropertyTexture} propertyTexture The PropertyTexture with properties to be described
* @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
* @returns {Object[]} An array of objects containing information about each PropertyTextureProperty
* @private
*/
function getPropertyTextureInfo(propertyTexture, statistics) {
const { sanitizeGlslIdentifier } = ModelUtility;
const classId = propertyTexture.class.id;
const classStatistics = statistics?.classes[classId];
const propertiesArray = Object.entries(propertyTexture.properties).filter(
([id, property]) => property.isGpuCompatible(),
);
const infoArray = new Array(propertiesArray.length);
for (let i = 0; i < propertiesArray.length; i++) {
const [propertyId, property] = propertiesArray[i];
infoArray[i] = {
metadataVariable: sanitizeGlslIdentifier(propertyId),
property,
type: property.classProperty.type,
glslType: property.getGlslType(),
propertyStatistics: classStatistics?.properties[propertyId],
shaderDestination: ShaderDestination.FRAGMENT,
};
}
return infoArray;
}
/**
* Declare <type>MetadataClass structs in the shader for each PropertyAttributeProperty and PropertyTextureProperty
* @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
* @param {Object[]} propertyInfos Information about the PropertyAttributeProperties and PropertyTextureProperties
* @private
*/
function declareMetadataTypeStructs(shaderBuilder, propertyInfos) {
const classTypes = new Set();
const statisticsTypes = new Set();
for (let i = 0; i < propertyInfos.length; i++) {
const { type, glslType, propertyStatistics } = propertyInfos[i];
classTypes.add(glslType);
if (!defined(propertyStatistics)) {
continue;
}
if (type !== MetadataType.ENUM) {
statisticsTypes.add(glslType);
}
}
const classFields = MetadataPipelineStage.METADATA_CLASS_FIELDS;
for (const metadataType of classTypes) {
const classStructName = `${metadataType}MetadataClass`;
declareTypeStruct(classStructName, metadataType, classFields);
}
const statisticsFields = MetadataPipelineStage.METADATA_STATISTICS_FIELDS;
for (const metadataType of statisticsTypes) {
const statisticsStructName = `${metadataType}MetadataStatistics`;
declareTypeStruct(statisticsStructName, metadataType, statisticsFields);
}
function declareTypeStruct(structName, type, fields) {
shaderBuilder.addStruct(structName, structName, ShaderDestination.BOTH);
for (let i = 0; i < fields.length; i++) {
const { shaderName } = fields[i];
const shaderType =
fields[i].type === "float" ? convertToFloatComponents(type) : type;
shaderBuilder.addStructField(structName, shaderType, shaderName);
}
}
}
const floatConversions = {
int: "float",
ivec2: "vec2",
ivec3: "vec3",
ivec4: "vec4",
};
/**
* For a type with integer components, find a corresponding float-component type
* @param {string} type The name of a GLSL type with integer components
* @returns {string} The name of a GLSL type of the same dimension with float components, if available; otherwise the input type
* @private
*/
function convertToFloatComponents(type) {
const converted = floatConversions[type];
return defined(converted) ? converted : type;
}
/**
* Declare the main Metadata, MetadataClass, and MetadataStatistics structs
* and the initializeMetadata() function.
* @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
* @private
*/
function declareStructsAndFunctions(shaderBuilder) {
// Declare the Metadata struct.
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_VS,
MetadataPipelineStage.STRUCT_NAME_METADATA,
ShaderDestination.VERTEX,
);
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_FS,
MetadataPipelineStage.STRUCT_NAME_METADATA,
ShaderDestination.FRAGMENT,
);
// Declare the MetadataClass struct
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_VS,
MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
ShaderDestination.VERTEX,
);
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_FS,
MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
ShaderDestination.FRAGMENT,
);
// Declare the MetadataStatistics struct
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_VS,
MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
ShaderDestination.VERTEX,
);
shaderBuilder.addStruct(
MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_FS,
MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
ShaderDestination.FRAGMENT,
);
// declare the initializeMetadata() function. The details may differ
// between vertex and fragment shader
shaderBuilder.addFunction(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
ShaderDestination.VERTEX,
);
shaderBuilder.addFunction(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
ShaderDestination.FRAGMENT,
);
// declare the setMetadataVaryings() function in the vertex shader only.
shaderBuilder.addFunction(
MetadataPipelineStage.FUNCTION_ID_SET_METADATA_VARYINGS,
MetadataPipelineStage.FUNCTION_SIGNATURE_SET_METADATA_VARYINGS,
ShaderDestination.VERTEX,
);
}
/**
* Update the shader for a single PropertyAttributeProperty
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
* @param {object} propertyInfo Info about the PropertyAttributeProperty
* @private
*/
function processPropertyAttributeProperty(renderResources, propertyInfo) {
addPropertyAttributePropertyMetadata(renderResources, propertyInfo);
addPropertyMetadataClass(renderResources.shaderBuilder, propertyInfo);
addPropertyMetadataStatistics(renderResources.shaderBuilder, propertyInfo);
}
/**
* Add fields to the Metadata struct, and metadata value assignments to the
* initializeMetadata function, for a PropertyAttributeProperty
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
* @param {object} propertyInfo Info about the PropertyAttributeProperty
* @private
*/
function addPropertyAttributePropertyMetadata(renderResources, propertyInfo) {
const { shaderBuilder } = renderResources;
const { metadataVariable, property, glslType } = propertyInfo;
const valueExpression = addValueTransformUniforms({
valueExpression: `attributes.${propertyInfo.variableName}`,
renderResources: renderResources,
glslType: glslType,
metadataVariable: metadataVariable,
shaderDestination: ShaderDestination.BOTH,
property: property,
});
// declare the struct field
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_VS,
glslType,
metadataVariable,
);
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_FS,
glslType,
metadataVariable,
);
// assign the result to the metadata struct property.
const initializationLine = `metadata.${metadataVariable} = ${valueExpression};`;
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
[initializationLine],
);
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
[initializationLine],
);
}
/**
* Update the shader for a single PropertyTextureProperty
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
* @param {Object[]} propertyInfo Info about the PropertyTextureProperty
* @private
*/
function processPropertyTextureProperty(renderResources, propertyInfo) {
addPropertyTexturePropertyMetadata(renderResources, propertyInfo);
addPropertyMetadataClass(renderResources.shaderBuilder, propertyInfo);
addPropertyMetadataStatistics(renderResources.shaderBuilder, propertyInfo);
}
/**
* Add fields to the Metadata struct, and metadata value expressions to the
* initializeMetadata function, for a PropertyTextureProperty
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
* @param {object} propertyInfo Info about the PropertyTextureProperty
* @private
*/
function addPropertyTexturePropertyMetadata(renderResources, propertyInfo) {
const { shaderBuilder, uniformMap } = renderResources;
const { metadataVariable, glslType, property } = propertyInfo;
const { texCoord, channels, index, texture, transform } =
property.textureReader;
const textureUniformName = `u_propertyTexture_${index}`;
// Property texture properties may share the same physical texture, so only
// add the texture uniform the first time we encounter it.
if (!uniformMap.hasOwnProperty(textureUniformName)) {
shaderBuilder.addUniform(
"sampler2D",
textureUniformName,
ShaderDestination.FRAGMENT,
);
uniformMap[textureUniformName] = () => texture;
}
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_FS,
glslType,
metadataVariable,
);
// Get a GLSL expression for the texture coordinates of the property.
// By default, this will be taken directly from the attributes.
const texCoordVariable = `attributes.texCoord_${texCoord}`;
let texCoordVariableExpression = texCoordVariable;
// Check if the texture defines a `transform` from a `KHR_texture_transform`
if (defined(transform) && !Matrix3.equals(transform, Matrix3.IDENTITY)) {
// Add a uniform for the transformation matrix
const transformUniformName = `${textureUniformName}Transform`;
shaderBuilder.addUniform(
"mat3",
transformUniformName,
ShaderDestination.FRAGMENT,
);
uniformMap[transformUniformName] = function () {
return transform;
};
// Update the expression for the texture coordinates
// with one that transforms the texture coordinates
// with the transform matrix first
texCoordVariableExpression = `vec2(${transformUniformName} * vec3(${texCoordVariable}, 1.0))`;
}
const valueExpression = `texture(${textureUniformName}, ${texCoordVariableExpression}).${channels}`;
// Some types need an unpacking step or two. For example, since texture reads
// are always normalized, UINT8 (not normalized) properties need to be
// un-normalized in the shader.
const unpackedValue = property.unpackInShader(valueExpression);
const transformedValue = addValueTransformUniforms({
valueExpression: unpackedValue,
renderResources: renderResources,
glslType: glslType,
metadataVariable: metadataVariable,
shaderDestination: ShaderDestination.FRAGMENT,
property: property,
});
const initializationLine = `metadata.${metadataVariable} = ${transformedValue};`;
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
[initializationLine],
);
}
/**
* Add fields to the MetadataClass struct, and metadataClass value expressions
* to the initializeMetadata function, for a PropertyAttributeProperty or
* PropertyTextureProperty
* @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
* @param {object} propertyInfo Info about the PropertyAttributeProperty or PropertyTextureProperty
* @private
*/
function addPropertyMetadataClass(shaderBuilder, propertyInfo) {
const { classProperty } = propertyInfo.property;
const { metadataVariable, glslType, shaderDestination } = propertyInfo;
// Construct assignment statements to set values in the metadataClass struct
const assignments = getStructAssignments(
MetadataPipelineStage.METADATA_CLASS_FIELDS,
classProperty,
`metadataClass.${metadataVariable}`,
glslType,
);
// Struct field: Prefix to get the appropriate <type>MetadataClass struct
const metadataType = `${glslType}MetadataClass`;
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_FS,
metadataType,
metadataVariable,
);
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
assignments,
);
if (!ShaderDestination.includesVertexShader(shaderDestination)) {
return;
}
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_VS,
metadataType,
metadataVariable,
);
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
assignments,
);
}
/**
* Add fields to the MetadataStatistics struct, and metadataStatistics value
* expressions to the initializeMetadata function, for a
* PropertyAttributeProperty or PropertyTextureProperty
* @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
* @param {object} propertyInfo Info about the PropertyAttributeProperty or PropertyTextureProperty
* @private
*/
function addPropertyMetadataStatistics(shaderBuilder, propertyInfo) {
const { propertyStatistics } = propertyInfo;
if (!defined(propertyStatistics)) {
return;
}
const { metadataVariable, type, glslType } = propertyInfo;
if (type === MetadataType.ENUM) {
// enums have an "occurrences" statistic which is not implemented yet
return;
}
// Construct assignment statements to set values in the metadataStatistics struct
const fields = MetadataPipelineStage.METADATA_STATISTICS_FIELDS;
const struct = `metadataStatistics.${metadataVariable}`;
const assignments = getStructAssignments(
fields,
propertyStatistics,
struct,
glslType,
);
// Struct field: Prefix to get the appropriate <type>MetadataStatistics struct
const statisticsType = `${glslType}MetadataStatistics`;
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_FS,
statisticsType,
metadataVariable,
);
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
assignments,
);
if (!ShaderDestination.includesVertexShader(propertyInfo.shaderDestination)) {
return;
}
shaderBuilder.addStructField(
MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_VS,
statisticsType,
metadataVariable,
);
shaderBuilder.addFunctionLines(
MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
assignments,
);
}
/**
* Construct GLSL assignment statements to set metadata spec values in a struct
* @param {Object[]} fieldNames An object with the following properties:
* @param {string} fieldNames[].specName The name of the property in the spec
* @param {string} fieldNames[].shaderName The name of the property in the shader
* @param {object} values A source of property values, keyed on fieldNames[].specName
* @param {string} struct The name of the struct to which values will be assigned
* @param {string} type The type of the values to be assigned
* @returns {Array<{name: string, value: any}>} Objects containing the property name (in the shader) and a GLSL assignment statement for the property value
* @private
*/
function getStructAssignments(fieldNames, values, struct, type) {
function constructAssignment(field) {
const value = values[field.specName];
if (defined(value)) {
return `${struct}.${field.shaderName} = ${type}(${value});`;
}
}
return defined(values)
? fieldNames.map(constructAssignment).filter(defined)
: [];
}
/**
* Handle offset/scale transform for a property value
* This wraps the GLSL value expression with a czm_valueTransform() call
*
* @param {object} options Object with the following properties:
* @param {string} options.valueExpression The GLSL value expression without the transform
* @param {string} options.metadataVariable The name of the GLSL variable that will contain the property value
* @param {string} options.glslType The GLSL type of the variable
* @param {ShaderDestination} options.shaderDestination Which shader(s) use this variable
* @param {PrimitiveRenderResources} options.renderResources The render resources for this primitive
* @param {(PropertyAttributeProperty|PropertyTextureProperty)} options.property The property from which the value is derived
* @returns {string} A wrapped GLSL value expression
* @private
*/
function addValueTransformUniforms(options) {
const { valueExpression, property } = options;
if (!property.hasValueTransform) {
return valueExpression;
}
const metadataVariable = options.metadataVariable;
const offsetUniformName = `u_${metadataVariable}_offset`;
const scaleUniformName = `u_${metadataVariable}_scale`;
const { shaderBuilder, uniformMap } = options.renderResources;
const { glslType, shaderDestination } = options;
shaderBuilder.addUniform(glslType, offsetUniformName, shaderDestination);
shaderBuilder.addUniform(glslType, scaleUniformName, shaderDestination);
const { offset, scale } = property;
uniformMap[offsetUniformName] = () => offset;
uniformMap[scaleUniformName] = () => scale;
return `czm_valueTransform(${offsetUniformName}, ${scaleUniformName}, ${valueExpression})`;
}
export default MetadataPipelineStage;