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)); + }); });