diff --git a/CHANGES.md b/CHANGES.md index a12796cd0796..9782e8b22899 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Added `enableVerticalExaggeration` option to models. Set this value to `false` to prevent model exaggeration when `Scene.verticalExaggeration` is set to a value other than `1.0`. [#12141](https://github.com/CesiumGS/cesium/pull/12141) - Added `CallbackPositionProperty` to allow lazy entity position evaluation. [#12170](https://github.com/CesiumGS/cesium/pull/12170) +- Added `Scene.prototype.pickMetadata` and `Scene.prototype.pickMetadataSchema`, enabling experimental support for picking property textures or property attributes [#12075](https://github.com/CesiumGS/cesium/pull/12075) - Added experimental support for the `NGA_gpm_local` glTF extension, for GPM 1.2 [#12204](https://github.com/CesiumGS/cesium/pull/12204) ##### Fixes :wrench: diff --git a/packages/engine/Source/Renderer/DrawCommand.js b/packages/engine/Source/Renderer/DrawCommand.js index 0b7335f05305..acdcaccc317e 100644 --- a/packages/engine/Source/Renderer/DrawCommand.js +++ b/packages/engine/Source/Renderer/DrawCommand.js @@ -43,6 +43,8 @@ function DrawCommand(options) { this._owner = options.owner; this._debugOverlappingFrustums = 0; this._pickId = options.pickId; + this._pickMetadataAllowed = options.pickMetadataAllowed === true; + this._pickedMetadataInfo = undefined; // Set initial flags. this._flags = 0; @@ -514,7 +516,7 @@ Object.defineProperties(DrawCommand.prototype, { * during the pick pass. * * @memberof DrawCommand.prototype - * @type {string} + * @type {string|undefined} * @default undefined */ pickId: { @@ -528,6 +530,44 @@ Object.defineProperties(DrawCommand.prototype, { } }, }, + + /** + * Whether metadata picking is allowed. + * + * This is essentially only set to `true` for draw commands that are + * part of a `ModelDrawCommand`, to check whether a derived command + * for metadata picking has to be created. + * + * @memberof DrawCommand.prototype + * @type {boolean} + * @default undefined + * @private + */ + pickMetadataAllowed: { + get: function () { + return this._pickMetadataAllowed; + }, + }, + + /** + * Information about picked metadata. + * + * @memberof DrawCommand.prototype + * @type {PickedMetadataInfo|undefined} + * @default undefined + */ + pickedMetadataInfo: { + get: function () { + return this._pickedMetadataInfo; + }, + set: function (value) { + if (this._pickedMetadataInfo !== value) { + this._pickedMetadataInfo = value; + this.dirty = true; + } + }, + }, + /** * Whether this command should be executed in the pick pass only. * @@ -593,6 +633,8 @@ DrawCommand.shallowClone = function (command, result) { result._owner = command._owner; result._debugOverlappingFrustums = command._debugOverlappingFrustums; result._pickId = command._pickId; + result._pickMetadataAllowed = command._pickMetadataAllowed; + result._pickedMetadataInfo = command._pickedMetadataInfo; result._flags = command._flags; result.dirty = true; diff --git a/packages/engine/Source/Scene/DerivedCommand.js b/packages/engine/Source/Scene/DerivedCommand.js index e0d7d2cb3de5..8d70e283ac6a 100644 --- a/packages/engine/Source/Scene/DerivedCommand.js +++ b/packages/engine/Source/Scene/DerivedCommand.js @@ -2,6 +2,8 @@ import defined from "../Core/defined.js"; import DrawCommand from "../Renderer/DrawCommand.js"; import RenderState from "../Renderer/RenderState.js"; import ShaderSource from "../Renderer/ShaderSource.js"; +import MetadataType from "./MetadataType.js"; +import MetadataPickingPipelineStage from "./Model/MetadataPickingPipelineStage.js"; /** * @private @@ -343,6 +345,227 @@ DerivedCommand.createPickDerivedCommand = function ( return result; }; +/** + * Replaces the value of the specified 'define' directive identifier + * with the given value. + * + * The given defines are the parts of the define directives that are + * stored in the `ShaderSource`. For example, the defines may be + * `["EXAMPLE", "EXAMPLE_VALUE 123"]` + * + * Calling `replaceDefine(defines, "EXAMPLE", 999)` will result in + * the defines being + * `["EXAMPLE 999", "EXAMPLE_VALUE 123"]` + * + * @param {string[]} defines The define directive identifiers + * @param {string} defineName The name (identifier) of the define directive + * @param {any} newDefineValue The new value whose string representation + * will become the token string for the define directive + * @private + */ +function replaceDefine(defines, defineName, newDefineValue) { + const n = defines.length; + for (let i = 0; i < n; i++) { + const define = defines[i]; + const tokens = define.trimStart().split(/\s+/); + if (tokens[0] === defineName) { + defines[i] = `${defineName} ${newDefineValue}`; + } + } +} + +/** + * Returns the component count for the given class property, or + * its array length if it is an array. + * + * This will be + * `[1, 2, 3, 4]` for `[SCALAR, VEC2, VEC3, VEC4`] types, + * or the array length if it is an array. + * + * @param {MetadataClassProperty} classProperty The class property + * @returns {number} The component count + * @private + */ +function getComponentCount(classProperty) { + if (!classProperty.isArray) { + return MetadataType.getComponentCount(classProperty.type); + } + return classProperty.arrayLength; +} + +/** + * Returns the type that the given class property has in a GLSL shader. + * + * It returns the same string as `PropertyTextureProperty.prototype.getGlslType` + * for a property texture property with the given class property + * + * @param {MetadataClassProperty} classProperty The class property + * @returns {string} The GLSL shader type string for the property + */ +function getGlslType(classProperty) { + const componentCount = getComponentCount(classProperty); + if (classProperty.normalized) { + if (componentCount === 1) { + return "float"; + } + return `vec${componentCount}`; + } + if (componentCount === 1) { + return "int"; + } + return `ivec${componentCount}`; +} + +/** + * Creates a new `ShaderProgram` from the given input that renders metadata + * values into the frame buffer, according to the given picked metadata info. + * + * This will update the `defines` of the fragment shader of the given shader + * program, by setting `METADATA_PICKING_ENABLED`, and updating the + * `METADATA_PICKING_VALUE_*` defines so that they reflect the components + * of the metadata that should be written into the RGBA (vec4) that + * ends up as the 'color' in the frame buffer. + * + * The RGBA values will eventually be converted back into an actual metadata + * value in `Picking.js`, by calling `MetadataPicking.decodeMetadataValues`. + * + * @param {Context} context The context + * @param {ShaderProgram} shaderProgram The shader program + * @param {PickedMetadataInfo} pickedMetadataInfo The picked metadata info + * @returns {ShaderProgram} The new shader program + * @private + */ +function getPickMetadataShaderProgram( + context, + shaderProgram, + pickedMetadataInfo, +) { + const schemaId = pickedMetadataInfo.schemaId; + const className = pickedMetadataInfo.className; + const propertyName = pickedMetadataInfo.propertyName; + const keyword = `pickMetadata-${schemaId}-${className}-${propertyName}`; + const shader = context.shaderCache.getDerivedShaderProgram( + shaderProgram, + keyword, + ); + if (defined(shader)) { + return shader; + } + + const classProperty = pickedMetadataInfo.classProperty; + const glslType = getGlslType(classProperty); + + // Define the components that will go into the output `metadataValues`. + // By default, all of them are 0.0. + const sourceValueStrings = ["0.0", "0.0", "0.0", "0.0"]; + const componentCount = getComponentCount(classProperty); + if (componentCount === 1) { + // When the property is a scalar, store its value directly + // in `metadataValues.x` + sourceValueStrings[0] = `float(value)`; + } else { + // When the property is an array, store the array elements + // in `metadataValues.x/y/z/w` + const components = ["x", "y", "z", "w"]; + for (let i = 0; i < componentCount; i++) { + const component = components[i]; + const valueString = `value.${component}`; + sourceValueStrings[i] = `float(${valueString})`; + } + } + + // Make sure that the `metadataValues` components are all in + // the range [0, 1] (which will result in RGBA components + // in [0, 255] during rendering) + if (!classProperty.normalized) { + for (let i = 0; i < componentCount; i++) { + sourceValueStrings[i] += " / 255.0"; + } + } + + const newDefines = shaderProgram.fragmentShaderSource.defines.slice(); + newDefines.push(MetadataPickingPipelineStage.METADATA_PICKING_ENABLED); + + // Replace the defines of the shader, using the type, property + // access, and value components that have been determined + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE, + glslType, + ); + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_STRING, + `metadata.${propertyName}`, + ); + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_X, + sourceValueStrings[0], + ); + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Y, + sourceValueStrings[1], + ); + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Z, + sourceValueStrings[2], + ); + replaceDefine( + newDefines, + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_W, + sourceValueStrings[3], + ); + + const newFragmentShaderSource = new ShaderSource({ + sources: shaderProgram.fragmentShaderSource.sources, + defines: newDefines, + }); + const newShader = context.shaderCache.createDerivedShaderProgram( + shaderProgram, + keyword, + { + vertexShaderSource: shaderProgram.vertexShaderSource, + fragmentShaderSource: newFragmentShaderSource, + attributeLocations: shaderProgram._attributeLocations, + }, + ); + return newShader; +} + +/** + * @private + */ +DerivedCommand.createPickMetadataDerivedCommand = function ( + scene, + command, + context, + result, +) { + if (!defined(result)) { + result = {}; + } + result.pickMetadataCommand = DrawCommand.shallowClone( + command, + result.pickMetadataCommand, + ); + + result.pickMetadataCommand.shaderProgram = getPickMetadataShaderProgram( + context, + command.shaderProgram, + command.pickedMetadataInfo, + ); + result.pickMetadataCommand.renderState = getPickRenderState( + scene, + command.renderState, + ); + result.shaderProgramId = command.shaderProgram.id; + + return result; +}; + function getHdrShaderProgram(context, shaderProgram) { const cachedShader = context.shaderCache.getDerivedShaderProgram( shaderProgram, diff --git a/packages/engine/Source/Scene/FrameState.js b/packages/engine/Source/Scene/FrameState.js index 5db2dee20264..f2b45b73c44c 100644 --- a/packages/engine/Source/Scene/FrameState.js +++ b/packages/engine/Source/Scene/FrameState.js @@ -421,6 +421,37 @@ function FrameState(context, creditDisplay, jobScheduler) { * @default 0.0 */ this.minimumTerrainHeight = 0.0; + + /** + * Whether metadata picking is currently in progress. + * + * This is set to `true` in the `Picking.pickMetadata` function, + * immediately before updating and executing the draw commands, + * and set back to `false` immediately afterwards. It will be + * used to determine whether the metadata picking draw commands + * should be executed, in the `Scene.executeCommand` function. + * + * @type {boolean} + * @default false + */ + this.pickingMetadata = false; + + /** + * Metadata picking information. + * + * This describes the metadata property that is supposed to be picked + * in a `Picking.pickMetadata` call. + * + * This is stored in the frame state and in the metadata picking draw + * commands. In the `Scene.updateDerivedCommands` call, it will be + * checked whether the instance that is stored in the frame state + * is different from the one in the draw command, and if necessary, + * the derived commands for metadata picking will be updated based + * on this information. + * + * @type {PickedMetadataInfo|undefined} + */ + this.pickedMetadataInfo = undefined; } /** diff --git a/packages/engine/Source/Scene/MetadataPicking.js b/packages/engine/Source/Scene/MetadataPicking.js new file mode 100644 index 000000000000..87f33a91d7d3 --- /dev/null +++ b/packages/engine/Source/Scene/MetadataPicking.js @@ -0,0 +1,305 @@ +import Cartesian2 from "../Core/Cartesian2.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Cartesian4 from "../Core/Cartesian4.js"; +import defined from "../Core/defined.js"; +import Matrix2 from "../Core/Matrix2.js"; +import Matrix3 from "../Core/Matrix3.js"; +import Matrix4 from "../Core/Matrix4.js"; +import RuntimeError from "../Core/RuntimeError.js"; +import MetadataComponentType from "./MetadataComponentType.js"; +import MetadataType from "./MetadataType.js"; + +/** + * Utility functions for metadata picking. + * + * These are used by the `Picking.pickMetadata` function to decode + * the metadata values that have been read from the frame buffer + * into the actual metadata values, according to the structure + * defined by the `MetadataClassProperty`. + * + * @private + */ +const MetadataPicking = {}; + +/** + * Returns the value at the specified index of the given data view, + * interpreting the data to have the given component type. + * + * @param {MetadataComponentType} componentType The `MetadataComponentType` + * @param {DataView} dataView The data view + * @param {number} index The index (byte offset) + * @returns {number|bigint|undefined} The value + * + * @private + */ +MetadataPicking.decodeRawMetadataValue = function ( + componentType, + dataView, + index, +) { + switch (componentType) { + case MetadataComponentType.INT8: + return dataView.getInt8(index); + case MetadataComponentType.UINT8: + return dataView.getUint8(index); + case MetadataComponentType.INT16: + return dataView.getInt16(index); + case MetadataComponentType.UINT16: + return dataView.getUint16(index); + case MetadataComponentType.INT32: + return dataView.getInt32(index); + case MetadataComponentType.UINT32: + return dataView.getUint32(index); + case MetadataComponentType.INT64: + return dataView.getBigInt64(index); + case MetadataComponentType.UINT64: + return dataView.getBigUint64(index); + case MetadataComponentType.FLOAT32: + return dataView.getFloat32(index); + case MetadataComponentType.FLOAT64: + return dataView.getFloat64(index); + } + throw new RuntimeError(`Invalid component type: ${componentType}`); +}; + +/** + * Decodes one component of a metadata value with the given property type + * from the given data view. + * + * This will decode one component (e.g. one entry of a SCALAR array, + * or one component of a VEC2 element). + * + * This will apply normalization to the raw component value if the given + * class property is 'normalized'. + * + * @param {MetadataClassProperty} classProperty The class property + * @param {DataView} dataView The data view containing the raw metadata values + * @param {number} dataViewOffset The byte offset within the data view from + * which the component should be read + * @returns {number|bigint|undefined} The metadata value component + */ +MetadataPicking.decodeRawMetadataValueComponent = function ( + classProperty, + dataView, + dataViewOffset, +) { + const componentType = classProperty.componentType; + const component = MetadataPicking.decodeRawMetadataValue( + componentType, + dataView, + dataViewOffset, + ); + if (classProperty.normalized) { + return MetadataComponentType.normalize(component, componentType); + } + return component; +}; + +/** + * Decodes one element of a metadata value with the given property type + * from the given data view. + * + * When the given class property is vector- or matrix typed, then the + * result will be an array, with a length that corresponds to the + * number of vector- or matrix components. + * + * Otherwise, it will be a single value. + * + * In any case, the return value will be the "raw" value, which does + * take into account normalization, but does NOT take into account + * default/noData value handling. + * + * @param {MetadataClassProperty} classProperty The metadata class property + * @param {DataView} dataView The data view containing the raw metadata values + * @param {number} elementIndex The index of the element. This is the index + * inside the array for array-typed properties, and 0 for non-array types. + * @returns {number|number[]|bigint|bigint[]|undefined} The decoded metadata value element + */ +MetadataPicking.decodeRawMetadataValueElement = function ( + classProperty, + dataView, + elementIndex, +) { + const componentType = classProperty.componentType; + const componentSizeInBytes = + MetadataComponentType.getSizeInBytes(componentType); + const type = classProperty.type; + const componentCount = MetadataType.getComponentCount(type); + const elementSizeInBytes = componentSizeInBytes * componentCount; + if (componentCount > 1) { + const result = Array(componentCount); + for (let i = 0; i < componentCount; i++) { + const offset = + elementIndex * elementSizeInBytes + i * componentSizeInBytes; + const component = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + offset, + ); + result[i] = component; + } + return result; + } + const offset = elementIndex * elementSizeInBytes; + const result = MetadataPicking.decodeRawMetadataValueComponent( + classProperty, + dataView, + offset, + ); + return result; +}; + +/** + * Decode the given raw values into the raw (array-based) form of + * a metadata property value. + * + * (For decoding to types like `CartesianN`, the `decodeMetadataValues` + * function can be used) + * + * The given values are a `Uint8Array` containing the RGBA + * values that have been read from the metadata picking + * frame buffer. They are assumed to contain the value for + * the given class property, as encoded by the + * `MetadataPickingPipelineStage` for metadata picking. + * + * When the given class property is an array, then (it has to be + * a fixed-length array, and) the result will be an array with + * the respective length. + * + * When the given class property is vector- or matrix typed, + * then the result will be an array, with a length that corresponds + * to the number of vector- or matrix components. + * + * (The case that the property is an array of vector- or matrix + * elements is not supported on the side of the general metadata + * shader infrastructure, but handled here nevertheless. For such + * an input, the result would be an array of arrays, with each + * element representing one of the vectors or matrices). + * + * In any case, the return value will be the "raw" value, which does + * take into account normalization, but does NOT take into account + * any offset/scale, or default/noData value handling. + * + * @param {MetadataClassProperty} classProperty The `MetadataClassProperty` + * @param {Uint8Array} rawPixelValues The raw values + * @returns {number|bigint|number[]|bigint[]|undefined} The value + * + * @private + */ +MetadataPicking.decodeRawMetadataValues = function ( + classProperty, + rawPixelValues, +) { + const dataView = new DataView( + rawPixelValues.buffer, + rawPixelValues.byteOffset, + rawPixelValues.byteLength, + ); + if (classProperty.isArray) { + const arrayLength = classProperty.arrayLength; + const result = Array(arrayLength); + for (let i = 0; i < arrayLength; i++) { + const element = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + i, + ); + result[i] = element; + } + return result; + } + const result = MetadataPicking.decodeRawMetadataValueElement( + classProperty, + dataView, + 0, + ); + return result; +}; + +/** + * Converts the given type into an object representation where appropriate. + * + * When the given type is `SCALAR`, `STRING`, `BOOLEAN`, or `ENUM`, or + * when the given value is `undefined`, then the given value will be + * returned. + * + * Otherwise, for the `VECn/MATn` types, the given value is assumed to be + * a numeric array, and is converted into the matching `CartesianN/MatrixN` + * value. + * + * @param {string} type The `ClassProperty` type + * @param {number|bigint|number[]|bigint[]|undefined} value The input value + * @returns {any} The object representation + */ +MetadataPicking.convertToObjectType = function (type, value) { + if (!defined(value)) { + return value; + } + if ( + type === MetadataType.SCALAR || + type === MetadataType.STRING || + type === MetadataType.BOOLEAN || + type === MetadataType.ENUM + ) { + return value; + } + const numbers = value.map((n) => Number(n)); + switch (type) { + case MetadataType.VEC2: + return Cartesian2.unpack(numbers, 0, new Cartesian2()); + case MetadataType.VEC3: + return Cartesian3.unpack(numbers, 0, new Cartesian3()); + case MetadataType.VEC4: + return Cartesian4.unpack(numbers, 0, new Cartesian3()); + case MetadataType.MAT2: + return Matrix2.unpack(numbers, 0, new Matrix2()); + case MetadataType.MAT3: + return Matrix3.unpack(numbers, 0, new Matrix3()); + case MetadataType.MAT4: + return Matrix4.unpack(numbers, 0, new Matrix4()); + } + // Should never happen: + return value; +}; + +/** + * Decode the given raw values into a metadata property value. + * + * This just converts the result of `decodeRawMetadataValues` + * from array-based types into object types like `CartesianN`. + * + * @param {MetadataClassProperty} classProperty The `MetadataClassProperty` + * @param {Uint8Array} rawPixelValues The raw values + * @returns {any} The value + * + * @private + */ +MetadataPicking.decodeMetadataValues = function ( + classProperty, + rawPixelValues, +) { + const arrayBasedResult = MetadataPicking.decodeRawMetadataValues( + classProperty, + rawPixelValues, + ); + if (classProperty.isArray) { + const arrayLength = classProperty.arrayLength; + const result = Array(arrayLength); + for (let i = 0; i < arrayLength; i++) { + const arrayBasedValue = arrayBasedResult[i]; + const objectBasedValue = MetadataPicking.convertToObjectType( + classProperty.type, + arrayBasedValue, + ); + result[i] = objectBasedValue; + } + return result; + } + const result = MetadataPicking.convertToObjectType( + classProperty.type, + arrayBasedResult, + ); + return result; +}; + +export default Object.freeze(MetadataPicking); diff --git a/packages/engine/Source/Scene/Model/MetadataPickingPipelineStage.js b/packages/engine/Source/Scene/Model/MetadataPickingPipelineStage.js new file mode 100644 index 000000000000..9603652b2f4e --- /dev/null +++ b/packages/engine/Source/Scene/Model/MetadataPickingPipelineStage.js @@ -0,0 +1,96 @@ +import ShaderDestination from "../../Renderer/ShaderDestination.js"; + +/** + * The MetadataPickingPipelineStage is inserting the + * metadataPickingStage function into the shader code, + * including the 'defines' that will be filled with + * the proper values for metadata picking in the + * 'DerivedCommands' + * + * @namespace MetadataPickingPipelineStage + * @private + */ +const MetadataPickingPipelineStage = { + name: "MetadataPickingPipelineStage", // Helps with debugging + + // The identifiers for 'define' directives that are inserted into the + // shader code. The values of these defines will be be assigned + // in the `DerivedCommands` class when a derived command for metadata + // picking is created. + METADATA_PICKING_ENABLED: "METADATA_PICKING_ENABLED", + METADATA_PICKING_VALUE_TYPE: "METADATA_PICKING_VALUE_TYPE", + METADATA_PICKING_VALUE_STRING: "METADATA_PICKING_VALUE_STRING", + METADATA_PICKING_VALUE_COMPONENT_X: "METADATA_PICKING_VALUE_COMPONENT_X", + METADATA_PICKING_VALUE_COMPONENT_Y: "METADATA_PICKING_VALUE_COMPONENT_Y", + METADATA_PICKING_VALUE_COMPONENT_Z: "METADATA_PICKING_VALUE_COMPONENT_Z", + METADATA_PICKING_VALUE_COMPONENT_W: "METADATA_PICKING_VALUE_COMPONENT_W", +}; + +/** + * Process a primitive. This modifies the following parts of the render resources: + * + * + * @param {PrimitiveRenderResources} renderResources The render resources for this primitive. + * @param {ModelComponents.Primitive} primitive The primitive. + * @param {FrameState} frameState The frame state. + */ +MetadataPickingPipelineStage.process = function ( + renderResources, + primitive, + frameState, +) { + const shaderBuilder = renderResources.shaderBuilder; + + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE, + "float", + ShaderDestination.FRAGMENT, + ); + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_STRING, + "0.0", + ShaderDestination.FRAGMENT, + ); + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_X, + "0.0", + ShaderDestination.FRAGMENT, + ); + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Y, + "0.0", + ShaderDestination.FRAGMENT, + ); + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Z, + "0.0", + ShaderDestination.FRAGMENT, + ); + shaderBuilder.addDefine( + MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_W, + "0.0", + ShaderDestination.FRAGMENT, + ); + + shaderBuilder.addFunction( + "metadataPickingStage", + "void metadataPickingStage(Metadata metadata, MetadataClass metadataClass, inout vec4 metadataValues)", + ShaderDestination.FRAGMENT, + ); + + shaderBuilder.addFunctionLines( + "metadataPickingStage", + [ + `${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE} value = ${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE}(${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_STRING});`, + `metadataValues.x = ${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_X};`, + `metadataValues.y = ${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Y};`, + `metadataValues.z = ${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Z};`, + `metadataValues.w = ${MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_W};`, + ], + ShaderDestination.FRAGMENT, + ); +}; + +export default MetadataPickingPipelineStage; diff --git a/packages/engine/Source/Scene/Model/buildDrawCommand.js b/packages/engine/Source/Scene/Model/ModelDrawCommands.js similarity index 76% rename from packages/engine/Source/Scene/Model/buildDrawCommand.js rename to packages/engine/Source/Scene/Model/ModelDrawCommands.js index b0acbf980623..7d609ac36c82 100644 --- a/packages/engine/Source/Scene/Model/buildDrawCommand.js +++ b/packages/engine/Source/Scene/Model/ModelDrawCommands.js @@ -1,18 +1,28 @@ import BoundingSphere from "../../Core/BoundingSphere.js"; import clone from "../../Core/clone.js"; import defined from "../../Core/defined.js"; -import DeveloperError from "../../Core/DeveloperError.js"; import Matrix4 from "../../Core/Matrix4.js"; import DrawCommand from "../../Renderer/DrawCommand.js"; import RenderState from "../../Renderer/RenderState.js"; -import VertexArray from "../../Renderer/VertexArray.js"; -import ModelFS from "../../Shaders/Model/ModelFS.js"; -import ModelVS from "../../Shaders/Model/ModelVS.js"; import SceneMode from "../SceneMode.js"; import ShadowMode from "../ShadowMode.js"; import ClassificationModelDrawCommand from "./ClassificationModelDrawCommand.js"; -import ModelUtility from "./ModelUtility.js"; import ModelDrawCommand from "./ModelDrawCommand.js"; +import VertexArray from "../../Renderer/VertexArray.js"; +import ModelVS from "../../Shaders/Model/ModelVS.js"; +import ModelFS from "../../Shaders/Model/ModelFS.js"; +import ModelUtility from "./ModelUtility.js"; +import DeveloperError from "../../Core/DeveloperError.js"; + +/** + * Internal functions to build draw commands for models. + * + * (The core of these functions was taken from `buildDrawCommand.jsĀ“, + * as of commit hash 7b93161da1cc03bdc796b204e7aa51fb7acebf04) + * + * @private + */ +function ModelDrawCommands() {} /** * Builds the {@link ModelDrawCommand} for a {@link ModelRuntimePrimitive} @@ -21,16 +31,77 @@ import ModelDrawCommand from "./ModelDrawCommand.js"; * * @param {PrimitiveRenderResources} primitiveRenderResources The render resources for a primitive. * @param {FrameState} frameState The frame state for creating GPU resources. - * * @returns {ModelDrawCommand|ClassificationModelDrawCommand} The generated ModelDrawCommand or ClassificationModelDrawCommand. * * @private */ -function buildDrawCommand(primitiveRenderResources, frameState) { +ModelDrawCommands.buildModelDrawCommand = function ( + primitiveRenderResources, + frameState, +) { const shaderBuilder = primitiveRenderResources.shaderBuilder; + const shaderProgram = createShaderProgram( + primitiveRenderResources, + shaderBuilder, + frameState, + ); + + const command = buildDrawCommandForModel( + primitiveRenderResources, + shaderProgram, + frameState, + ); + + const model = primitiveRenderResources.model; + const hasClassification = defined(model.classificationType); + if (hasClassification) { + return new ClassificationModelDrawCommand({ + primitiveRenderResources: primitiveRenderResources, + command: command, + }); + } + + return new ModelDrawCommand({ + primitiveRenderResources: primitiveRenderResources, + command: command, + }); +}; + +/** + * @private + */ +function createShaderProgram( + primitiveRenderResources, + shaderBuilder, + frameState, +) { shaderBuilder.addVertexLines(ModelVS); shaderBuilder.addFragmentLines(ModelFS); + const model = primitiveRenderResources.model; + const shaderProgram = shaderBuilder.buildShaderProgram(frameState.context); + model._pipelineResources.push(shaderProgram); + return shaderProgram; +} + +/** + * Builds the {@link DrawCommand} that serves as the basis for either creating + * a {@link ModelDrawCommand} or a {@link ModelRuntimePrimitive} + * + * @param {PrimitiveRenderResources} primitiveRenderResources The render resources for a primitive. + * @param {ShaderProgram} shaderProgram The shader program + * @param {FrameState} frameState The frame state for creating GPU resources. + * + * @returns {DrawCommand} The generated DrawCommand, to be passed to + * the ModelDrawCommand or ClassificationModelDrawCommand + * + * @private + */ +function buildDrawCommandForModel( + primitiveRenderResources, + shaderProgram, + frameState, +) { const indexBuffer = getIndexBuffer(primitiveRenderResources); const vertexArray = new VertexArray({ @@ -42,9 +113,6 @@ function buildDrawCommand(primitiveRenderResources, frameState) { const model = primitiveRenderResources.model; model._pipelineResources.push(vertexArray); - const shaderProgram = shaderBuilder.buildShaderProgram(frameState.context); - model._pipelineResources.push(shaderProgram); - const pass = primitiveRenderResources.alphaOptions.pass; const sceneGraph = model.sceneGraph; @@ -74,7 +142,6 @@ function buildDrawCommand(primitiveRenderResources, frameState) { boundingSphere = BoundingSphere.transform( primitiveRenderResources.boundingSphere, modelMatrix, - primitiveRenderResources.boundingSphere, ); } @@ -115,24 +182,14 @@ function buildDrawCommand(primitiveRenderResources, frameState) { count: primitiveRenderResources.count, owner: model, pickId: pickId, + pickMetadataAllowed: true, instanceCount: primitiveRenderResources.instanceCount, primitiveType: primitiveRenderResources.primitiveType, debugShowBoundingVolume: model.debugShowBoundingVolume, castShadows: castShadows, receiveShadows: receiveShadows, }); - - if (hasClassification) { - return new ClassificationModelDrawCommand({ - primitiveRenderResources: primitiveRenderResources, - command: command, - }); - } - - return new ModelDrawCommand({ - primitiveRenderResources: primitiveRenderResources, - command: command, - }); + return command; } /** @@ -158,4 +215,4 @@ function getIndexBuffer(primitiveRenderResources) { return indices.buffer; } -export default buildDrawCommand; +export default ModelDrawCommands; diff --git a/packages/engine/Source/Scene/Model/ModelMatrixUpdateStage.js b/packages/engine/Source/Scene/Model/ModelMatrixUpdateStage.js index 4b34a2468dd7..dbc23e758d6b 100644 --- a/packages/engine/Source/Scene/Model/ModelMatrixUpdateStage.js +++ b/packages/engine/Source/Scene/Model/ModelMatrixUpdateStage.js @@ -48,6 +48,23 @@ ModelMatrixUpdateStage.update = function (runtimeNode, sceneGraph, frameState) { } }; +/** + * Update the modelMatrix and cullFrace of the given draw command. + * + * @private + */ +function updateDrawCommand(drawCommand, modelMatrix, transformToRoot) { + drawCommand.modelMatrix = Matrix4.multiplyTransformation( + modelMatrix, + transformToRoot, + drawCommand.modelMatrix, + ); + drawCommand.cullFace = ModelUtility.getCullFace( + drawCommand.modelMatrix, + drawCommand.primitiveType, + ); +} + /** * Recursively update all child runtime nodes and their runtime primitives. * @@ -73,15 +90,10 @@ function updateRuntimeNode( const primitivesLength = runtimeNode.runtimePrimitives.length; for (i = 0; i < primitivesLength; i++) { const runtimePrimitive = runtimeNode.runtimePrimitives[i]; - const drawCommand = runtimePrimitive.drawCommand; - drawCommand.modelMatrix = Matrix4.multiplyTransformation( + updateDrawCommand( + runtimePrimitive.drawCommand, modelMatrix, transformToRoot, - drawCommand.modelMatrix, - ); - drawCommand.cullFace = ModelUtility.getCullFace( - drawCommand.modelMatrix, - drawCommand.primitiveType, ); } diff --git a/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js b/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js index e5568acb35e4..dca730224b55 100644 --- a/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js +++ b/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js @@ -14,6 +14,7 @@ import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js"; import GeometryPipelineStage from "./GeometryPipelineStage.js"; import LightingPipelineStage from "./LightingPipelineStage.js"; import MaterialPipelineStage from "./MaterialPipelineStage.js"; +import MetadataPickingPipelineStage from "./MetadataPickingPipelineStage.js"; import MetadataPipelineStage from "./MetadataPipelineStage.js"; import ModelUtility from "./ModelUtility.js"; import MorphTargetsPipelineStage from "./MorphTargetsPipelineStage.js"; @@ -277,6 +278,7 @@ ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) { // are declared to avoid compilation errors. pipelineStages.push(FeatureIdPipelineStage); pipelineStages.push(MetadataPipelineStage); + pipelineStages.push(MetadataPickingPipelineStage); if (featureIdFlags.hasPropertyTable) { pipelineStages.push(SelectedFeatureIdPipelineStage); diff --git a/packages/engine/Source/Scene/Model/ModelSceneGraph.js b/packages/engine/Source/Scene/Model/ModelSceneGraph.js index aac9461e7981..11e3918c0589 100644 --- a/packages/engine/Source/Scene/Model/ModelSceneGraph.js +++ b/packages/engine/Source/Scene/Model/ModelSceneGraph.js @@ -7,7 +7,6 @@ import Matrix4 from "../../Core/Matrix4.js"; import Transforms from "../../Core/Transforms.js"; import SceneMode from "../SceneMode.js"; import SplitDirection from "../SplitDirection.js"; -import buildDrawCommand from "./buildDrawCommand.js"; import TilesetPipelineStage from "./TilesetPipelineStage.js"; import AtmospherePipelineStage from "./AtmospherePipelineStage.js"; import ImageBasedLightingPipelineStage from "./ImageBasedLightingPipelineStage.js"; @@ -26,6 +25,7 @@ import ModelSplitterPipelineStage from "./ModelSplitterPipelineStage.js"; import ModelType from "./ModelType.js"; import NodeRenderResources from "./NodeRenderResources.js"; import PrimitiveRenderResources from "./PrimitiveRenderResources.js"; +import ModelDrawCommands from "./ModelDrawCommands.js"; /** * An in memory representation of the scene graph for a {@link Model} @@ -560,7 +560,7 @@ ModelSceneGraph.prototype.buildDrawCommands = function (frameState) { modelPositionMax, ); - const drawCommand = buildDrawCommand( + const drawCommand = ModelDrawCommands.buildModelDrawCommand( primitiveRenderResources, frameState, ); @@ -922,7 +922,6 @@ function pushPrimitiveDrawCommands(runtimePrimitive, options) { const passes = frameState.passes; const silhouetteCommands = scratchSilhouetteCommands; const primitiveDrawCommand = runtimePrimitive.drawCommand; - primitiveDrawCommand.pushCommands(frameState, frameState.commandList); // If a model has silhouettes, the commands that draw the silhouettes for diff --git a/packages/engine/Source/Scene/Model/PrimitiveRenderResources.js b/packages/engine/Source/Scene/Model/PrimitiveRenderResources.js index 6d2036079814..b354b9b51958 100644 --- a/packages/engine/Source/Scene/Model/PrimitiveRenderResources.js +++ b/packages/engine/Source/Scene/Model/PrimitiveRenderResources.js @@ -287,7 +287,7 @@ function PrimitiveRenderResources(nodeRenderResources, runtimePrimitive) { * The shader variable to use for picking. If picking is enabled, this value * is set by PickingPipelineStage. * - * @type {string} + * @type {string|undefined} * * @private */ diff --git a/packages/engine/Source/Scene/Model/SelectedFeatureIdPipelineStage.js b/packages/engine/Source/Scene/Model/SelectedFeatureIdPipelineStage.js index 8c6144cd7c63..6a95e14384c3 100644 --- a/packages/engine/Source/Scene/Model/SelectedFeatureIdPipelineStage.js +++ b/packages/engine/Source/Scene/Model/SelectedFeatureIdPipelineStage.js @@ -17,10 +17,6 @@ const SelectedFeatureIdPipelineStage = { STRUCT_ID_SELECTED_FEATURE: "SelectedFeature", STRUCT_NAME_SELECTED_FEATURE: "SelectedFeature", - FUNCTION_ID_FEATURE_VARYINGS_VS: "updateFeatureStructVS", - FUNCTION_ID_FEATURE_VARYINGS_FS: "updateFeatureStructFS", - FUNCTION_SIGNATURE_UPDATE_FEATURE: - "void updateFeatureStruct(inout SelectedFeature feature)", }; /** diff --git a/packages/engine/Source/Scene/PickFramebuffer.js b/packages/engine/Source/Scene/PickFramebuffer.js index e7f2fb08b03a..69645bb7cc50 100644 --- a/packages/engine/Source/Scene/PickFramebuffer.js +++ b/packages/engine/Source/Scene/PickFramebuffer.js @@ -49,7 +49,7 @@ PickFramebuffer.prototype.begin = function (screenSpaceRectangle, viewport) { return this._passState; }; -const colorScratch = new Color(); +const colorScratchForPickFramebuffer = new Color(); /** * Return the picked object rendered within a given rectangle. @@ -94,12 +94,20 @@ PickFramebuffer.prototype.end = function (screenSpaceRectangle) { ) { const index = 4 * ((halfHeight - y) * width + x + halfWidth); - colorScratch.red = Color.byteToFloat(pixels[index]); - colorScratch.green = Color.byteToFloat(pixels[index + 1]); - colorScratch.blue = Color.byteToFloat(pixels[index + 2]); - colorScratch.alpha = Color.byteToFloat(pixels[index + 3]); - - const object = context.getObjectByPickColor(colorScratch); + colorScratchForPickFramebuffer.red = Color.byteToFloat(pixels[index]); + colorScratchForPickFramebuffer.green = Color.byteToFloat( + pixels[index + 1], + ); + colorScratchForPickFramebuffer.blue = Color.byteToFloat( + pixels[index + 2], + ); + colorScratchForPickFramebuffer.alpha = Color.byteToFloat( + pixels[index + 3], + ); + + const object = context.getObjectByPickColor( + colorScratchForPickFramebuffer, + ); if (defined(object)) { return object; } @@ -121,13 +129,17 @@ PickFramebuffer.prototype.end = function (screenSpaceRectangle) { }; /** - * Return voxel tile and sample information as rendered by a pickVoxel pass, - * within a given rectangle. + * Return a typed array containing the RGBA (byte) components of the + * pixel that is at the center of the given rectangle. + * + * This may, for example, be voxel tile and sample information as rendered + * by a pickVoxel pass, within a given rectangle. Or it may be the result + * of a metadata picking rendering pass. * * @param {BoundingRectangle} screenSpaceRectangle - * @returns {TypedArray} + * @returns {Uint8Array} The RGBA components */ -PickFramebuffer.prototype.readVoxelInfo = function (screenSpaceRectangle) { +PickFramebuffer.prototype.readCenterPixel = function (screenSpaceRectangle) { const width = defaultValue(screenSpaceRectangle.width, 1.0); const height = defaultValue(screenSpaceRectangle.height, 1.0); diff --git a/packages/engine/Source/Scene/PickedMetadataInfo.js b/packages/engine/Source/Scene/PickedMetadataInfo.js new file mode 100644 index 000000000000..1e8de1902b17 --- /dev/null +++ b/packages/engine/Source/Scene/PickedMetadataInfo.js @@ -0,0 +1,39 @@ +/** + * Information about metadata that is supposed to be picked. + * + * This is initialized in the `Scene.pickMetadata` function, and passed to + * the `FrameState`. It is used to configure the draw commands that render + * the metadata values of an object into the picking frame buffer. The + * raw values are read from that buffer, and are then translated back into + * proper metadata values in `Picking.pickMetadata`, using the structural + * information about the metadata `classProperty` that is stored here. + * + * @private + */ +function PickedMetadataInfo(schemaId, className, propertyName, classProperty) { + /** + * The optional ID of the metadata schema + * + * @type {string|undefined} + */ + this.schemaId = schemaId; + /** + * The name of the metadata class + * + * @type {string} + */ + this.className = className; + /** + * The name of the metadata property + * + * @type {string} + */ + this.propertyName = propertyName; + /** + * The optional ID of the metadata schema + * + * @type {MetadataClassProperty} + */ + this.classProperty = classProperty; +} +export default PickedMetadataInfo; diff --git a/packages/engine/Source/Scene/Picking.js b/packages/engine/Source/Scene/Picking.js index 3d742f0db973..fc38a4a90955 100644 --- a/packages/engine/Source/Scene/Picking.js +++ b/packages/engine/Source/Scene/Picking.js @@ -19,6 +19,7 @@ import Camera from "./Camera.js"; import Cesium3DTileFeature from "./Cesium3DTileFeature.js"; import Cesium3DTilePass from "./Cesium3DTilePass.js"; import Cesium3DTilePassState from "./Cesium3DTilePassState.js"; +import MetadataPicking from "./MetadataPicking.js"; import PickDepth from "./PickDepth.js"; import PrimitiveCollection from "./PrimitiveCollection.js"; import SceneMode from "./SceneMode.js"; @@ -224,18 +225,43 @@ function getPickCullingVolume( ); } -// pick rectangle width and height, assumed odd -let scratchRectangleWidth = 3.0; -let scratchRectangleHeight = 3.0; -let scratchRectangle = new BoundingRectangle( - 0.0, - 0.0, - scratchRectangleWidth, - scratchRectangleHeight, -); +// Pick position and rectangle, used in all picking functions, +// filled in computePickingDrawingBufferRectangle and passed +// the the FrameBuffer begin/end methods +const scratchRectangle = new BoundingRectangle(0.0, 0.0, 3.0, 3.0); const scratchPosition = new Cartesian2(); + +// Dummy color that is passed to updateAndExecuteCommands in +// all picking functions, used as the "background color" const scratchColorZero = new Color(0.0, 0.0, 0.0, 0.0); +/** + * Compute the rectangle that describes the part of the drawing buffer + * that is relevant for picking. + * + * @param {number} drawingBufferHeight The height of the drawing buffer + * @param {Cartesian2} position The position inside the drawing buffer + * @param {number|undefined} width The width of the rectangle, assumed to + * be an odd integer number, default : 3.0 + * @param {number|undefined} height The height of the rectangle. If unspecified, + * height will default to the value of width + * @param {BoundingRectangle} result The result rectangle + * @returns {BoundingRectangle} The result rectangle + */ +function computePickingDrawingBufferRectangle( + drawingBufferHeight, + position, + width, + height, + result, +) { + result.width = defaultValue(width, 3.0); + result.height = defaultValue(height, result.width); + result.x = position.x - (result.width - 1.0) * 0.5; + result.y = drawingBufferHeight - position.y - (result.height - 1.0) * 0.5; + return result; +} + /** * Returns an object with a primitive property that contains the first (top) primitive in the scene * at a particular window coordinate or undefined if nothing is at the location. Other properties may @@ -254,9 +280,6 @@ Picking.prototype.pick = function (scene, windowPosition, width, height) { Check.defined("windowPosition", windowPosition); //>>includeEnd('debug'); - scratchRectangleWidth = defaultValue(width, 3.0); - scratchRectangleHeight = defaultValue(height, scratchRectangleWidth); - const { context, frameState, defaultView } = scene; const { viewport, pickFramebuffer } = defaultView; @@ -275,6 +298,13 @@ Picking.prototype.pick = function (scene, windowPosition, width, height) { windowPosition, scratchPosition, ); + const drawingBufferRectangle = computePickingDrawingBufferRectangle( + context.drawingBufferHeight, + drawingBufferPosition, + width, + height, + scratchRectangle, + ); scene.jobScheduler.disableThisFrame(); @@ -282,8 +312,8 @@ Picking.prototype.pick = function (scene, windowPosition, width, height) { frameState.cullingVolume = getPickCullingVolume( scene, drawingBufferPosition, - scratchRectangleWidth, - scratchRectangleHeight, + drawingBufferRectangle.width, + drawingBufferRectangle.height, viewport, ); frameState.invertClassification = false; @@ -294,20 +324,12 @@ Picking.prototype.pick = function (scene, windowPosition, width, height) { scene.updateEnvironment(); - scratchRectangle.x = - drawingBufferPosition.x - (scratchRectangleWidth - 1.0) * 0.5; - scratchRectangle.y = - scene.drawingBufferHeight - - drawingBufferPosition.y - - (scratchRectangleHeight - 1.0) * 0.5; - scratchRectangle.width = scratchRectangleWidth; - scratchRectangle.height = scratchRectangleHeight; - passState = pickFramebuffer.begin(scratchRectangle, viewport); + passState = pickFramebuffer.begin(drawingBufferRectangle, viewport); scene.updateAndExecuteCommands(passState, scratchColorZero); scene.resolveFramebuffers(passState); - const object = pickFramebuffer.end(scratchRectangle); + const object = pickFramebuffer.end(drawingBufferRectangle); context.endFrame(); return object; }; @@ -333,9 +355,6 @@ Picking.prototype.pickVoxelCoordinate = function ( Check.defined("windowPosition", windowPosition); //>>includeEnd('debug'); - scratchRectangleWidth = defaultValue(width, 3.0); - scratchRectangleHeight = defaultValue(height, scratchRectangleWidth); - const { context, frameState, defaultView } = scene; const { viewport, pickFramebuffer } = defaultView; @@ -354,6 +373,13 @@ Picking.prototype.pickVoxelCoordinate = function ( windowPosition, scratchPosition, ); + const drawingBufferRectangle = computePickingDrawingBufferRectangle( + context.drawingBufferHeight, + drawingBufferPosition, + width, + height, + scratchRectangle, + ); scene.jobScheduler.disableThisFrame(); @@ -361,8 +387,8 @@ Picking.prototype.pickVoxelCoordinate = function ( frameState.cullingVolume = getPickCullingVolume( scene, drawingBufferPosition, - scratchRectangleWidth, - scratchRectangleHeight, + drawingBufferRectangle.width, + drawingBufferRectangle.height, viewport, ); frameState.invertClassification = false; @@ -373,24 +399,141 @@ Picking.prototype.pickVoxelCoordinate = function ( scene.updateEnvironment(); - scratchRectangle.x = - drawingBufferPosition.x - (scratchRectangleWidth - 1.0) * 0.5; - scratchRectangle.y = - scene.drawingBufferHeight - - drawingBufferPosition.y - - (scratchRectangleHeight - 1.0) * 0.5; - scratchRectangle.width = scratchRectangleWidth; - scratchRectangle.height = scratchRectangleHeight; - passState = pickFramebuffer.begin(scratchRectangle, viewport); + passState = pickFramebuffer.begin(drawingBufferRectangle, viewport); scene.updateAndExecuteCommands(passState, scratchColorZero); scene.resolveFramebuffers(passState); - const voxelInfo = pickFramebuffer.readVoxelInfo(scratchRectangle); + const voxelInfo = pickFramebuffer.readCenterPixel(drawingBufferRectangle); context.endFrame(); return voxelInfo; }; +/** + * Pick a metadata value at the given window position. + * + * The given `pickedMetadataInfo` defines the metadata value that is + * supposed to be picked. + * + * The return type will depend on the type of the metadata property + * that is picked. Given the current limitations of the types that + * are supported for metadata picking, the return type will be one + * of the following: + * + * - For `SCALAR`, the return type will be a `number` + * - For `SCALAR` arrays, the return type will be a `number[]` + * - For `VEC2`, the return type will be a `Cartesian2` + * - For `VEC3`, the return type will be a `Cartesian3` + * - For `VEC4`, the return type will be a `Cartesian4` + * + * @param {Cartesian2} windowPosition Window coordinates to perform picking on. + * @param {PickedMetadataInfo} pickedMetadataInfo Information about the picked metadata. + * @returns {any} The metadata values + * + * @private + */ +Picking.prototype.pickMetadata = function ( + scene, + windowPosition, + pickedMetadataInfo, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("windowPosition", windowPosition); + Check.typeOf.object("pickedMetadataInfo", pickedMetadataInfo); + //>>includeEnd('debug'); + + const { context, frameState, defaultView } = scene; + const { viewport, pickFramebuffer } = defaultView; + + scene.view = defaultView; + + viewport.x = 0; + viewport.y = 0; + viewport.width = context.drawingBufferWidth; + viewport.height = context.drawingBufferHeight; + + let passState = defaultView.passState; + passState.viewport = BoundingRectangle.clone(viewport, passState.viewport); + + const drawingBufferPosition = SceneTransforms.transformWindowToDrawingBuffer( + scene, + windowPosition, + scratchPosition, + ); + const drawingBufferRectangle = computePickingDrawingBufferRectangle( + context.drawingBufferHeight, + drawingBufferPosition, + 1.0, + 1.0, + scratchRectangle, + ); + + scene.jobScheduler.disableThisFrame(); + + scene.updateFrameState(); + frameState.cullingVolume = getPickCullingVolume( + scene, + drawingBufferPosition, + drawingBufferRectangle.width, + drawingBufferRectangle.height, + viewport, + ); + frameState.invertClassification = false; + + frameState.passes.pick = true; + frameState.tilesetPassState = pickTilesetPassState; + + // Insert the information about the picked metadata property + // into the frame state, so that the `Scene.updateDerivedCommands` + // call can detect any changes in the picked metadata description, + // and update the derived commands for the new picked metadata + // property + frameState.pickingMetadata = true; + frameState.pickedMetadataInfo = pickedMetadataInfo; + context.uniformState.update(frameState); + + scene.updateEnvironment(); + + passState = pickFramebuffer.begin(drawingBufferRectangle, viewport); + + scene.updateAndExecuteCommands(passState, scratchColorZero); + + // When OIT is enabled, then the resolveFrameBuffers function + // will juggle around several frame buffers, and eventually use + // the "environmentState.originalFramebuffer" instead of the + // picking frame buffer. Skipping a million questions, just + // switch OIT off here: + const oldOIT = scene._environmentState.useOIT; + scene._environmentState.useOIT = false; + scene.resolveFramebuffers(passState); + scene._environmentState.useOIT = oldOIT; + + const rawMetadataPixel = pickFramebuffer.readCenterPixel( + drawingBufferRectangle, + ); + context.endFrame(); + + frameState.pickingMetadata = false; + + const metadataValue = MetadataPicking.decodeMetadataValues( + pickedMetadataInfo.classProperty, + rawMetadataPixel, + ); + + return metadataValue; +}; + +/** + * @typedef {object} PickedMetadataInfo + * + * Information about metadata that is supposed to be picked + * + * @property {string|undefined} schemaId The optional ID of the metadata schema + * @property {string} className The name of the metadata class + * @property {string} propertyName The name of the metadata property + * @property {MetadataClassProperty} classProperty The metadata class property + */ + function renderTranslucentDepthForPick(scene, drawingBufferPosition) { // PERFORMANCE_IDEA: render translucent only and merge with the previous frame const { defaultView, context, frameState, environmentState } = scene; @@ -828,9 +971,15 @@ function getRayIntersection( updateOffscreenCameraFromRay(picking, ray, width, view.camera); - scratchRectangle = BoundingRectangle.clone(view.viewport, scratchRectangle); + const drawingBufferRectangle = BoundingRectangle.clone( + view.viewport, + scratchRectangle, + ); - const passState = view.pickFramebuffer.begin(scratchRectangle, view.viewport); + const passState = view.pickFramebuffer.begin( + drawingBufferRectangle, + view.viewport, + ); scene.jobScheduler.disableThisFrame(); @@ -852,7 +1001,7 @@ function getRayIntersection( scene.resolveFramebuffers(passState); let position; - const object = view.pickFramebuffer.end(scratchRectangle); + const object = view.pickFramebuffer.end(drawingBufferRectangle); if (scene.context.depthTexture) { const { frustumCommandsList } = view; diff --git a/packages/engine/Source/Scene/PropertyTextureProperty.js b/packages/engine/Source/Scene/PropertyTextureProperty.js index 9c1ed66239b9..765c2507313e 100644 --- a/packages/engine/Source/Scene/PropertyTextureProperty.js +++ b/packages/engine/Source/Scene/PropertyTextureProperty.js @@ -4,6 +4,7 @@ import defined from "../Core/defined.js"; import GltfLoaderUtil from "./GltfLoaderUtil.js"; import MetadataType from "./MetadataType.js"; import MetadataComponentType from "./MetadataComponentType.js"; +import oneTimeWarning from "../Core/oneTimeWarning.js"; /** * A property in a property texture. @@ -185,20 +186,48 @@ PropertyTextureProperty.prototype.isGpuCompatible = function () { if (classProperty.isArray) { // only support arrays of 1-4 UINT8 scalars (normalized or unnormalized) - return ( - !classProperty.isVariableLengthArray && - classProperty.arrayLength <= 4 && - type === MetadataType.SCALAR && - componentType === MetadataComponentType.UINT8 - ); + if (classProperty.isVariableLengthArray) { + oneTimeWarning( + `Property texture property ${classProperty.id} is a variable-length array, which is not supported`, + ); + return false; + } + if (classProperty.arrayLength > 4) { + oneTimeWarning( + `Property texture property ${classProperty.id} is an array of length ${classProperty.arrayLength}, but may have at most a length of 4`, + ); + return false; + } + if (type !== MetadataType.SCALAR) { + oneTimeWarning( + `Property texture property ${classProperty.id} is an array of type ${type}, but only SCALAR is supported`, + ); + return false; + } + if (componentType !== MetadataComponentType.UINT8) { + oneTimeWarning( + `Property texture property ${classProperty.id} is an array with component type ${componentType}, but only UINT8 is supported`, + ); + return false; + } + return true; } if (MetadataType.isVectorType(type) || type === MetadataType.SCALAR) { - return componentType === MetadataComponentType.UINT8; + if (componentType !== MetadataComponentType.UINT8) { + oneTimeWarning( + `Property texture property ${classProperty.id} has component type ${componentType}, but only UINT8 is supported`, + ); + return false; + } + return true; } // For this initial implementation, only UINT8-based properties // are supported. + oneTimeWarning( + `Property texture property ${classProperty.id} has an unsupported type`, + ); return false; }; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 7fc05b1b3dd1..e751dda94087 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -77,6 +77,8 @@ import View from "./View.js"; import DebugInspector from "./DebugInspector.js"; import VoxelCell from "./VoxelCell.js"; import VoxelPrimitive from "./VoxelPrimitive.js"; +import getMetadataClassProperty from "./getMetadataClassProperty.js"; +import PickedMetadataInfo from "./PickedMetadataInfo.js"; const requestRenderAfterFrame = function (scene) { return function () { @@ -1721,6 +1723,23 @@ Scene.prototype.getCompressedTextureFormatSupported = function (format) { ); }; +function pickedMetadataInfoChanged(command, frameState) { + const oldPickedMetadataInfo = command.pickedMetadataInfo; + const newPickedMetadataInfo = frameState.pickedMetadataInfo; + if (oldPickedMetadataInfo?.schemaId !== newPickedMetadataInfo?.schemaId) { + return true; + } + if (oldPickedMetadataInfo?.className !== newPickedMetadataInfo?.className) { + return true; + } + if ( + oldPickedMetadataInfo?.propertyName !== newPickedMetadataInfo?.propertyName + ) { + return true; + } + return false; +} + function updateDerivedCommands(scene, command, shadowsDirty) { const frameState = scene._frameState; const context = scene._context; @@ -1737,7 +1756,18 @@ function updateDerivedCommands(scene, command, shadowsDirty) { derivedCommands.picking, ); } - + if (frameState.pickingMetadata && command.pickMetadataAllowed) { + command.pickedMetadataInfo = frameState.pickedMetadataInfo; + if (defined(command.pickedMetadataInfo)) { + derivedCommands.pickingMetadata = + DerivedCommand.createPickMetadataDerivedCommand( + scene, + command, + context, + derivedCommands.pickingMetadata, + ); + } + } if (!command.pickOnly) { derivedCommands.depth = DerivedCommand.createDepthOnlyDerivedCommand( scene, @@ -1799,6 +1829,7 @@ Scene.prototype.updateDerivedCommands = function (command) { return; } + const frameState = this._frameState; const { shadowState, useLogDepth } = this._frameState; const context = this._context; @@ -1819,11 +1850,15 @@ Scene.prototype.updateDerivedCommands = function (command) { useLogDepth && !hasLogDepthDerivedCommands; const needsHdrCommands = useHdr && !hasHdrCommands; const needsDerivedCommands = (!useLogDepth || !useHdr) && !hasDerivedCommands; + const needsUpdateForMetadataPicking = + frameState.pickingMetadata && + pickedMetadataInfoChanged(command, frameState); command.dirty = command.dirty || needsLogDepthDerivedCommands || needsHdrCommands || - needsDerivedCommands; + needsDerivedCommands || + needsUpdateForMetadataPicking; if (!command.dirty) { return; @@ -2195,14 +2230,23 @@ function executeCommand(command, scene, passState, debugFramebuffer) { } if (passes.pick || passes.depth) { - if ( - passes.pick && - !passes.depth && - defined(command.derivedCommands.picking) - ) { - command = command.derivedCommands.picking.pickCommand; - command.execute(context, passState); - return; + if (passes.pick && !passes.depth) { + if ( + frameState.pickingMetadata && + defined(command.derivedCommands.pickingMetadata) + ) { + command = command.derivedCommands.pickingMetadata.pickMetadataCommand; + command.execute(context, passState); + return; + } + if ( + !frameState.pickingMetadata && + defined(command.derivedCommands.picking) + ) { + command = command.derivedCommands.picking.pickCommand; + command.execute(context, passState); + return; + } } else if (defined(command.derivedCommands.depth)) { command = command.derivedCommands.depth.depthOnlyCommand; command.execute(context, passState); @@ -2255,7 +2299,11 @@ function executeIdCommand(command, scene, passState) { command = derivedCommands.logDepth.command; } - const { picking, depth } = command.derivedCommands; + const { picking, pickingMetadata, depth } = command.derivedCommands; + if (defined(pickingMetadata)) { + command = derivedCommands.pickingMetadata.pickMetadataCommand; + command.execute(context, passState); + } if (defined(picking)) { command = picking.pickCommand; command.execute(context, passState); @@ -4343,6 +4391,90 @@ Scene.prototype.pickVoxel = function (windowPosition, width, height) { ); }; +/** + * Pick a metadata value at the given window position. + * + * @param {Cartesian2} windowPosition Window coordinates to perform picking on. + * @param {string|undefined} schemaId The ID of the metadata schema to pick values + * from. If this is `undefined`, then it will pick the values from the object + * that match the given class- and property name, regardless of the schema ID. + * @param {string} className The name of the metadata class to pick + * values from + * @param {string} propertyName The name of the metadata property to pick + * values from + * @returns The metadata value + * + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. + */ +Scene.prototype.pickMetadata = function ( + windowPosition, + schemaId, + className, + propertyName, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("windowPosition", windowPosition); + Check.typeOf.string("className", className); + Check.typeOf.string("propertyName", propertyName); + //>>includeEnd('debug'); + + const pickedObject = this.pick(windowPosition); + if (!defined(pickedObject)) { + return undefined; + } + + // Check if the picked object is a model that has structural + // metadata, with a schema that contains the specified + // property. + const schema = pickedObject.detail?.model?.structuralMetadata?.schema; + const classProperty = getMetadataClassProperty( + schema, + schemaId, + className, + propertyName, + ); + if (!defined(classProperty)) { + return undefined; + } + + const pickedMetadataInfo = new PickedMetadataInfo( + schemaId, + className, + propertyName, + classProperty, + ); + + const pickedMetadataValues = this._picking.pickMetadata( + this, + windowPosition, + pickedMetadataInfo, + ); + + return pickedMetadataValues; +}; + +/** + * Pick the schema of the metadata of the object at the given position + * + * @param {Cartesian2} windowPosition Window coordinates to perform picking on. + * @returns {MetadataSchema} The metadata schema, or `undefined` if there is no object with + * associated metadata at the given position. + * + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. + */ +Scene.prototype.pickMetadataSchema = function (windowPosition) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("windowPosition", windowPosition); + //>>includeEnd('debug'); + + const pickedObject = this.pick(windowPosition); + if (!defined(pickedObject)) { + return undefined; + } + const schema = pickedObject.detail?.model?.structuralMetadata?.schema; + return schema; +}; + /** * Returns the cartesian position reconstructed from the depth buffer and window position. * The returned position is in world coordinates. Used internally by camera functions to diff --git a/packages/engine/Source/Scene/getMetadataClassProperty.js b/packages/engine/Source/Scene/getMetadataClassProperty.js new file mode 100644 index 000000000000..3fb88ad7367e --- /dev/null +++ b/packages/engine/Source/Scene/getMetadataClassProperty.js @@ -0,0 +1,43 @@ +import defined from "../Core/defined.js"; + +/** + * Return the `MetadataClassProperty` from the given schema that + * matches the given description. + * + * If the given schema is `undefined`, then `undefined` is returned. + * If the given `schemaId` is defined but does not match the ID + * of the given schema, then `undefined` is returned. + * If the given schema does not have a class with the given name, + * or the class does not have a property with the given name, + * then `undefined` is returned. + * + * Otherwise, the `MetadataClassProperty` is returned. + * + * @param {object} schema The schema object + * @param {string|undefined} schemaId The ID of the metadata schema + * @param {string} className The name of the metadata class + * @param {string} propertyName The name of the metadata property + * @returns {MetadataClassProperty|undefined} + * @private + */ +function getMetadataClassProperty(schema, schemaId, className, propertyName) { + if (!defined(schema)) { + return undefined; + } + if (defined(schemaId) && schema.id !== schemaId) { + return undefined; + } + const classes = schema.classes || {}; + const metadataClass = classes[className]; + if (!defined(metadataClass)) { + return undefined; + } + const properties = metadataClass.properties || {}; + const metadataProperty = properties[propertyName]; + if (!defined(metadataProperty)) { + return undefined; + } + return metadataProperty; +} + +export default getMetadataClassProperty; diff --git a/packages/engine/Source/Shaders/Model/ModelFS.glsl b/packages/engine/Source/Shaders/Model/ModelFS.glsl index 9756cbe5cda3..0500c0b9f1ff 100644 --- a/packages/engine/Source/Shaders/Model/ModelFS.glsl +++ b/packages/engine/Source/Shaders/Model/ModelFS.glsl @@ -45,6 +45,10 @@ void main() MetadataStatistics metadataStatistics; metadataStage(metadata, metadataClass, metadataStatistics, attributes); + //======================================================================== + // When not picking metadata START + #ifndef METADATA_PICKING_ENABLED + #ifdef HAS_SELECTED_FEATURE_ID selectedFeatureIdStage(selectedFeature, featureIds); #endif @@ -73,6 +77,20 @@ void main() vec4 color = handleAlpha(material.diffuse, material.alpha); + // When not picking metadata END + //======================================================================== + #else + //======================================================================== + // When picking metadata START + + vec4 metadataValues = vec4(0.0, 0.0, 0.0, 0.0); + metadataPickingStage(metadata, metadataClass, metadataValues); + vec4 color = metadataValues; + + #endif + // When picking metadata END + //======================================================================== + #ifdef HAS_CLIPPING_PLANES modelClippingPlanesStage(color); #endif @@ -81,6 +99,10 @@ void main() modelClippingPolygonsStage(); #endif + //======================================================================== + // When not picking metadata START + #ifndef METADATA_PICKING_ENABLED + #if defined(HAS_SILHOUETTE) && defined(HAS_NORMALS) silhouetteStage(color); #endif @@ -89,5 +111,9 @@ void main() atmosphereStage(color, attributes); #endif + #endif + // When not picking metadata END + //======================================================================== + out_FragColor = color; } diff --git a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js index 273a138e3dcb..985f7a5f5ad9 100644 --- a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js @@ -30,6 +30,7 @@ import { VerticalExaggerationPipelineStage, WireframePipelineStage, ClassificationType, + MetadataPickingPipelineStage, } from "../../../index.js"; import createFrameState from "../../../../../Specs/createFrameState.js"; @@ -128,6 +129,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, @@ -151,6 +153,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -190,6 +193,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, SelectedFeatureIdPipelineStage, BatchTexturePipelineStage, CPUStylingPipelineStage, @@ -236,6 +240,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, SelectedFeatureIdPipelineStage, BatchTexturePipelineStage, CPUStylingPipelineStage, @@ -295,6 +300,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, @@ -322,6 +328,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -352,6 +359,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { GeometryPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -382,6 +390,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -422,6 +431,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -456,6 +466,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -489,6 +500,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -519,6 +531,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -550,6 +563,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -579,6 +593,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -611,6 +626,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -652,6 +668,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -684,6 +701,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -715,6 +733,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -746,6 +765,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -776,6 +796,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -807,6 +828,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -837,6 +859,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -867,6 +890,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -898,6 +922,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, PrimitiveOutlinePipelineStage, AlphaPipelineStage, @@ -930,6 +955,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -960,6 +986,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -983,6 +1010,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, VerticalExaggerationPipelineStage, LightingPipelineStage, PickingPipelineStage, @@ -1011,6 +1039,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + MetadataPickingPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, diff --git a/packages/engine/Specs/Scene/SceneSpec.js b/packages/engine/Specs/Scene/SceneSpec.js index e305f094ce3d..a2f21c789697 100644 --- a/packages/engine/Specs/Scene/SceneSpec.js +++ b/packages/engine/Specs/Scene/SceneSpec.js @@ -53,6 +53,478 @@ import createCanvas from "../../../../Specs/createCanvas.js"; import createScene from "../../../../Specs/createScene.js"; import pollToPromise from "../../../../Specs/pollToPromise.js"; import render from "../../../../Specs/render.js"; +import { Cartesian4, Model } from "@cesium/engine"; + +// The size of the property texture +const textureSizeX = 16; +const textureSizeY = 16; + +// A scaling factor (to be applied to the texture size) for +// determining the size of the ("debug") canvas that shows +// the scene where the picking takes place +const canvasScaling = 32; + +// The 'toEqualEpsilon' matcher (which is which is defined +// in `Specs/addDefaultMatchers.js`, by the way...) uses +// the epsilon as a relative epsilon, and there is no way +// to pass in an absolute epsilon. For comparing the elements +// of a Cartesian2 that stores UINT8 values, an absolute +// epsilon of 1.0 would be handy. But... here we go: +const propertyValueEpsilon = 0.01; + +/** + * Creates an embedded glTF asset with a property texture. + * + * This creates an assed that represents a unit square and uses + * the `EXT_structural_metadata` extension to assign a single + * property texture to this square. + * + * @param {object} schema The metadata schema + * @param {object} propertyTextureProperties The property texture properties + * @returns The gltf + */ +function createEmbeddedGltfWithPropertyTexture( + schema, + propertyTextureProperties, +) { + const result = { + extensions: { + EXT_structural_metadata: { + schema: schema, + propertyTextures: [ + { + class: "exampleClass", + properties: propertyTextureProperties, + }, + ], + }, + }, + extensionsUsed: ["EXT_structural_metadata"], + accessors: [ + { + bufferView: 0, + byteOffset: 0, + componentType: 5123, + count: 6, + type: "SCALAR", + max: [3], + min: [0], + }, + { + bufferView: 1, + byteOffset: 0, + componentType: 5126, + count: 4, + type: "VEC3", + max: [1.0, 1.0, 0.0], + min: [0.0, 0.0, 0.0], + }, + { + bufferView: 1, + byteOffset: 48, + componentType: 5126, + count: 4, + type: "VEC3", + max: [0.0, 0.0, 1.0], + min: [0.0, 0.0, 1.0], + }, + { + bufferView: 1, + byteOffset: 96, + componentType: 5126, + count: 4, + type: "VEC2", + max: [1.0, 1.0], + min: [0.0, 0.0], + }, + ], + asset: { + generator: "JglTF from https://github.com/javagl/JglTF", + version: "2.0", + }, + buffers: [ + { + uri: "data:application/gltf-buffer;base64,AAABAAIAAQADAAIAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA", + byteLength: 156, + }, + ], + bufferViews: [ + { + buffer: 0, + byteOffset: 0, + byteLength: 12, + target: 34963, + }, + { + buffer: 0, + byteOffset: 12, + byteLength: 144, + byteStride: 12, + target: 34962, + }, + ], + images: [ + { + // A 16x16 pixels image that contains all combinations of + // (0, 127, 255) in its upper-left 9x9 pixels + uri: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAi0lEQVR42u2RUQ6AMAhDd3OO/qQt8VP8NRHjNpf0leI5ruqXbNVL4c9Dn+E8ljV+iLaXaoAY1YDaADaynBg2gFZLR1+wAdJEWZpW1AIVqmjCruqybw4qnEJbbQBHdWoS2XIUXdp+F8DNUOpM0tIZCusQJrzHNTnsOy2pFTZ7xpKhYFUu4M1v+OvrdQGABqEpS2kSLgAAAABJRU5ErkJggg==", + mimeType: "image/png", + }, + ], + materials: [ + { + pbrMetallicRoughness: { + baseColorFactor: [1.0, 1.0, 1.0, 1.0], + metallicFactor: 0.0, + roughnessFactor: 1.0, + }, + alphaMode: "OPAQUE", + doubleSided: true, + }, + ], + meshes: [ + { + primitives: [ + { + extensions: { + EXT_structural_metadata: { + propertyTextures: [0], + }, + }, + attributes: { + POSITION: 1, + NORMAL: 2, + TEXCOORD_0: 3, + }, + indices: 0, + material: 0, + mode: 4, + }, + ], + }, + ], + nodes: [ + { + mesh: 0, + }, + ], + samplers: [ + { + magFilter: 9728, + minFilter: 9728, + }, + ], + scene: 0, + scenes: [ + { + nodes: [0], + }, + ], + textures: [ + { + sampler: 0, + source: 0, + }, + { + sampler: 0, + source: 1, + }, + ], + }; + return result; +} + +/** + * Create an embedded glTF with the default property texture, + * and the given schema and property texture properties. + * + * @param {object} schema The JSON form of the metadata schema + * @param {object[]} properties The JSON form of the property texture properties + * @returns The glTF + */ +function createPropertyTextureGltf(schema, properties) { + const gltf = createEmbeddedGltfWithPropertyTexture(schema, properties); + /*/ + // Copy-and-paste this into a file to have the actual glTF: + console.log("SPEC GLTF:"); + console.log("-".repeat(80)); + console.log(JSON.stringify(gltf, null, 2)); + console.log("-".repeat(80)); + //*/ + return gltf; +} + +/** + * Creates the glTF for the 'scalar' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfScalar() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_UINT8_SCALAR: { + name: "Example SCALAR property with UINT8 components", + type: "SCALAR", + componentType: "UINT8", + }, + example_normalized_UINT8_SCALAR: { + name: "Example SCALAR property with normalized UINT8 components", + type: "SCALAR", + componentType: "UINT8", + normalized: true, + }, + }, + }, + }, + }; + const properties = { + example_UINT8_SCALAR: { + index: 0, + texCoord: 0, + channels: [0], + }, + example_normalized_UINT8_SCALAR: { + index: 0, + texCoord: 0, + channels: [1], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the 'scalar array' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfScalarArray() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_fixed_length_UINT8_SCALAR_array: { + name: "Example fixed-length SCALAR array property with UINT8 components", + type: "SCALAR", + componentType: "UINT8", + array: true, + count: 3, + }, + }, + }, + }, + }; + const properties = { + example_fixed_length_UINT8_SCALAR_array: { + index: 0, + texCoord: 0, + channels: [0, 1, 2], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the 'vec2' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfVec2() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_UINT8_VEC2: { + name: "Example VEC2 property with UINT8 components", + type: "VEC2", + componentType: "UINT8", + }, + }, + }, + }, + }; + const properties = { + example_UINT8_VEC2: { + index: 0, + texCoord: 0, + channels: [0, 1], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the normalized 'vec2' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfNormalizedVec2() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_normalized_UINT8_VEC2: { + name: "Example VEC2 property with normalized UINT8 components", + type: "VEC2", + componentType: "UINT8", + normalized: true, + }, + }, + }, + }, + }; + const properties = { + example_normalized_UINT8_VEC2: { + index: 0, + texCoord: 0, + channels: [0, 1], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the 'vec3' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfVec3() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_UINT8_VEC3: { + name: "Example VEC3 property with UINT8 components", + type: "VEC3", + componentType: "UINT8", + }, + }, + }, + }, + }; + const properties = { + example_UINT8_VEC3: { + index: 0, + texCoord: 0, + channels: [0, 1, 2], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Creates the glTF for the 'vec4' test case + * + * @returns The glTF + */ +function createPropertyTextureGltfVec4() { + const schema = { + id: "ExampleSchema", + classes: { + exampleClass: { + name: "Example class", + properties: { + example_UINT8_VEC4: { + name: "Example VEC4 property with UINT8 components", + type: "VEC4", + componentType: "UINT8", + }, + }, + }, + }, + }; + const properties = { + example_UINT8_VEC4: { + index: 0, + texCoord: 0, + channels: [0, 1, 2, 3], + }, + }; + return createPropertyTextureGltf(schema, properties); +} + +/** + * Create a model from the given glTF, add it as a primitive + * to the given scene, and wait until it is fully loaded. + * + * @param {Scene} scene The scene + * @param {object} gltf The gltf + */ +async function loadAsModel(scene, gltf) { + const basePath = "SPEC_BASE_PATH"; + const model = await Model.fromGltfAsync({ + gltf: gltf, + basePath: basePath, + // This is important to make sure that the property + // texture is fully loaded when the model is rendered! + incrementallyLoadTextures: false, + }); + scene.primitives.add(model); + + await pollToPromise( + function () { + scene.renderForSpecs(); + return model.ready; + }, + { timeout: 10000 }, + ); +} + +/** + * Move the camera to exactly look at the unit square along -X + * + * @param {Camera} camera + */ +function fitCameraToUnitSquare(camera) { + const fov = CesiumMath.PI_OVER_THREE; + camera.frustum.fov = fov; + camera.frustum.near = 0.01; + camera.frustum.far = 100.0; + const distance = 1.0 / (2.0 * Math.tan(fov * 0.5)); + camera.position = new Cartesian3(distance, 0.5, 0.5); + camera.direction = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()); + camera.up = Cartesian3.clone(Cartesian3.UNIT_Z); + camera.right = Cartesian3.clone(Cartesian3.UNIT_Y); +} + +/** + * Pick the specified metadata value from the screen that is contained in + * the property texture at the given coordinates. + * + * (This assumes that the property texture is on a unit square, and + * fitCameraToUnitSquare was called) + * + * @param {Scene} scene The scene + * @param {string|undefined} schemaId The schema ID + * @param {string} className The class name + * @param {string} propertyName The property name + * @param {number} x The x-coordinate in the texture + * @param {number} y The y-coordinate in the texture + * @returns The metadata value + */ +function pickMetadataAt(scene, schemaId, className, propertyName, x, y) { + const screenX = Math.floor(x * canvasScaling + canvasScaling / 2); + const screenY = Math.floor(y * canvasScaling + canvasScaling / 2); + const screenPosition = new Cartesian2(screenX, screenY); + const metadataValue = scene.pickMetadata( + screenPosition, + schemaId, + className, + propertyName, + ); + return metadataValue; +} describe( "Scene/Scene", @@ -2367,5 +2839,681 @@ describe( }); }, + describe("pickMetadata", () => { + // When using a WebGL stub, the functionality of reading metadata + // values back from the frame buffer is not supported. So nearly + // all the tests have to be skipped. + const webglStub = !!window.webglStub; + + const defaultDate = JulianDate.fromDate( + new Date("January 1, 2014 12:00:00 UTC"), + ); + + it("throws without windowPosition", async function () { + const windowPosition = undefined; // For spec + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_SCALAR"; + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + scene.initializeFrame(); + scene.render(defaultDate); + expect(() => { + scene.pickMetadata(windowPosition, schemaId, className, propertyName); + }).toThrowDeveloperError(); + scene.destroyForSpecs(); + }); + + it("throws without className", async function () { + const windowPosition = new Cartesian2(); + const schemaId = undefined; + const className = undefined; // For spec + const propertyName = "example_UINT8_SCALAR"; + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + scene.initializeFrame(); + scene.render(defaultDate); + expect(() => { + scene.pickMetadata(windowPosition, schemaId, className, propertyName); + }).toThrowDeveloperError(); + scene.destroyForSpecs(); + }); + + it("throws without propertyName", async function () { + const windowPosition = new Cartesian2(); + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = undefined; // For spec + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + scene.initializeFrame(); + scene.render(defaultDate); + expect(() => { + scene.pickMetadata(windowPosition, schemaId, className, propertyName); + }).toThrowDeveloperError(); + scene.destroyForSpecs(); + }); + + it("returns undefined for class name that does not exist", async function () { + const schemaId = undefined; + const className = "exampleClass_THAT_DOES_NOT_EXIST"; // For spec + const propertyName = "example_UINT8_SCALAR"; + const gltf = createPropertyTextureGltfScalar(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + const windowPosition = new Cartesian2( + Math.floor(canvasSizeX / 2), + Math.floor(canvasSizeY / 2), + ); + const actualMetadataValue = scene.pickMetadata( + windowPosition, + schemaId, + className, + propertyName, + ); + expect(actualMetadataValue).toBeUndefined(); + scene.destroyForSpecs(); + }); + + it("returns undefined when there is no object with metadata", async function () { + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_SCALAR"; + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + fitCameraToUnitSquare(scene.camera); + + const windowPosition = new Cartesian2( + Math.floor(canvasSizeX / 2), + Math.floor(canvasSizeY / 2), + ); + const actualMetadataValue = scene.pickMetadata( + windowPosition, + schemaId, + className, + propertyName, + ); + expect(actualMetadataValue).toBeUndefined(); + scene.destroyForSpecs(); + }); + + it("pickMetadataSchema returns undefined when there is no object with metadata", async function () { + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + fitCameraToUnitSquare(scene.camera); + + const windowPosition = new Cartesian2( + Math.floor(canvasSizeX / 2), + Math.floor(canvasSizeY / 2), + ); + const metadataSchema = scene.pickMetadataSchema(windowPosition); + + expect(metadataSchema).toBeUndefined(); + scene.destroyForSpecs(); + }); + + it("pickMetadataSchema picks the metadata schema object", async function () { + if (webglStub) { + return; + } + + const gltf = createPropertyTextureGltfScalar(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const windowPosition = new Cartesian2( + Math.floor(canvasSizeX / 2), + Math.floor(canvasSizeY / 2), + ); + + // The pickMetadataSchema call should return the schema that + // was defined in createPropertyTextureGltfScalar + const metadataSchema = scene.pickMetadataSchema(windowPosition); + + expect(metadataSchema).toBeDefined(); + expect(metadataSchema.id).toEqual("ExampleSchema"); + expect(metadataSchema.classes).toBeDefined(); + scene.destroyForSpecs(); + }); + + it("picks UINT8 SCALAR from a property texture", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_SCALAR"; + const gltf = createPropertyTextureGltfScalar(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 2, + ); + const expectedMetadataValue0 = 0; + const expectedMetadataValue1 = 127; + const expectedMetadataValue2 = 255; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks normalized UINT8 SCALAR from a property texture", async function () { + if (webglStub) { + return; + } + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_SCALAR"; + const gltf = createPropertyTextureGltfScalar(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 3, + 0, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 6, + 0, + ); + const expectedMetadataValue0 = 0.0; + const expectedMetadataValue1 = 0.5; + const expectedMetadataValue2 = 1.0; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks fixed length UINT8 SCALAR array from a property texture", async function () { + if (webglStub) { + return; + } + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_fixed_length_UINT8_SCALAR_array"; + const gltf = createPropertyTextureGltfScalarArray(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + const expectedMetadataValue0 = [0, 0, 0]; + const expectedMetadataValue1 = [127, 0, 127]; + const expectedMetadataValue2 = [255, 0, 255]; + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks UINT8 VEC2 from a property texture", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_VEC2"; + const gltf = createPropertyTextureGltfVec2(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + const expectedMetadataValue0 = new Cartesian2(0, 0); + const expectedMetadataValue1 = new Cartesian2(127, 0); + const expectedMetadataValue2 = new Cartesian2(255, 0); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks normalized UINT8 VEC2 from a property texture", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_normalized_UINT8_VEC2"; + const gltf = createPropertyTextureGltfNormalizedVec2(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + + const expectedMetadataValue0 = new Cartesian2(0.0, 0.0); + const expectedMetadataValue1 = new Cartesian2(0.5, 0.0); + const expectedMetadataValue2 = new Cartesian2(1.0, 0.0); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks UINT8 VEC3 from a property texture", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_VEC3"; + const gltf = createPropertyTextureGltfVec3(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + const expectedMetadataValue0 = new Cartesian3(0, 0, 0); + const expectedMetadataValue1 = new Cartesian3(127, 0, 127); + const expectedMetadataValue2 = new Cartesian3(255, 0, 255); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + + it("picks UINT8 VEC4 from a property texture", async function () { + if (webglStub) { + return; + } + + const schemaId = undefined; + const className = "exampleClass"; + const propertyName = "example_UINT8_VEC4"; + const gltf = createPropertyTextureGltfVec4(); + + const canvasSizeX = textureSizeX * canvasScaling; + const canvasSizeY = textureSizeY * canvasScaling; + const scene = createScene({ + canvas: createCanvas(canvasSizeX, canvasSizeY), + contextOptions: { + requestWebgl1: true, + }, + }); + + await loadAsModel(scene, gltf); + fitCameraToUnitSquare(scene.camera); + + scene.initializeFrame(); + scene.render(defaultDate); + + const actualMetadataValue0 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 0, + 0, + ); + const actualMetadataValue1 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 1, + 1, + ); + const actualMetadataValue2 = pickMetadataAt( + scene, + schemaId, + className, + propertyName, + 2, + 2, + ); + + const expectedMetadataValue0 = new Cartesian4(0, 0, 0, 0); + const expectedMetadataValue1 = new Cartesian4(127, 0, 127, 0); + const expectedMetadataValue2 = new Cartesian4(255, 0, 255, 0); + + expect(actualMetadataValue0).toEqualEpsilon( + expectedMetadataValue0, + propertyValueEpsilon, + ); + expect(actualMetadataValue1).toEqualEpsilon( + expectedMetadataValue1, + propertyValueEpsilon, + ); + expect(actualMetadataValue2).toEqualEpsilon( + expectedMetadataValue2, + propertyValueEpsilon, + ); + scene.destroyForSpecs(); + }); + }), "WebGL", );