diff --git a/CHANGES.md b/CHANGES.md
index ef25f25b74e5..5dac5ae228bf 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -19,6 +19,7 @@
- Added image-based lighting to `ModelExperimental`. [#10234](https://github.com/CesiumGS/cesium/pull/10234)
- Added a 'renderable' property to 'Fog' to disable its visual rendering while preserving tiles culling at a distance
- Refactored metadata API so `tileset.metadata` and `content.group.metadata` are more symmetric with `content.metadata` and `tile.metadata`. [#10224](https://github.com/CesiumGS/cesium/pull/10224)
+- Added support for `EXT_structural_metadata` property attributes in `CustomShader` [#10228](https://github.com/CesiumGS/cesium/pull/10228)
##### Fixes :wrench:
diff --git a/Documentation/CustomShaderGuide/README.md b/Documentation/CustomShaderGuide/README.md
index 88585c9d1a6e..fe087b289d47 100644
--- a/Documentation/CustomShaderGuide/README.md
+++ b/Documentation/CustomShaderGuide/README.md
@@ -201,7 +201,8 @@ struct VertexInput {
Attributes attributes;
// Feature IDs/Batch IDs. See the FeatureIds Struct section below.
FeatureIds featureIds;
- // In the future, metadata will be added here.
+ // Metadata properties. See the Metadata Struct section below.
+ Metadata metadata;
};
```
@@ -216,7 +217,8 @@ struct FragmentInput {
Attributes attributes;
// Feature IDs/Batch IDs. See the FeatureIds Struct section below.
FeatureIds featureIds;
- // In the future, metadata will be added here.
+ // Metadata properties. See the Metadata Struct section below.
+ Metadata metadata;
};
```
@@ -554,6 +556,123 @@ to the `EXT_feature_metadata` extension:
]
```
+## `Metadata` struct
+
+This struct contains the relevant metadata properties accessible to the
+model from the
+[`EXT_structural_metadata`](https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata)
+glTF extension (or the older
+[`EXT_feature_metadata`](https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_feature_metadata) extension).
+
+The following types of metadata are currently supported. We plan to add more
+in the near future:
+
+- property attributes from the `EXT_structural_metadata` glTF extension.
+
+Regardless of the source of metadata, the properties are collected into a single
+struct by property ID. For example, if the metadata class looked like this:
+
+```jsonc
+"schema": {
+ "classes": {
+ "wall": {
+ "properties": {
+ "temperature": {
+ "name": "Surface Temperature",
+ "type": "SCALAR",
+ "componentType": "FLOAT32"
+ }
+ }
+ }
+ }
+}
+```
+
+This will show up in the shader as the struct field as follows:
+
+```
+struct Metadata {
+ float temperature;
+}
+```
+
+Now the temperature can be accessed as `vsInput.metadata.temperature` or
+`fsInput.metadata.temperature`.
+
+### Normalized values
+
+If the class property specifies `normalized: true`, the property will appear
+in the shader as the appropriate floating point type (e.g. `float` or `vec3`).
+All components will be between the range of `[0, 1]` (unsigned) or `[-1, 1]`
+(signed).
+
+For example,
+
+```jsonc
+"schema": {
+ "classes": {
+ "wall": {
+ "properties": {
+ // damage normalized between 0.0 and 1.0 though stored as a UINT8 in
+ // the glTF
+ "damageAmount": {
+ "name": "Wall damage (normalized)",
+ "type": "SCALAR",
+ "componentType": "UINT32",
+ "normalized": true
+ }
+ }
+ }
+ }
+}
+```
+
+This will appear as a `float` value from 0.0 to 1.0, accessible via
+`(vsInput|fsInput).metadata.damageAmount`
+
+### Offset and scale
+
+If the property provides an `offset` or `scale`, this is automatically applied
+after normalization (when applicable). This is useful to pre-scale values into
+a convenient range.
+
+For example, consider taking a normalized temperature value and automatically
+converting this to Celsius or Fahrenheit:
+
+```jsonc
+"schema": {
+ "classes": {
+ "wall": {
+ "properties": {
+ // scaled to the range [0, 100] in °C
+ "temperatureCelsius": {
+ "name": "Temperature (°C)",
+ "type": "SCALAR",
+ "componentType": "UINT32",
+ "normalized": true,
+ // offset defaults to 0, scale defaults to 1
+ "scale": 100
+ },
+ // scaled/shifted to the range [32, 212] in °F
+ "temperatureFahrenheit": {
+ "name": "Temperature (°C)",
+ "type": "SCALAR",
+ "componentType": "UINT32",
+ "normalized": true,
+ "offset": 32,
+ "scale": 180
+ }
+ }
+ }
+ }
+}
+```
+
+In the shader, `(vsInput|fsInput).metadata.temperatureCelsius` will be a `float`
+with a value between 0.0 and 100.0, while
+`(vsInput|fsInput).metadata.temperatureFahrenheit` will be a `float` with a
+range of `[32.0, 212.0]`.
+
## `czm_modelVertexOutput` struct
This struct is built-in, see the [documentation comment](../../../Shaders/Builtin/Structs/modelVertexOutput.glsl).
diff --git a/Source/Renderer/ShaderBuilder.js b/Source/Renderer/ShaderBuilder.js
index f2a0cb3b80da..683e9f097427 100644
--- a/Source/Renderer/ShaderBuilder.js
+++ b/Source/Renderer/ShaderBuilder.js
@@ -353,7 +353,10 @@ ShaderBuilder.prototype.addAttribute = function (type, identifier) {
const location = this._nextAttributeLocation;
this._attributeLocations[identifier] = location;
- this._nextAttributeLocation++;
+
+ // Most attributes only require a single attribute location, but matrices
+ // require more.
+ this._nextAttributeLocation += getAttributeLocationCount(type);
return location;
};
@@ -519,6 +522,19 @@ function generateStructLines(shaderBuilder) {
};
}
+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 = [];
diff --git a/Source/Scene/AttributeType.js b/Source/Scene/AttributeType.js
index 083d0a9ab358..0cb63d6f8c9f 100644
--- a/Source/Scene/AttributeType.js
+++ b/Source/Scene/AttributeType.js
@@ -133,6 +133,35 @@ AttributeType.getNumberOfComponents = function (attributeType) {
}
};
+/**
+ * Get the number of attribute locations needed to fit this attribute. Most
+ * types require one, but matrices require multiple attribute locations.
+ *
+ * @param {AttributeType} attributeType The attribute type.
+ * @returns {Number} The number of attribute locations needed in the shader
+ *
+ * @private
+ */
+AttributeType.getAttributeLocationCount = function (attributeType) {
+ switch (attributeType) {
+ case AttributeType.SCALAR:
+ case AttributeType.VEC2:
+ case AttributeType.VEC3:
+ case AttributeType.VEC4:
+ return 1;
+ case AttributeType.MAT2:
+ return 2;
+ case AttributeType.MAT3:
+ return 3;
+ case AttributeType.MAT4:
+ return 4;
+ //>>includeStart('debug', pragmas.debug);
+ default:
+ throw new DeveloperError("attributeType is not a valid value.");
+ //>>includeEnd('debug');
+ }
+};
+
/**
* Gets the GLSL type for the attribute type.
*
diff --git a/Source/Scene/ModelExperimental/CustomShader.js b/Source/Scene/ModelExperimental/CustomShader.js
index 90e5566a447f..53edd0e2caa8 100644
--- a/Source/Scene/ModelExperimental/CustomShader.js
+++ b/Source/Scene/ModelExperimental/CustomShader.js
@@ -37,6 +37,7 @@ import TextureManager from "./TextureManager.js";
* @typedef {Object} VertexVariableSets
* @property {VariableSet} attributeSet A set of all unique attributes used in the vertex shader via the vsInput.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
*/
@@ -45,6 +46,7 @@ import TextureManager from "./TextureManager.js";
* @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
*/
@@ -212,6 +214,7 @@ export default function CustomShader(options) {
this.usedVariablesVertex = {
attributeSet: {},
featureIdSet: {},
+ metadataSet: {},
};
/**
* A collection of variables used in fragmentShaderText
. This
@@ -222,6 +225,7 @@ export default function CustomShader(options) {
this.usedVariablesFragment = {
attributeSet: {},
featureIdSet: {},
+ metadataSet: {},
materialSet: {},
};
@@ -291,6 +295,7 @@ function getVariables(shaderText, regex, outputSet) {
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;
@@ -300,6 +305,9 @@ function findUsedVariables(customShader) {
attributeSet = customShader.usedVariablesVertex.featureIdSet;
getVariables(vertexShaderText, featureIdRegex, attributeSet);
+
+ attributeSet = customShader.usedVariablesVertex.metadataSet;
+ getVariables(vertexShaderText, metadataRegex, attributeSet);
}
const fragmentShaderText = customShader.fragmentShaderText;
@@ -310,6 +318,9 @@ function findUsedVariables(customShader) {
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);
diff --git a/Source/Scene/ModelExperimental/CustomShaderPipelineStage.js b/Source/Scene/ModelExperimental/CustomShaderPipelineStage.js
index 6cb30213c75f..0f33c0188700 100644
--- a/Source/Scene/ModelExperimental/CustomShaderPipelineStage.js
+++ b/Source/Scene/ModelExperimental/CustomShaderPipelineStage.js
@@ -8,6 +8,7 @@ import CustomShaderStageFS from "../../Shaders/ModelExperimental/CustomShaderSta
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";
/**
@@ -512,6 +513,12 @@ function addVertexLinesToShader(shaderBuilder, vertexLines) {
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;
@@ -562,6 +569,12 @@ function addFragmentLinesToShader(shaderBuilder, fragmentLines) {
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;
diff --git a/Source/Scene/ModelExperimental/GeometryPipelineStage.js b/Source/Scene/ModelExperimental/GeometryPipelineStage.js
index 18ec75e5ae1c..bd9c4aa86348 100644
--- a/Source/Scene/ModelExperimental/GeometryPipelineStage.js
+++ b/Source/Scene/ModelExperimental/GeometryPipelineStage.js
@@ -1,4 +1,5 @@
import defined from "../../Core/defined.js";
+import ComponentDatatype from "../../Core/ComponentDatatype.js";
import PrimitiveType from "../../Core/PrimitiveType.js";
import AttributeType from "../AttributeType.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
@@ -110,16 +111,32 @@ GeometryPipelineStage.process = function (renderResources, primitive) {
ShaderDestination.FRAGMENT
);
- let index;
+ // .pnts point clouds store sRGB color rather than linear color
+ const modelType = renderResources.model.type;
+ if (modelType === ModelExperimentalType.TILE_PNTS) {
+ shaderBuilder.addDefine(
+ "HAS_SRGB_COLOR",
+ undefined,
+ ShaderDestination.FRAGMENT
+ );
+ }
+
for (let i = 0; i < primitive.attributes.length; i++) {
const attribute = primitive.attributes[i];
- if (attribute.semantic === VertexAttributeSemantic.POSITION) {
+ const attributeLocationCount = AttributeType.getAttributeLocationCount(
+ attribute.type
+ );
+
+ let index;
+ if (attributeLocationCount > 1) {
+ index = renderResources.attributeIndex;
+ renderResources.attributeIndex += attributeLocationCount;
+ } else if (attribute.semantic === VertexAttributeSemantic.POSITION) {
index = 0;
} else {
- // The attribute index is taken from the node render resources, which may have added some attributes of its own.
index = renderResources.attributeIndex++;
}
- processAttribute(renderResources, attribute, index);
+ processAttribute(renderResources, attribute, index, attributeLocationCount);
}
handleBitangents(shaderBuilder, primitive.attributes);
@@ -132,11 +149,26 @@ GeometryPipelineStage.process = function (renderResources, primitive) {
shaderBuilder.addFragmentLines([GeometryStageFS]);
};
-function processAttribute(renderResources, attribute, attributeIndex) {
+function processAttribute(
+ renderResources,
+ attribute,
+ attributeIndex,
+ attributeLocationCount
+) {
const shaderBuilder = renderResources.shaderBuilder;
const attributeInfo = ModelExperimentalUtility.getAttributeInfo(attribute);
- addAttributeToRenderResources(renderResources, attribute, attributeIndex);
+ if (attributeLocationCount > 1) {
+ // matrices are stored as multiple attributes, one per column vector.
+ addMatrixAttributeToRenderResources(
+ renderResources,
+ attribute,
+ attributeIndex,
+ attributeLocationCount
+ );
+ } else {
+ addAttributeToRenderResources(renderResources, attribute, attributeIndex);
+ }
addAttributeDeclaration(shaderBuilder, attributeInfo);
addVaryingDeclaration(shaderBuilder, attributeInfo);
@@ -146,16 +178,6 @@ function processAttribute(renderResources, attribute, attributeIndex) {
addSemanticDefine(shaderBuilder, attribute);
}
- // .pnts point clouds store sRGB color rather than linear color
- const modelType = renderResources.model.type;
- if (modelType === ModelExperimentalType.TILE_PNTS) {
- shaderBuilder.addDefine(
- "HAS_SRGB_COLOR",
- undefined,
- ShaderDestination.FRAGMENT
- );
- }
-
// Some GLSL code must be dynamically generated
updateAttributesStruct(shaderBuilder, attributeInfo);
updateInitialzeAttributesFunction(shaderBuilder, attributeInfo);
@@ -222,6 +244,58 @@ function addAttributeToRenderResources(
renderResources.attributes.push(vertexAttribute);
}
+function addMatrixAttributeToRenderResources(
+ renderResources,
+ attribute,
+ attributeIndex,
+ columnCount
+) {
+ const quantization = attribute.quantization;
+ let type;
+ let componentDatatype;
+ if (defined(quantization)) {
+ type = quantization.type;
+ componentDatatype = quantization.componentDatatype;
+ } else {
+ type = attribute.type;
+ componentDatatype = attribute.componentDatatype;
+ }
+
+ const normalized = attribute.normalized;
+
+ // componentCount is either 4, 9 or 16
+ const componentCount = AttributeType.getNumberOfComponents(type);
+ // componentsPerColumn is either 2, 3, or 4
+ const componentsPerColumn = componentCount / columnCount;
+
+ const componentSizeInBytes = ComponentDatatype.getSizeInBytes(
+ componentDatatype
+ );
+
+ const columnLengthInBytes = componentsPerColumn * componentSizeInBytes;
+
+ // The stride between corresponding columns of two matrices is constant
+ // regardless of where you start
+ const strideInBytes = attribute.byteStride;
+
+ for (let i = 0; i < columnCount; i++) {
+ const offsetInBytes = attribute.byteOffset + i * columnLengthInBytes;
+
+ // upload a single column vector.
+ const columnAttribute = {
+ index: attributeIndex + i,
+ vertexBuffer: attribute.buffer,
+ componentsPerAttribute: componentsPerColumn,
+ componentDatatype: componentDatatype,
+ offsetInBytes: offsetInBytes,
+ strideInBytes: strideInBytes,
+ normalize: normalized,
+ };
+
+ renderResources.attributes.push(columnAttribute);
+ }
+}
+
function addVaryingDeclaration(shaderBuilder, attributeInfo) {
const variableName = attributeInfo.variableName;
let varyingName = `v_${variableName}`;
diff --git a/Source/Scene/ModelExperimental/InstancingPipelineStage.js b/Source/Scene/ModelExperimental/InstancingPipelineStage.js
index 1b47b1475b26..e6a227833450 100644
--- a/Source/Scene/ModelExperimental/InstancingPipelineStage.js
+++ b/Source/Scene/ModelExperimental/InstancingPipelineStage.js
@@ -352,6 +352,7 @@ function processMatrixAttributes(node, count, renderResources, frameState) {
const componentByteSize = ComponentDatatype.getSizeInBytes(
ComponentDatatype.FLOAT
);
+ const strideInBytes = componentByteSize * vertexSizeInFloats;
const instancingVertexAttributes = [
{
@@ -361,7 +362,7 @@ function processMatrixAttributes(node, count, renderResources, frameState) {
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: 0,
- strideInBytes: componentByteSize * vertexSizeInFloats,
+ strideInBytes: strideInBytes,
instanceDivisor: 1,
},
{
@@ -371,7 +372,7 @@ function processMatrixAttributes(node, count, renderResources, frameState) {
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: componentByteSize * 4,
- strideInBytes: componentByteSize * vertexSizeInFloats,
+ strideInBytes: strideInBytes,
instanceDivisor: 1,
},
{
@@ -381,7 +382,7 @@ function processMatrixAttributes(node, count, renderResources, frameState) {
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: componentByteSize * 8,
- strideInBytes: componentByteSize * vertexSizeInFloats,
+ strideInBytes: strideInBytes,
instanceDivisor: 1,
},
];
diff --git a/Source/Scene/ModelExperimental/MetadataPipelineStage.js b/Source/Scene/ModelExperimental/MetadataPipelineStage.js
new file mode 100644
index 000000000000..77fba78ca236
--- /dev/null
+++ b/Source/Scene/ModelExperimental/MetadataPipelineStage.js
@@ -0,0 +1,204 @@
+import defined from "../../Core/defined.js";
+import ShaderDestination from "../../Renderer/ShaderDestination.js";
+import MetadataStageFS from "../../Shaders/ModelExperimental/MetadataStageFS.js";
+import MetadataStageVS from "../../Shaders/ModelExperimental/MetadataStageVS.js";
+import ModelExperimentalUtility from "./ModelExperimentalUtility.js";
+
+const MetadataPipelineStage = {};
+MetadataPipelineStage.name = "MetadataPipelineStage";
+
+MetadataPipelineStage.STRUCT_ID_METADATA_VS = "MetadataVS";
+MetadataPipelineStage.STRUCT_ID_METADATA_FS = "MetadataFS";
+MetadataPipelineStage.STRUCT_NAME_METADATA = "Metadata";
+MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS =
+ "initializeMetadataVS";
+MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS =
+ "initializeMetadataFS";
+MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA =
+ "void initializeMetadata(out Metadata metadata, ProcessedAttributes attributes)";
+MetadataPipelineStage.FUNCTION_ID_SET_METADATA_VARYINGS = "setMetadataVaryings";
+MetadataPipelineStage.FUNCTION_SIGNATURE_SET_METADATA_VARYINGS =
+ "void setMetadataVaryings()";
+
+MetadataPipelineStage.process = function (
+ renderResources,
+ primitive,
+ frameState
+) {
+ const shaderBuilder = renderResources.shaderBuilder;
+
+ // Always declare structs, even if not used
+ declareStructsAndFunctions(shaderBuilder);
+ shaderBuilder.addVertexLines([MetadataStageVS]);
+ shaderBuilder.addFragmentLines([MetadataStageFS]);
+
+ const structuralMetadata = renderResources.model.structuralMetadata;
+ if (!defined(structuralMetadata)) {
+ return;
+ }
+
+ processPropertyAttributes(renderResources, primitive, structuralMetadata);
+};
+
+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 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
+ );
+}
+
+function processPropertyAttributes(
+ renderResources,
+ primitive,
+ structuralMetadata
+) {
+ const propertyAttributes = structuralMetadata.propertyAttributes;
+
+ if (!defined(propertyAttributes)) {
+ return;
+ }
+
+ for (let i = 0; i < propertyAttributes.length; i++) {
+ const propertyAttribute = propertyAttributes[i];
+ const properties = propertyAttribute.properties;
+ for (const propertyId in properties) {
+ if (properties.hasOwnProperty(propertyId)) {
+ const property = properties[propertyId];
+
+ // Get information about the attribute the same way as the
+ // GeometryPipelineStage to ensure we have the correct GLSL type and
+ // variable name.
+ const modelAttribute = ModelExperimentalUtility.getAttributeByName(
+ primitive,
+ property.attribute
+ );
+ const attributeInfo = ModelExperimentalUtility.getAttributeInfo(
+ modelAttribute
+ );
+
+ addPropertyAttributeProperty(
+ renderResources,
+ attributeInfo,
+ propertyId,
+ property
+ );
+ }
+ }
+ }
+}
+
+function addPropertyAttributeProperty(
+ renderResources,
+ attributeInfo,
+ propertyId,
+ property
+) {
+ const metadataVariable = sanitizeGlslIdentifier(propertyId);
+ const attributeVariable = attributeInfo.variableName;
+
+ // in WebGL 1, attributes must have floating point components, so it's safe
+ // to assume here that the types will match. Even if the property was
+ // normalized, this is handled at upload time, not in the shader.
+ const glslType = attributeInfo.glslType;
+
+ const shaderBuilder = renderResources.shaderBuilder;
+
+ // declare the struct field, e.g.
+ // struct Metadata {
+ // float property;
+ // }
+ shaderBuilder.addStructField(
+ MetadataPipelineStage.STRUCT_ID_METADATA_VS,
+ glslType,
+ metadataVariable
+ );
+ shaderBuilder.addStructField(
+ MetadataPipelineStage.STRUCT_ID_METADATA_FS,
+ glslType,
+ metadataVariable
+ );
+
+ let unpackedValue = `attributes.${attributeVariable}`;
+
+ // handle offset/scale transform. This wraps the GLSL expression with
+ // the czm_valueTransform() call.
+ if (property.hasValueTransform) {
+ unpackedValue = addValueTransformUniforms(unpackedValue, {
+ renderResources: renderResources,
+ glslType: glslType,
+ metadataVariable: metadataVariable,
+ shaderDestination: ShaderDestination.BOTH,
+ offset: property.offset,
+ scale: property.scale,
+ });
+ }
+
+ // assign the result to the metadata struct property.
+ // e.g. metadata.property = unpackingSteps(attributes.property);
+ const initializationLine = `metadata.${metadataVariable} = ${unpackedValue};`;
+ shaderBuilder.addFunctionLines(
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
+ [initializationLine]
+ );
+ shaderBuilder.addFunctionLines(
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
+ [initializationLine]
+ );
+}
+
+function addValueTransformUniforms(valueExpression, options) {
+ const metadataVariable = options.metadataVariable;
+ const offsetUniformName = `u_${metadataVariable}_offset`;
+ const scaleUniformName = `u_${metadataVariable}_scale`;
+
+ const renderResources = options.renderResources;
+ const shaderBuilder = renderResources.shaderBuilder;
+ const glslType = options.glslType;
+ shaderBuilder.addUniform(glslType, offsetUniformName, ShaderDestination.BOTH);
+ shaderBuilder.addUniform(glslType, scaleUniformName, ShaderDestination.BOTH);
+
+ const uniformMap = renderResources.uniformMap;
+ uniformMap[offsetUniformName] = function () {
+ return options.offset;
+ };
+ uniformMap[scaleUniformName] = function () {
+ return options.scale;
+ };
+
+ return `czm_valueTransform(${offsetUniformName}, ${scaleUniformName}, ${valueExpression})`;
+}
+
+function sanitizeGlslIdentifier(identifier) {
+ // for use in the shader, the property ID must be a valid GLSL identifier,
+ // so replace invalid characters with _
+ return identifier.replaceAll(/[^_a-zA-Z0-9]+/g, "_");
+}
+
+export default MetadataPipelineStage;
diff --git a/Source/Scene/ModelExperimental/ModelExperimental.js b/Source/Scene/ModelExperimental/ModelExperimental.js
index e685bde6fad2..e48c1093b113 100644
--- a/Source/Scene/ModelExperimental/ModelExperimental.js
+++ b/Source/Scene/ModelExperimental/ModelExperimental.js
@@ -483,6 +483,21 @@ Object.defineProperties(ModelExperimental.prototype, {
},
},
+ /**
+ * The structural metadata from the EXT_structural_metadata extension
+ *
+ * @memberof ModelExperimental.prototype
+ *
+ * @type {StructuralMetadata}
+ * @readonly
+ * @private
+ */
+ structuralMetadata: {
+ get: function () {
+ return this._sceneGraph.components.structuralMetadata;
+ },
+ },
+
/**
* The ID for the feature table to use for picking and styling in this model.
*
diff --git a/Source/Scene/ModelExperimental/ModelExperimentalPrimitive.js b/Source/Scene/ModelExperimental/ModelExperimentalPrimitive.js
index eae1b5308724..cc932e28d5d9 100644
--- a/Source/Scene/ModelExperimental/ModelExperimentalPrimitive.js
+++ b/Source/Scene/ModelExperimental/ModelExperimentalPrimitive.js
@@ -12,6 +12,7 @@ import DequantizationPipelineStage from "./DequantizationPipelineStage.js";
import GeometryPipelineStage from "./GeometryPipelineStage.js";
import LightingPipelineStage from "./LightingPipelineStage.js";
import MaterialPipelineStage from "./MaterialPipelineStage.js";
+import MetadataPipelineStage from "./MetadataPipelineStage.js";
import ModelExperimentalUtility from "./ModelExperimentalUtility.js";
import PickingPipelineStage from "./PickingPipelineStage.js";
import PointCloudAttenuationPipelineStage from "./PointCloudAttenuationPipelineStage.js";
@@ -151,7 +152,10 @@ ModelExperimentalPrimitive.prototype.configurePipeline = function () {
pipelineStages.push(MaterialPipelineStage);
}
+ // These stages are always run to ensure structs
+ // are declared to avoid compilation errors.
pipelineStages.push(FeatureIdPipelineStage);
+ pipelineStages.push(MetadataPipelineStage);
if (featureIdFlags.hasPropertyTable) {
pipelineStages.push(SelectedFeatureIdPipelineStage);
diff --git a/Source/Scene/ModelExperimental/ModelExperimentalUtility.js b/Source/Scene/ModelExperimental/ModelExperimentalUtility.js
index 311e414ba925..6a6fddb039cd 100644
--- a/Source/Scene/ModelExperimental/ModelExperimentalUtility.js
+++ b/Source/Scene/ModelExperimental/ModelExperimentalUtility.js
@@ -83,6 +83,27 @@ ModelExperimentalUtility.getAttributeBySemantic = function (
}
};
+/**
+ * Similar to getAttributeBySemantic, but search using the name field only,
+ * as custom attributes do not have a semantic.
+ *
+ * @param {ModelComponents.Primitive|ModelComponents.Instances} object The primitive components or instances object
+ * @param {String} name The name of the attribute as it appears in the model file.
+ * @return {ModelComponents.Attribute} The selected attribute, or undefined if not found.
+ *
+ * @private
+ */
+ModelExperimentalUtility.getAttributeByName = function (object, name) {
+ const attributes = object.attributes;
+ const attributesLength = attributes.length;
+ for (let i = 0; i < attributesLength; ++i) {
+ const attribute = attributes[i];
+ if (attribute.name === name) {
+ return attribute;
+ }
+ }
+};
+
/**
* Find a feature ID from an array with label or positionalLabel matching the
* given label
diff --git a/Source/Scene/PropertyAttribute.js b/Source/Scene/PropertyAttribute.js
index 14e8db3dcc83..2479bf3a5306 100644
--- a/Source/Scene/PropertyAttribute.js
+++ b/Source/Scene/PropertyAttribute.js
@@ -57,6 +57,7 @@ Object.defineProperties(PropertyAttribute.prototype, {
* A human-readable name for this attribute
*
* @memberof PropertyAttribute.prototype
+ *
* @type {String}
* @readonly
* @private
@@ -70,6 +71,7 @@ Object.defineProperties(PropertyAttribute.prototype, {
* An identifier for this attribute. Useful for debugging.
*
* @memberof PropertyAttribute.prototype
+ *
* @type {String|Number}
* @readonly
* @private
@@ -83,6 +85,7 @@ Object.defineProperties(PropertyAttribute.prototype, {
* The class that properties conform to.
*
* @memberof PropertyAttribute.prototype
+ *
* @type {MetadataClass}
* @readonly
* @private
@@ -93,10 +96,26 @@ Object.defineProperties(PropertyAttribute.prototype, {
},
},
+ /**
+ * The properties in this property attribute.
+ *
+ * @memberof PropertyAttribute.prototype
+ *
+ * @type {PropertyAttributeProperty}
+ * @readonly
+ * @private
+ */
+ properties: {
+ get: function () {
+ return this._properties;
+ },
+ },
+
/**
* Extras in the JSON object.
*
* @memberof PropertyAttribute.prototype
+ *
* @type {*}
* @readonly
* @private
@@ -111,6 +130,7 @@ Object.defineProperties(PropertyAttribute.prototype, {
* Extensions in the JSON object.
*
* @memberof PropertyAttribute.prototype
+ *
* @type {Object}
* @readonly
* @private
diff --git a/Source/Scene/PropertyAttributeProperty.js b/Source/Scene/PropertyAttributeProperty.js
index 3572e0040a4c..15cb0000efd4 100644
--- a/Source/Scene/PropertyAttributeProperty.js
+++ b/Source/Scene/PropertyAttributeProperty.js
@@ -1,5 +1,6 @@
import Check from "../Core/Check.js";
import defaultValue from "../Core/defaultValue.js";
+import defined from "../Core/defined.js";
/**
* A property in a property attribute from EXT_structural_metadata.
@@ -29,11 +30,33 @@ export default function PropertyAttributeProperty(options) {
//>>includeEnd('debug');
this._attribute = property.attribute;
- this._offset = property.offset;
- this._scale = property.scale;
+ this._classProperty = classProperty;
this._min = property.min;
this._max = property.max;
+ let offset = property.offset;
+ let scale = property.scale;
+
+ // This needs to be set before handling default values
+ const hasValueTransform =
+ classProperty.hasValueTransform || defined(offset) || defined(scale);
+
+ // If the property attribute does not define an offset/scale, it inherits from
+ // the class property. The class property handles setting the default of
+ // identity: (offset 0, scale 1) with the same scalar/vector/matrix types.
+ // array types are disallowed by the spec.
+ offset = defaultValue(offset, classProperty.offset);
+ scale = defaultValue(scale, classProperty.scale);
+
+ // offset and scale are applied on the GPU, so unpack the values
+ // as math types we can use in uniform callbacks.
+ offset = classProperty.unpackVectorAndMatrixTypes(offset);
+ scale = classProperty.unpackVectorAndMatrixTypes(scale);
+
+ this._offset = offset;
+ this._scale = scale;
+ this._hasValueTransform = hasValueTransform;
+
this._extras = property.extras;
this._extensions = property.extensions;
}
@@ -53,6 +76,49 @@ Object.defineProperties(PropertyAttributeProperty.prototype, {
},
},
+ /**
+ * True if offset/scale should be applied. If both offset/scale were
+ * undefined, they default to identity so this property is set false
+ *
+ * @memberof MetadataClassProperty.prototype
+ * @type {Boolean}
+ * @readonly
+ * @private
+ */
+ hasValueTransform: {
+ get: function () {
+ return this._hasValueTransform;
+ },
+ },
+
+ /**
+ * The offset to be added to property values as part of the value transform.
+ *
+ * @memberof MetadataClassProperty.prototype
+ * @type {Number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4}
+ * @readonly
+ * @private
+ */
+ offset: {
+ get: function () {
+ return this._offset;
+ },
+ },
+
+ /**
+ * The scale to be multiplied to property values as part of the value transform.
+ *
+ * @memberof MetadataClassProperty.prototype
+ * @type {Number|Cartesian2|Cartesian3|Cartesian4|Matrix2|Matrix3|Matrix4}
+ * @readonly
+ * @private
+ */
+ scale: {
+ get: function () {
+ return this._scale;
+ },
+ },
+
/**
* Extras in the JSON object.
*
diff --git a/Source/Shaders/Builtin/Functions/unpackUint.glsl b/Source/Shaders/Builtin/Functions/unpackUint.glsl
index 15597f6d096c..d2e6f5bb2946 100644
--- a/Source/Shaders/Builtin/Functions/unpackUint.glsl
+++ b/Source/Shaders/Builtin/Functions/unpackUint.glsl
@@ -8,7 +8,7 @@
*
* @param {float|vec2|vec3|vec4} packed The packed value. For vectors, the components are listed in little-endian order.
*
- * @param {int} The unpacked value.
+ * @return {int} The unpacked value.
*/
int czm_unpackUint(float packedValue) {
float rounded = czm_round(packedValue * 255.0);
diff --git a/Source/Shaders/Builtin/Functions/valueTransform.glsl b/Source/Shaders/Builtin/Functions/valueTransform.glsl
new file mode 100644
index 000000000000..f81619cae6ac
--- /dev/null
+++ b/Source/Shaders/Builtin/Functions/valueTransform.glsl
@@ -0,0 +1,38 @@
+/**
+ * Transform metadata values following the EXT_structural_metadata spec
+ * by multiplying by scale and adding the offset. Operations are always
+ * performed component-wise, even for matrices.
+ *
+ * @param {float|vec2|vec3|vec4|mat2|mat3|mat4} offset The offset to add
+ * @param {float|vec2|vec3|vec4|mat2|mat3|mat4} scale The scale factor to multiply
+ * @param {float|vec2|vec3|vec4|mat2|mat3|mat4} value The original value.
+ *
+ * @return {float|vec2|vec3|vec4|mat2|mat3|mat4} The transformed value of the same scalar/vector/matrix type as the input.
+ */
+float czm_valueTransform(float offset, float scale, float value) {
+ return scale * value + offset;
+}
+
+vec2 czm_valueTransform(vec2 offset, vec2 scale, vec2 value) {
+ return scale * value + offset;
+}
+
+vec3 czm_valueTransform(vec3 offset, vec3 scale, vec3 value) {
+ return scale * value + offset;
+}
+
+vec4 czm_valueTransform(vec4 offset, vec4 scale, vec4 value) {
+ return scale * value + offset;
+}
+
+mat2 czm_valueTransform(mat2 offset, mat2 scale, mat2 value) {
+ return matrixCompMult(scale, value) + offset;
+}
+
+mat3 czm_valueTransform(mat3 offset, mat3 scale, mat3 value) {
+ return matrixCompMult(scale, value) + offset;
+}
+
+mat4 czm_valueTransform(mat4 offset, mat4 scale, mat4 value) {
+ return matrixCompMult(scale, value) + offset;
+}
diff --git a/Source/Shaders/ModelExperimental/CustomShaderStageFS.glsl b/Source/Shaders/ModelExperimental/CustomShaderStageFS.glsl
index b821a14bf71a..088b2c3587a6 100644
--- a/Source/Shaders/ModelExperimental/CustomShaderStageFS.glsl
+++ b/Source/Shaders/ModelExperimental/CustomShaderStageFS.glsl
@@ -1,12 +1,14 @@
void customShaderStage(
inout czm_modelMaterial material,
ProcessedAttributes attributes,
- FeatureIds featureIds
+ FeatureIds featureIds,
+ Metadata metadata
) {
// FragmentInput and initializeInputStruct() are dynamically generated in JS,
// see CustomShaderPipelineStage.js
FragmentInput fsInput;
initializeInputStruct(fsInput, attributes);
fsInput.featureIds = featureIds;
+ fsInput.metadata = metadata;
fragmentMain(fsInput, material);
}
diff --git a/Source/Shaders/ModelExperimental/CustomShaderStageVS.glsl b/Source/Shaders/ModelExperimental/CustomShaderStageVS.glsl
index 6dddb31328d4..2e30630259e7 100644
--- a/Source/Shaders/ModelExperimental/CustomShaderStageVS.glsl
+++ b/Source/Shaders/ModelExperimental/CustomShaderStageVS.glsl
@@ -1,13 +1,15 @@
void customShaderStage(
inout czm_modelVertexOutput vsOutput,
inout ProcessedAttributes attributes,
- FeatureIds featureIds
+ FeatureIds featureIds,
+ Metadata metadata
) {
// VertexInput and initializeInputStruct() are dynamically generated in JS,
// see CustomShaderPipelineStage.js
VertexInput vsInput;
initializeInputStruct(vsInput, attributes);
vsInput.featureIds = featureIds;
+ vsInput.metadata = metadata;
vertexMain(vsInput, vsOutput);
attributes.positionMC = vsOutput.positionMC;
}
diff --git a/Source/Shaders/ModelExperimental/MetadataStageFS.glsl b/Source/Shaders/ModelExperimental/MetadataStageFS.glsl
new file mode 100644
index 000000000000..0441808760e8
--- /dev/null
+++ b/Source/Shaders/ModelExperimental/MetadataStageFS.glsl
@@ -0,0 +1,4 @@
+void metadataStage(out Metadata metadata, ProcessedAttributes attributes)
+{
+ initializeMetadata(metadata, attributes);
+}
diff --git a/Source/Shaders/ModelExperimental/MetadataStageVS.glsl b/Source/Shaders/ModelExperimental/MetadataStageVS.glsl
new file mode 100644
index 000000000000..a618d9e61417
--- /dev/null
+++ b/Source/Shaders/ModelExperimental/MetadataStageVS.glsl
@@ -0,0 +1,5 @@
+void metadataStage(out Metadata metadata, ProcessedAttributes attributes)
+{
+ initializeMetadata(metadata, attributes);
+ setMetadataVaryings();
+}
diff --git a/Source/Shaders/ModelExperimental/ModelExperimentalFS.glsl b/Source/Shaders/ModelExperimental/ModelExperimentalFS.glsl
index 949eb1aff3ab..949df7075980 100644
--- a/Source/Shaders/ModelExperimental/ModelExperimentalFS.glsl
+++ b/Source/Shaders/ModelExperimental/ModelExperimentalFS.glsl
@@ -47,6 +47,9 @@ void main()
FeatureIds featureIds;
featureIdStage(featureIds, attributes);
+ Metadata metadata;
+ metadataStage(metadata, attributes);
+
#ifdef HAS_SELECTED_FEATURE_ID
selectedFeatureIdStage(selectedFeature, featureIds);
#endif
@@ -56,7 +59,7 @@ void main()
#endif
#ifdef HAS_CUSTOM_FRAGMENT_SHADER
- customShaderStage(material, attributes, featureIds);
+ customShaderStage(material, attributes, featureIds, metadata);
#endif
lightingStage(material, attributes);
diff --git a/Source/Shaders/ModelExperimental/ModelExperimentalVS.glsl b/Source/Shaders/ModelExperimental/ModelExperimentalVS.glsl
index dcef29807a06..da94fdf8c515 100644
--- a/Source/Shaders/ModelExperimental/ModelExperimentalVS.glsl
+++ b/Source/Shaders/ModelExperimental/ModelExperimentalVS.glsl
@@ -58,9 +58,12 @@ void main()
#endif
+ Metadata metadata;
+ metadataStage(metadata, attributes);
+
#ifdef HAS_CUSTOM_VERTEX_SHADER
czm_modelVertexOutput vsOutput = defaultVertexOutput(attributes.positionMC);
- customShaderStage(vsOutput, attributes, featureIds);
+ customShaderStage(vsOutput, attributes, featureIds, metadata);
#endif
// Compute the final position in each coordinate system needed.
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/README.md b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/README.md
new file mode 100644
index 000000000000..ded6ce7b3fa4
--- /dev/null
+++ b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/README.md
@@ -0,0 +1,25 @@
+# Box Textured with Property Attributes
+
+This is a variation on BoxTextured that adds EXT_structural_metadata to put matrices and normalized vectors at each vertex. This model is quite contrived; it was designed for unit testing only.
+
+## Screenshot
+
+![screenshot](screenshot/screenshot.png)
+
+## License Information
+
+Donated by Cesium for glTF testing. Please follow the [Cesium Trademark Terms and Conditions](https://github.com/AnalyticalGraphicsInc/cesium/wiki/CesiumTrademark.pdf).
+
+This model is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
+
+## Metadata Generation Code
+
+The metadata buffer was generated by the following Python 3 script. The only dependency is NumPy for easy exporting of matrices.
+
+In the `glTF` subdirectory:
+
+```
+python3 make_box_textured_metadata.py
+```
+
+will generate `metadata.bin`.
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTextured0.bin b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTextured0.bin
new file mode 100644
index 000000000000..d2a73551f945
Binary files /dev/null and b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTextured0.bin differ
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTexturedWithPropertyAttributes.gltf b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTexturedWithPropertyAttributes.gltf
new file mode 100644
index 000000000000..1445febbd7ef
--- /dev/null
+++ b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTexturedWithPropertyAttributes.gltf
@@ -0,0 +1,278 @@
+{
+ "asset": {
+ "generator": "COLLADA2GLTF with hand edits",
+ "version": "2.0"
+ },
+ "extensionsUsed": [
+ "EXT_structural_metadata"
+ ],
+ "extensions": {
+ "EXT_structural_metadata": {
+ "schema": {
+ "classes": {
+ "warpedBox": {
+ "properties": {
+ "warpMatrix": {
+ "description": "Scaling matrices at every vertex for scaling texture coordinates in a shader. It's contrived but helpful for unit testing",
+ "type": "MAT2",
+ "componentType": "FLOAT32"
+ },
+ "transformedWarpMatrix": {
+ "description": "For unit testing, apply an offset and scale to the warp matrix.",
+ "type": "MAT2",
+ "componentType": "FLOAT32",
+ "offset": [
+ 0.5, 0.5,
+ 0.5, 0.5
+ ],
+ "scale": [
+ 2, 2,
+ 2, 2
+ ]
+ },
+ "temperatures": {
+ "description": "(insideTemperature, outsideTemperature) where inside temperature is between [20, 25] °C and outside temperature is between [10, 30] °C. Values are stored in normalized form, but are scaled to these ranges using offset/scale",
+ "type": "VEC2",
+ "componentType": "UINT16",
+ "normalized": true,
+ "offset": [20, 10],
+ "scale": [5, 20]
+ }
+ }
+ }
+ }
+ },
+ "propertyAttributes": [
+ {
+ "class": "warpedBox",
+ "properties": {
+ "warpMatrix": {
+ "attribute": "_WARP_MATRIX"
+ },
+ "transformedWarpMatrix": {
+ "attribute": "_WARP_MATRIX"
+ },
+ "temperatures": {
+ "attribute": "_TEMPERATURES"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "scene": 0,
+ "scenes": [
+ {
+ "nodes": [
+ 0
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "matrix": [
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ -1.0,
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ {
+ "mesh": 0
+ }
+ ],
+ "meshes": [
+ {
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 2,
+ "TEXCOORD_0": 3,
+ "_WARP_MATRIX": 4,
+ "_TEMPERATURES": 5
+ },
+ "indices": 0,
+ "mode": 4,
+ "material": 0,
+ "extensions": {
+ "EXT_structural_metadata": {
+ "propertyAttributes": [
+ 0
+ ]
+ }
+ }
+ }
+ ],
+ "name": "Mesh"
+ }
+ ],
+ "accessors": [
+ {
+ "bufferView": 0,
+ "byteOffset": 0,
+ "componentType": 5123,
+ "count": 36,
+ "max": [
+ 23
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": 24,
+ "max": [
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "min": [
+ -1.0,
+ -1.0,
+ -1.0
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 288,
+ "componentType": 5126,
+ "count": 24,
+ "max": [
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "min": [
+ -0.5,
+ -0.5,
+ -0.5
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 2,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": 24,
+ "max": [
+ 6.0,
+ 1.0
+ ],
+ "min": [
+ 0.0,
+ 0.0
+ ],
+ "type": "VEC2"
+ },
+ {
+ "bufferView": 3,
+ "byteOffset": 0,
+ "type": "MAT2",
+ "componentType": 5126,
+ "count": 24
+ },
+ {
+ "bufferView": 4,
+ "byteOffset": 0,
+ "type": "VEC2",
+ "componentType": 5123,
+ "normalized": true,
+ "count": 24
+ }
+ ],
+ "materials": [
+ {
+ "pbrMetallicRoughness": {
+ "baseColorTexture": {
+ "index": 0
+ },
+ "metallicFactor": 0.0
+ },
+ "name": "Texture"
+ }
+ ],
+ "textures": [
+ {
+ "sampler": 0,
+ "source": 0
+ }
+ ],
+ "images": [
+ {
+ "uri": "CesiumLogoFlat.png"
+ }
+ ],
+ "samplers": [
+ {
+ "magFilter": 9729,
+ "minFilter": 9986,
+ "wrapS": 10497,
+ "wrapT": 10497
+ }
+ ],
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteOffset": 768,
+ "byteLength": 72,
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 0,
+ "byteLength": 576,
+ "byteStride": 12,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 576,
+ "byteLength": 192,
+ "byteStride": 8,
+ "target": 34962
+ },
+ {
+ "buffer": 1,
+ "byteOffset": 0,
+ "byteLength": 768,
+ "target": 34962
+ },
+ {
+ "buffer": 1,
+ "byteOffset": 768,
+ "byteLength": 96,
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 840,
+ "uri": "BoxTextured0.bin"
+ },
+ {
+ "byteLength": 864,
+ "uri": "metadata.bin"
+ }
+ ]
+}
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/CesiumLogoFlat.png b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/CesiumLogoFlat.png
new file mode 100644
index 000000000000..8159c4c4afd6
Binary files /dev/null and b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/CesiumLogoFlat.png differ
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/make_box_textured_metadata.py b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/make_box_textured_metadata.py
new file mode 100644
index 000000000000..fd834061a590
--- /dev/null
+++ b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/make_box_textured_metadata.py
@@ -0,0 +1,84 @@
+import numpy
+
+VERTEX_COUNT = 24
+ROW_STRIDE = 2
+ROWS_PER_MATRIX = 2
+COLUMNS_PER_MATRIX = 4
+
+# make the random numbers reproducible
+RNG = numpy.random.RandomState(2022)
+
+def make_scale(scale_factor):
+ # uniform scales are the same whether row or column
+ # major
+ return numpy.array(
+ [
+ [scale_factor, 0.0],
+ [0.0, scale_factor]
+ ],
+ dtype=numpy.float32
+ )
+
+MATRICES = [
+ make_scale(0.25),
+ make_scale(0.5),
+ make_scale(1.0),
+ make_scale(2.0),
+]
+
+def set_mat2(big_matrix, vertex_id, matrix):
+ start_row = vertex_id * 2
+ end_row = start_row + 2
+ big_matrix[start_row:end_row, 0:2] = matrix
+
+def make_warp_matrices():
+ buffer_view = numpy.zeros(
+ (ROWS_PER_MATRIX * VERTEX_COUNT, COLUMNS_PER_MATRIX),
+ dtype=numpy.float32
+ )
+
+ for i in range(VERTEX_COUNT):
+ warp_matrix = MATRICES[i % len(MATRICES)]
+ set_mat2(buffer_view, i, warp_matrix)
+
+ # The individual matrices are stored column-major, but
+ # the overall bufferView should be exported row-by-row
+ # for reference, order='C' is C-style (row major),
+ # order='F' is Fortran style (column major). Go figure :shrug:.
+ return buffer_view.tobytes(order='C')
+
+def make_temperature_vectors():
+ # this property will be scaled into the proper range
+ # via offset/scale. So let's just pick random UINT16 values
+ buffer_view = RNG.randint(
+ low=0,
+ high=(1 << 16) - 1,
+ size=(VERTEX_COUNT, 2),
+ dtype=numpy.uint16
+ )
+
+ return buffer_view.tobytes(order='C')
+
+
+def main():
+ warp_matrices_bin = make_warp_matrices()
+ warp_matrices_len = len(warp_matrices_bin)
+ print("Warp Matrices")
+ print("offset:", 0)
+ print("length:", warp_matrices_len)
+
+ temperatures_bin = make_temperature_vectors()
+ temperatures_len = len(temperatures_bin)
+ print("\nTemperatures")
+ print("offset:", len(warp_matrices_bin))
+ print("length:", temperatures_len)
+
+ total_len = warp_matrices_len + temperatures_len
+ print("\nTotal length:", total_len)
+
+ with open("metadata.bin", "wb") as f:
+ f.write(warp_matrices_bin)
+ f.write(temperatures_bin)
+
+if __name__ == "__main__":
+ main()
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/metadata.bin b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/metadata.bin
new file mode 100644
index 000000000000..fe114dc988bd
Binary files /dev/null and b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/metadata.bin differ
diff --git a/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/screenshot/screenshot.png b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/screenshot/screenshot.png
new file mode 100644
index 000000000000..47c5d4ca378d
Binary files /dev/null and b/Specs/Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/screenshot/screenshot.png differ
diff --git a/Specs/Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf b/Specs/Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf
index 3862e61000f5..540a930f7bcd 100644
--- a/Specs/Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf
+++ b/Specs/Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf
@@ -29,6 +29,29 @@
"description": "Integer point ID from [0, 20), stored as a float for easier use in shaders. The value increases around one of the circular rings (in the poloidal direction).",
"type": "SCALAR",
"componentType": "FLOAT32"
+ },
+ "toroidalNormalized": {
+ "description": "toroidal angle normalized in [0.0, 1.0]",
+ "type": "SCALAR",
+ "componentType": "FLOAT32",
+ "scale": 0.034482758620689655
+ },
+ "poloidalNormalized": {
+ "description": "toroidal angle normalized in [0.0, 1.0]",
+ "type": "SCALAR",
+ "componentType": "FLOAT32",
+ "scale": 0.05263157894736842
+ },
+ "toroidalAngle": {
+ "description": "toroidal angle in radians in [0, 2pi]. This is a test of offset/scale with property attribute override",
+ "type": "SCALAR",
+ "componentType": "FLOAT32",
+ "scale": 0.034482758620689655
+ },
+ "poloidalAngle": {
+ "description": "poloidal angle in radians in [-pi, pi]. This is a test of offset/scale with property attribute override",
+ "type": "SCALAR",
+ "scale": 0.05263157894736842
}
}
}
@@ -46,6 +69,21 @@
},
"pointId": {
"attribute": "_FEATURE_ID_1"
+ },
+ "toroidalNormalized": {
+ "attribute": "_FEATURE_ID_0"
+ },
+ "poloidalNormalized": {
+ "attribute": "_FEATURE_ID_1"
+ },
+ "toroidalAngle": {
+ "attribute": "_FEATURE_ID_0",
+ "scale": 0.21666156231653746
+ },
+ "poloidalAngle": {
+ "attribute": "_FEATURE_ID_1",
+ "offset": -3.141592653589793,
+ "scale": 0.3306939635357677
}
}
}
diff --git a/Specs/Renderer/ShaderBuilderSpec.js b/Specs/Renderer/ShaderBuilderSpec.js
index f14f4735c162..686045c979e6 100644
--- a/Specs/Renderer/ShaderBuilderSpec.js
+++ b/Specs/Renderer/ShaderBuilderSpec.js
@@ -562,7 +562,9 @@ describe(
it("setPositionAttribute creates a position attribute in location 0", function () {
const shaderBuilder = new ShaderBuilder();
- // even though these are declared out of order, the results to
+ // Even though these are declared out of order, position will always
+ // be assigned to location 0, and other attributes are assigned to
+ // locations 1 or greater.
const normalLocation = shaderBuilder.addAttribute("vec3", "a_normal");
const positionLocation = shaderBuilder.setPositionAttribute(
"vec3",
@@ -629,7 +631,6 @@ describe(
it("addAttribute creates an attribute in the vertex shader", function () {
const shaderBuilder = new ShaderBuilder();
- // even though these are declared out of order, the results to
const colorLocation = shaderBuilder.addAttribute("vec4", "a_color");
const normalLocation = shaderBuilder.addAttribute("vec3", "a_normal");
expect(colorLocation).toBe(1);
@@ -649,6 +650,30 @@ describe(
expect(shaderProgram._attributeLocations).toEqual(expectedLocations);
});
+ it("addAttribute handles matrix attribute locations correctly", function () {
+ const shaderBuilder = new ShaderBuilder();
+
+ const matrixLocation = shaderBuilder.addAttribute("mat3", "a_warpMatrix");
+ const colorLocation = shaderBuilder.addAttribute("vec3", "a_color");
+ expect(matrixLocation).toBe(1);
+
+ // this is 4 because the mat3 takes up locations 1, 2 and 3
+ expect(colorLocation).toBe(4);
+ const shaderProgram = shaderBuilder.buildShaderProgram(context);
+ const expectedAttributes = [
+ "attribute mat3 a_warpMatrix;",
+ "attribute vec3 a_color;",
+ ];
+ checkVertexShader(shaderProgram, [], expectedAttributes);
+ checkFragmentShader(shaderProgram, [], []);
+ const expectedLocations = {
+ a_warpMatrix: 1,
+ a_color: 4,
+ };
+ expect(shaderBuilder.attributeLocations).toEqual(expectedLocations);
+ expect(shaderProgram._attributeLocations).toEqual(expectedLocations);
+ });
+
it("addVarying throws for undefined type", function () {
const shaderBuilder = new ShaderBuilder();
expect(function () {
diff --git a/Specs/Scene/AttributeTypeSpec.js b/Specs/Scene/AttributeTypeSpec.js
index cf86669939d2..d7cac6283a28 100644
--- a/Specs/Scene/AttributeTypeSpec.js
+++ b/Specs/Scene/AttributeTypeSpec.js
@@ -57,6 +57,18 @@ describe("Scene/AttributeType", function () {
expect(AttributeType.getNumberOfComponents(AttributeType.MAT4)).toBe(16);
});
+ it("getAttributeLocationCount works", function () {
+ expect(AttributeType.getAttributeLocationCount(AttributeType.SCALAR)).toBe(
+ 1
+ );
+ expect(AttributeType.getAttributeLocationCount(AttributeType.VEC2)).toBe(1);
+ expect(AttributeType.getAttributeLocationCount(AttributeType.VEC3)).toBe(1);
+ expect(AttributeType.getAttributeLocationCount(AttributeType.VEC4)).toBe(1);
+ expect(AttributeType.getAttributeLocationCount(AttributeType.MAT2)).toBe(2);
+ expect(AttributeType.getAttributeLocationCount(AttributeType.MAT3)).toBe(3);
+ expect(AttributeType.getAttributeLocationCount(AttributeType.MAT4)).toBe(4);
+ });
+
it("getNumberOfComponents throws with invalid type", function () {
expect(function () {
AttributeType.getNumberOfComponents("Invalid");
diff --git a/Specs/Scene/GltfLoaderSpec.js b/Specs/Scene/GltfLoaderSpec.js
index f6d6b90dfa20..67d4ecc6defe 100644
--- a/Specs/Scene/GltfLoaderSpec.js
+++ b/Specs/Scene/GltfLoaderSpec.js
@@ -17,6 +17,7 @@ import {
InstanceAttributeSemantic,
JobScheduler,
PrimitiveType,
+ Matrix2,
Matrix4,
MetadataComponentType,
MetadataType,
@@ -78,6 +79,8 @@ describe(
"./Data/Models/GltfLoader/Weather/glTF/weather_EXT_feature_metadata.gltf";
const pointCloudWithPropertyAttributes =
"./Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf";
+ const boxWithPropertyAttributes =
+ "./Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTexturedWithPropertyAttributes.gltf";
const boxInstanced =
"./Data/Models/GltfLoader/BoxInstanced/glTF/box-instanced.gltf";
const boxInstancedLegacy =
@@ -1657,6 +1660,263 @@ describe(
expect(propertyAttribute.getProperty("pointId").attribute).toBe(
"_FEATURE_ID_1"
);
+
+ // A few more properties were added to test offset/scale
+ const toroidalNormalized = propertyAttribute.getProperty(
+ "toroidalNormalized"
+ );
+ expect(toroidalNormalized.attribute).toBe("_FEATURE_ID_0");
+ expect(toroidalNormalized.hasValueTransform).toBe(true);
+ expect(toroidalNormalized.offset).toBe(0);
+ expect(toroidalNormalized.scale).toBe(0.034482758620689655);
+
+ const poloidalNormalized = propertyAttribute.getProperty(
+ "poloidalNormalized"
+ );
+ expect(poloidalNormalized.attribute).toBe("_FEATURE_ID_1");
+ expect(poloidalNormalized.hasValueTransform).toBe(true);
+ expect(poloidalNormalized.offset).toBe(0);
+ expect(poloidalNormalized.scale).toBe(0.05263157894736842);
+
+ // These two properties have offset/scale in both the class definition
+ // and the property attribute. The latter should be used.
+ const toroidalAngle = propertyAttribute.getProperty("toroidalAngle");
+ expect(toroidalAngle.attribute).toBe("_FEATURE_ID_0");
+ expect(toroidalAngle.hasValueTransform).toBe(true);
+ expect(toroidalAngle.offset).toBe(0);
+ expect(toroidalAngle.scale).toBe(0.21666156231653746);
+
+ const poloidalAngle = propertyAttribute.getProperty("poloidalAngle");
+ expect(poloidalAngle.attribute).toBe("_FEATURE_ID_1");
+ expect(poloidalAngle.hasValueTransform).toBe(true);
+ expect(poloidalAngle.offset).toBe(-3.141592653589793);
+ expect(poloidalAngle.scale).toBe(0.3306939635357677);
+ });
+ });
+
+ it("loads BoxTexturedWithPropertyAttributes", function () {
+ return loadGltf(boxWithPropertyAttributes).then(function (gltfLoader) {
+ const components = gltfLoader.components;
+ const scene = components.scene;
+ const nodes = components.nodes;
+ const rootNode = scene.nodes[0];
+ const childNode = rootNode.children[0];
+ const primitive = childNode.primitives[0];
+ const attributes = primitive.attributes;
+ const positionAttribute = getAttribute(
+ attributes,
+ VertexAttributeSemantic.POSITION
+ );
+ const normalAttribute = getAttribute(
+ attributes,
+ VertexAttributeSemantic.NORMAL
+ );
+ const texcoordAttribute = getAttribute(
+ attributes,
+ VertexAttributeSemantic.TEXCOORD,
+ 0
+ );
+ const warpMatrixAttribute = getAttributeByName(
+ attributes,
+ "_WARP_MATRIX"
+ );
+ const temperaturesAttribute = getAttributeByName(
+ attributes,
+ "_TEMPERATURES"
+ );
+
+ const indices = primitive.indices;
+ const material = primitive.material;
+ const metallicRoughness = material.metallicRoughness;
+
+ expect(primitive.attributes.length).toBe(5);
+ expect(primitive.primitiveType).toBe(PrimitiveType.TRIANGLES);
+
+ expect(positionAttribute.name).toBe("POSITION");
+ expect(positionAttribute.semantic).toBe(
+ VertexAttributeSemantic.POSITION
+ );
+ expect(positionAttribute.setIndex).toBeUndefined();
+ expect(positionAttribute.componentDatatype).toBe(
+ ComponentDatatype.FLOAT
+ );
+ expect(positionAttribute.type).toBe(AttributeType.VEC3);
+ expect(positionAttribute.normalized).toBe(false);
+ expect(positionAttribute.count).toBe(24);
+ expect(positionAttribute.min).toEqual(new Cartesian3(-0.5, -0.5, -0.5));
+ expect(positionAttribute.max).toEqual(new Cartesian3(0.5, 0.5, 0.5));
+ expect(positionAttribute.constant).toEqual(Cartesian3.ZERO);
+ expect(positionAttribute.quantization).toBeUndefined();
+ expect(positionAttribute.typedArray).toBeUndefined();
+ expect(positionAttribute.buffer).toBeDefined();
+ expect(positionAttribute.byteOffset).toBe(288);
+ expect(positionAttribute.byteStride).toBe(12);
+
+ expect(normalAttribute.name).toBe("NORMAL");
+ expect(normalAttribute.semantic).toBe(VertexAttributeSemantic.NORMAL);
+ expect(normalAttribute.setIndex).toBeUndefined();
+ expect(normalAttribute.componentDatatype).toBe(ComponentDatatype.FLOAT);
+ expect(normalAttribute.type).toBe(AttributeType.VEC3);
+ expect(normalAttribute.normalized).toBe(false);
+ expect(normalAttribute.count).toBe(24);
+ expect(normalAttribute.min).toEqual(new Cartesian3(-1.0, -1.0, -1.0));
+ expect(normalAttribute.max).toEqual(new Cartesian3(1.0, 1.0, 1.0));
+ expect(normalAttribute.constant).toEqual(Cartesian3.ZERO);
+ expect(normalAttribute.quantization).toBeUndefined();
+ expect(normalAttribute.typedArray).toBeUndefined();
+ expect(normalAttribute.buffer).toBeDefined();
+ expect(normalAttribute.byteOffset).toBe(0);
+ expect(normalAttribute.byteStride).toBe(12);
+
+ expect(texcoordAttribute.name).toBe("TEXCOORD_0");
+ expect(texcoordAttribute.semantic).toBe(
+ VertexAttributeSemantic.TEXCOORD
+ );
+ expect(texcoordAttribute.setIndex).toBe(0);
+ expect(texcoordAttribute.componentDatatype).toBe(
+ ComponentDatatype.FLOAT
+ );
+ expect(texcoordAttribute.type).toBe(AttributeType.VEC2);
+ expect(texcoordAttribute.normalized).toBe(false);
+ expect(texcoordAttribute.count).toBe(24);
+ expect(texcoordAttribute.min).toEqual(new Cartesian2(0.0, 0.0));
+ expect(texcoordAttribute.max).toEqual(new Cartesian2(6.0, 1.0));
+ expect(texcoordAttribute.constant).toEqual(Cartesian2.ZERO);
+ expect(texcoordAttribute.quantization).toBeUndefined();
+ expect(texcoordAttribute.typedArray).toBeUndefined();
+ expect(texcoordAttribute.buffer).toBeDefined();
+ expect(texcoordAttribute.byteOffset).toBe(0);
+ expect(texcoordAttribute.byteStride).toBe(8);
+
+ expect(warpMatrixAttribute.name).toBe("_WARP_MATRIX");
+ expect(warpMatrixAttribute.semantic).toBeUndefined();
+ expect(warpMatrixAttribute.setIndex).toBeUndefined();
+ expect(warpMatrixAttribute.componentDatatype).toBe(
+ ComponentDatatype.FLOAT
+ );
+ expect(warpMatrixAttribute.type).toBe(AttributeType.MAT2);
+ expect(warpMatrixAttribute.normalized).toBe(false);
+ expect(warpMatrixAttribute.count).toBe(24);
+ expect(warpMatrixAttribute.min).toBeUndefined();
+ expect(warpMatrixAttribute.max).toBeUndefined();
+ expect(warpMatrixAttribute.constant).toEqual(Matrix2.ZERO);
+ expect(warpMatrixAttribute.quantization).toBeUndefined();
+ expect(warpMatrixAttribute.typedArray).toBeUndefined();
+ expect(warpMatrixAttribute.buffer).toBeDefined();
+ expect(warpMatrixAttribute.byteOffset).toBe(0);
+ expect(warpMatrixAttribute.byteStride).toBe(16);
+
+ expect(temperaturesAttribute.name).toBe("_TEMPERATURES");
+ expect(temperaturesAttribute.semantic).toBeUndefined();
+ expect(temperaturesAttribute.setIndex).toBeUndefined();
+ expect(temperaturesAttribute.componentDatatype).toBe(
+ ComponentDatatype.UNSIGNED_SHORT
+ );
+ expect(temperaturesAttribute.type).toBe(AttributeType.VEC2);
+ expect(temperaturesAttribute.normalized).toBe(true);
+ expect(temperaturesAttribute.count).toBe(24);
+ expect(temperaturesAttribute.min).toBeUndefined();
+ expect(temperaturesAttribute.max).toBeUndefined();
+ expect(temperaturesAttribute.constant).toEqual(Cartesian2.ZERO);
+ expect(temperaturesAttribute.quantization).toBeUndefined();
+ expect(temperaturesAttribute.typedArray).toBeUndefined();
+ expect(temperaturesAttribute.buffer).toBeDefined();
+ expect(temperaturesAttribute.byteOffset).toBe(0);
+ expect(temperaturesAttribute.byteStride).toBe(4);
+
+ expect(indices.indexDatatype).toBe(IndexDatatype.UNSIGNED_SHORT);
+ expect(indices.count).toBe(36);
+ expect(indices.buffer).toBeDefined();
+ expect(indices.buffer.sizeInBytes).toBe(72);
+
+ expect(positionAttribute.buffer).toBe(normalAttribute.buffer);
+ expect(positionAttribute.buffer).not.toBe(texcoordAttribute.buffer);
+
+ expect(positionAttribute.buffer.sizeInBytes).toBe(576);
+ expect(texcoordAttribute.buffer.sizeInBytes).toBe(192);
+
+ expect(metallicRoughness.baseColorFactor).toEqual(
+ new Cartesian4(1.0, 1.0, 1.0, 1.0)
+ );
+ expect(metallicRoughness.metallicFactor).toBe(0.0);
+ expect(metallicRoughness.roughnessFactor).toBe(1.0);
+ expect(metallicRoughness.baseColorTexture.texture.width).toBe(256);
+ expect(metallicRoughness.baseColorTexture.texture.height).toBe(256);
+ expect(metallicRoughness.baseColorTexture.texCoord).toBe(0);
+
+ const sampler = metallicRoughness.baseColorTexture.texture.sampler;
+ expect(sampler.wrapS).toBe(TextureWrap.REPEAT);
+ expect(sampler.wrapT).toBe(TextureWrap.REPEAT);
+ expect(sampler.magnificationFilter).toBe(
+ TextureMagnificationFilter.LINEAR
+ );
+ expect(sampler.minificationFilter).toBe(
+ TextureMinificationFilter.NEAREST_MIPMAP_LINEAR
+ );
+
+ expect(nodes.length).toBe(2);
+ expect(scene.nodes.length).toBe(1);
+
+ const structuralMetadata = components.structuralMetadata;
+ const boxClass = structuralMetadata.schema.classes.warpedBox;
+ const boxProperties = boxClass.properties;
+
+ const warpMatrixProperty = boxProperties.warpMatrix;
+ expect(warpMatrixProperty.type).toBe(MetadataType.MAT2);
+ expect(warpMatrixProperty.componentType).toBe(
+ MetadataComponentType.FLOAT32
+ );
+ expect(warpMatrixProperty.hasValueTransform).toBe(false);
+
+ const transformedWarpMatrixProperty =
+ boxProperties.transformedWarpMatrix;
+ expect(transformedWarpMatrixProperty.type).toBe(MetadataType.MAT2);
+ expect(transformedWarpMatrixProperty.componentType).toBe(
+ MetadataComponentType.FLOAT32
+ );
+ expect(transformedWarpMatrixProperty.hasValueTransform).toBe(true);
+ expect(transformedWarpMatrixProperty.offset).toEqual([
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5,
+ ]);
+ expect(transformedWarpMatrixProperty.scale).toEqual([2, 2, 2, 2]);
+
+ const temperaturesProperty = boxProperties.temperatures;
+ expect(temperaturesProperty.type).toBe(MetadataType.VEC2);
+ expect(temperaturesProperty.componentType).toBe(
+ MetadataComponentType.UINT16
+ );
+ expect(temperaturesProperty.normalized).toBe(true);
+ expect(temperaturesProperty.hasValueTransform).toBe(true);
+ expect(temperaturesProperty.offset).toEqual([20, 10]);
+ expect(temperaturesProperty.scale).toEqual([5, 20]);
+
+ const propertyAttribute = structuralMetadata.getPropertyAttribute(0);
+ expect(propertyAttribute.id).toBe(0);
+ expect(propertyAttribute.name).toBeUndefined();
+ expect(propertyAttribute.class).toBe(boxClass);
+
+ const warpMatrix = propertyAttribute.getProperty("warpMatrix");
+ expect(warpMatrix.attribute).toBe("_WARP_MATRIX");
+ expect(warpMatrix.hasValueTransform).toBe(false);
+
+ const transformedWarpMatrix = propertyAttribute.getProperty(
+ "transformedWarpMatrix"
+ );
+ expect(transformedWarpMatrix.attribute).toBe("_WARP_MATRIX");
+ expect(transformedWarpMatrix.hasValueTransform).toBe(true);
+ expect(transformedWarpMatrix.offset).toEqual(
+ new Matrix2(0.5, 0.5, 0.5, 0.5)
+ );
+ expect(transformedWarpMatrix.scale).toEqual(new Matrix2(2, 2, 2, 2));
+
+ const temperatures = propertyAttribute.getProperty("temperatures");
+ expect(temperatures.attribute).toBe("_TEMPERATURES");
+ expect(temperatures.hasValueTransform).toBe(true);
+ expect(temperatures.offset).toEqual(new Cartesian2(20, 10));
+ expect(temperatures.scale).toEqual(new Cartesian2(5, 20));
});
});
diff --git a/Specs/Scene/ModelExperimental/CustomShaderPipelineStageSpec.js b/Specs/Scene/ModelExperimental/CustomShaderPipelineStageSpec.js
index 74c6406ca21f..0d2d48781a61 100644
--- a/Specs/Scene/ModelExperimental/CustomShaderPipelineStageSpec.js
+++ b/Specs/Scene/ModelExperimental/CustomShaderPipelineStageSpec.js
@@ -314,13 +314,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -394,13 +402,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -475,13 +491,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -552,13 +576,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -629,7 +661,11 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
});
@@ -679,13 +715,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -750,13 +794,21 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT,
"VertexInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasFragmentStruct(
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
ShaderBuilderTester.expectHasVertexFunction(
@@ -926,7 +978,11 @@ describe("Scene/ModelExperimental/CustomShaderPipelineStage", function () {
shaderBuilder,
CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT,
"FragmentInput",
- [" Attributes attributes;", " FeatureIds featureIds;"]
+ [
+ " Attributes attributes;",
+ " FeatureIds featureIds;",
+ " Metadata metadata;",
+ ]
);
expect(shaderBuilder._vertexShaderParts.functionIds).toEqual([]);
diff --git a/Specs/Scene/ModelExperimental/CustomShaderSpec.js b/Specs/Scene/ModelExperimental/CustomShaderSpec.js
index aafb1e7b4144..671ec3ae095d 100644
--- a/Specs/Scene/ModelExperimental/CustomShaderSpec.js
+++ b/Specs/Scene/ModelExperimental/CustomShaderSpec.js
@@ -167,6 +167,7 @@ describe("Scene/ModelExperimental/CustomShader", function () {
"void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput)",
"{",
" float value = vsInput.featureIds.featureId_0;",
+ " float value2 = vsInput.metadata.temperature;",
" positionMC += vsInput.attributes.expansion * vsInput.attributes.normalMC;",
"}",
].join("\n"),
@@ -174,6 +175,7 @@ describe("Scene/ModelExperimental/CustomShader", function () {
"void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)",
"{",
" float value = fsInput.featureIds.featureId_1 + fsInput.featureIds.instanceFeatureId_0;",
+ " float value2 = fsInput.metadata.pressure;",
" material.normalEC = normalize(fsInput.attributes.normalEC);",
" material.diffuse = fsInput.attributes.color_0;",
" material.specular = fsInput.attributes.positionWC / 1.0e6;",
@@ -189,6 +191,9 @@ describe("Scene/ModelExperimental/CustomShader", function () {
featureIdSet: {
featureId_0: true,
},
+ metadataSet: {
+ temperature: true,
+ },
};
const expectedFragmentVariables = {
attributeSet: {
@@ -205,6 +210,9 @@ describe("Scene/ModelExperimental/CustomShader", function () {
featureId_1: true,
instanceFeatureId_0: true,
},
+ metadataSet: {
+ pressure: true,
+ },
};
expect(customShader.usedVariablesVertex).toEqual(expectedVertexVariables);
diff --git a/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js b/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js
index a781fed50bb4..d223226f29c3 100644
--- a/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js
+++ b/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js
@@ -56,6 +56,8 @@ describe(
"./Data/Models/GltfLoader/BoomBox/glTF-pbrSpecularGlossiness/BoomBox.gltf";
const boxTextured =
"./Data/Models/GltfLoader/BoxTextured/glTF-Binary/BoxTextured.glb";
+ const boxTexturedWithPropertyAttributes =
+ "./Data/Models/GltfLoader/BoxTexturedWithPropertyAttributes/glTF/BoxTexturedWithPropertyAttributes.gltf";
const boxVertexColors =
"./Data/Models/GltfLoader/BoxVertexColors/glTF/BoxVertexColors.gltf";
const pointCloudRGB =
@@ -1192,6 +1194,178 @@ describe(
]);
});
});
+
+ it("processes model with matrix attributes", function () {
+ const renderResources = {
+ attributes: [],
+ shaderBuilder: new ShaderBuilder(),
+ attributeIndex: 1,
+ model: {
+ type: ModelExperimentalType.TILE_GLTF,
+ },
+ };
+
+ return loadGltf(boxTexturedWithPropertyAttributes).then(function (
+ gltfLoader
+ ) {
+ const components = gltfLoader.components;
+ const primitive = components.nodes[1].primitives[0];
+
+ GeometryPipelineStage.process(renderResources, primitive);
+
+ const shaderBuilder = renderResources.shaderBuilder;
+ const attributes = renderResources.attributes;
+
+ expect(attributes.length).toEqual(6);
+
+ const normalAttribute = attributes[0];
+ expect(normalAttribute.index).toEqual(1);
+ expect(normalAttribute.vertexBuffer).toBeDefined();
+ expect(normalAttribute.componentsPerAttribute).toEqual(3);
+ expect(normalAttribute.componentDatatype).toEqual(
+ ComponentDatatype.FLOAT
+ );
+ expect(normalAttribute.offsetInBytes).toBe(0);
+ expect(normalAttribute.strideInBytes).toBe(12);
+
+ const positionAttribute = attributes[1];
+ expect(positionAttribute.index).toEqual(0);
+ expect(positionAttribute.vertexBuffer).toBeDefined();
+ expect(positionAttribute.componentsPerAttribute).toEqual(3);
+ expect(positionAttribute.componentDatatype).toEqual(
+ ComponentDatatype.FLOAT
+ );
+ expect(positionAttribute.offsetInBytes).toBe(288);
+ expect(positionAttribute.strideInBytes).toBe(12);
+
+ const texCoord0Attribute = attributes[2];
+ expect(texCoord0Attribute.index).toEqual(2);
+ expect(texCoord0Attribute.vertexBuffer).toBeDefined();
+ expect(texCoord0Attribute.componentsPerAttribute).toEqual(2);
+ expect(texCoord0Attribute.componentDatatype).toEqual(
+ ComponentDatatype.FLOAT
+ );
+ expect(texCoord0Attribute.offsetInBytes).toBe(0);
+ expect(texCoord0Attribute.strideInBytes).toBe(8);
+
+ const warpMatrixAttribute = attributes[3];
+ expect(warpMatrixAttribute.index).toEqual(3);
+ expect(warpMatrixAttribute.vertexBuffer).toBeDefined();
+ expect(warpMatrixAttribute.componentsPerAttribute).toEqual(2);
+ expect(warpMatrixAttribute.componentDatatype).toEqual(
+ ComponentDatatype.FLOAT
+ );
+ expect(warpMatrixAttribute.offsetInBytes).toBe(0);
+ expect(warpMatrixAttribute.strideInBytes).toBe(16);
+
+ const warpMatrixAttributePart2 = attributes[4];
+ expect(warpMatrixAttributePart2.index).toEqual(4);
+ expect(warpMatrixAttributePart2.vertexBuffer).toBeDefined();
+ expect(warpMatrixAttributePart2.componentsPerAttribute).toEqual(2);
+ expect(warpMatrixAttributePart2.componentDatatype).toEqual(
+ ComponentDatatype.FLOAT
+ );
+ expect(warpMatrixAttributePart2.offsetInBytes).toBe(8);
+ expect(warpMatrixAttributePart2.strideInBytes).toBe(16);
+
+ const temperaturesAttribute = attributes[5];
+ expect(temperaturesAttribute.index).toEqual(5);
+ expect(temperaturesAttribute.vertexBuffer).toBeDefined();
+ expect(temperaturesAttribute.componentsPerAttribute).toEqual(2);
+ expect(temperaturesAttribute.componentDatatype).toEqual(
+ ComponentDatatype.UNSIGNED_SHORT
+ );
+ expect(temperaturesAttribute.offsetInBytes).toBe(0);
+ expect(temperaturesAttribute.strideInBytes).toBe(4);
+
+ ShaderBuilderTester.expectHasVertexStruct(
+ shaderBuilder,
+ GeometryPipelineStage.STRUCT_ID_PROCESSED_ATTRIBUTES_VS,
+ GeometryPipelineStage.STRUCT_NAME_PROCESSED_ATTRIBUTES,
+ [
+ " vec3 positionMC;",
+ " vec3 normalMC;",
+ " vec2 texCoord_0;",
+ " mat2 warp_matrix;",
+ " vec2 temperatures;",
+ ]
+ );
+ ShaderBuilderTester.expectHasFragmentStruct(
+ shaderBuilder,
+ GeometryPipelineStage.STRUCT_ID_PROCESSED_ATTRIBUTES_FS,
+ GeometryPipelineStage.STRUCT_NAME_PROCESSED_ATTRIBUTES,
+ [
+ " vec3 positionMC;",
+ " vec3 positionWC;",
+ " vec3 positionEC;",
+ " vec3 normalEC;",
+ " vec2 texCoord_0;",
+ " mat2 warp_matrix;",
+ " vec2 temperatures;",
+ ]
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ GeometryPipelineStage.FUNCTION_ID_INITIALIZE_ATTRIBUTES,
+ GeometryPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_ATTRIBUTES,
+ [
+ " attributes.positionMC = a_positionMC;",
+ " attributes.normalMC = a_normalMC;",
+ " attributes.texCoord_0 = a_texCoord_0;",
+ " attributes.warp_matrix = a_warp_matrix;",
+ " attributes.temperatures = a_temperatures;",
+ ]
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ GeometryPipelineStage.FUNCTION_ID_SET_DYNAMIC_VARYINGS_VS,
+ GeometryPipelineStage.FUNCTION_SIGNATURE_SET_DYNAMIC_VARYINGS,
+ [
+ " v_texCoord_0 = attributes.texCoord_0;",
+ " v_warp_matrix = attributes.warp_matrix;",
+ " v_temperatures = attributes.temperatures;",
+ ]
+ );
+ ShaderBuilderTester.expectHasFragmentFunction(
+ shaderBuilder,
+ GeometryPipelineStage.FUNCTION_ID_SET_DYNAMIC_VARYINGS_FS,
+ GeometryPipelineStage.FUNCTION_SIGNATURE_SET_DYNAMIC_VARYINGS,
+ [
+ " attributes.texCoord_0 = v_texCoord_0;",
+ " attributes.warp_matrix = v_warp_matrix;",
+ " attributes.temperatures = v_temperatures;",
+ ]
+ );
+ ShaderBuilderTester.expectHasVaryings(shaderBuilder, [
+ "varying vec3 v_normalEC;",
+ "varying vec2 v_texCoord_0;",
+ "varying vec3 v_positionEC;",
+ "varying vec3 v_positionMC;",
+ "varying vec3 v_positionWC;",
+ "varying mat2 v_warp_matrix;",
+ "varying vec2 v_temperatures;",
+ ]);
+ ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [
+ "HAS_NORMALS",
+ "HAS_TEXCOORD_0",
+ ]);
+ ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [
+ "HAS_NORMALS",
+ "HAS_TEXCOORD_0",
+ ]);
+ ShaderBuilderTester.expectHasAttributes(
+ shaderBuilder,
+ "attribute vec3 a_positionMC;",
+ [
+ "attribute vec3 a_normalMC;",
+ "attribute vec2 a_texCoord_0;",
+ "attribute mat2 a_warp_matrix;",
+ "attribute vec2 a_temperatures;",
+ ]
+ );
+ verifyFeatureStruct(shaderBuilder);
+ });
+ });
},
"WebGL"
);
diff --git a/Specs/Scene/ModelExperimental/MetadataPipelineStageSpec.js b/Specs/Scene/ModelExperimental/MetadataPipelineStageSpec.js
new file mode 100644
index 000000000000..8bdad4356073
--- /dev/null
+++ b/Specs/Scene/ModelExperimental/MetadataPipelineStageSpec.js
@@ -0,0 +1,240 @@
+import {
+ combine,
+ GltfLoader,
+ MetadataPipelineStage,
+ Resource,
+ ResourceCache,
+ ShaderBuilder,
+} from "../../../Source/Cesium.js";
+import createScene from "../../createScene.js";
+import ShaderBuilderTester from "../../ShaderBuilderTester.js";
+import waitForLoaderProcess from "../../waitForLoaderProcess.js";
+
+describe(
+ "Scene/ModelExperimental/MetadataPipelineStage",
+ function () {
+ const pointCloudWithPropertyAttributes =
+ "./Data/Models/GltfLoader/PointCloudWithPropertyAttributes/glTF/PointCloudWithPropertyAttributes.gltf";
+ const boxTexturedBinary =
+ "./Data/Models/GltfLoader/BoxTextured/glTF-Binary/BoxTextured.glb";
+
+ let scene;
+ const gltfLoaders = [];
+ const resources = [];
+
+ beforeAll(function () {
+ scene = createScene();
+ });
+
+ afterAll(function () {
+ scene.destroyForSpecs();
+ });
+
+ function cleanup(resourcesArray) {
+ for (let i = 0; i < resourcesArray.length; i++) {
+ const resource = resourcesArray[i];
+ if (!resource.isDestroyed()) {
+ resource.destroy();
+ }
+ }
+ resourcesArray.length = 0;
+ }
+
+ afterEach(function () {
+ cleanup(resources);
+ cleanup(gltfLoaders);
+ ResourceCache.clearForSpecs();
+ });
+
+ function getOptions(gltfPath, options) {
+ const resource = new Resource({
+ url: gltfPath,
+ });
+
+ return combine(options, {
+ gltfResource: resource,
+ incrementallyLoadTextures: false, // Default to false if not supplied
+ });
+ }
+
+ function loadGltf(gltfPath, options) {
+ const gltfLoader = new GltfLoader(getOptions(gltfPath, options));
+ gltfLoaders.push(gltfLoader);
+ gltfLoader.load();
+
+ return waitForLoaderProcess(gltfLoader, scene);
+ }
+
+ function mockRenderResources(components) {
+ return {
+ shaderBuilder: new ShaderBuilder(),
+ model: {
+ structuralMetadata: components.structuralMetadata,
+ },
+ uniformMap: {},
+ };
+ }
+
+ it("Handles primitives without metadata gracefully", function () {
+ return loadGltf(boxTexturedBinary).then(function (gltfLoader) {
+ const components = gltfLoader.components;
+ const node = components.nodes[1];
+ const primitive = node.primitives[0];
+ const frameState = scene.frameState;
+ const renderResources = mockRenderResources(components);
+
+ MetadataPipelineStage.process(renderResources, primitive, frameState);
+
+ const shaderBuilder = renderResources.shaderBuilder;
+ ShaderBuilderTester.expectHasVertexStruct(
+ shaderBuilder,
+ MetadataPipelineStage.STRUCT_ID_METADATA_VS,
+ MetadataPipelineStage.STRUCT_NAME_METADATA,
+ []
+ );
+ ShaderBuilderTester.expectHasFragmentStruct(
+ shaderBuilder,
+ MetadataPipelineStage.STRUCT_ID_METADATA_FS,
+ MetadataPipelineStage.STRUCT_NAME_METADATA,
+ []
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
+ []
+ );
+ ShaderBuilderTester.expectHasFragmentFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
+ []
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_SET_METADATA_VARYINGS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_SET_METADATA_VARYINGS,
+ []
+ );
+ ShaderBuilderTester.expectHasVertexUniforms(shaderBuilder, []);
+ ShaderBuilderTester.expectHasFragmentUniforms(shaderBuilder, []);
+
+ expect(renderResources.uniformMap).toEqual({});
+ });
+ });
+
+ it("Adds property attributes to the shader", function () {
+ return loadGltf(pointCloudWithPropertyAttributes).then(function (
+ gltfLoader
+ ) {
+ const components = gltfLoader.components;
+ const node = components.nodes[0];
+ const primitive = node.primitives[0];
+ const frameState = scene.frameState;
+ const renderResources = mockRenderResources(components);
+
+ MetadataPipelineStage.process(renderResources, primitive, frameState);
+
+ const shaderBuilder = renderResources.shaderBuilder;
+ ShaderBuilderTester.expectHasVertexStruct(
+ shaderBuilder,
+ MetadataPipelineStage.STRUCT_ID_METADATA_VS,
+ MetadataPipelineStage.STRUCT_NAME_METADATA,
+ [
+ " float circleT;",
+ " float iteration;",
+ " float pointId;",
+ " float toroidalNormalized;",
+ " float poloidalNormalized;",
+ " float toroidalAngle;",
+ " float poloidalAngle;",
+ ]
+ );
+ ShaderBuilderTester.expectHasFragmentStruct(
+ shaderBuilder,
+ MetadataPipelineStage.STRUCT_ID_METADATA_FS,
+ MetadataPipelineStage.STRUCT_NAME_METADATA,
+ [
+ " float circleT;",
+ " float iteration;",
+ " float pointId;",
+ " float toroidalNormalized;",
+ " float poloidalNormalized;",
+ " float toroidalAngle;",
+ " float poloidalAngle;",
+ ]
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
+ [
+ " metadata.circleT = attributes.circle_t;",
+ " metadata.iteration = attributes.featureId_0;",
+ " metadata.pointId = attributes.featureId_1;",
+ " metadata.toroidalNormalized = czm_valueTransform(u_toroidalNormalized_offset, u_toroidalNormalized_scale, attributes.featureId_0);",
+ " metadata.poloidalNormalized = czm_valueTransform(u_poloidalNormalized_offset, u_poloidalNormalized_scale, attributes.featureId_1);",
+ " metadata.toroidalAngle = czm_valueTransform(u_toroidalAngle_offset, u_toroidalAngle_scale, attributes.featureId_0);",
+ " metadata.poloidalAngle = czm_valueTransform(u_poloidalAngle_offset, u_poloidalAngle_scale, attributes.featureId_1);",
+ ]
+ );
+ ShaderBuilderTester.expectHasFragmentFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
+ [
+ " metadata.circleT = attributes.circle_t;",
+ " metadata.iteration = attributes.featureId_0;",
+ " metadata.pointId = attributes.featureId_1;",
+ " metadata.toroidalNormalized = czm_valueTransform(u_toroidalNormalized_offset, u_toroidalNormalized_scale, attributes.featureId_0);",
+ " metadata.poloidalNormalized = czm_valueTransform(u_poloidalNormalized_offset, u_poloidalNormalized_scale, attributes.featureId_1);",
+ " metadata.toroidalAngle = czm_valueTransform(u_toroidalAngle_offset, u_toroidalAngle_scale, attributes.featureId_0);",
+ " metadata.poloidalAngle = czm_valueTransform(u_poloidalAngle_offset, u_poloidalAngle_scale, attributes.featureId_1);",
+ ]
+ );
+ ShaderBuilderTester.expectHasVertexFunction(
+ shaderBuilder,
+ MetadataPipelineStage.FUNCTION_ID_SET_METADATA_VARYINGS,
+ MetadataPipelineStage.FUNCTION_SIGNATURE_SET_METADATA_VARYINGS,
+ []
+ );
+ ShaderBuilderTester.expectHasVertexUniforms(shaderBuilder, [
+ "uniform float u_toroidalNormalized_offset;",
+ "uniform float u_toroidalNormalized_scale;",
+ "uniform float u_poloidalNormalized_offset;",
+ "uniform float u_poloidalNormalized_scale;",
+ "uniform float u_toroidalAngle_offset;",
+ "uniform float u_toroidalAngle_scale;",
+ "uniform float u_poloidalAngle_offset;",
+ "uniform float u_poloidalAngle_scale;",
+ ]);
+ ShaderBuilderTester.expectHasFragmentUniforms(shaderBuilder, [
+ "uniform float u_toroidalNormalized_offset;",
+ "uniform float u_toroidalNormalized_scale;",
+ "uniform float u_poloidalNormalized_offset;",
+ "uniform float u_poloidalNormalized_scale;",
+ "uniform float u_toroidalAngle_offset;",
+ "uniform float u_toroidalAngle_scale;",
+ "uniform float u_poloidalAngle_offset;",
+ "uniform float u_poloidalAngle_scale;",
+ ]);
+
+ // The offsets and scales should be exactly as they appear in the glTF
+ const uniformMap = renderResources.uniformMap;
+ expect(uniformMap.u_toroidalNormalized_offset()).toBe(0);
+ expect(uniformMap.u_toroidalNormalized_scale()).toBe(
+ 0.034482758620689655
+ );
+ expect(uniformMap.u_poloidalNormalized_offset()).toBe(0);
+ expect(uniformMap.u_poloidalNormalized_scale()).toBe(
+ 0.05263157894736842
+ );
+ expect(uniformMap.u_toroidalAngle_offset()).toBe(0);
+ expect(uniformMap.u_toroidalAngle_scale()).toBe(0.21666156231653746);
+ expect(uniformMap.u_poloidalAngle_offset()).toBe(-3.141592653589793);
+ expect(uniformMap.u_poloidalAngle_scale()).toBe(0.3306939635357677);
+ });
+ });
+ },
+ "WebGL"
+);
diff --git a/Specs/Scene/ModelExperimental/ModelExperimentalPrimitiveSpec.js b/Specs/Scene/ModelExperimental/ModelExperimentalPrimitiveSpec.js
index bb725e691c4d..46d254414c61 100644
--- a/Specs/Scene/ModelExperimental/ModelExperimentalPrimitiveSpec.js
+++ b/Specs/Scene/ModelExperimental/ModelExperimentalPrimitiveSpec.js
@@ -9,6 +9,7 @@ import {
GeometryPipelineStage,
LightingPipelineStage,
MaterialPipelineStage,
+ MetadataPipelineStage,
ModelExperimentalType,
PickingPipelineStage,
PointCloudAttenuationPipelineStage,
@@ -97,6 +98,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
PickingPipelineStage,
AlphaPipelineStage,
@@ -118,6 +120,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
];
@@ -154,6 +157,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
SelectedFeatureIdPipelineStage,
BatchTexturePipelineStage,
CPUStylingPipelineStage,
@@ -197,6 +201,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
SelectedFeatureIdPipelineStage,
BatchTexturePipelineStage,
CPUStylingPipelineStage,
@@ -251,6 +256,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
DequantizationPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
PickingPipelineStage,
AlphaPipelineStage,
@@ -276,6 +282,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -303,6 +310,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
const expectedStages = [
GeometryPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -330,6 +338,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -367,6 +376,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
PointCloudAttenuationPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
];
@@ -398,6 +408,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
PointCloudAttenuationPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
];
@@ -428,6 +439,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
];
@@ -455,6 +467,7 @@ describe("Scene/ModelExperimental/ModelExperimentalPrimitive", function () {
GeometryPipelineStage,
MaterialPipelineStage,
FeatureIdPipelineStage,
+ MetadataPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
];
diff --git a/Specs/Scene/PropertyAttributePropertySpec.js b/Specs/Scene/PropertyAttributePropertySpec.js
index fd08c0f784a7..346e1e93fa77 100644
--- a/Specs/Scene/PropertyAttributePropertySpec.js
+++ b/Specs/Scene/PropertyAttributePropertySpec.js
@@ -1,6 +1,8 @@
import {
+ Cartesian2,
PropertyAttributeProperty,
- MetadataClass,
+ Matrix2,
+ MetadataClassProperty,
} from "../../Source/Cesium.js";
describe("Scene/PropertyAttributeProperty", function () {
@@ -10,20 +12,14 @@ describe("Scene/PropertyAttributeProperty", function () {
let propertyAttributeProperty;
beforeAll(function () {
- const classDefinition = new MetadataClass({
- id: "pointCloud",
- class: {
- properties: {
- intensity: {
- type: "SCALAR",
- componentType: "FLOAT32",
- },
- },
+ classProperty = new MetadataClassProperty({
+ id: "intensity",
+ property: {
+ type: "SCALAR",
+ componentType: "FLOAT32",
},
});
- classProperty = classDefinition.properties.intensity;
-
extras = {
description: "Extra",
};
@@ -46,6 +42,9 @@ describe("Scene/PropertyAttributeProperty", function () {
it("creates property attribute property", function () {
expect(propertyAttributeProperty.attribute).toBe("_INTENSITY");
+ expect(propertyAttributeProperty.hasValueTransform).toBe(false);
+ expect(propertyAttributeProperty.offset).toBe(0);
+ expect(propertyAttributeProperty.scale).toBe(1);
expect(propertyAttributeProperty.extras).toBe(extras);
expect(propertyAttributeProperty.extensions).toBe(extensions);
});
@@ -67,4 +66,104 @@ describe("Scene/PropertyAttributeProperty", function () {
});
}).toThrowDeveloperError();
});
+
+ it("creates property with value transform from class definition", function () {
+ const classProperty = new MetadataClassProperty({
+ id: "transformed",
+ property: {
+ type: "SCALAR",
+ componentType: "UINT8",
+ normalized: true,
+ offset: 1,
+ scale: 2,
+ },
+ });
+
+ propertyAttributeProperty = new PropertyAttributeProperty({
+ property: {
+ attribute: "_TRANSFORMED",
+ },
+ classProperty: classProperty,
+ });
+
+ expect(propertyAttributeProperty.attribute).toBe("_TRANSFORMED");
+ expect(propertyAttributeProperty.hasValueTransform).toBe(true);
+ expect(propertyAttributeProperty.offset).toBe(1);
+ expect(propertyAttributeProperty.scale).toBe(2);
+ });
+
+ it("creates property with value transform override", function () {
+ const classProperty = new MetadataClassProperty({
+ id: "transformed",
+ property: {
+ type: "SCALAR",
+ componentType: "UINT8",
+ normalized: true,
+ offset: 1,
+ scale: 2,
+ },
+ });
+
+ propertyAttributeProperty = new PropertyAttributeProperty({
+ property: {
+ attribute: "_TRANSFORMED",
+ offset: 2,
+ scale: 4,
+ },
+ classProperty: classProperty,
+ });
+
+ expect(propertyAttributeProperty.attribute).toBe("_TRANSFORMED");
+ expect(propertyAttributeProperty.hasValueTransform).toBe(true);
+ expect(propertyAttributeProperty.offset).toBe(2);
+ expect(propertyAttributeProperty.scale).toBe(4);
+ });
+
+ it("unpacks property and scale for vectors and matrices", function () {
+ let classProperty = new MetadataClassProperty({
+ id: "transformed",
+ property: {
+ type: "VEC2",
+ componentType: "UINT8",
+ normalized: true,
+ offset: [1, 2],
+ scale: [2, 4],
+ },
+ });
+
+ propertyAttributeProperty = new PropertyAttributeProperty({
+ property: {
+ attribute: "_TRANSFORMED",
+ },
+ classProperty: classProperty,
+ });
+
+ expect(propertyAttributeProperty.attribute).toBe("_TRANSFORMED");
+ expect(propertyAttributeProperty.hasValueTransform).toBe(true);
+ expect(propertyAttributeProperty.offset).toEqual(new Cartesian2(1, 2));
+ expect(propertyAttributeProperty.scale).toEqual(new Cartesian2(2, 4));
+
+ classProperty = new MetadataClassProperty({
+ id: "transformed",
+ property: {
+ type: "MAT2",
+ componentType: "UINT8",
+ normalized: true,
+ offset: [1, 2, 2, 1],
+ scale: [2, 4, 4, 1],
+ },
+ });
+
+ propertyAttributeProperty = new PropertyAttributeProperty({
+ property: {
+ attribute: "_TRANSFORMED",
+ },
+ classProperty: classProperty,
+ });
+
+ expect(propertyAttributeProperty.attribute).toBe("_TRANSFORMED");
+ expect(propertyAttributeProperty.hasValueTransform).toBe(true);
+ expect(propertyAttributeProperty.offset).toEqual(new Matrix2(1, 2, 2, 1));
+ expect(propertyAttributeProperty.scale).toEqual(new Matrix2(2, 4, 4, 1));
+ });
});