diff --git a/Apps/Sandcastle/gallery/3D Tiles Interior.html b/Apps/Sandcastle/gallery/3D Tiles Interior.html index 38c63747e69e..3fa0c0ea077e 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Interior.html +++ b/Apps/Sandcastle/gallery/3D Tiles Interior.html @@ -40,7 +40,9 @@ const viewer = new Cesium.Viewer("cesiumContainer"); try { - const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(125737); + const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(125737, { + disableCollision: true, + }); viewer.scene.primitives.add(tileset); } catch (error) { console.log(`Error loading tileset: ${error}`); diff --git a/Apps/Sandcastle/gallery/development/3D Tiles Picking.html b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html new file mode 100644 index 000000000000..843b1d4f30e4 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html @@ -0,0 +1,146 @@ + + +
+ + + + + +undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Cesium3DTileContent.prototype.pick = function (ray, frameState, result) {
+ DeveloperError.throwInstantiationError();
+};
+
/**
* Returns true if this object was destroyed; otherwise, false.
* true
the camera will be allowed to go in or below the tileset surface if {@link ScreenSpaceCameraController#enableCollisionDetection} is true.
+ * @property {boolean} [projectTo2D=false] Whether to accurately project the tileset to 2D. If this is true, the tileset will be projected accurately to 2D, but it will use more memory to do so. If this is false, the tileset will use less memory and will still render in 2D / CV mode, but its projected positions may be inaccurate. This cannot be set after the tileset has been created.
+ * @property {boolean} [enablePick=false] Whether to allow collision and CPU picking with pick
when using WebGL 1. If using WebGL 2 or above, this option will be ignored. If using WebGL 1 and this is true, the pick
operation will work correctly, but it will use more memory to do so. If running with WebGL 1 and this is false, the model will use less memory, but pick
will always return undefined
. This cannot be set after the tileset has loaded.
* @property {string} [debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value.
* @property {boolean} [debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering.
* @property {boolean} [debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile.
- * @property {boolean} [enableDebugWireframe] For debugging only. This must be true for debugWireframe to work in WebGL1. This cannot be set after the tileset has loaded.
+ * @property {boolean} [enableDebugWireframe=false] For debugging only. This must be true for debugWireframe to work in WebGL1. This cannot be set after the tileset has been created.
* @property {boolean} [debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe.
* @property {boolean} [debugShowBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile.
* @property {boolean} [debugShowContentBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile's content.
@@ -149,6 +154,18 @@ import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js";
* }
*
* @example
+ * // Allow camera to go inside and under 3D tileset
+ * try {
+ * const tileset = await Cesium.Cesium3DTileset.fromUrl(
+ * "http://localhost:8002/tilesets/Seattle/tileset.json",
+ * { disableCollision: true }
+ * );
+ * scene.primitives.add(tileset);
+ * } catch (error) {
+ * console.error(`Error creating tileset: ${error}`);
+ * }
+ *
+ * @example
* // Common setting for the skipLevelOfDetail optimization
* const tileset = await Cesium.Cesium3DTileset.fromUrl(
* "http://localhost:8002/tilesets/Seattle/tileset.json", {
@@ -843,7 +860,16 @@ function Cesium3DTileset(options) {
SplitDirection.NONE
);
+ /**
+ * Whether to turn off collisions for camera collisions or picking. While this is true
the camera will be allowed to go in or below the tileset surface if {@link ScreenSpaceCameraController#enableCollisionDetection} is true.
+ *
+ * @type {boolean}
+ * @default false
+ */
+ this.disableCollision = defaultValue(options.disableCollision, false);
+
this._projectTo2D = defaultValue(options.projectTo2D, false);
+ this._enablePick = defaultValue(options.enablePick, false);
/**
* This property is for debugging only; it is not optimized for production use.
@@ -3411,6 +3437,117 @@ Cesium3DTileset.checkSupportedExtensions = function (extensionsRequired) {
}
};
+const scratchGetHeightRay = new Ray();
+const scratchIntersection = new Cartesian3();
+const scratchGetHeightCartographic = new Cartographic();
+
+/**
+ * Get the height of the loaded surface at a given cartographic. This function will only take into account meshes for loaded tiles, not neccisarily the most detailed tiles available for a tileset. This function will always return undefined when sampling a point cloud.
+ *
+ * @param {Cartographic} cartographic The cartographic for which to find the height.
+ * @param {Scene} scene The scene where visualization is taking place.
+ * @returns {number|undefined} The height of the cartographic or undefined if it could not be found.
+ *
+ * @example
+ * const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(124624234);
+ * scene.primitives.add(tileset);
+ *
+ * const height = tileset.getHeight(scene.camera.positionCartographic, scene);
+ */
+Cesium3DTileset.prototype.getHeight = function (cartographic, scene) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("cartographic", cartographic);
+ Check.typeOf.object("scene", scene);
+ //>>includeEnd('debug');
+
+ let ellipsoid = scene.globe?.ellipsoid;
+ if (!defined(ellipsoid)) {
+ ellipsoid = Ellipsoid.WGS84;
+ }
+
+ const ray = scratchGetHeightRay;
+ const position = ellipsoid.cartographicToCartesian(
+ cartographic,
+ ray.direction
+ );
+
+ ray.direction = Cartesian3.normalize(position, ray.direction);
+ ray.direction = Cartesian3.negate(position, ray.direction);
+ ray.origin = Cartesian3.multiplyByScalar(
+ ray.direction,
+ -2 * ellipsoid.maximumRadius,
+ ray.origin
+ );
+
+ const intersection = this.pick(ray, scene.frameState, scratchIntersection);
+ if (!defined(intersection)) {
+ return;
+ }
+
+ return ellipsoid.cartesianToCartographic(
+ intersection,
+ scratchGetHeightCartographic
+ )?.height;
+};
+
+const scratchSphereIntersection = new Interval();
+const scratchPickIntersection = new Cartesian3();
+
+/**
+ * Find an intersection between a ray and the tileset surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Cesium3DTileset.prototype.pick = function (ray, frameState, result) {
+ if (!frameState.context.webgl2 && !this._enablePick) {
+ return;
+ }
+
+ const selectedTiles = this._selectedTiles;
+ const selectedLength = selectedTiles.length;
+
+ let intersection;
+ let minDistance = Number.POSITIVE_INFINITY;
+ for (let i = 0; i < selectedLength; ++i) {
+ const tile = selectedTiles[i];
+ const boundsIntersection = IntersectionTests.raySphere(
+ ray,
+ tile.boundingSphere,
+ scratchSphereIntersection
+ );
+ if (!defined(boundsIntersection)) {
+ continue;
+ }
+
+ const candidate = tile.content?.pick(
+ ray,
+ frameState,
+ scratchPickIntersection
+ );
+
+ if (!defined(candidate)) {
+ continue;
+ }
+
+ const distance = Cartesian3.distance(ray.origin, candidate);
+ if (distance < minDistance) {
+ intersection = Cartesian3.clone(candidate, result);
+ minDistance = distance;
+ }
+ }
+
+ if (!defined(intersection)) {
+ return undefined;
+ }
+
+ return intersection;
+};
+
/**
* Optimization option. Used as a callback when {@link Cesium3DTileset#foveatedScreenSpaceError} is true to control how much to raise the screen space error for tiles outside the foveated cone,
* interpolating between {@link Cesium3DTileset#foveatedMinimumScreenSpaceErrorRelaxation} and {@link Cesium3DTileset#maximumScreenSpaceError}.
diff --git a/packages/engine/Source/Scene/Composite3DTileContent.js b/packages/engine/Source/Scene/Composite3DTileContent.js
index 900a901cc75b..02be6c08ac6c 100644
--- a/packages/engine/Source/Scene/Composite3DTileContent.js
+++ b/packages/engine/Source/Scene/Composite3DTileContent.js
@@ -1,3 +1,4 @@
+import Cartesian3 from "../Core/Cartesian3.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
@@ -347,6 +348,47 @@ Composite3DTileContent.prototype.update = function (tileset, frameState) {
}
};
+/**
+ * Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Composite3DTileContent.prototype.pick = function (ray, frameState, result) {
+ if (!this._ready) {
+ return undefined;
+ }
+
+ let intersection;
+ let minDistance = Number.POSITIVE_INFINITY;
+ const contents = this._contents;
+ const length = contents.length;
+
+ for (let i = 0; i < length; ++i) {
+ const candidate = contents[i].pick(ray, frameState, result);
+
+ if (!defined(candidate)) {
+ continue;
+ }
+
+ const distance = Cartesian3.distance(ray.origin, candidate);
+ if (distance < minDistance) {
+ intersection = candidate;
+ minDistance = distance;
+ }
+ }
+
+ if (!defined(intersection)) {
+ return undefined;
+ }
+
+ return result;
+};
+
Composite3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/Empty3DTileContent.js b/packages/engine/Source/Scene/Empty3DTileContent.js
index 28f3c1d854bf..0b5c4459da35 100644
--- a/packages/engine/Source/Scene/Empty3DTileContent.js
+++ b/packages/engine/Source/Scene/Empty3DTileContent.js
@@ -150,6 +150,10 @@ Empty3DTileContent.prototype.applyStyle = function (style) {};
Empty3DTileContent.prototype.update = function (tileset, frameState) {};
+Empty3DTileContent.prototype.pick = function (ray, frameState, result) {
+ return undefined;
+};
+
Empty3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/Geometry3DTileContent.js b/packages/engine/Source/Scene/Geometry3DTileContent.js
index 93007bd46a1f..d89a278eb4b1 100644
--- a/packages/engine/Source/Scene/Geometry3DTileContent.js
+++ b/packages/engine/Source/Scene/Geometry3DTileContent.js
@@ -525,6 +525,10 @@ Geometry3DTileContent.prototype.update = function (tileset, frameState) {
}
};
+Geometry3DTileContent.prototype.pick = function (ray, frameState, result) {
+ return undefined;
+};
+
Geometry3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js
index 42077c0234d9..68989c3fe5c7 100644
--- a/packages/engine/Source/Scene/GltfLoader.js
+++ b/packages/engine/Source/Scene/GltfLoader.js
@@ -179,6 +179,7 @@ const GltfLoaderState = {
* @param {Axis} [options.forwardAxis=Axis.Z] The forward-axis of the glTF model.
* @param {boolean} [options.loadAttributesAsTypedArray=false] Load all attributes and indices as typed arrays instead of GPU buffers. If the attributes are interleaved in the glTF they will be de-interleaved in the typed array.
* @param {boolean} [options.loadAttributesFor2D=false] If true
, load the positions buffer and any instanced attribute buffers as typed arrays for accurately projecting models to 2D.
+ * @param {boolean} [options.enablePick=false] If true
, load the positions buffer, any instanced attribute buffers, and index buffer as typed arrays for CPU-enabled picking in WebGL1.
* @param {boolean} [options.loadIndicesForWireframe=false] If true
, load the index buffer as both a buffer and typed array. The latter is useful for creating wireframe indices in WebGL1.
* @param {boolean} [options.loadPrimitiveOutline=true] If true
, load outlines from the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set false to avoid post-processing geometry at load time.
* @param {boolean} [options.loadForClassification=false] If true
and if the model has feature IDs, load the feature IDs and indices as typed arrays. This is useful for batching features for classification.
@@ -203,6 +204,7 @@ function GltfLoader(options) {
false
);
const loadAttributesFor2D = defaultValue(options.loadAttributesFor2D, false);
+ const enablePick = defaultValue(options.enablePick);
const loadIndicesForWireframe = defaultValue(
options.loadIndicesForWireframe,
false
@@ -234,6 +236,7 @@ function GltfLoader(options) {
this._forwardAxis = forwardAxis;
this._loadAttributesAsTypedArray = loadAttributesAsTypedArray;
this._loadAttributesFor2D = loadAttributesFor2D;
+ this._enablePick = enablePick;
this._loadIndicesForWireframe = loadIndicesForWireframe;
this._loadPrimitiveOutline = loadPrimitiveOutline;
this._loadForClassification = loadForClassification;
@@ -1241,6 +1244,8 @@ function loadVertexAttribute(
!hasInstances &&
loader._loadAttributesFor2D &&
!frameState.scene3DOnly;
+ const loadTypedArrayForPicking =
+ isPositionAttribute && loader._enablePick && !frameState.context.webgl2;
const loadTypedArrayForClassification =
loader._loadForClassification && isFeatureIdAttribute;
@@ -1252,6 +1257,7 @@ function loadVertexAttribute(
const outputTypedArray =
outputTypedArrayOnly ||
loadTypedArrayFor2D ||
+ loadTypedArrayForPicking ||
loadTypedArrayForClassification;
// Determine what to load right now:
@@ -1319,6 +1325,8 @@ function loadInstancedAttribute(
loader._loadAttributesAsTypedArray ||
(hasRotation && isTransformAttribute) ||
!frameState.context.instancedArrays;
+ const loadTypedArrayForPicking =
+ loader._enablePick && !frameState.context.webgl2;
const loadBuffer = !loadAsTypedArrayOnly;
@@ -1328,7 +1336,8 @@ function loadInstancedAttribute(
// - the model will be projected to 2D.
const loadFor2D = loader._loadAttributesFor2D && !frameState.scene3DOnly;
const loadTranslationAsTypedArray =
- isTranslationAttribute && (!hasTranslationMinMax || loadFor2D);
+ isTranslationAttribute &&
+ (!hasTranslationMinMax || loadFor2D || loadTypedArrayForPicking);
const loadTypedArray = loadAsTypedArrayOnly || loadTranslationAsTypedArray;
@@ -1365,9 +1374,10 @@ function loadIndices(
indices.count = accessor.count;
const loadAttributesAsTypedArray = loader._loadAttributesAsTypedArray;
- // Load the index buffer as a typed array to generate wireframes in WebGL1.
- const loadForWireframe =
- loader._loadIndicesForWireframe && !frameState.context.webgl2;
+ // Load the index buffer as a typed array to generate wireframes or pick in WebGL1.
+ const loadForCpuOperations =
+ (loader._loadIndicesForWireframe || loader._enablePick) &&
+ !frameState.context.webgl2;
// Load the index buffer as a typed array to batch features together for classification.
const loadForClassification = loader._loadForClassification && hasFeatureIds;
@@ -1377,7 +1387,7 @@ function loadIndices(
const outputTypedArrayOnly = loadAttributesAsTypedArray;
const outputBuffer = !outputTypedArrayOnly;
const outputTypedArray =
- loadAttributesAsTypedArray || loadForWireframe || loadForClassification;
+ loadAttributesAsTypedArray || loadForCpuOperations || loadForClassification;
// Determine what to load right now:
//
diff --git a/packages/engine/Source/Scene/Implicit3DTileContent.js b/packages/engine/Source/Scene/Implicit3DTileContent.js
index 96af9c099ad5..96eebae51c1d 100644
--- a/packages/engine/Source/Scene/Implicit3DTileContent.js
+++ b/packages/engine/Source/Scene/Implicit3DTileContent.js
@@ -1177,6 +1177,10 @@ Implicit3DTileContent.prototype.applyStyle = function (style) {};
Implicit3DTileContent.prototype.update = function (tileset, frameState) {};
+Implicit3DTileContent.prototype.pick = function (ray, frameState, result) {
+ return undefined;
+};
+
Implicit3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/Model/B3dmLoader.js b/packages/engine/Source/Scene/Model/B3dmLoader.js
index 7385f18a690d..600221ac4dde 100644
--- a/packages/engine/Source/Scene/Model/B3dmLoader.js
+++ b/packages/engine/Source/Scene/Model/B3dmLoader.js
@@ -50,6 +50,7 @@ const FeatureIdAttribute = ModelComponents.FeatureIdAttribute;
* @param {Axis} [options.forwardAxis=Axis.X] The forward-axis of the glTF model.
* @param {boolean} [options.loadAttributesAsTypedArray=false] If true
, load all attributes as typed arrays instead of GPU buffers. If the attributes are interleaved in the glTF they will be de-interleaved in the typed array.
* @param {boolean} [options.loadAttributesFor2D=false] If true
, load the positions buffer and any instanced attribute buffers as typed arrays for accurately projecting models to 2D.
+ * @param {boolean} [options.enablePick=false] If true
, load the positions buffer, any instanced attribute buffers, and index buffer as typed arrays for CPU-enabled picking in WebGL1.
* @param {boolean} [options.loadIndicesForWireframe=false] If true
, load the index buffer as a typed array. This is useful for creating wireframe indices in WebGL1.
* @param {boolean} [options.loadPrimitiveOutline=true] If true
, load outlines from the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set false to avoid post-processing geometry at load time.
* @param {boolean} [options.loadForClassification=false] If true
and if the model has feature IDs, load the feature IDs and indices as typed arrays. This is useful for batching features for classification.
@@ -74,6 +75,7 @@ function B3dmLoader(options) {
false
);
const loadAttributesFor2D = defaultValue(options.loadAttributesFor2D, false);
+ const enablePick = defaultValue(options.enablePick, false);
const loadIndicesForWireframe = defaultValue(
options.loadIndicesForWireframe,
false
@@ -102,6 +104,7 @@ function B3dmLoader(options) {
this._forwardAxis = forwardAxis;
this._loadAttributesAsTypedArray = loadAttributesAsTypedArray;
this._loadAttributesFor2D = loadAttributesFor2D;
+ this._enablePick = enablePick;
this._loadIndicesForWireframe = loadIndicesForWireframe;
this._loadPrimitiveOutline = loadPrimitiveOutline;
this._loadForClassification = loadForClassification;
@@ -223,6 +226,7 @@ B3dmLoader.prototype.load = function () {
incrementallyLoadTextures: this._incrementallyLoadTextures,
loadAttributesAsTypedArray: this._loadAttributesAsTypedArray,
loadAttributesFor2D: this._loadAttributesFor2D,
+ enablePick: this._enablePick,
loadIndicesForWireframe: this._loadIndicesForWireframe,
loadPrimitiveOutline: this._loadPrimitiveOutline,
loadForClassification: this._loadForClassification,
diff --git a/packages/engine/Source/Scene/Model/I3dmLoader.js b/packages/engine/Source/Scene/Model/I3dmLoader.js
index 2a1aff969bd2..0f9457624419 100644
--- a/packages/engine/Source/Scene/Model/I3dmLoader.js
+++ b/packages/engine/Source/Scene/Model/I3dmLoader.js
@@ -64,6 +64,7 @@ const Instances = ModelComponents.Instances;
* @param {Axis} [options.upAxis=Axis.Y] The up-axis of the glTF model.
* @param {Axis} [options.forwardAxis=Axis.X] The forward-axis of the glTF model.
* @param {boolean} [options.loadAttributesAsTypedArray=false] Load all attributes as typed arrays instead of GPU buffers. If the attributes are interleaved in the glTF they will be de-interleaved in the typed array.
+ * @param {boolean} [options.enablePick=false] If true
, load the positions buffer, any instanced attribute buffers, and index buffer as typed arrays for CPU-enabled picking in WebGL1.
* @param {boolean} [options.loadIndicesForWireframe=false] Load the index buffer as a typed array so wireframe indices can be created for WebGL1.
* @param {boolean} [options.loadPrimitiveOutline=true] If true, load outlines from the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set false to avoid post-processing geometry at load time.
*/
@@ -91,6 +92,7 @@ function I3dmLoader(options) {
false
);
const loadPrimitiveOutline = defaultValue(options.loadPrimitiveOutline, true);
+ const enablePick = defaultValue(options.enablePick, false);
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.i3dmResource", i3dmResource);
@@ -111,6 +113,7 @@ function I3dmLoader(options) {
this._loadAttributesAsTypedArray = loadAttributesAsTypedArray;
this._loadIndicesForWireframe = loadIndicesForWireframe;
this._loadPrimitiveOutline = loadPrimitiveOutline;
+ this._enablePick = enablePick;
this._state = I3dmLoaderState.NOT_LOADED;
this._promise = undefined;
@@ -240,6 +243,7 @@ I3dmLoader.prototype.load = function () {
releaseGltfJson: this._releaseGltfJson,
incrementallyLoadTextures: this._incrementallyLoadTextures,
loadAttributesAsTypedArray: this._loadAttributesAsTypedArray,
+ enablePick: this._enablePick,
loadIndicesForWireframe: this._loadIndicesForWireframe,
loadPrimitiveOutline: this._loadPrimitiveOutline,
};
diff --git a/packages/engine/Source/Scene/Model/InstancingPipelineStage.js b/packages/engine/Source/Scene/Model/InstancingPipelineStage.js
index 850410c2fd2b..809a1dd06d32 100644
--- a/packages/engine/Source/Scene/Model/InstancingPipelineStage.js
+++ b/packages/engine/Source/Scene/Model/InstancingPipelineStage.js
@@ -73,6 +73,7 @@ InstancingPipelineStage.process = function (renderResources, node, frameState) {
frameState.mode !== SceneMode.SCENE3D &&
!frameState.scene3DOnly &&
model._projectTo2D;
+ const keepTypedArray = model._enablePick && !frameState.context.webgl2;
const instancingVertexAttributes = [];
@@ -81,7 +82,8 @@ InstancingPipelineStage.process = function (renderResources, node, frameState) {
frameState,
instances,
instancingVertexAttributes,
- use2D
+ use2D,
+ keepTypedArray
);
processFeatureIdAttributes(
@@ -697,7 +699,8 @@ function processTransformAttributes(
frameState,
instances,
instancingVertexAttributes,
- use2D
+ use2D,
+ keepTypedArray
) {
const rotationAttribute = ModelUtility.getAttributeBySemantic(
instances,
@@ -711,7 +714,8 @@ function processTransformAttributes(
instances,
instancingVertexAttributes,
frameState,
- use2D
+ use2D,
+ keepTypedArray
);
} else {
processTransformVec3Attributes(
@@ -729,7 +733,8 @@ function processTransformMatrixAttributes(
instances,
instancingVertexAttributes,
frameState,
- use2D
+ use2D,
+ keepTypedArray
) {
const shaderBuilder = renderResources.shaderBuilder;
const count = instances.attributes[0].count;
@@ -755,6 +760,10 @@ function processTransformMatrixAttributes(
buffer = createVertexBuffer(transformsTypedArray, frameState);
model._modelResources.push(buffer);
+ if (keepTypedArray) {
+ runtimeNode.transformsTypedArray = transformsTypedArray;
+ }
+
runtimeNode.instancingTransformsBuffer = buffer;
}
@@ -812,7 +821,8 @@ function processTransformVec3Attributes(
instances,
instancingVertexAttributes,
frameState,
- use2D
+ use2D,
+ keepTypedArray
) {
const shaderBuilder = renderResources.shaderBuilder;
const runtimeNode = renderResources.runtimeNode;
@@ -872,7 +882,7 @@ function processTransformVec3Attributes(
attributeString
);
- if (!use2D) {
+ if (!use2D && !keepTypedArray) {
return;
}
@@ -898,6 +908,10 @@ function processTransformVec3Attributes(
);
const projectedTypedArray = translationsToTypedArray(projectedTranslations);
+ if (keepTypedArray) {
+ runtimeNode.transformsTypedArray = projectedTypedArray;
+ }
+
// This memory is counted during the statistics stage at the end
// of the pipeline.
buffer2D = createVertexBuffer(projectedTypedArray, frameState);
@@ -906,6 +920,10 @@ function processTransformVec3Attributes(
runtimeNode.instancingTranslationBuffer2D = buffer2D;
}
+ if (!use2D) {
+ return;
+ }
+
const byteOffset = 0;
const byteStride = undefined;
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index 3fcda4ea4fb7..1a6a56c5e032 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -37,6 +37,7 @@ import ModelUtility from "./ModelUtility.js";
import oneTimeWarning from "../../Core/oneTimeWarning.js";
import PntsLoader from "./PntsLoader.js";
import StyleCommandsNeeded from "./StyleCommandsNeeded.js";
+import pickModel from "./pickModel.js";
/**
* pick
when not using WebGL 2 or above. If using WebGL 2 or above, this option will be ignored. If using WebGL 1 and this is true, the pick
operation will work correctly, but it will use more memory to do so. If running with WebGL 1 and this is false, the model will use less memory, but pick
will always return undefined
. This cannot be set after the model has loaded.
* @privateParam {string|number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
* @privateParam {string|number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
* @privateParam {object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting.
@@ -455,6 +457,7 @@ function Model(options) {
this._sceneMode = undefined;
this._projectTo2D = defaultValue(options.projectTo2D, false);
+ this._enablePick = defaultValue(options.enablePick, false);
this._skipLevelOfDetail = false;
this._ignoreCommands = defaultValue(options.ignoreCommands, false);
@@ -2496,6 +2499,35 @@ Model.prototype.isClippingEnabled = function () {
);
};
+/**
+ * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {number} [verticalExaggeration=1.0] A scalar used to exaggerate the height of a position relative to the ellipsoid. If the value is 1.0 there will be no effect.
+ * @param {number} [relativeHeight=0.0] The height above the ellipsoid relative to which a position is exaggerated. If the value is 0.0 the position will be exaggerated relative to the ellipsoid surface.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Model.prototype.pick = function (
+ ray,
+ frameState,
+ verticalExaggeration,
+ relativeHeight,
+ result
+) {
+ return pickModel(
+ this,
+ ray,
+ frameState,
+ verticalExaggeration,
+ relativeHeight,
+ result
+ );
+};
+
/**
* Returns true if this object was destroyed; otherwise, false.
* pick
when not using WebGL 2 or above. If using WebGL 2 or above, this option will be ignored. If using WebGL 1 and this is true, the pick
operation will work correctly, but it will use more memory to do so. If running with WebGL 1 and this is false, the model will use less memory, but pick
will always return undefined
. This cannot be set after the model has loaded.
* @param {string|number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
* @param {string|number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
* @param {object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation and lighting.
@@ -2749,6 +2782,7 @@ Model.fromGltfAsync = async function (options) {
upAxis: options.upAxis,
forwardAxis: options.forwardAxis,
loadAttributesFor2D: options.projectTo2D,
+ enablePick: options.enablePick,
loadIndicesForWireframe: options.enableDebugWireframe,
loadPrimitiveOutline: options.enableShowOutline,
loadForClassification: defined(options.classificationType),
@@ -2825,6 +2859,7 @@ Model.fromB3dm = async function (options) {
upAxis: options.upAxis,
forwardAxis: options.forwardAxis,
loadAttributesFor2D: options.projectTo2D,
+ enablePick: options.enablePick,
loadIndicesForWireframe: options.enableDebugWireframe,
loadPrimitiveOutline: options.enableShowOutline,
loadForClassification: defined(options.classificationType),
@@ -2880,6 +2915,7 @@ Model.fromI3dm = async function (options) {
upAxis: options.upAxis,
forwardAxis: options.forwardAxis,
loadAttributesFor2D: options.projectTo2D,
+ enablePick: options.enablePick,
loadIndicesForWireframe: options.enableDebugWireframe,
loadPrimitiveOutline: options.enableShowOutline,
};
@@ -3013,6 +3049,7 @@ function makeModelOptions(loader, modelType, options) {
showCreditsOnScreen: options.showCreditsOnScreen,
splitDirection: options.splitDirection,
projectTo2D: options.projectTo2D,
+ enablePick: options.enablePick,
featureIdLabel: options.featureIdLabel,
instanceFeatureIdLabel: options.instanceFeatureIdLabel,
pointCloudShading: options.pointCloudShading,
diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js
index 7e1930ee1bae..20707a03909f 100644
--- a/packages/engine/Source/Scene/Model/Model3DTileContent.js
+++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js
@@ -3,6 +3,7 @@ import combine from "../../Core/combine.js";
import defined from "../../Core/defined.js";
import destroyObject from "../../Core/destroyObject.js";
import DeveloperError from "../../Core/DeveloperError.js";
+import Ellipsoid from "../../Core/Ellipsoid.js";
import Pass from "../../Renderer/Pass.js";
import ModelAnimationLoop from "../ModelAnimationLoop.js";
import Model from "./Model.js";
@@ -416,6 +417,35 @@ Model3DTileContent.fromGeoJson = async function (
return content;
};
+/**
+ * Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Model3DTileContent.prototype.pick = function (ray, frameState, result) {
+ if (!defined(this._model) || !this._ready) {
+ return undefined;
+ }
+
+ const verticalExaggeration = frameState.verticalExaggeration;
+ const relativeHeight = frameState.verticalExaggerationRelativeHeight;
+
+ // All tilesets assume a WGS84 ellipsoid
+ return this._model.pick(
+ ray,
+ frameState,
+ verticalExaggeration,
+ relativeHeight,
+ Ellipsoid.WGS84,
+ result
+ );
+};
+
function makeModelOptions(tileset, tile, content, additionalOptions) {
const mainOptions = {
cull: false, // The model is already culled by 3D Tiles
@@ -442,6 +472,7 @@ function makeModelOptions(tileset, tile, content, additionalOptions) {
enableDebugWireframe: tileset._enableDebugWireframe,
debugWireframe: tileset.debugWireframe,
projectTo2D: tileset._projectTo2D,
+ enablePick: tileset._enablePick,
enableShowOutline: tileset._enableShowOutline,
showOutline: tileset.showOutline,
outlineColor: tileset.outlineColor,
diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js
new file mode 100644
index 000000000000..5ce238f2237d
--- /dev/null
+++ b/packages/engine/Source/Scene/Model/pickModel.js
@@ -0,0 +1,408 @@
+import AttributeCompression from "../../Core/AttributeCompression.js";
+import BoundingSphere from "../../Core/BoundingSphere.js";
+import Cartesian3 from "../../Core/Cartesian3.js";
+import Cartographic from "../../Core/Cartographic.js";
+import Check from "../../Core/Check.js";
+import ComponentDatatype from "../../Core/ComponentDatatype.js";
+import defaultValue from "../../Core/defaultValue.js";
+import defined from "../../Core/defined.js";
+import Ellipsoid from "../../Core/Ellipsoid.js";
+import IndexDatatype from "../../Core/IndexDatatype.js";
+import IntersectionTests from "../../Core/IntersectionTests.js";
+import Ray from "../../Core/Ray.js";
+import Matrix4 from "../../Core/Matrix4.js";
+import Transforms from "../../Core/Transforms.js";
+import VerticalExaggeration from "../../Core/VerticalExaggeration.js";
+import AttributeType from "../AttributeType.js";
+import SceneMode from "../SceneMode.js";
+import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
+import ModelUtility from "./ModelUtility.js";
+
+const scratchV0 = new Cartesian3();
+const scratchV1 = new Cartesian3();
+const scratchV2 = new Cartesian3();
+const scratchNodeComputedTransform = new Matrix4();
+const scratchModelMatrix = new Matrix4();
+const scratchcomputedModelMatrix = new Matrix4();
+const scratchPickCartographic = new Cartographic();
+const scratchBoundingSphere = new BoundingSphere();
+
+/**
+ * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Model} model The model to pick.
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {number} [verticalExaggeration=1.0] A scalar used to exaggerate the height of a position relative to the ellipsoid. If the value is 1.0 there will be no effect.
+ * @param {number} [relativeHeight=0.0] The ellipsoid height relative to which a position is exaggerated. If the value is 0.0 the position will be exaggerated relative to the ellipsoid surface.
+ * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to which the exaggerated position is relative.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+export default function pickModel(
+ model,
+ ray,
+ frameState,
+ verticalExaggeration,
+ relativeHeight,
+ ellipsoid,
+ result
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("model", model);
+ Check.typeOf.object("ray", ray);
+ Check.typeOf.object("frameState", frameState);
+ //>>includeEnd('debug');
+
+ if (!model._ready || frameState.mode === SceneMode.MORPHING) {
+ return;
+ }
+
+ let minT = Number.MAX_VALUE;
+ const sceneGraph = model.sceneGraph;
+
+ const nodes = sceneGraph._runtimeNodes;
+ for (let i = 0; i < nodes.length; i++) {
+ const runtimeNode = nodes[i];
+ const node = runtimeNode.node;
+
+ let nodeComputedTransform = Matrix4.clone(
+ runtimeNode.computedTransform,
+ scratchNodeComputedTransform
+ );
+ let modelMatrix = Matrix4.clone(
+ sceneGraph.computedModelMatrix,
+ scratchModelMatrix
+ );
+
+ const instances = node.instances;
+ if (defined(instances)) {
+ if (instances.transformInWorldSpace) {
+ // Replicate the multiplication order in LegacyInstancingStageVS.
+ modelMatrix = Matrix4.multiplyTransformation(
+ model.modelMatrix,
+ sceneGraph.components.transform,
+ modelMatrix
+ );
+
+ nodeComputedTransform = Matrix4.multiplyTransformation(
+ sceneGraph.axisCorrectionMatrix,
+ runtimeNode.computedTransform,
+ nodeComputedTransform
+ );
+ }
+ }
+
+ let computedModelMatrix = Matrix4.multiplyTransformation(
+ modelMatrix,
+ nodeComputedTransform,
+ scratchcomputedModelMatrix
+ );
+
+ if (frameState.mode !== SceneMode.SCENE3D) {
+ computedModelMatrix = Transforms.basisTo2D(
+ frameState.mapProjection,
+ computedModelMatrix,
+ computedModelMatrix
+ );
+ }
+
+ const transforms = [];
+ if (defined(instances)) {
+ const transformsCount = instances.attributes[0].count;
+ const instanceComponentDatatype =
+ instances.attributes[0].componentDatatype;
+
+ const transformElements = 12;
+ let transformsTypedArray = runtimeNode.transformsTypedArray;
+ if (!defined(transformsTypedArray)) {
+ const instanceTransformsBuffer = runtimeNode.instancingTransformsBuffer;
+ if (defined(instanceTransformsBuffer) && frameState.context.webgl2) {
+ transformsTypedArray = ComponentDatatype.createTypedArray(
+ instanceComponentDatatype,
+ transformsCount * transformElements
+ );
+ instanceTransformsBuffer.getBufferData(transformsTypedArray);
+ }
+ }
+
+ if (defined(transformsTypedArray)) {
+ for (let i = 0; i < transformsCount; i++) {
+ const index = i * transformElements;
+
+ const transform = new Matrix4(
+ transformsTypedArray[index],
+ transformsTypedArray[index + 1],
+ transformsTypedArray[index + 2],
+ transformsTypedArray[index + 3],
+ transformsTypedArray[index + 4],
+ transformsTypedArray[index + 5],
+ transformsTypedArray[index + 6],
+ transformsTypedArray[index + 7],
+ transformsTypedArray[index + 8],
+ transformsTypedArray[index + 9],
+ transformsTypedArray[index + 10],
+ transformsTypedArray[index + 11],
+ 0,
+ 0,
+ 0,
+ 1
+ );
+
+ if (instances.transformInWorldSpace) {
+ Matrix4.multiplyTransformation(
+ transform,
+ nodeComputedTransform,
+ transform
+ );
+ Matrix4.multiplyTransformation(modelMatrix, transform, transform);
+ } else {
+ Matrix4.multiplyTransformation(
+ transform,
+ computedModelMatrix,
+ transform
+ );
+ }
+ transforms.push(transform);
+ }
+ }
+ }
+
+ if (transforms.length === 0) {
+ transforms.push(computedModelMatrix);
+ }
+
+ const primitivesLength = runtimeNode.runtimePrimitives.length;
+ for (let j = 0; j < primitivesLength; j++) {
+ const runtimePrimitive = runtimeNode.runtimePrimitives[j];
+ const primitive = runtimePrimitive.primitive;
+
+ if (defined(runtimePrimitive.boundingSphere) && !defined(instances)) {
+ const boundingSphere = BoundingSphere.transform(
+ runtimePrimitive.boundingSphere,
+ computedModelMatrix,
+ scratchBoundingSphere
+ );
+ const boundsIntersection = IntersectionTests.raySphere(
+ ray,
+ boundingSphere
+ );
+ if (!defined(boundsIntersection)) {
+ continue;
+ }
+ }
+
+ const positionAttribute = ModelUtility.getAttributeBySemantic(
+ primitive,
+ VertexAttributeSemantic.POSITION
+ );
+ const vertexCount = positionAttribute.count;
+
+ if (!defined(primitive.indices)) {
+ // Point clouds
+ continue;
+ }
+
+ let indices = primitive.indices.typedArray;
+ if (!defined(indices)) {
+ const indicesBuffer = primitive.indices.buffer;
+ const indicesCount = primitive.indices.count;
+ const indexDatatype = primitive.indices.indexDatatype;
+ if (defined(indicesBuffer) && frameState.context.webgl2) {
+ if (indexDatatype === IndexDatatype.UNSIGNED_BYTE) {
+ indices = new Uint8Array(indicesCount);
+ } else if (indexDatatype === IndexDatatype.UNSIGNED_SHORT) {
+ indices = new Uint16Array(indicesCount);
+ } else if (indexDatatype === IndexDatatype.UNSIGNED_INT) {
+ indices = new Uint32Array(indicesCount);
+ }
+
+ indicesBuffer.getBufferData(indices);
+ }
+ }
+
+ let vertices = positionAttribute.typedArray;
+ let componentDatatype = positionAttribute.componentDatatype;
+ let attributeType = positionAttribute.type;
+
+ const quantization = positionAttribute.quantization;
+ if (defined(quantization)) {
+ componentDatatype = positionAttribute.quantization.componentDatatype;
+ attributeType = positionAttribute.quantization.type;
+ }
+
+ const numComponents = AttributeType.getNumberOfComponents(attributeType);
+ const elementCount = vertexCount * numComponents;
+
+ if (!defined(vertices)) {
+ const verticesBuffer = positionAttribute.buffer;
+
+ if (defined(verticesBuffer) && frameState.context.webgl2) {
+ vertices = ComponentDatatype.createTypedArray(
+ componentDatatype,
+ elementCount
+ );
+ verticesBuffer.getBufferData(
+ vertices,
+ positionAttribute.byteOffset,
+ 0,
+ elementCount
+ );
+ }
+
+ if (quantization && positionAttribute.normalized) {
+ vertices = AttributeCompression.dequantize(
+ vertices,
+ componentDatatype,
+ attributeType,
+ vertexCount
+ );
+ }
+ }
+
+ if (!defined(indices) || !defined(vertices)) {
+ return;
+ }
+
+ ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);
+ verticalExaggeration = defaultValue(verticalExaggeration, 1.0);
+ relativeHeight = defaultValue(relativeHeight, 0.0);
+
+ const indicesLength = indices.length;
+ for (let i = 0; i < indicesLength; i += 3) {
+ const i0 = indices[i];
+ const i1 = indices[i + 1];
+ const i2 = indices[i + 2];
+
+ for (const instanceTransform of transforms) {
+ const v0 = getVertexPosition(
+ vertices,
+ i0,
+ numComponents,
+ quantization,
+ instanceTransform,
+ verticalExaggeration,
+ relativeHeight,
+ ellipsoid,
+ scratchV0
+ );
+ const v1 = getVertexPosition(
+ vertices,
+ i1,
+ numComponents,
+ quantization,
+ instanceTransform,
+ verticalExaggeration,
+ relativeHeight,
+ ellipsoid,
+ scratchV1
+ );
+ const v2 = getVertexPosition(
+ vertices,
+ i2,
+ numComponents,
+ quantization,
+ instanceTransform,
+ verticalExaggeration,
+ relativeHeight,
+ ellipsoid,
+ scratchV2
+ );
+
+ const t = IntersectionTests.rayTriangleParametric(
+ ray,
+ v0,
+ v1,
+ v2,
+ defaultValue(model.backFaceCulling, true)
+ );
+
+ if (defined(t)) {
+ if (t < minT && t >= 0.0) {
+ minT = t;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (minT === Number.MAX_VALUE) {
+ return undefined;
+ }
+
+ result = Ray.getPoint(ray, minT, result);
+ if (frameState.mode !== SceneMode.SCENE3D) {
+ Cartesian3.fromElements(result.y, result.z, result.x, result);
+
+ const projection = frameState.mapProjection;
+ const ellipsoid = projection.ellipsoid;
+
+ const cartographic = projection.unproject(result, scratchPickCartographic);
+ ellipsoid.cartographicToCartesian(cartographic, result);
+ }
+
+ return result;
+}
+
+function getVertexPosition(
+ vertices,
+ index,
+ numComponents,
+ quantization,
+ instanceTransform,
+ verticalExaggeration,
+ relativeHeight,
+ ellipsoid,
+ result
+) {
+ const i = index * numComponents;
+ result.x = vertices[i];
+ result.y = vertices[i + 1];
+ result.z = vertices[i + 2];
+
+ if (defined(quantization)) {
+ if (quantization.octEncoded) {
+ result = AttributeCompression.octDecodeInRange(
+ result,
+ quantization.normalizationRange,
+ result
+ );
+
+ if (quantization.octEncodedZXY) {
+ const x = result.x;
+ result.x = result.z;
+ result.z = result.y;
+ result.y = x;
+ }
+ } else {
+ result = Cartesian3.multiplyComponents(
+ result,
+ quantization.quantizedVolumeStepSize,
+ result
+ );
+
+ result = Cartesian3.add(
+ result,
+ quantization.quantizedVolumeOffset,
+ result
+ );
+ }
+ }
+
+ result = Matrix4.multiplyByPoint(instanceTransform, result, result);
+
+ if (verticalExaggeration !== 1.0) {
+ VerticalExaggeration.getPosition(
+ result,
+ ellipsoid,
+ verticalExaggeration,
+ relativeHeight,
+ result
+ );
+ }
+
+ return result;
+}
diff --git a/packages/engine/Source/Scene/Multiple3DTileContent.js b/packages/engine/Source/Scene/Multiple3DTileContent.js
index cd62641172b4..42ede698a349 100644
--- a/packages/engine/Source/Scene/Multiple3DTileContent.js
+++ b/packages/engine/Source/Scene/Multiple3DTileContent.js
@@ -1,3 +1,4 @@
+import Cartesian3 from "../Core/Cartesian3.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
@@ -650,6 +651,47 @@ Multiple3DTileContent.prototype.update = function (tileset, frameState) {
}
};
+/**
+ * Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
+ *
+ * @param {Ray} ray The ray to test for intersection.
+ * @param {FrameState} frameState The frame state.
+ * @param {Cartesian3|undefined} [result] The intersection or undefined
if none was found.
+ * @returns {Cartesian3|undefined} The intersection or undefined
if none was found.
+ *
+ * @private
+ */
+Multiple3DTileContent.prototype.pick = function (ray, frameState, result) {
+ if (!this._ready) {
+ return undefined;
+ }
+
+ let intersection;
+ let minDistance = Number.POSITIVE_INFINITY;
+ const contents = this._contents;
+ const length = contents.length;
+
+ for (let i = 0; i < length; ++i) {
+ const candidate = contents[i].pick(ray, frameState, result);
+
+ if (!defined(candidate)) {
+ continue;
+ }
+
+ const distance = Cartesian3.distance(ray.origin, candidate);
+ if (distance < minDistance) {
+ intersection = candidate;
+ minDistance = distance;
+ }
+ }
+
+ if (!defined(intersection)) {
+ return undefined;
+ }
+
+ return result;
+};
+
Multiple3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js
index 8b3a207ec133..0571d06eaf3d 100644
--- a/packages/engine/Source/Scene/Scene.js
+++ b/packages/engine/Source/Scene/Scene.js
@@ -3604,16 +3604,38 @@ function getGlobeHeight(scene) {
const globe = scene._globe;
const camera = scene.camera;
const cartographic = camera.positionCartographic;
+
+ let maxHeight = Number.NEGATIVE_INFINITY;
+ const length = scene.primitives.length;
+ for (let i = 0; i < length; ++i) {
+ const primitive = scene.primitives.get(i);
+ if (!primitive.isCesium3DTileset || primitive.disableCollision) {
+ continue;
+ }
+
+ const result = primitive.getHeight(cartographic, scene);
+ if (defined(result) && result > maxHeight) {
+ maxHeight = result;
+ }
+ }
+
if (defined(globe) && globe.show && defined(cartographic)) {
- return globe.getHeight(cartographic);
+ const result = globe.getHeight(cartographic);
+ if (result > maxHeight) {
+ maxHeight = result;
+ }
+ }
+
+ if (maxHeight > Number.NEGATIVE_INFINITY) {
+ return maxHeight;
}
+
return undefined;
}
function isCameraUnderground(scene) {
const camera = scene.camera;
const mode = scene._mode;
- const globe = scene.globe;
const cameraController = scene._screenSpaceCameraController;
const cartographic = camera.positionCartographic;
@@ -3627,12 +3649,7 @@ function isCameraUnderground(scene) {
return true;
}
- if (
- !defined(globe) ||
- !globe.show ||
- mode === SceneMode.SCENE2D ||
- mode === SceneMode.MORPHING
- ) {
+ if (mode === SceneMode.SCENE2D || mode === SceneMode.MORPHING) {
return false;
}
diff --git a/packages/engine/Source/Scene/ScreenSpaceCameraController.js b/packages/engine/Source/Scene/ScreenSpaceCameraController.js
index b2f64010731a..ded8ddfed4ce 100644
--- a/packages/engine/Source/Scene/ScreenSpaceCameraController.js
+++ b/packages/engine/Source/Scene/ScreenSpaceCameraController.js
@@ -2861,16 +2861,12 @@ function adjustHeightForTerrain(controller) {
const mode = scene.mode;
const globe = scene.globe;
- if (
- !defined(globe) ||
- mode === SceneMode.SCENE2D ||
- mode === SceneMode.MORPHING
- ) {
+ if (mode === SceneMode.SCENE2D || mode === SceneMode.MORPHING) {
return;
}
const camera = scene.camera;
- const ellipsoid = globe.ellipsoid;
+ const ellipsoid = defaultValue(globe?.ellipsoid, Ellipsoid.WGS84);
const projection = scene.mapProjection;
let transform;
diff --git a/packages/engine/Source/Scene/Tileset3DTileContent.js b/packages/engine/Source/Scene/Tileset3DTileContent.js
index 655f0e675ef1..41d1652453c1 100644
--- a/packages/engine/Source/Scene/Tileset3DTileContent.js
+++ b/packages/engine/Source/Scene/Tileset3DTileContent.js
@@ -168,6 +168,10 @@ Tileset3DTileContent.prototype.applyStyle = function (style) {};
Tileset3DTileContent.prototype.update = function (tileset, frameState) {};
+Tileset3DTileContent.prototype.pick = function (ray, frameState, result) {
+ return undefined;
+};
+
Tileset3DTileContent.prototype.isDestroyed = function () {
return false;
};
diff --git a/packages/engine/Source/Scene/Vector3DTileContent.js b/packages/engine/Source/Scene/Vector3DTileContent.js
index e191ede706a9..120a22db5801 100644
--- a/packages/engine/Source/Scene/Vector3DTileContent.js
+++ b/packages/engine/Source/Scene/Vector3DTileContent.js
@@ -727,6 +727,10 @@ Vector3DTileContent.prototype.update = function (tileset, frameState) {
}
};
+Vector3DTileContent.prototype.pick = function (ray, frameState, result) {
+ return undefined;
+};
+
Vector3DTileContent.prototype.getPolylinePositions = function (batchId) {
const polylines = this._polylines;
if (!defined(polylines)) {
diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js
index d0ba216f7cfd..6653ca4e04f7 100644
--- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js
+++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js
@@ -46,6 +46,7 @@ import Cesium3DTilesTester from "../../../../Specs/Cesium3DTilesTester.js";
import createScene from "../../../../Specs/createScene.js";
import generateJsonBuffer from "../../../../Specs/generateJsonBuffer.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
+import Ellipsoid from "../../Source/Core/Ellipsoid.js";
describe(
"Scene/Cesium3DTileset",
@@ -211,6 +212,7 @@ describe(
});
afterEach(function () {
+ scene.verticalExaggeration = 1.0;
scene.primitives.removeAll();
ResourceCache.clearForSpecs();
});
@@ -2444,6 +2446,199 @@ describe(
});
});
+ it("picks", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl, {
+ enablePick: !scene.frameState.context.webgl2,
+ });
+ viewRootOnly();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(
+ 1215026.8094312553,
+ -4736367.339076743,
+ 4081652.238842398
+ );
+ expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("picks tileset of tilesets", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(
+ scene,
+ tilesetOfTilesetsUrl,
+ {
+ enablePick: !scene.frameState.context.webgl2,
+ }
+ );
+ viewRootOnly();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(
+ 1215026.8094312553,
+ -4736367.339076743,
+ 4081652.238842398
+ );
+ expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("picks instanced tileset", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(
+ scene,
+ instancedUrl,
+ {
+ enablePick: !scene.frameState.context.webgl2,
+ }
+ );
+ viewInstances();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(
+ 1215015.7820120894,
+ -4736324.352446682,
+ 4081615.004915994
+ );
+ expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("picks translucent tileset", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(
+ scene,
+ translucentUrl,
+ {
+ enablePick: !scene.frameState.context.webgl2,
+ }
+ );
+ viewAllTiles();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(
+ 1215013.1035421563,
+ -4736313.911345786,
+ 4081605.96109977
+ );
+ expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("picks tileset with transforms", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(
+ scene,
+ tilesetWithTransformsUrl,
+ {
+ enablePick: !scene.frameState.context.webgl2,
+ }
+ );
+ viewAllTiles();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(
+ 1215013.8353220497,
+ -4736316.763939952,
+ 4081608.4319443353
+ );
+ expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("picking point cloud tileset returns undefined", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(
+ scene,
+ pointCloudUrl,
+ {
+ enablePick: !scene.frameState.context.webgl2,
+ }
+ );
+ viewAllTiles();
+ scene.renderForSpecs();
+
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ expect(tileset.pick(ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("getHeight samples height at a cartographic position", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl, {
+ enablePick: !scene.frameState.context.webgl2,
+ });
+ viewRootOnly();
+ await Cesium3DTilesTester.waitForTilesLoaded(scene, tileset);
+ scene.renderForSpecs();
+
+ const center = Ellipsoid.WGS84.cartesianToCartographic(
+ tileset.boundingSphere.center
+ );
+ const height = tileset.getHeight(center, scene);
+ expect(height).toEqualEpsilon(78.1558019795064, CesiumMath.EPSILON12);
+ });
+
+ it("getHeight samples height accounting for vertical exaggeration", async function () {
+ const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl, {
+ enablePick: !scene.frameState.context.webgl2,
+ });
+ viewRootOnly();
+ await Cesium3DTilesTester.waitForTilesLoaded(scene, tileset);
+ scene.verticalExaggeration = 2.0;
+ scene.renderForSpecs();
+
+ const center = Ellipsoid.WGS84.cartesianToCartographic(
+ tileset.boundingSphere.center
+ );
+ const height = tileset.getHeight(center, scene);
+ expect(height).toEqualEpsilon(156.31161477299992, CesiumMath.EPSILON12);
+ });
+
it("destroys", function () {
return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
tileset
diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js
index 5a15b226c468..25c1f5c802a2 100644
--- a/packages/engine/Specs/Scene/Model/ModelSpec.js
+++ b/packages/engine/Specs/Scene/Model/ModelSpec.js
@@ -4411,6 +4411,28 @@ describe(
});
});
+ it("pick returns position of intersection between ray and model surface", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0.5, 0, 0.5);
+ expect(model.pick(ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
it("destroy works", function () {
spyOn(ShaderProgram.prototype, "destroy").and.callThrough();
return loadAndZoomToModelAsync({ gltf: boxTexturedGlbUrl }, scene).then(
diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js
new file mode 100644
index 000000000000..4cf4ee77670d
--- /dev/null
+++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js
@@ -0,0 +1,401 @@
+import {
+ pickModel,
+ Cartesian2,
+ Cartesian3,
+ Ellipsoid,
+ HeadingPitchRange,
+ Math as CesiumMath,
+ Model,
+ Ray,
+ SceneMode,
+} from "../../../index.js";
+
+import loadAndZoomToModelAsync from "./loadAndZoomToModelAsync.js";
+import createScene from "../../../../../Specs/createScene.js";
+
+describe("Scene/Model/pickModel", function () {
+ const boxTexturedGltfUrl =
+ "./Data/Models/glTF-2.0/BoxTextured/glTF/BoxTextured.gltf";
+ const boxInstanced =
+ "./Data/Models/glTF-2.0/BoxInstancedNoNormals/glTF/BoxInstancedNoNormals.gltf";
+ const boxWithOffsetUrl =
+ "./Data/Models/glTF-2.0/BoxWithOffset/glTF/BoxWithOffset.gltf";
+ const pointCloudUrl =
+ "./Data/Models/glTF-2.0/PointCloudWithRGBColors/glTF-Binary/PointCloudWithRGBColors.glb";
+ const boxWithMixedCompression =
+ "./Data/Models/glTF-2.0/BoxMixedCompression/glTF/BoxMixedCompression.gltf";
+ const boxWithQuantizedAttributes =
+ "./Data/Models/glTF-2.0/BoxWeb3dQuantizedAttributes/glTF/BoxWeb3dQuantizedAttributes.gltf";
+ const boxCesiumRtcUrl =
+ "./Data/Models/glTF-2.0/BoxCesiumRtc/glTF/BoxCesiumRtc.gltf";
+ const boxBackFaceCullingUrl =
+ "./Data/Models/glTF-2.0/BoxBackFaceCulling/glTF/BoxBackFaceCulling.gltf";
+
+ let scene;
+ beforeAll(function () {
+ scene = createScene();
+ });
+
+ afterAll(function () {
+ scene.destroyForSpecs();
+ });
+
+ afterEach(function () {
+ scene.frameState.mode = SceneMode.SCENE3D;
+ scene.primitives.removeAll();
+ });
+
+ it("throws without model", function () {
+ expect(() => pickModel()).toThrowDeveloperError();
+ });
+
+ it("throws without ray", async function () {
+ const model = await Model.fromGltfAsync({
+ url: boxTexturedGltfUrl,
+ });
+ expect(() => pickModel(model)).toThrowDeveloperError();
+ });
+
+ it("throws without frameState", async function () {
+ const model = await Model.fromGltfAsync({
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ });
+ const ray = new Ray();
+ expect(() => pickModel(model, ray)).toThrowDeveloperError();
+ });
+
+ it("returns undefined if model is not ready", async function () {
+ const model = await Model.fromGltfAsync({
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ });
+ const ray = new Ray();
+ expect(pickModel(model, ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("returns undefined if ray does not intersect model surface", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = new Ray();
+ expect(pickModel(model, ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("returns position of intersection between ray and model surface", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0.5, 0, 0.5);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("returns position of intersection between ray and model surface with enablePick in WebGL 1", async function () {
+ const sceneWithWebgl1 = createScene({
+ contextOptions: {
+ requestWebgl1: true,
+ },
+ });
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: true,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0.5, 0, 0.5);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+
+ sceneWithWebgl1.destroyForSpecs();
+ });
+
+ it("returns position of intersection accounting for node transforms", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxWithOffsetUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0.0, 5.5, -0.5);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("returns position of intersection with RTC model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxCesiumRtcUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(6378137.5, 0.0, -0.499999996649);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON8
+ );
+ });
+
+ it("returns position of intersection with quantzed model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxWithQuantizedAttributes,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0.5, 0, 0.5);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("returns position of intersection with mixed compression model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxWithMixedCompression,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(1.0, 0, 1.0);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("returns position of intersection with instanced model", async function () {
+ // None of the 4 instanced cubes are in the center of the model's bounding
+ // sphere, so set up a camera view that focuses in on one of them.
+ const offset = new HeadingPitchRange(
+ CesiumMath.PI_OVER_TWO,
+ -CesiumMath.PI_OVER_FOUR,
+ 1
+ );
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxInstanced,
+ enablePick: !scene.frameState.context.webgl2,
+ offset,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(0, -0.5, 0.5);
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("returns undefined for point cloud", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: pointCloudUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ expect(pickModel(model, ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("cullsBackFaces by default", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+ ray.origin = model.boundingSphere.center;
+
+ expect(pickModel(model, ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("includes back faces results when model disables backface culling", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxBackFaceCullingUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ backFaceCulling: false,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ ray.origin = model.boundingSphere.center;
+
+ const expected = new Cartesian3(
+ -0.9999998807907355,
+ 0,
+ -0.9999998807907104
+ );
+ expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon(
+ expected,
+ CesiumMath.EPSILON12
+ );
+ });
+
+ it("uses result parameter if specified", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const result = new Cartesian3();
+ const expected = new Cartesian3(0.5, 0, 0.5);
+ const returned = pickModel(
+ model,
+ ray,
+ scene.frameState,
+ undefined,
+ undefined,
+ undefined,
+ result
+ );
+ expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON12);
+ expect(returned).toBe(result);
+ });
+
+ it("returns undefined when morphing", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ scene.frameState.mode = SceneMode.MORPHING;
+ expect(pickModel(model, ray, scene.frameState)).toBeUndefined();
+ });
+
+ it("returns position with vertical exaggeration", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ enablePick: !scene.frameState.context.webgl2,
+ },
+ scene
+ );
+ const ray = scene.camera.getPickRay(
+ new Cartesian2(
+ scene.drawingBufferWidth / 2.0,
+ scene.drawingBufferHeight / 2.0
+ )
+ );
+
+ const expected = new Cartesian3(-65.51341504, 0, -65.51341504);
+ expect(
+ pickModel(
+ model,
+ ray,
+ scene.frameState,
+ 2.0,
+ -Ellipsoid.WGS84.minimumRadius
+ )
+ ).toEqualEpsilon(expected, CesiumMath.EPSILON8);
+ });
+});