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: + *
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",
);