From 51d34ecbac9ee722b3ed3ac74cb83cc7fc754280 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 19 Oct 2023 13:14:12 -0400 Subject: [PATCH 01/23] pick 3D Tiles modelContent --- .../Sandcastle/gallery/3D Tiles Clamping.html | 114 ++++++++++++++ .../Source/Scene/Cesium3DTileContent.js | 20 +++ .../engine/Source/Scene/Cesium3DTileset.js | 18 +++ .../engine/Source/Scene/Empty3DTileContent.js | 4 + packages/engine/Source/Scene/Model/Model.js | 145 ++++++++++++++++++ .../Source/Scene/Model/Model3DTileContent.js | 24 +++ packages/engine/Source/Scene/Scene.js | 11 ++ 7 files changed, 336 insertions(+) create mode 100644 Apps/Sandcastle/gallery/3D Tiles Clamping.html diff --git a/Apps/Sandcastle/gallery/3D Tiles Clamping.html b/Apps/Sandcastle/gallery/3D Tiles Clamping.html new file mode 100644 index 000000000000..abd31c542755 --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Clamping.html @@ -0,0 +1,114 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/packages/engine/Source/Scene/Cesium3DTileContent.js b/packages/engine/Source/Scene/Cesium3DTileContent.js index d67e91519b71..d6613a1a7f5f 100644 --- a/packages/engine/Source/Scene/Cesium3DTileContent.js +++ b/packages/engine/Source/Scene/Cesium3DTileContent.js @@ -350,6 +350,26 @@ Cesium3DTileContent.prototype.update = function (tileset, frameState) { DeveloperError.throwInstantiationError(); }; +/** + * 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 {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. + * @param {Cartesian3|undefined} result The intersection or undefined if none was found. + * @returns {Cartesian3|undefined} The intersection or undefined if none was found. + * + * @private + */ +Cesium3DTileContent.prototype.pick = function ( + ray, + frameState, + cullBackFaces, + result +) { + DeveloperError.throwInstantiationError(); +}; + /** * Returns true if this object was destroyed; otherwise, false. *

diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 6d8e005f84a3..f72abc6b2049 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3439,6 +3439,24 @@ Cesium3DTileset.checkSupportedExtensions = function (extensionsRequired) { } }; +/** + * Get the height of the loaded surface at a given cartographic. + * + * TODO + * @param {Cartographic} cartographic + * @returns {number|undefined} The height of the cartographic or undefined if it could not be found. + */ +Cesium3DTileset.prototype.getHeight = function (cartographic) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("cartographic", cartographic); + //>>includeEnd('debug'); + + // Get loaded tile at that location + // Drill down to content + // Read existing data + // const intersection = tile.content.pick(ray, frameState, true, result) +}; + /** * 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/Empty3DTileContent.js b/packages/engine/Source/Scene/Empty3DTileContent.js index 28f3c1d854bf..2f4f16f57ce3 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) { + return undefined; +}; + Empty3DTileContent.prototype.isDestroyed = function () { return false; }; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 57c174ce725e..d6ae6a5142d6 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -1,17 +1,22 @@ +import AttributeType from "../AttributeType.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 Credit from "../../Core/Credit.js"; import Color from "../../Core/Color.js"; +import ComponentDatatype from "../../Core/ComponentDatatype.js"; import defined from "../../Core/defined.js"; import defaultValue from "../../Core/defaultValue.js"; import DeveloperError from "../../Core/DeveloperError.js"; import destroyObject from "../../Core/destroyObject.js"; import DistanceDisplayCondition from "../../Core/DistanceDisplayCondition.js"; import Event from "../../Core/Event.js"; +import IndexDatatype from "../../Core/IndexDatatype.js"; +import IntersectionTests from "../../Core/IntersectionTests.js"; import Matrix3 from "../../Core/Matrix3.js"; import Matrix4 from "../../Core/Matrix4.js"; +import Ray from "../../Core/Ray.js"; import Resource from "../../Core/Resource.js"; import RuntimeError from "../../Core/RuntimeError.js"; import Pass from "../../Renderer/Pass.js"; @@ -37,6 +42,7 @@ import ModelUtility from "./ModelUtility.js"; import oneTimeWarning from "../../Core/oneTimeWarning.js"; import PntsLoader from "./PntsLoader.js"; import StyleCommandsNeeded from "./StyleCommandsNeeded.js"; +import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; /** *
@@ -2481,6 +2487,145 @@ Model.prototype.isClippingEnabled = function () { ); }; +const scratchV0 = new Cartesian3(); +const scratchV1 = new Cartesian3(); +const scratchV2 = new Cartesian3(); + +/** + * 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 {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. + * @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, cullBackFaces, result) { + if (!frameState.context.webgl2) { + // TODO: error? + } + + let minT = Number.MAX_VALUE; + + // Check all the primitive positions + const nodes = this._sceneGraph._runtimeNodes; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + for (let j = 0; j < node.node.primitives.length; j++) { + const primitive = node.node.primitives[j]; + + const positionAttribute = ModelUtility.getAttributeBySemantic( + primitive, + VertexAttributeSemantic.POSITION + ); + const vertexCount = positionAttribute.count; + + let indices = primitive.indices.typedArray; + if (!defined(indices)) { + const indicesBuffer = primitive.indices.buffer; + const indicesCount = primitive.indices.count; + if (defined(indicesBuffer) && frameState.context.webgl2) { + const useUint8Array = indicesBuffer.sizeInBytes === indicesCount; + indices = useUint8Array + ? new Uint8Array(indicesCount) + : IndexDatatype.createTypedArray(vertexCount, indicesCount); + indicesBuffer.getBufferData(indices); + } + } + + let vertices = positionAttribute.typedArray; + let componentDatatype = positionAttribute.componentDatatype; + let attributeType = positionAttribute.type; + + const count = + vertexCount * AttributeType.getNumberOfComponents(attributeType); + const quantization = positionAttribute.quantization; + if (defined(quantization)) { + componentDatatype = positionAttribute.quantization.componentDatatype; + attributeType = positionAttribute.quantization.type; + } + + if (!defined(vertices)) { + const verticesBuffer = positionAttribute.buffer; + if (defined(verticesBuffer) && frameState.context.webgl2) { + vertices = ComponentDatatype.createTypedArray( + componentDatatype, + count + ); + verticesBuffer.getBufferData(vertices); + } + } + + const computedNodeTransform = node.computedTransform; + const computedModelMatrix = this._sceneGraph.computedModelMatrix; + 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]; + + const getPosition = (vertices, index, result) => { + // TODO: Exaggeration + + const i = index * 3; + result.x = vertices[i]; + result.y = vertices[i + 1]; + result.z = vertices[i + 2]; + + if (defined(quantization)) { + result = Cartesian3.multiplyComponents( + result, + quantization.quantizedVolumeStepSize, + result + ); + + result = Cartesian3.add( + result, + quantization.quantizedVolumeOffset, + result + ); + } + + result = Matrix4.multiplyByPoint( + computedNodeTransform, + result, + result + ); + result = Matrix4.multiplyByPoint(computedModelMatrix, result, result); + + return result; + }; + + const v0 = getPosition(vertices, i0, scratchV0); + const v1 = getPosition(vertices, i1, scratchV1); + const v2 = getPosition(vertices, i2, scratchV2); + + const t = IntersectionTests.rayTriangleParametric( + ray, + v0, + v1, + v2, + cullBackFaces + ); + + if (defined(t)) { + if (t < minT && t >= 0.0) { + minT = t; + } + } + } + } + } + + if (minT === Number.MAX_VALUE) { + return undefined; + } + + return Ray.getPoint(ray, minT, result); +}; + /** * Returns true if this object was destroyed; otherwise, false. *

diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index 7e1930ee1bae..b53ffc5bdcaa 100644 --- a/packages/engine/Source/Scene/Model/Model3DTileContent.js +++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js @@ -416,6 +416,30 @@ 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 {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. + * @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, + cullBackFaces, + result +) { + if (!defined(this._model) || !this._ready) { + return undefined; + } + + return this._model.pick(ray, frameState, cullBackFaces, result); +}; + function makeModelOptions(tileset, tile, content, additionalOptions) { const mainOptions = { cull: false, // The model is already culled by 3D Tiles diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 73c95a563f58..957aa804854e 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -162,6 +162,8 @@ function Scene(options) { this._globeTranslucencyState = new GlobeTranslucencyState(); this._primitives = new PrimitiveCollection(); this._groundPrimitives = new PrimitiveCollection(); + // List of tilesets to clamp to, in order of preference + this._terrainTilesets = []; this._globeHeight = undefined; this._cameraUnderground = false; @@ -3576,9 +3578,18 @@ function getGlobeHeight(scene) { const globe = scene._globe; const camera = scene.camera; const cartographic = camera.positionCartographic; + + for (const tileset of scene._terrainTilesets) { + const result = tileset.getHeight(cartographic); + if (defined(result)) { + return result; + } + } + if (defined(globe) && globe.show && defined(cartographic)) { return globe.getHeight(cartographic); } + return undefined; } From caa6b71c0ec1341e0b787499eeee74c3b25f9fd6 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Fri, 20 Oct 2023 12:01:20 -0400 Subject: [PATCH 02/23] Factor in tilesets to camera controller adjust height --- .../gallery/3D Tiles Next S2 Globe.html | 1 + ...es Clamping.html => 3D Tiles Picking.html} | 50 ++++---- .../Sandcastle/gallery/Clamp to 3D Tiles.html | 12 +- ...alistic 3D Tiles with Building Insert.html | 1 + .../Google Photorealistic 3D Tiles.html | 1 + .../engine/Source/Scene/Cesium3DTileset.js | 108 ++++++++++++++++-- packages/engine/Source/Scene/Model/Model.js | 2 + packages/engine/Source/Scene/Scene.js | 54 +++++++-- .../Scene/ScreenSpaceCameraController.js | 8 +- 9 files changed, 189 insertions(+), 48 deletions(-) rename Apps/Sandcastle/gallery/{3D Tiles Clamping.html => 3D Tiles Picking.html} (75%) diff --git a/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html index 28f1198ab04b..0ac29a48c476 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html +++ b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html @@ -81,6 +81,7 @@ maximumScreenSpaceError: 4, }); scene.primitives.add(tileset); + scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading tileset: ${error}`); } diff --git a/Apps/Sandcastle/gallery/3D Tiles Clamping.html b/Apps/Sandcastle/gallery/3D Tiles Picking.html similarity index 75% rename from Apps/Sandcastle/gallery/3D Tiles Clamping.html rename to Apps/Sandcastle/gallery/3D Tiles Picking.html index abd31c542755..2402bd345d4d 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Clamping.html +++ b/Apps/Sandcastle/gallery/3D Tiles Picking.html @@ -48,10 +48,12 @@ // Enable rendering the sky viewer.scene.skyAtmosphere.show = true; + let tileset; // Add Photorealistic 3D Tiles try { - const tileset = await Cesium.createGooglePhotorealistic3DTileset(); + tileset = await Cesium.createGooglePhotorealistic3DTileset(); viewer.scene.primitives.add(tileset); + viewer.scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); @@ -62,7 +64,7 @@ const scratchCartesian = new Cesium.Cartesian3(); const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas); handler.setInputAction(function (movement) { - const feature = scene.pick(movement.position); + // const feature = scene.pick(movement.position); const pickedPositionResult = scene.pickPosition(movement.position); console.log(pickedPositionResult); if (Cesium.defined(pickedPositionResult)) { @@ -78,27 +80,33 @@ const ray = scene.camera.getPickRay(movement.position); - if (Cesium.defined(feature.content)) { - const content = feature.content; - const picked = content.pick( - ray, - scene.frameState, - true, - scratchCartesian - ); + const picked = tileset.pick( + ray, + scene.frameState, + true, + scratchCartesian + ); + // if (Cesium.defined(feature.content)) { + // const content = feature.content; + // const picked = content.pick( + // ray, + // scene.frameState, + // true, + // scratchCartesian + // ); - console.log(picked); - if (Cesium.defined(picked)) { - viewer.entities.add({ - position: picked, - point: { - pixelSize: 10, - color: Cesium.Color.YELLOW, - disableDepthTestDistance: Number.POSITIVE_INFINITY, - }, - }); - } + console.log(picked); + if (Cesium.defined(picked)) { + viewer.entities.add({ + position: picked, + point: { + pixelSize: 10, + color: Cesium.Color.YELLOW, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }, + }); } + // } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); //Sandcastle_End }; if (typeof Cesium !== "undefined") { diff --git a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html index dadb13795bf0..f86206cb91d0 100644 --- a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html +++ b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html @@ -65,8 +65,9 @@ endTransform: Cesium.Matrix4.IDENTITY, }); + let tileset; try { - const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(40866); + tileset = await Cesium.Cesium3DTileset.fromIonAssetId(40866); viewer.scene.primitives.add(tileset); if (scene.clampToHeightSupported) { @@ -78,12 +79,19 @@ console.log(`Error loading tileset: ${error}`); } + const ray = new Cesium.Ray(); + function start() { clock.shouldAnimate = true; const objectsToExclude = [entity]; scene.postRender.addEventListener(function () { const position = positionProperty.getValue(clock.currentTime); - entity.position = scene.clampToHeight(position, objectsToExclude); + ray.origin = Cesium.Cartesian3.clone(position, ray.origin); + ray.direction = Cesium.Cartesian3.negate( + scene.globe.ellipsoid.geodeticSurfaceNormal(position), + ray.direction + ); + entity.position = tileset.pick(ray, scene, true, position); }); } //Sandcastle_End }; diff --git a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html index 5cd69c623b28..c829ea6bae57 100644 --- a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html +++ b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html @@ -45,6 +45,7 @@ try { const googleTileset = await Cesium.createGooglePhotorealistic3DTileset(); viewer.scene.primitives.add(googleTileset); + viewer.scene.enableCollisionDetectionForTileset(googleTileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); diff --git a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html index df403ed08f70..833abb2471da 100644 --- a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html +++ b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html @@ -45,6 +45,7 @@ try { const tileset = await Cesium.createGooglePhotorealistic3DTileset(); viewer.scene.primitives.add(tileset); + viewer.scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index f72abc6b2049..42836228ed2a 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -13,6 +13,8 @@ import destroyObject from "../Core/destroyObject.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import Event from "../Core/Event.js"; import ImageBasedLighting from "./ImageBasedLighting.js"; +import Interval from "../Core/Interval.js"; +import IntersectionTests from "../Core/IntersectionTests.js"; import IonResource from "../Core/IonResource.js"; import JulianDate from "../Core/JulianDate.js"; import ManagedArray from "../Core/ManagedArray.js"; @@ -55,6 +57,7 @@ import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; import Cesium3DTilesetMostDetailedTraversal from "./Cesium3DTilesetMostDetailedTraversal.js"; import Cesium3DTilesetBaseTraversal from "./Cesium3DTilesetBaseTraversal.js"; import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js"; +import Ray from "../Core/Ray.js"; /** * @typedef {Object} Cesium3DTileset.ConstructorOptions @@ -3439,22 +3442,113 @@ 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. * - * TODO - * @param {Cartographic} cartographic + * @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. */ -Cesium3DTileset.prototype.getHeight = function (cartographic) { +Cesium3DTileset.prototype.getHeight = function (cartographic, scene) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("cartographic", cartographic); + Check.typeOf.object("scene", scene); //>>includeEnd('debug'); - // Get loaded tile at that location - // Drill down to content - // Read existing data - // const intersection = tile.content.pick(ray, frameState, true, result) + let ellipsoid = scene.globe?.ellipsoid; + if (!defined(ellipsoid)) { + ellipsoid = Ellipsoid.WGS84; + } + + const ray = scratchGetHeightRay; + ray.direction = ellipsoid.geodeticSurfaceNormalCartographic( + cartographic, + ray.direction + ); + + const intersection = this.pick( + ray, + scene.frameState, + false, + scratchIntersection + ); + if (!defined(intersection)) { + return; + } + + return ellipsoid.cartesianToCartographic( + intersection, + scratchGetHeightCartographic + )?.height; +}; + +const scratchSphereIntersection = new Interval(); + +/** + * 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 {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. + * @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, + cullBackFaces, + result +) { + const selectedTiles = this._selectedTiles; + const selectedLength = selectedTiles.length; + + let intersection; + let minT = Number.POSITIVE_INFINITY; + 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; + } + + if (boundsIntersection.stop <= minT) { + minT = Math.min(boundsIntersection.stop, minT); + const candidate = tile.content.pick( + ray, + frameState, + cullBackFaces, + result + ); + + if (!defined(candidate)) { + continue; + } + + const distance = Cartesian3.distance(ray.origin, candidate); + if (distance < minDistance) { + intersection = candidate; + minDistance = distance; + } + } + } + + if (!defined(intersection)) { + return undefined; + } + + Cartesian3.clone(intersection, result); + return result; }; /** diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index d6ae6a5142d6..ff7fdb8a33da 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -2595,6 +2595,8 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { ); result = Matrix4.multiplyByPoint(computedModelMatrix, result, result); + // TODO: Transforms for 2D? SceneTransforms.computeActualWgs84Position + return result; }; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 957aa804854e..3a20f6852e91 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -162,7 +162,6 @@ function Scene(options) { this._globeTranslucencyState = new GlobeTranslucencyState(); this._primitives = new PrimitiveCollection(); this._groundPrimitives = new PrimitiveCollection(); - // List of tilesets to clamp to, in order of preference this._terrainTilesets = []; this._globeHeight = undefined; @@ -3574,20 +3573,57 @@ function callAfterRenderFunctions(scene) { functions.length = 0; } +/** + * Allow camera collisions, if enabled for the camera, on a tileset surface + * @param {Cesium3DTileset} tileset Tileset fo which to enable collision. + */ +Scene.prototype.enableCollisionDetectionForTileset = function (tileset) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("tileset", tileset); + //>>includeEnd('debug'); + + this._terrainTilesets.push(tileset); +}; + +/** + * Disallow camera collisions on a tileset surface + * @param {Cesium3DTileset} tileset Tileset for which to disable collision. + */ +Scene.prototype.disableCollisionDetectionForTileset = function (tileset) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("tileset", tileset); + //>>includeEnd('debug'); + + const i = this._terrainTilesets.indexOf(tileset); + if (i === -1) { + return; + } + + this._terrainTilesets.splice(i, 1); +}; + function getGlobeHeight(scene) { const globe = scene._globe; const camera = scene.camera; const cartographic = camera.positionCartographic; + let maxHeight = Number.NEGATIVE_INFINITY; for (const tileset of scene._terrainTilesets) { - const result = tileset.getHeight(cartographic); - if (defined(result)) { - return result; + const result = tileset.getHeight(cartographic, scene); + if (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; @@ -3596,7 +3632,6 @@ function getGlobeHeight(scene) { function isCameraUnderground(scene) { const camera = scene.camera; const mode = scene._mode; - const globe = scene.globe; const cameraController = scene._screenSpaceCameraController; const cartographic = camera.positionCartographic; @@ -3610,12 +3645,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 80e05cc5bb3e..5df8c6a5d547 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; From 6d998b1d908f1b7ca9d52bc12a104415814a131f Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Mon, 23 Oct 2023 14:54:26 -0400 Subject: [PATCH 03/23] Adjust dequantization --- packages/engine/Source/Scene/Model/Model.js | 45 +++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index ff7fdb8a33da..87c84ffcb950 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -1,4 +1,5 @@ import AttributeType from "../AttributeType.js"; +import AttributeCompression from "../../Core/AttributeCompression.js"; import BoundingSphere from "../../Core/BoundingSphere.js"; import Cartesian3 from "../../Core/Cartesian3.js"; import Cartographic from "../../Core/Cartographic.js"; @@ -2554,7 +2555,12 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { componentDatatype, count ); - verticesBuffer.getBufferData(vertices); + verticesBuffer.getBufferData( + vertices, + positionAttribute.byteOffset, + 0, + count + ); } } @@ -2567,7 +2573,7 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { const i2 = indices[i + 2]; const getPosition = (vertices, index, result) => { - // TODO: Exaggeration + // TODO: Exaggeration? const i = index * 3; result.x = vertices[i]; @@ -2575,17 +2581,30 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { result.z = vertices[i + 2]; if (defined(quantization)) { - result = Cartesian3.multiplyComponents( - result, - quantization.quantizedVolumeStepSize, - result - ); - - result = Cartesian3.add( - result, - quantization.quantizedVolumeOffset, - result - ); + if (quantization.octEncoded) { + if (quantization.octEncodedZXY) { + result.z = vertices[i]; + result.x = vertices[i + 1]; + result.y = vertices[i + 2]; + } + result = AttributeCompression.octDecodeInRange( + result, + quantization.normalizationRange, + result + ); + } else { + result = Cartesian3.multiplyComponents( + result, + quantization.quantizedVolumeStepSize, + result + ); + + result = Cartesian3.add( + result, + quantization.quantizedVolumeOffset, + result + ); + } } result = Matrix4.multiplyByPoint( From 1ab14c97cb11b5a0fc4baf9c1e6097722636d710 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Fri, 27 Oct 2023 15:11:13 -0400 Subject: [PATCH 04/23] Fix bounding volume issue --- Apps/Sandcastle/gallery/3D Tiles Picking.html | 23 +++-- .../engine/Source/Scene/Cesium3DTileset.js | 25 ++--- packages/engine/Source/Scene/Model/Model.js | 99 +++++++++++++------ 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/Apps/Sandcastle/gallery/3D Tiles Picking.html b/Apps/Sandcastle/gallery/3D Tiles Picking.html index 2402bd345d4d..a5e09173953b 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Picking.html +++ b/Apps/Sandcastle/gallery/3D Tiles Picking.html @@ -38,7 +38,6 @@ const viewer = new Cesium.Viewer("cesiumContainer", { timeline: false, animation: false, - sceneModePicker: false, baseLayerPicker: false, // The globe does not need to be displayed, // since the Photorealistic 3D Tiles include terrain @@ -51,17 +50,26 @@ let tileset; // Add Photorealistic 3D Tiles try { - tileset = await Cesium.createGooglePhotorealistic3DTileset(); + tileset = await Cesium.createGooglePhotorealistic3DTileset( + undefined, + { + enableDebugWireframe: true, + } + ); viewer.scene.primitives.add(tileset); - viewer.scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); } + viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin); + const inspectorViewModel = viewer.cesium3DTilesInspector.viewModel; + inspectorViewModel.tileset = tileset; + const scene = viewer.scene; const scratchCartesian = new Cesium.Cartesian3(); + const scratchTo2D = new Cesium.Matrix4(); const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas); handler.setInputAction(function (movement) { // const feature = scene.pick(movement.position); @@ -79,21 +87,12 @@ } const ray = scene.camera.getPickRay(movement.position); - const picked = tileset.pick( ray, scene.frameState, true, scratchCartesian ); - // if (Cesium.defined(feature.content)) { - // const content = feature.content; - // const picked = content.pick( - // ray, - // scene.frameState, - // true, - // scratchCartesian - // ); console.log(picked); if (Cesium.defined(picked)) { diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 85966cbaa045..0f30ce7c9252 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3449,7 +3449,6 @@ Cesium3DTileset.prototype.pick = function ( const selectedLength = selectedTiles.length; let intersection; - let minT = Number.POSITIVE_INFINITY; let minDistance = Number.POSITIVE_INFINITY; for (let i = 0; i < selectedLength; ++i) { const tile = selectedTiles[i]; @@ -3462,24 +3461,16 @@ Cesium3DTileset.prototype.pick = function ( continue; } - if (boundsIntersection.stop <= minT) { - minT = Math.min(boundsIntersection.stop, minT); - const candidate = tile.content.pick( - ray, - frameState, - cullBackFaces, - result - ); + const candidate = tile.content.pick(ray, frameState, cullBackFaces, result); - if (!defined(candidate)) { - continue; - } + if (!defined(candidate)) { + continue; + } - const distance = Cartesian3.distance(ray.origin, candidate); - if (distance < minDistance) { - intersection = candidate; - minDistance = distance; - } + const distance = Cartesian3.distance(ray.origin, candidate); + if (distance < minDistance) { + intersection = candidate; + minDistance = distance; } } diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 87c84ffcb950..b62df02f594f 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -44,6 +44,7 @@ import oneTimeWarning from "../../Core/oneTimeWarning.js"; import PntsLoader from "./PntsLoader.js"; import StyleCommandsNeeded from "./StyleCommandsNeeded.js"; import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; +import Transforms from "../../Core/Transforms.js"; /** *
@@ -2491,6 +2492,8 @@ Model.prototype.isClippingEnabled = function () { const scratchV0 = new Cartesian3(); const scratchV1 = new Cartesian3(); const scratchV2 = new Cartesian3(); +const scratchModelMatrix = new Matrix4(); +const scratchPickCartographic = new Cartographic(); /** * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates. @@ -2504,19 +2507,39 @@ const scratchV2 = new Cartesian3(); * @private */ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { - if (!frameState.context.webgl2) { - // TODO: error? + if (frameState.mode === SceneMode.MORPHING) { + return; } let minT = Number.MAX_VALUE; + const modelMatrix = this.sceneGraph.computedModelMatrix; // Check all the primitive positions const nodes = this._sceneGraph._runtimeNodes; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; + const instances = node.node.instances; + if (defined(instances)) { + // TODO: Instances + return; + } + + const nodeComputedTransform = node.computedTransform; + let computedModelMatrix = Matrix4.multiplyTransformation( + modelMatrix, + nodeComputedTransform, + scratchModelMatrix + ); + if (frameState.mode !== SceneMode.SCENE3D) { + computedModelMatrix = Transforms.basisTo2D( + frameState.mapProjection, + computedModelMatrix, + computedModelMatrix + ); + } + for (let j = 0; j < node.node.primitives.length; j++) { const primitive = node.node.primitives[j]; - const positionAttribute = ModelUtility.getAttributeBySemantic( primitive, VertexAttributeSemantic.POSITION @@ -2532,66 +2555,78 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { indices = useUint8Array ? new Uint8Array(indicesCount) : IndexDatatype.createTypedArray(vertexCount, indicesCount); - indicesBuffer.getBufferData(indices); + indicesBuffer.getBufferData(indices, 0, 0, indicesCount); } + primitive.indices.typedArray = indices; } let vertices = positionAttribute.typedArray; let componentDatatype = positionAttribute.componentDatatype; let attributeType = positionAttribute.type; - const count = - vertexCount * AttributeType.getNumberOfComponents(attributeType); 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, - count + elementCount ); verticesBuffer.getBufferData( vertices, positionAttribute.byteOffset, 0, - count + elementCount + ); + } + + if (quantization && positionAttribute.normalized) { + vertices = AttributeCompression.dequantize( + vertices, + componentDatatype, + attributeType, + vertexCount ); } + + positionAttribute.typedArray = vertices; } - const computedNodeTransform = node.computedTransform; - const computedModelMatrix = this._sceneGraph.computedModelMatrix; 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]; - const getPosition = (vertices, index, result) => { - // TODO: Exaggeration? - - const i = index * 3; + const getPosition = (vertices, index, numComponents, 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) { - if (quantization.octEncodedZXY) { - result.z = vertices[i]; - result.x = vertices[i + 1]; - result.y = vertices[i + 2]; - } 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, @@ -2607,21 +2642,14 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { } } - result = Matrix4.multiplyByPoint( - computedNodeTransform, - result, - result - ); result = Matrix4.multiplyByPoint(computedModelMatrix, result, result); - // TODO: Transforms for 2D? SceneTransforms.computeActualWgs84Position - return result; }; - const v0 = getPosition(vertices, i0, scratchV0); - const v1 = getPosition(vertices, i1, scratchV1); - const v2 = getPosition(vertices, i2, scratchV2); + const v0 = getPosition(vertices, i0, numComponents, scratchV0); + const v1 = getPosition(vertices, i1, numComponents, scratchV1); + const v2 = getPosition(vertices, i2, numComponents, scratchV2); const t = IntersectionTests.rayTriangleParametric( ray, @@ -2644,7 +2672,18 @@ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { return undefined; } - return Ray.getPoint(ray, minT, result); + 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 cart = projection.unproject(result, scratchPickCartographic); + ellipsoid.cartographicToCartesian(cart, result); + } + + return result; }; /** From 4199e100ca0d597ee6a4e1366e8eb125b7888b49 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Fri, 3 Nov 2023 16:29:27 -0400 Subject: [PATCH 05/23] Fix types --- packages/engine/Source/Scene/Cesium3DTileContent.js | 4 ++-- packages/engine/Source/Scene/Cesium3DTileset.js | 4 ++-- packages/engine/Source/Scene/Model/Model.js | 4 ++-- packages/engine/Source/Scene/Model/Model3DTileContent.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTileContent.js b/packages/engine/Source/Scene/Cesium3DTileContent.js index d6613a1a7f5f..76dcdd255b47 100644 --- a/packages/engine/Source/Scene/Cesium3DTileContent.js +++ b/packages/engine/Source/Scene/Cesium3DTileContent.js @@ -355,8 +355,8 @@ Cesium3DTileContent.prototype.update = function (tileset, frameState) { * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. - * @param {Cartesian3|undefined} result The intersection or undefined if none was found. + * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @param {Cartesian3|undefined} [result] The intersection or undefined if none was found. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. * * @private diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 0f30ce7c9252..3322452c427c 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3433,8 +3433,8 @@ const scratchSphereIntersection = new Interval(); * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. - * @param {Cartesian3|undefined} result The intersection or undefined if none was found. + * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @param {Cartesian3|undefined} [result] The intersection or undefined if none was found. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. * * @private diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index b62df02f594f..36718deaf74b 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -2500,8 +2500,8 @@ const scratchPickCartographic = new Cartographic(); * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. - * @param {Cartesian3|undefined} result The intersection or undefined if none was found. + * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @param {Cartesian3|undefined} [result] The intersection or undefined if none was found. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. * * @private diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index b53ffc5bdcaa..c0e71069a2c4 100644 --- a/packages/engine/Source/Scene/Model/Model3DTileContent.js +++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js @@ -421,8 +421,8 @@ Model3DTileContent.fromGeoJson = async function ( * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean=true} cullBackFaces If false, back faces are not culled and will return an intersection if picked. - * @param {Cartesian3|undefined} result The intersection or undefined if none was found. + * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @param {Cartesian3|undefined} [result] The intersection or undefined if none was found. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. * * @private From 590b46e4081ac6f007a7dd1ca2f6b48490771567 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Tue, 7 Nov 2023 15:41:55 -0500 Subject: [PATCH 06/23] Unit tests, instancing --- .../Source/Scene/Composite3DTileContent.js | 48 +++ .../engine/Source/Scene/Empty3DTileContent.js | 7 +- .../Source/Scene/Geometry3DTileContent.js | 9 + .../Source/Scene/Implicit3DTileContent.js | 9 + packages/engine/Source/Scene/Model/Model.js | 193 +--------- .../engine/Source/Scene/Model/pickModel.js | 335 ++++++++++++++++++ .../Source/Scene/Multiple3DTileContent.js | 48 +++ .../Source/Scene/Vector3DTileContent.js | 9 + .../engine/Specs/Scene/Model/pickModelSpec.js | 303 ++++++++++++++++ 9 files changed, 769 insertions(+), 192 deletions(-) create mode 100644 packages/engine/Source/Scene/Model/pickModel.js create mode 100644 packages/engine/Specs/Scene/Model/pickModelSpec.js diff --git a/packages/engine/Source/Scene/Composite3DTileContent.js b/packages/engine/Source/Scene/Composite3DTileContent.js index 900a901cc75b..0925c389b785 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,53 @@ 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 {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @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, + cullBackFaces, + 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, cullBackFaces, 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 2f4f16f57ce3..8c2b22435a5d 100644 --- a/packages/engine/Source/Scene/Empty3DTileContent.js +++ b/packages/engine/Source/Scene/Empty3DTileContent.js @@ -150,7 +150,12 @@ Empty3DTileContent.prototype.applyStyle = function (style) {}; Empty3DTileContent.prototype.update = function (tileset, frameState) {}; -Empty3DTileContent.prototype.pick = function (ray, frameState) { +Empty3DTileContent.prototype.pick = function ( + ray, + frameState, + cullBackFaces, + result +) { return undefined; }; diff --git a/packages/engine/Source/Scene/Geometry3DTileContent.js b/packages/engine/Source/Scene/Geometry3DTileContent.js index 93007bd46a1f..d13b21be91a0 100644 --- a/packages/engine/Source/Scene/Geometry3DTileContent.js +++ b/packages/engine/Source/Scene/Geometry3DTileContent.js @@ -525,6 +525,15 @@ Geometry3DTileContent.prototype.update = function (tileset, frameState) { } }; +Geometry3DTileContent.prototype.pick = function ( + ray, + frameState, + cullBackFaces, + result +) { + return undefined; +}; + Geometry3DTileContent.prototype.isDestroyed = function () { return false; }; diff --git a/packages/engine/Source/Scene/Implicit3DTileContent.js b/packages/engine/Source/Scene/Implicit3DTileContent.js index 96af9c099ad5..f3f8e6c9ff2f 100644 --- a/packages/engine/Source/Scene/Implicit3DTileContent.js +++ b/packages/engine/Source/Scene/Implicit3DTileContent.js @@ -1177,6 +1177,15 @@ Implicit3DTileContent.prototype.applyStyle = function (style) {}; Implicit3DTileContent.prototype.update = function (tileset, frameState) {}; +Implicit3DTileContent.prototype.pick = function ( + ray, + frameState, + cullBackFaces, + result +) { + return undefined; +}; + Implicit3DTileContent.prototype.isDestroyed = function () { return false; }; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 36718deaf74b..a3831f98c138 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -1,23 +1,17 @@ -import AttributeType from "../AttributeType.js"; -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 Credit from "../../Core/Credit.js"; import Color from "../../Core/Color.js"; -import ComponentDatatype from "../../Core/ComponentDatatype.js"; import defined from "../../Core/defined.js"; import defaultValue from "../../Core/defaultValue.js"; import DeveloperError from "../../Core/DeveloperError.js"; import destroyObject from "../../Core/destroyObject.js"; import DistanceDisplayCondition from "../../Core/DistanceDisplayCondition.js"; import Event from "../../Core/Event.js"; -import IndexDatatype from "../../Core/IndexDatatype.js"; -import IntersectionTests from "../../Core/IntersectionTests.js"; import Matrix3 from "../../Core/Matrix3.js"; import Matrix4 from "../../Core/Matrix4.js"; -import Ray from "../../Core/Ray.js"; import Resource from "../../Core/Resource.js"; import RuntimeError from "../../Core/RuntimeError.js"; import Pass from "../../Renderer/Pass.js"; @@ -43,8 +37,7 @@ import ModelUtility from "./ModelUtility.js"; import oneTimeWarning from "../../Core/oneTimeWarning.js"; import PntsLoader from "./PntsLoader.js"; import StyleCommandsNeeded from "./StyleCommandsNeeded.js"; -import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; -import Transforms from "../../Core/Transforms.js"; +import pickModel from "./pickModel.js"; /** *
@@ -2489,12 +2482,6 @@ Model.prototype.isClippingEnabled = function () { ); }; -const scratchV0 = new Cartesian3(); -const scratchV1 = new Cartesian3(); -const scratchV2 = new Cartesian3(); -const scratchModelMatrix = new Matrix4(); -const scratchPickCartographic = new Cartographic(); - /** * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates. * @@ -2507,183 +2494,7 @@ const scratchPickCartographic = new Cartographic(); * @private */ Model.prototype.pick = function (ray, frameState, cullBackFaces, result) { - if (frameState.mode === SceneMode.MORPHING) { - return; - } - - let minT = Number.MAX_VALUE; - const modelMatrix = this.sceneGraph.computedModelMatrix; - - // Check all the primitive positions - const nodes = this._sceneGraph._runtimeNodes; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - const instances = node.node.instances; - if (defined(instances)) { - // TODO: Instances - return; - } - - const nodeComputedTransform = node.computedTransform; - let computedModelMatrix = Matrix4.multiplyTransformation( - modelMatrix, - nodeComputedTransform, - scratchModelMatrix - ); - if (frameState.mode !== SceneMode.SCENE3D) { - computedModelMatrix = Transforms.basisTo2D( - frameState.mapProjection, - computedModelMatrix, - computedModelMatrix - ); - } - - for (let j = 0; j < node.node.primitives.length; j++) { - const primitive = node.node.primitives[j]; - const positionAttribute = ModelUtility.getAttributeBySemantic( - primitive, - VertexAttributeSemantic.POSITION - ); - const vertexCount = positionAttribute.count; - - let indices = primitive.indices.typedArray; - if (!defined(indices)) { - const indicesBuffer = primitive.indices.buffer; - const indicesCount = primitive.indices.count; - if (defined(indicesBuffer) && frameState.context.webgl2) { - const useUint8Array = indicesBuffer.sizeInBytes === indicesCount; - indices = useUint8Array - ? new Uint8Array(indicesCount) - : IndexDatatype.createTypedArray(vertexCount, indicesCount); - indicesBuffer.getBufferData(indices, 0, 0, indicesCount); - } - primitive.indices.typedArray = 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 - ); - } - - positionAttribute.typedArray = vertices; - } - - 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]; - - const getPosition = (vertices, index, numComponents, 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(computedModelMatrix, result, result); - - return result; - }; - - const v0 = getPosition(vertices, i0, numComponents, scratchV0); - const v1 = getPosition(vertices, i1, numComponents, scratchV1); - const v2 = getPosition(vertices, i2, numComponents, scratchV2); - - const t = IntersectionTests.rayTriangleParametric( - ray, - v0, - v1, - v2, - cullBackFaces - ); - - 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 cart = projection.unproject(result, scratchPickCartographic); - ellipsoid.cartographicToCartesian(cart, result); - } - - return result; + return pickModel(this, ray, frameState, cullBackFaces, result); }; /** diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js new file mode 100644 index 000000000000..a1444e76e9fc --- /dev/null +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -0,0 +1,335 @@ +import AttributeCompression from "../../Core/AttributeCompression.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 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 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 scratchModelMatrix = new Matrix4(); +const scratchPickCartographic = new Cartographic(); +const scratchInstanceMatrix = new Matrix4(); + +/** + * 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 {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @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, + cullBackFaces, + 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 = runtimeNode.computedTransform; + let modelMatrix = sceneGraph.computedModelMatrix; + + 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 + ); + } else { + // The node transform should be pre-multiplied with the instancing transform. + modelMatrix = Matrix4.clone( + sceneGraph.computedModelMatrix, + modelMatrix + ); + modelMatrix = Matrix4.multiplyTransformation( + modelMatrix, + runtimeNode.computedTransform, + modelMatrix + ); + + nodeComputedTransform = Matrix4.clone( + Matrix4.IDENTITY, + nodeComputedTransform + ); + } + } + + let computedModelMatrix = Matrix4.multiplyTransformation( + modelMatrix, + nodeComputedTransform, + scratchModelMatrix + ); + 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; + if (!defined(transformsTypedArray)) { + const instanceTransformsBuffer = runtimeNode.instancingTransformsBuffer; + if (defined(instanceTransformsBuffer) && frameState.context.webgl2) { + transformsTypedArray = ComponentDatatype.createTypedArray( + instanceComponentDatatype, + transformsCount * transformElements + ); + instanceTransformsBuffer.getBufferData(transformsTypedArray); + } + } + + for (let i = 0; i < transformsCount; i++) { + const transform = Matrix4.unpack( + transformsTypedArray, + i * transformElements, + scratchInstanceMatrix + ); + transform[12] = 0.0; + transform[13] = 0.0; + transform[14] = 0.0; + transform[15] = 1.0; + transforms.push(transform); + } + } + + if (transforms.length === 0) { + transforms.push(Matrix4.IDENTITY); + } + + for (let j = 0; j < node.primitives.length; j++) { + const primitive = node.primitives[j]; + 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; + if (defined(indicesBuffer) && frameState.context.webgl2) { + const useUint8Array = indicesBuffer.sizeInBytes === indicesCount; + indices = useUint8Array + ? new Uint8Array(indicesCount) + : IndexDatatype.createTypedArray(vertexCount, indicesCount); + indicesBuffer.getBufferData(indices, 0, 0, indicesCount); + } + primitive.indices.typedArray = 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 + ); + } + + positionAttribute.typedArray = vertices; + } + + 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, + computedModelMatrix, + scratchV0 + ); + const v1 = getVertexPosition( + vertices, + i1, + numComponents, + quantization, + instanceTransform, + computedModelMatrix, + scratchV1 + ); + const v2 = getVertexPosition( + vertices, + i2, + numComponents, + quantization, + instanceTransform, + computedModelMatrix, + scratchV2 + ); + + const t = IntersectionTests.rayTriangleParametric( + ray, + v0, + v1, + v2, + defaultValue(cullBackFaces, 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 cart = projection.unproject(result, scratchPickCartographic); + ellipsoid.cartographicToCartesian(cart, result); + } + + return result; +} + +function getVertexPosition( + vertices, + index, + numComponents, + quantization, + instanceTransform, + computedModelMatrix, + 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); + result = Matrix4.multiplyByPoint(computedModelMatrix, result, result); + + return result; +} diff --git a/packages/engine/Source/Scene/Multiple3DTileContent.js b/packages/engine/Source/Scene/Multiple3DTileContent.js index cd62641172b4..bef1cb3eb6cb 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,53 @@ 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 {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. + * @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, + cullBackFaces, + 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, cullBackFaces, 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/Vector3DTileContent.js b/packages/engine/Source/Scene/Vector3DTileContent.js index e191ede706a9..4d779bbf562b 100644 --- a/packages/engine/Source/Scene/Vector3DTileContent.js +++ b/packages/engine/Source/Scene/Vector3DTileContent.js @@ -727,6 +727,15 @@ Vector3DTileContent.prototype.update = function (tileset, frameState) { } }; +Vector3DTileContent.prototype.pick = function ( + ray, + frameState, + cullBackFaces, + result +) { + return undefined; +}; + Vector3DTileContent.prototype.getPolylinePositions = function (batchId) { const polylines = this._polylines; if (!defined(polylines)) { diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js new file mode 100644 index 000000000000..bf9324a14a30 --- /dev/null +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -0,0 +1,303 @@ +import { + pickModel, + Cartesian2, + Cartesian3, + 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/BoxInstanced/glTF/box-instanced.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"; + + 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, + }); + 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, + }); + 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, + }, + 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, + }, + 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 accounting for node transforms", async function () { + const model = await loadAndZoomToModelAsync( + { + url: boxWithOffsetUrl, + }, + 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, + }, + 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.EPSILON12 + ); + }); + + it("returns position of intersection with quantzed model", async function () { + const model = await loadAndZoomToModelAsync( + { + url: boxWithQuantizedAttributes, + }, + 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, + }, + 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 () { + const model = await loadAndZoomToModelAsync( + { + url: boxInstanced, + }, + scene + ); + const ray = scene.camera.getPickRay( + new Cartesian2( + scene.drawingBufferWidth / 2.0, + scene.drawingBufferHeight / 2.0 + ) + ); + + const expected = new Cartesian3(0.278338500214, 0, 0.278338500214); + expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon( + expected, + CesiumMath.EPSILON12 + ); + }); + + it("returns undefined for point cloud", async function () { + const model = await loadAndZoomToModelAsync( + { + url: pointCloudUrl, + }, + 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, + }, + 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 cullsBackFaces is false", async function () { + const model = await loadAndZoomToModelAsync( + { + url: boxTexturedGltfUrl, + }, + 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.5, 0, -0.5); + expect(pickModel(model, ray, scene.frameState, false)).toEqualEpsilon( + expected, + CesiumMath.EPSILON12 + ); + }); + + it("uses result parameter if specified", async function () { + const model = await loadAndZoomToModelAsync( + { + url: boxTexturedGltfUrl, + }, + 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, true, result); + expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON12); + expect(returned).toBe(result); + }); + + it("returns undefined when morphing", async function () { + const model = await loadAndZoomToModelAsync( + { + url: boxTexturedGltfUrl, + }, + 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(); + }); +}); From bd2a86c62daf3a6b21639e6625bb3b7cacabdd42 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Tue, 7 Nov 2023 15:45:06 -0500 Subject: [PATCH 07/23] Move sandcastle to development --- .../gallery/{ => development}/3D Tiles Picking.html | 6 ------ 1 file changed, 6 deletions(-) rename Apps/Sandcastle/gallery/{ => development}/3D Tiles Picking.html (93%) diff --git a/Apps/Sandcastle/gallery/3D Tiles Picking.html b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html similarity index 93% rename from Apps/Sandcastle/gallery/3D Tiles Picking.html rename to Apps/Sandcastle/gallery/development/3D Tiles Picking.html index a5e09173953b..0ca0d98ad2e0 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Picking.html +++ b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html @@ -39,8 +39,6 @@ timeline: false, animation: false, baseLayerPicker: false, - // The globe does not need to be displayed, - // since the Photorealistic 3D Tiles include terrain globe: false, }); @@ -69,10 +67,8 @@ const scene = viewer.scene; const scratchCartesian = new Cesium.Cartesian3(); - const scratchTo2D = new Cesium.Matrix4(); const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas); handler.setInputAction(function (movement) { - // const feature = scene.pick(movement.position); const pickedPositionResult = scene.pickPosition(movement.position); console.log(pickedPositionResult); if (Cesium.defined(pickedPositionResult)) { @@ -94,7 +90,6 @@ scratchCartesian ); - console.log(picked); if (Cesium.defined(picked)) { viewer.entities.add({ position: picked, @@ -105,7 +100,6 @@ }, }); } - // } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); //Sandcastle_End }; if (typeof Cesium !== "undefined") { From 5b9a348840da48806252733cc3906f6b8e68c340 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 9 Nov 2023 14:44:49 -0500 Subject: [PATCH 08/23] Ensure specs work with webgl-stub --- packages/engine/Source/Scene/GltfLoader.js | 20 ++++++-- .../engine/Source/Scene/Model/B3dmLoader.js | 4 ++ .../Scene/Model/InstancingPipelineStage.js | 30 +++++++++--- packages/engine/Source/Scene/Model/Model.js | 7 +++ .../engine/Source/Scene/Model/pickModel.js | 31 +++++++------ .../engine/Specs/Scene/Model/pickModelSpec.js | 46 ++++++++++++++++++- 6 files changed, 112 insertions(+), 26 deletions(-) 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/Model/B3dmLoader.js b/packages/engine/Source/Scene/Model/B3dmLoader.js index 7385f18a690d..2a47ba71aa99 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); 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/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 a3831f98c138..a781e442e6f7 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -151,6 +151,7 @@ import pickModel from "./pickModel.js"; * @privateParam {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen. * @privateParam {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this model. * @privateParam {boolean} [options.projectTo2D=false] Whether to accurately project the model's positions in 2D. If this is true, the model will be projected accurately to 2D, but it will use more memory to do so. If this is false, the model will use less memory and will still render in 2D / CV mode, but its positions may be inaccurate. This disables minimumPixelSize and prevents future modification to the model matrix. This also cannot be set after the model has loaded. + * @privateParam {boolean} [options.enablePick=false] Whether to allow with CPU picking with 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. @@ -454,6 +455,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); @@ -2656,6 +2658,7 @@ Model.prototype.destroyModelResources = function () { * @param {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen. * @param {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this model. * @param {boolean} [options.projectTo2D=false] Whether to accurately project the model's positions in 2D. If this is true, the model will be projected accurately to 2D, but it will use more memory to do so. If this is false, the model will use less memory and will still render in 2D / CV mode, but its positions may be inaccurate. This disables minimumPixelSize and prevents future modification to the model matrix. This also cannot be set after the model has loaded. + * @param {boolean} [options.enablePick=false] Whether to allow with CPU picking with 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. @@ -2750,6 +2753,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), @@ -2826,6 +2830,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), @@ -2881,6 +2886,7 @@ Model.fromI3dm = async function (options) { upAxis: options.upAxis, forwardAxis: options.forwardAxis, loadAttributesFor2D: options.projectTo2D, + enablePick: options.enablePick, loadIndicesForWireframe: options.enableDebugWireframe, loadPrimitiveOutline: options.enableShowOutline, }; @@ -3014,6 +3020,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/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js index a1444e76e9fc..de0be7e8cb98 100644 --- a/packages/engine/Source/Scene/Model/pickModel.js +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -116,7 +116,7 @@ export default function pickModel( instances.attributes[0].componentDatatype; const transformElements = 12; - let transformsTypedArray; + let transformsTypedArray = runtimeNode.transformsTypedArray; if (!defined(transformsTypedArray)) { const instanceTransformsBuffer = runtimeNode.instancingTransformsBuffer; if (defined(instanceTransformsBuffer) && frameState.context.webgl2) { @@ -128,17 +128,19 @@ export default function pickModel( } } - for (let i = 0; i < transformsCount; i++) { - const transform = Matrix4.unpack( - transformsTypedArray, - i * transformElements, - scratchInstanceMatrix - ); - transform[12] = 0.0; - transform[13] = 0.0; - transform[14] = 0.0; - transform[15] = 1.0; - transforms.push(transform); + if (defined(transformsTypedArray)) { + for (let i = 0; i < transformsCount; i++) { + const transform = Matrix4.unpack( + transformsTypedArray, + i * transformElements, + scratchInstanceMatrix + ); + transform[12] = 0.0; + transform[13] = 0.0; + transform[14] = 0.0; + transform[15] = 1.0; + transforms.push(transform); + } } } @@ -170,7 +172,6 @@ export default function pickModel( : IndexDatatype.createTypedArray(vertexCount, indicesCount); indicesBuffer.getBufferData(indices, 0, 0, indicesCount); } - primitive.indices.typedArray = indices; } let vertices = positionAttribute.typedArray; @@ -210,8 +211,10 @@ export default function pickModel( vertexCount ); } + } - positionAttribute.typedArray = vertices; + if (!defined(indices) || !defined(vertices)) { + return; } const indicesLength = indices.length; diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js index bf9324a14a30..7aea7e85903d 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -55,6 +55,7 @@ describe("Scene/Model/pickModel", function () { 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(); @@ -63,6 +64,7 @@ describe("Scene/Model/pickModel", function () { 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(); @@ -72,6 +74,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -83,6 +86,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -100,10 +104,41 @@ describe("Scene/Model/pickModel", function () { ); }); + 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 ); @@ -125,6 +160,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxCesiumRtcUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -138,7 +174,7 @@ describe("Scene/Model/pickModel", function () { const expected = new Cartesian3(6378137.5, 0.0, -0.499999996649); expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon( expected, - CesiumMath.EPSILON12 + CesiumMath.EPSILON8 ); }); @@ -146,6 +182,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxWithQuantizedAttributes, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -167,6 +204,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxWithMixedCompression, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -188,6 +226,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxInstanced, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -209,6 +248,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: pointCloudUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -226,6 +266,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -244,6 +285,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -266,6 +308,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); @@ -287,6 +330,7 @@ describe("Scene/Model/pickModel", function () { const model = await loadAndZoomToModelAsync( { url: boxTexturedGltfUrl, + enablePick: !scene.frameState.context.webgl2, }, scene ); From bbdcac1b6924bfaa714fe4e9d81b683a608c9c50 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Mon, 13 Nov 2023 13:55:57 -0500 Subject: [PATCH 09/23] More testing --- .../gallery/development/3D Tiles Picking.html | 87 ++++++---- .../Source/Scene/Cesium3DTileContent.js | 8 +- .../engine/Source/Scene/Cesium3DTileset.js | 20 ++- .../Source/Scene/Composite3DTileContent.js | 10 +- .../engine/Source/Scene/Empty3DTileContent.js | 7 +- .../Source/Scene/Geometry3DTileContent.js | 7 +- .../Source/Scene/Implicit3DTileContent.js | 7 +- packages/engine/Source/Scene/Model/Model.js | 5 +- .../Source/Scene/Model/Model3DTileContent.js | 10 +- .../engine/Source/Scene/Model/pickModel.js | 149 +++++++++++------- .../Source/Scene/Multiple3DTileContent.js | 10 +- .../Source/Scene/Tileset3DTileContent.js | 4 + .../Source/Scene/Vector3DTileContent.js | 7 +- .../engine/Specs/Scene/Cesium3DTilesetSpec.js | 145 +++++++++++++++++ .../engine/Specs/Scene/Model/ModelSpec.js | 22 +++ .../engine/Specs/Scene/Model/pickModelSpec.js | 30 +++- 16 files changed, 366 insertions(+), 162 deletions(-) diff --git a/Apps/Sandcastle/gallery/development/3D Tiles Picking.html b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html index 0ca0d98ad2e0..843b1d4f30e4 100644 --- a/Apps/Sandcastle/gallery/development/3D Tiles Picking.html +++ b/Apps/Sandcastle/gallery/development/3D Tiles Picking.html @@ -41,36 +41,72 @@ baseLayerPicker: false, globe: false, }); - - // Enable rendering the sky - viewer.scene.skyAtmosphere.show = true; + const scene = viewer.scene; let tileset; - // Add Photorealistic 3D Tiles - try { - tileset = await Cesium.createGooglePhotorealistic3DTileset( - undefined, - { - enableDebugWireframe: true, - } - ); - viewer.scene.primitives.add(tileset); - } catch (error) { - console.log(`Error loading Photorealistic 3D Tiles tileset. - ${error}`); - } + const options = [ + { + text: "Google P3DT", + onselect: async () => { + scene.primitives.remove(tileset); + try { + tileset = await Cesium.createGooglePhotorealistic3DTileset(); + scene.primitives.add(tileset); + } catch (error) { + console.log(error); + } + }, + }, + { + text: "Maxar OWT WFF 1.2", + onselect: async () => { + scene.primitives.remove(tileset); + try { + tileset = await Cesium.Cesium3DTileset.fromIonAssetId(691510, { + maximumScreenSpaceError: 4, + }); + scene.primitives.add(tileset); + } catch (error) { + console.log(error); + } + }, + }, + { + text: "Bentley BIM Model", + onselect: async () => { + scene.primitives.remove(tileset); + try { + tileset = await Cesium.Cesium3DTileset.fromIonAssetId(1240402); + scene.primitives.add(tileset); + viewer.zoomTo(tileset); + } catch (error) { + console.log(error); + } + }, + }, + { + text: "Instanced", + onselect: async () => { + scene.primitives.remove(tileset); + try { + tileset = await Cesium.Cesium3DTileset.fromUrl( + "../../SampleData/Cesium3DTiles/Instanced/InstancedWithBatchTable/tileset.json" + ); + scene.primitives.add(tileset); + viewer.zoomTo(tileset); + } catch (error) { + console.log(error); + } + }, + }, + ]; - viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin); - const inspectorViewModel = viewer.cesium3DTilesInspector.viewModel; - inspectorViewModel.tileset = tileset; - - const scene = viewer.scene; + Sandcastle.addDefaultToolbarMenu(options); const scratchCartesian = new Cesium.Cartesian3(); const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas); handler.setInputAction(function (movement) { const pickedPositionResult = scene.pickPosition(movement.position); - console.log(pickedPositionResult); if (Cesium.defined(pickedPositionResult)) { viewer.entities.add({ position: pickedPositionResult, @@ -83,12 +119,7 @@ } const ray = scene.camera.getPickRay(movement.position); - const picked = tileset.pick( - ray, - scene.frameState, - true, - scratchCartesian - ); + const picked = tileset.pick(ray, scene.frameState, scratchCartesian); if (Cesium.defined(picked)) { viewer.entities.add({ diff --git a/packages/engine/Source/Scene/Cesium3DTileContent.js b/packages/engine/Source/Scene/Cesium3DTileContent.js index 76dcdd255b47..945242881b42 100644 --- a/packages/engine/Source/Scene/Cesium3DTileContent.js +++ b/packages/engine/Source/Scene/Cesium3DTileContent.js @@ -355,18 +355,12 @@ Cesium3DTileContent.prototype.update = function (tileset, frameState) { * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @param {Cartesian3|undefined} [result] The intersection or undefined if none was found. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. * * @private */ -Cesium3DTileContent.prototype.pick = function ( - ray, - frameState, - cullBackFaces, - result -) { +Cesium3DTileContent.prototype.pick = function (ray, frameState, result) { DeveloperError.throwInstantiationError(); }; diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 3322452c427c..ac53d8ffb8b4 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3427,24 +3427,19 @@ Cesium3DTileset.prototype.getHeight = function (cartographic, scene) { }; 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 {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, - cullBackFaces, - result -) { +Cesium3DTileset.prototype.pick = function (ray, frameState, result) { const selectedTiles = this._selectedTiles; const selectedLength = selectedTiles.length; @@ -3461,7 +3456,11 @@ Cesium3DTileset.prototype.pick = function ( continue; } - const candidate = tile.content.pick(ray, frameState, cullBackFaces, result); + const candidate = tile.content.pick( + ray, + frameState, + scratchPickIntersection + ); if (!defined(candidate)) { continue; @@ -3469,7 +3468,7 @@ Cesium3DTileset.prototype.pick = function ( const distance = Cartesian3.distance(ray.origin, candidate); if (distance < minDistance) { - intersection = candidate; + intersection = Cartesian3.clone(candidate, result); minDistance = distance; } } @@ -3478,8 +3477,7 @@ Cesium3DTileset.prototype.pick = function ( return undefined; } - Cartesian3.clone(intersection, result); - return result; + return intersection; }; /** diff --git a/packages/engine/Source/Scene/Composite3DTileContent.js b/packages/engine/Source/Scene/Composite3DTileContent.js index 0925c389b785..02be6c08ac6c 100644 --- a/packages/engine/Source/Scene/Composite3DTileContent.js +++ b/packages/engine/Source/Scene/Composite3DTileContent.js @@ -353,18 +353,12 @@ Composite3DTileContent.prototype.update = function (tileset, frameState) { * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, - cullBackFaces, - result -) { +Composite3DTileContent.prototype.pick = function (ray, frameState, result) { if (!this._ready) { return undefined; } @@ -375,7 +369,7 @@ Composite3DTileContent.prototype.pick = function ( const length = contents.length; for (let i = 0; i < length; ++i) { - const candidate = contents[i].pick(ray, frameState, cullBackFaces, result); + const candidate = contents[i].pick(ray, frameState, result); if (!defined(candidate)) { continue; diff --git a/packages/engine/Source/Scene/Empty3DTileContent.js b/packages/engine/Source/Scene/Empty3DTileContent.js index 8c2b22435a5d..0b5c4459da35 100644 --- a/packages/engine/Source/Scene/Empty3DTileContent.js +++ b/packages/engine/Source/Scene/Empty3DTileContent.js @@ -150,12 +150,7 @@ Empty3DTileContent.prototype.applyStyle = function (style) {}; Empty3DTileContent.prototype.update = function (tileset, frameState) {}; -Empty3DTileContent.prototype.pick = function ( - ray, - frameState, - cullBackFaces, - result -) { +Empty3DTileContent.prototype.pick = function (ray, frameState, result) { return undefined; }; diff --git a/packages/engine/Source/Scene/Geometry3DTileContent.js b/packages/engine/Source/Scene/Geometry3DTileContent.js index d13b21be91a0..d89a278eb4b1 100644 --- a/packages/engine/Source/Scene/Geometry3DTileContent.js +++ b/packages/engine/Source/Scene/Geometry3DTileContent.js @@ -525,12 +525,7 @@ Geometry3DTileContent.prototype.update = function (tileset, frameState) { } }; -Geometry3DTileContent.prototype.pick = function ( - ray, - frameState, - cullBackFaces, - result -) { +Geometry3DTileContent.prototype.pick = function (ray, frameState, result) { return undefined; }; diff --git a/packages/engine/Source/Scene/Implicit3DTileContent.js b/packages/engine/Source/Scene/Implicit3DTileContent.js index f3f8e6c9ff2f..96eebae51c1d 100644 --- a/packages/engine/Source/Scene/Implicit3DTileContent.js +++ b/packages/engine/Source/Scene/Implicit3DTileContent.js @@ -1177,12 +1177,7 @@ Implicit3DTileContent.prototype.applyStyle = function (style) {}; Implicit3DTileContent.prototype.update = function (tileset, frameState) {}; -Implicit3DTileContent.prototype.pick = function ( - ray, - frameState, - cullBackFaces, - result -) { +Implicit3DTileContent.prototype.pick = function (ray, frameState, result) { return undefined; }; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index a781e442e6f7..d9a2f8c596c3 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -2489,14 +2489,13 @@ Model.prototype.isClippingEnabled = function () { * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, cullBackFaces, result) { - return pickModel(this, ray, frameState, cullBackFaces, result); +Model.prototype.pick = function (ray, frameState, result) { + return pickModel(this, ray, frameState, result); }; /** diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index c0e71069a2c4..11146fe92d8e 100644 --- a/packages/engine/Source/Scene/Model/Model3DTileContent.js +++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js @@ -421,23 +421,17 @@ Model3DTileContent.fromGeoJson = async function ( * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, - cullBackFaces, - result -) { +Model3DTileContent.prototype.pick = function (ray, frameState, result) { if (!defined(this._model) || !this._ready) { return undefined; } - return this._model.pick(ray, frameState, cullBackFaces, result); + return this._model.pick(ray, frameState, result); }; function makeModelOptions(tileset, tile, content, additionalOptions) { diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js index de0be7e8cb98..16a2bb02f94f 100644 --- a/packages/engine/Source/Scene/Model/pickModel.js +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -1,4 +1,5 @@ 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"; @@ -18,9 +19,11 @@ 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 scratchInstanceMatrix = new Matrix4(); +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. @@ -28,19 +31,12 @@ const scratchInstanceMatrix = new Matrix4(); * @param {Model} model The model to pick. * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, - cullBackFaces, - result -) { +export default function pickModel(model, ray, frameState, result) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("model", model); Check.typeOf.object("ray", ray); @@ -59,8 +55,14 @@ export default function pickModel( const runtimeNode = nodes[i]; const node = runtimeNode.node; - let nodeComputedTransform = runtimeNode.computedTransform; - let modelMatrix = sceneGraph.computedModelMatrix; + let nodeComputedTransform = Matrix4.clone( + runtimeNode.computedTransform, + scratchNodeComputedTransform + ); + let modelMatrix = Matrix4.clone( + sceneGraph.computedModelMatrix, + scratchModelMatrix + ); const instances = node.instances; if (defined(instances)) { @@ -77,38 +79,23 @@ export default function pickModel( runtimeNode.computedTransform, nodeComputedTransform ); - } else { - // The node transform should be pre-multiplied with the instancing transform. - modelMatrix = Matrix4.clone( - sceneGraph.computedModelMatrix, - modelMatrix - ); - modelMatrix = Matrix4.multiplyTransformation( - modelMatrix, - runtimeNode.computedTransform, - modelMatrix - ); - - nodeComputedTransform = Matrix4.clone( - Matrix4.IDENTITY, - nodeComputedTransform - ); } } - let computedModelMatrix = Matrix4.multiplyTransformation( - modelMatrix, - nodeComputedTransform, - scratchModelMatrix - ); if (frameState.mode !== SceneMode.SCENE3D) { - computedModelMatrix = Transforms.basisTo2D( + modelMatrix = Transforms.basisTo2D( frameState.mapProjection, - computedModelMatrix, - computedModelMatrix + modelMatrix, + modelMatrix ); } + const computedModelMatrix = Matrix4.multiplyTransformation( + modelMatrix, + nodeComputedTransform, + scratchcomputedModelMatrix + ); + const transforms = []; if (defined(instances)) { const transformsCount = instances.attributes[0].count; @@ -130,26 +117,70 @@ export default function pickModel( if (defined(transformsTypedArray)) { for (let i = 0; i < transformsCount; i++) { - const transform = Matrix4.unpack( - transformsTypedArray, - i * transformElements, - scratchInstanceMatrix + 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 ); - transform[12] = 0.0; - transform[13] = 0.0; - transform[14] = 0.0; - transform[15] = 1.0; + + 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(Matrix4.IDENTITY); + transforms.push(computedModelMatrix); } - for (let j = 0; j < node.primitives.length; j++) { - const primitive = node.primitives[j]; + 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 @@ -165,12 +196,17 @@ export default function pickModel( if (!defined(indices)) { const indicesBuffer = primitive.indices.buffer; const indicesCount = primitive.indices.count; + const indexDatatype = primitive.indices.indexDatatype; if (defined(indicesBuffer) && frameState.context.webgl2) { - const useUint8Array = indicesBuffer.sizeInBytes === indicesCount; - indices = useUint8Array - ? new Uint8Array(indicesCount) - : IndexDatatype.createTypedArray(vertexCount, indicesCount); - indicesBuffer.getBufferData(indices, 0, 0, indicesCount); + 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); } } @@ -230,7 +266,6 @@ export default function pickModel( numComponents, quantization, instanceTransform, - computedModelMatrix, scratchV0 ); const v1 = getVertexPosition( @@ -239,7 +274,6 @@ export default function pickModel( numComponents, quantization, instanceTransform, - computedModelMatrix, scratchV1 ); const v2 = getVertexPosition( @@ -248,7 +282,6 @@ export default function pickModel( numComponents, quantization, instanceTransform, - computedModelMatrix, scratchV2 ); @@ -257,7 +290,7 @@ export default function pickModel( v0, v1, v2, - defaultValue(cullBackFaces, true) + defaultValue(model.backFaceCulling, true) ); if (defined(t)) { @@ -281,8 +314,8 @@ export default function pickModel( const projection = frameState.mapProjection; const ellipsoid = projection.ellipsoid; - const cart = projection.unproject(result, scratchPickCartographic); - ellipsoid.cartographicToCartesian(cart, result); + const cartographic = projection.unproject(result, scratchPickCartographic); + ellipsoid.cartographicToCartesian(cartographic, result); } return result; @@ -294,7 +327,6 @@ function getVertexPosition( numComponents, quantization, instanceTransform, - computedModelMatrix, result ) { const i = index * numComponents; @@ -332,7 +364,6 @@ function getVertexPosition( } result = Matrix4.multiplyByPoint(instanceTransform, result, result); - result = Matrix4.multiplyByPoint(computedModelMatrix, result, result); return result; } diff --git a/packages/engine/Source/Scene/Multiple3DTileContent.js b/packages/engine/Source/Scene/Multiple3DTileContent.js index bef1cb3eb6cb..42ede698a349 100644 --- a/packages/engine/Source/Scene/Multiple3DTileContent.js +++ b/packages/engine/Source/Scene/Multiple3DTileContent.js @@ -656,18 +656,12 @@ Multiple3DTileContent.prototype.update = function (tileset, frameState) { * * @param {Ray} ray The ray to test for intersection. * @param {FrameState} frameState The frame state. - * @param {boolean} [cullBackFaces=true] If false, back faces are not culled and will return an intersection if picked. * @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, - cullBackFaces, - result -) { +Multiple3DTileContent.prototype.pick = function (ray, frameState, result) { if (!this._ready) { return undefined; } @@ -678,7 +672,7 @@ Multiple3DTileContent.prototype.pick = function ( const length = contents.length; for (let i = 0; i < length; ++i) { - const candidate = contents[i].pick(ray, frameState, cullBackFaces, result); + const candidate = contents[i].pick(ray, frameState, result); if (!defined(candidate)) { continue; 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 4d779bbf562b..120a22db5801 100644 --- a/packages/engine/Source/Scene/Vector3DTileContent.js +++ b/packages/engine/Source/Scene/Vector3DTileContent.js @@ -727,12 +727,7 @@ Vector3DTileContent.prototype.update = function (tileset, frameState) { } }; -Vector3DTileContent.prototype.pick = function ( - ray, - frameState, - cullBackFaces, - result -) { +Vector3DTileContent.prototype.pick = function (ray, frameState, result) { return undefined; }; diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 2e3381f89d49..6b85a670e228 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -2414,6 +2414,151 @@ describe( }); }); + it("picks", async function () { + const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl); + 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 + ); + 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 + ); + 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 + ); + 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 + ); + 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 + ); + 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("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 df62257eb949..a93f00370367 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -4392,6 +4392,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 index 7aea7e85903d..a3a0d0ac4a2c 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -2,6 +2,7 @@ import { pickModel, Cartesian2, Cartesian3, + HeadingPitchRange, Math as CesiumMath, Model, Ray, @@ -15,7 +16,7 @@ describe("Scene/Model/pickModel", function () { const boxTexturedGltfUrl = "./Data/Models/glTF-2.0/BoxTextured/glTF/BoxTextured.gltf"; const boxInstanced = - "./Data/Models/glTF-2.0/BoxInstanced/glTF/box-instanced.gltf"; + "./Data/Models/glTF-2.0/BoxInstancedNoNormals/glTF/BoxInstancedNoNormals.gltf"; const boxWithOffsetUrl = "./Data/Models/glTF-2.0/BoxWithOffset/glTF/BoxWithOffset.gltf"; const pointCloudUrl = @@ -26,6 +27,8 @@ describe("Scene/Model/pickModel", function () { "./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 () { @@ -223,10 +226,19 @@ describe("Scene/Model/pickModel", function () { }); 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 ); @@ -237,7 +249,7 @@ describe("Scene/Model/pickModel", function () { ) ); - const expected = new Cartesian3(0.278338500214, 0, 0.278338500214); + const expected = new Cartesian3(0, -0.5, 0.5); expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon( expected, CesiumMath.EPSILON12 @@ -281,11 +293,12 @@ describe("Scene/Model/pickModel", function () { expect(pickModel(model, ray, scene.frameState)).toBeUndefined(); }); - it("includes back faces results when cullsBackFaces is false", async function () { + it("includes back faces results when model disbales backface culling", async function () { const model = await loadAndZoomToModelAsync( { - url: boxTexturedGltfUrl, + url: boxBackFaceCullingUrl, enablePick: !scene.frameState.context.webgl2, + backFaceCulling: false, }, scene ); @@ -295,10 +308,15 @@ describe("Scene/Model/pickModel", function () { scene.drawingBufferHeight / 2.0 ) ); + ray.origin = model.boundingSphere.center; - const expected = new Cartesian3(-0.5, 0, -0.5); - expect(pickModel(model, ray, scene.frameState, false)).toEqualEpsilon( + const expected = new Cartesian3( + -0.9999998807907355, + 0, + -0.9999998807907104 + ); + expect(pickModel(model, ray, scene.frameState)).toEqualEpsilon( expected, CesiumMath.EPSILON12 ); From 21c64e6c3dc26aee290209a0ad6f2deb53f75fc3 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Mon, 13 Nov 2023 15:08:55 -0500 Subject: [PATCH 10/23] Ensure latest tests work with webgl stub --- .../engine/Source/Scene/Cesium3DTileset.js | 2 ++ .../engine/Source/Scene/Model/B3dmLoader.js | 2 +- .../engine/Source/Scene/Model/I3dmLoader.js | 3 ++ .../Source/Scene/Model/Model3DTileContent.js | 1 + .../engine/Specs/Scene/Cesium3DTilesetSpec.js | 29 +++++++++++++++---- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index ac53d8ffb8b4..a69ca0caca5a 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -111,6 +111,7 @@ import Ray from "../Core/Ray.js"; * @property {boolean} [showCreditsOnScreen=false] Whether to display the credits of this tileset on screen. * @property {SplitDirection} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset. * @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 loaded. + * @property {boolean} [options.enablePick=false] Whether to allow with CPU picking with 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 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. @@ -826,6 +827,7 @@ function Cesium3DTileset(options) { ); 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. diff --git a/packages/engine/Source/Scene/Model/B3dmLoader.js b/packages/engine/Source/Scene/Model/B3dmLoader.js index 2a47ba71aa99..600221ac4dde 100644 --- a/packages/engine/Source/Scene/Model/B3dmLoader.js +++ b/packages/engine/Source/Scene/Model/B3dmLoader.js @@ -75,7 +75,7 @@ function B3dmLoader(options) { false ); const loadAttributesFor2D = defaultValue(options.loadAttributesFor2D, false); - const enablePick = defaultValue(options.enablePick); + const enablePick = defaultValue(options.enablePick, false); const loadIndicesForWireframe = defaultValue( options.loadIndicesForWireframe, false diff --git a/packages/engine/Source/Scene/Model/I3dmLoader.js b/packages/engine/Source/Scene/Model/I3dmLoader.js index 2a1aff969bd2..dec9597bae18 100644 --- a/packages/engine/Source/Scene/Model/I3dmLoader.js +++ b/packages/engine/Source/Scene/Model/I3dmLoader.js @@ -91,6 +91,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 +112,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 +242,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/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index 11146fe92d8e..3273c81146d6 100644 --- a/packages/engine/Source/Scene/Model/Model3DTileContent.js +++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js @@ -460,6 +460,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/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 6b85a670e228..9344b6e2fa17 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -2415,7 +2415,9 @@ describe( }); it("picks", async function () { - const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl); + const tileset = await Cesium3DTilesTester.loadTileset(scene, tilesetUrl, { + enablePick: !scene.frameState.context.webgl2, + }); viewRootOnly(); scene.renderForSpecs(); @@ -2440,7 +2442,10 @@ describe( it("picks tileset of tilesets", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - tilesetOfTilesetsUrl + tilesetOfTilesetsUrl, + { + enablePick: !scene.frameState.context.webgl2, + } ); viewRootOnly(); scene.renderForSpecs(); @@ -2466,7 +2471,10 @@ describe( it("picks instanced tileset", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - instancedUrl + instancedUrl, + { + enablePick: !scene.frameState.context.webgl2, + } ); viewInstances(); scene.renderForSpecs(); @@ -2492,7 +2500,10 @@ describe( it("picks translucent tileset", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - translucentUrl + translucentUrl, + { + enablePick: !scene.frameState.context.webgl2, + } ); viewAllTiles(); scene.renderForSpecs(); @@ -2518,7 +2529,10 @@ describe( it("picks tileset with transforms", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - tilesetWithTransformsUrl + tilesetWithTransformsUrl, + { + enablePick: !scene.frameState.context.webgl2, + } ); viewAllTiles(); scene.renderForSpecs(); @@ -2544,7 +2558,10 @@ describe( it("picking point cloud tileset returns undefined", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - pointCloudUrl + pointCloudUrl, + { + enablePick: !scene.frameState.context.webgl2, + } ); viewAllTiles(); scene.renderForSpecs(); From e206e8bc8a0729751e62091cf87d7b4f4591040c Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Mon, 13 Nov 2023 15:58:55 -0500 Subject: [PATCH 11/23] Fix spec --- packages/engine/Specs/Scene/Model/pickModelSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js index a3a0d0ac4a2c..af89fd06907d 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -339,7 +339,7 @@ describe("Scene/Model/pickModel", function () { const result = new Cartesian3(); const expected = new Cartesian3(0.5, 0, 0.5); - const returned = pickModel(model, ray, scene.frameState, true, result); + const returned = pickModel(model, ray, scene.frameState, result); expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON12); expect(returned).toBe(result); }); From c95f426a29f65c80032315d37ed7f7e3ab49f25d Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 6 Dec 2023 10:02:56 -0500 Subject: [PATCH 12/23] enableCameraCollision option --- .../gallery/3D Tiles Next S2 Globe.html | 2 +- ...alistic 3D Tiles with Building Insert.html | 1 - .../Google Photorealistic 3D Tiles.html | 1 - .../engine/Source/Scene/Cesium3DTileset.js | 39 ++++++++++++----- .../engine/Source/Scene/Model/I3dmLoader.js | 1 + packages/engine/Source/Scene/Scene.js | 42 ++++--------------- .../createGooglePhotorealistic3DTileset.js | 4 ++ 7 files changed, 44 insertions(+), 46 deletions(-) diff --git a/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html index 0ac29a48c476..f57e5a705972 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html +++ b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html @@ -79,9 +79,9 @@ // MAXAR OWT WFF 1.2 Base Globe tileset = await Cesium.Cesium3DTileset.fromIonAssetId(691510, { maximumScreenSpaceError: 4, + enableCameraCollision: true, }); scene.primitives.add(tileset); - scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading tileset: ${error}`); } diff --git a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html index fba90865bd20..84f5cd4e59c3 100644 --- a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html +++ b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles with Building Insert.html @@ -47,7 +47,6 @@ try { const googleTileset = await Cesium.createGooglePhotorealistic3DTileset(); viewer.scene.primitives.add(googleTileset); - viewer.scene.enableCollisionDetectionForTileset(googleTileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); diff --git a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html index 13e7fdd55c5b..63e67341a908 100644 --- a/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html +++ b/Apps/Sandcastle/gallery/Google Photorealistic 3D Tiles.html @@ -47,7 +47,6 @@ try { const tileset = await Cesium.createGooglePhotorealistic3DTileset(); viewer.scene.primitives.add(tileset); - viewer.scene.enableCollisionDetectionForTileset(tileset); } catch (error) { console.log(`Error loading Photorealistic 3D Tiles tileset. ${error}`); diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index a69ca0caca5a..e68ae55d0d97 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -110,12 +110,13 @@ import Ray from "../Core/Ray.js"; * @property {string|number} [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. * @property {boolean} [showCreditsOnScreen=false] Whether to display the credits of this tileset on screen. * @property {SplitDirection} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset. - * @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 loaded. - * @property {boolean} [options.enablePick=false] Whether to allow with CPU picking with 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 tileset has loaded. + * @property {boolean} [enableCameraCollision=false] When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, prevents the camera from going below the tileset surface. + * @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 with CPU picking with 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 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. @@ -826,8 +827,23 @@ function Cesium3DTileset(options) { SplitDirection.NONE ); + /** + * When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, prevents the camera from going below the tileset surface. + * If using WebGL 1, {@link Cesium3DTileset#ConstructorOptions} enablePick must be true for this behavior to work. + * + * @type {boolean} + * @default false + */ + this.enableCameraCollision = defaultValue( + options.enableCameraCollision, + false + ); + this._projectTo2D = defaultValue(options.projectTo2D, false); - this._enablePick = defaultValue(options.enablePick, false); + this._enablePick = defaultValue( + options.enablePick, + this.enableCameraCollision + ); /** * This property is for debugging only; it is not optimized for production use. @@ -3407,17 +3423,20 @@ Cesium3DTileset.prototype.getHeight = function (cartographic, scene) { } const ray = scratchGetHeightRay; - ray.direction = ellipsoid.geodeticSurfaceNormalCartographic( + const position = ellipsoid.cartographicToCartesian( cartographic, ray.direction ); - const intersection = this.pick( - ray, - scene.frameState, - false, - scratchIntersection + 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; } diff --git a/packages/engine/Source/Scene/Model/I3dmLoader.js b/packages/engine/Source/Scene/Model/I3dmLoader.js index dec9597bae18..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. */ diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 521cba34f5ab..47bf3c38011e 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -162,7 +162,6 @@ function Scene(options) { this._globeTranslucencyState = new GlobeTranslucencyState(); this._primitives = new PrimitiveCollection(); this._groundPrimitives = new PrimitiveCollection(); - this._terrainTilesets = []; this._globeHeight = undefined; this._cameraUnderground = false; @@ -3576,44 +3575,21 @@ function callAfterRenderFunctions(scene) { functions.length = 0; } -/** - * Allow camera collisions, if enabled for the camera, on a tileset surface - * @param {Cesium3DTileset} tileset Tileset fo which to enable collision. - */ -Scene.prototype.enableCollisionDetectionForTileset = function (tileset) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("tileset", tileset); - //>>includeEnd('debug'); - - this._terrainTilesets.push(tileset); -}; - -/** - * Disallow camera collisions on a tileset surface - * @param {Cesium3DTileset} tileset Tileset for which to disable collision. - */ -Scene.prototype.disableCollisionDetectionForTileset = function (tileset) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("tileset", tileset); - //>>includeEnd('debug'); - - const i = this._terrainTilesets.indexOf(tileset); - if (i === -1) { - return; - } - - this._terrainTilesets.splice(i, 1); -}; - function getGlobeHeight(scene) { const globe = scene._globe; const camera = scene.camera; const cartographic = camera.positionCartographic; let maxHeight = Number.NEGATIVE_INFINITY; - for (const tileset of scene._terrainTilesets) { - const result = tileset.getHeight(cartographic, scene); - if (result > maxHeight) { + const length = scene.primitives.length; + for (let i = 0; i < length; ++i) { + const primitive = scene.primitives.get(i); + if (!primitive.isCesium3DTileset || !primitive.enableCameraCollision) { + continue; + } + + const result = primitive.getHeight(cartographic, scene); + if (defined(result) && result > maxHeight) { maxHeight = result; } } diff --git a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js index aa678f6a0161..7877b3ac852b 100644 --- a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js +++ b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js @@ -46,6 +46,10 @@ async function createGooglePhotorealistic3DTileset(key, options) { options.maximumCacheOverflowBytes, 1024 * 1024 * 1024 ); + options.enableCameraCollision = defaultValue( + options.enableCameraCollision, + true + ); key = defaultValue(key, GoogleMaps.defaultApiKey); if (!defined(key)) { From ad2764cfcf23281065145b54ae41378a898366f2 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 6 Dec 2023 10:17:05 -0500 Subject: [PATCH 13/23] Update CHANGES.md --- CHANGES.md | 5 +++++ packages/engine/Source/Scene/Cesium3DTileset.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 50062ce6ea4f..d09272f3b9e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,11 @@ #### @cesium/engine +##### Additions :tada: + +- Added `Cesium3DTileset.getHeight` to sample height values of the loaded tiles. If using WebGL 1, the `enablePick` option must be set to true to use this function. [#11581](https://github.com/CesiumGS/cesium/pull/11581) +- Added `Cesium3DTileset.enableCameraCollision` to prevent the camera from going below a 3D tileset. Tilesets created with `createGooglePhotorealistic3DTileset` have this option enabled by default. [#11581](https://github.com/CesiumGS/cesium/pull/11581) + ##### Fixes :wrench: - Changes the default `RequestScheduler.maximumRequestsPerServer` from 6 to 18. This should improve performance on HTTP/2 servers and above [#11627](https://github.com/CesiumGS/cesium/issues/11627) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index b9a54fcd6361..6f48430e403f 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3405,7 +3405,7 @@ const scratchIntersection = new Cartesian3(); const scratchGetHeightCartographic = new Cartographic(); /** - * Get the height of the loaded surface at a given 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. From 7b5e54d5ca0fb0cdc0b1777b87e803b50fa7cff8 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 6 Dec 2023 11:52:08 -0500 Subject: [PATCH 14/23] Update specs --- .../engine/Source/Scene/Cesium3DTileset.js | 18 ++++++++++++++++++ .../engine/Specs/Scene/Cesium3DTilesetSpec.js | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 6f48430e403f..0e3d494c0e49 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -153,6 +153,18 @@ import Ray from "../Core/Ray.js"; * } * * @example + * // Keep camera from going under 3D tileset + * try { + * const tileset = await Cesium.Cesium3DTileset.fromUrl( + * "http://localhost:8002/tilesets/Seattle/tileset.json", + * { enableCameraCollision: 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", { @@ -3410,6 +3422,12 @@ const scratchGetHeightCartographic = new Cartographic(); * @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); diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 9344b6e2fa17..341c8e365a1d 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", @@ -2576,6 +2577,21 @@ describe( 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("destroys", function () { return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function ( tileset From fa94c3058e47955db3fc2cf46cce15f30aae716f Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 14 Dec 2023 14:02:20 -0500 Subject: [PATCH 15/23] Tweak defaults, update CHANGES.md --- .../Sandcastle/gallery/3D Tiles Interior.html | 4 +++- .../gallery/3D Tiles Next S2 Globe.html | 1 - CHANGES.md | 8 ++++++- .../engine/Source/Scene/Cesium3DTileset.js | 22 ++++++------------- 4 files changed, 17 insertions(+), 18 deletions(-) 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/3D Tiles Next S2 Globe.html b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html index f57e5a705972..28f1198ab04b 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html +++ b/Apps/Sandcastle/gallery/3D Tiles Next S2 Globe.html @@ -79,7 +79,6 @@ // MAXAR OWT WFF 1.2 Base Globe tileset = await Cesium.Cesium3DTileset.fromIonAssetId(691510, { maximumScreenSpaceError: 4, - enableCameraCollision: true, }); scene.primitives.add(tileset); } catch (error) { diff --git a/CHANGES.md b/CHANGES.md index d09272f3b9e7..59757eebe540 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,16 @@ #### @cesium/engine +##### Breaking Changes :mega: + +- By default, the screen space camera controller will no longer go inside or under instaces of `Cesium3DTileset`. [#11581](https://github.com/CesiumGS/cesium/pull/11581) + - This behavior can be disabled by setting `Cesium3DTileset.disableCameraCollision` to true. + - This feature is enabled by default only for WebGL 2 and above, but can be enabled for WebGL 1 by setting the `enablePick` option to true when creating the `Cesium3DTileset`. + ##### Additions :tada: - Added `Cesium3DTileset.getHeight` to sample height values of the loaded tiles. If using WebGL 1, the `enablePick` option must be set to true to use this function. [#11581](https://github.com/CesiumGS/cesium/pull/11581) -- Added `Cesium3DTileset.enableCameraCollision` to prevent the camera from going below a 3D tileset. Tilesets created with `createGooglePhotorealistic3DTileset` have this option enabled by default. [#11581](https://github.com/CesiumGS/cesium/pull/11581) +- Added `Cesium3DTileset.disableCameraCollision` to allow the camera from to go inside or below a 3D tileset, for instance, to be used with 3D Tiles interiors. [#11581](https://github.com/CesiumGS/cesium/pull/11581) ##### Fixes :wrench: diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 0e3d494c0e49..bc9620546268 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -109,10 +109,9 @@ import Ray from "../Core/Ray.js"; * @property {string|number} [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. * @property {string|number} [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. * @property {boolean} [showCreditsOnScreen=false] Whether to display the credits of this tileset on screen. - * @property {SplitDirection} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset. - * @property {boolean} [enableCameraCollision=false] When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, prevents the camera from going below the tileset surface. + * @property {boolean} [disableCollision=false] When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, allows the camera to go in or below the tileset surface. * @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 with CPU picking with 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 tileset has loaded. + * @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. @@ -153,11 +152,11 @@ import Ray from "../Core/Ray.js"; * } * * @example - * // Keep camera from going under 3D tileset + * // Allow camera to go inside and under 3D tileset * try { * const tileset = await Cesium.Cesium3DTileset.fromUrl( * "http://localhost:8002/tilesets/Seattle/tileset.json", - * { enableCameraCollision: true } + * { disableCollision: true } * ); * scene.primitives.add(tileset); * } catch (error) { @@ -840,22 +839,15 @@ function Cesium3DTileset(options) { ); /** - * When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, prevents the camera from going below the tileset surface. - * If using WebGL 1, {@link Cesium3DTileset#ConstructorOptions} enablePick must be true for this behavior to work. + * When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, allows the camera to go inside or below the tileset surface. * * @type {boolean} * @default false */ - this.enableCameraCollision = defaultValue( - options.enableCameraCollision, - false - ); + this.disableCollision = defaultValue(options.disableCollision, false); this._projectTo2D = defaultValue(options.projectTo2D, false); - this._enablePick = defaultValue( - options.enablePick, - this.enableCameraCollision - ); + this._enablePick = defaultValue(options.enablePick, false); /** * This property is for debugging only; it is not optimized for production use. From 813379e6a621c82c1238b6ecbb5691321cd12b60 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 14 Dec 2023 14:04:00 -0500 Subject: [PATCH 16/23] Cleanup --- Apps/Sandcastle/gallery/Clamp to 3D Tiles.html | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html index f86206cb91d0..dadb13795bf0 100644 --- a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html +++ b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html @@ -65,9 +65,8 @@ endTransform: Cesium.Matrix4.IDENTITY, }); - let tileset; try { - tileset = await Cesium.Cesium3DTileset.fromIonAssetId(40866); + const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(40866); viewer.scene.primitives.add(tileset); if (scene.clampToHeightSupported) { @@ -79,19 +78,12 @@ console.log(`Error loading tileset: ${error}`); } - const ray = new Cesium.Ray(); - function start() { clock.shouldAnimate = true; const objectsToExclude = [entity]; scene.postRender.addEventListener(function () { const position = positionProperty.getValue(clock.currentTime); - ray.origin = Cesium.Cartesian3.clone(position, ray.origin); - ray.direction = Cesium.Cartesian3.negate( - scene.globe.ellipsoid.geodeticSurfaceNormal(position), - ray.direction - ); - entity.position = tileset.pick(ray, scene, true, position); + entity.position = scene.clampToHeight(position, objectsToExclude); }); } //Sandcastle_End }; From 7a769e31c707fe7850c5726df4eb2f04d6bba214 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 14 Dec 2023 14:27:39 -0500 Subject: [PATCH 17/23] Fixes to picking --- packages/engine/Source/Core/Transforms.js | 39 ++++++++++++------- .../engine/Source/Scene/Model/pickModel.js | 18 ++++----- packages/engine/Source/Scene/Scene.js | 2 +- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/engine/Source/Core/Transforms.js b/packages/engine/Source/Core/Transforms.js index ffa21d51f7c8..37c5edfc46f6 100644 --- a/packages/engine/Source/Core/Transforms.js +++ b/packages/engine/Source/Core/Transforms.js @@ -1045,21 +1045,30 @@ Transforms.basisTo2D = function (projection, matrix, result) { const rtcCenter = Matrix4.getTranslation(matrix, scratchCenter); const ellipsoid = projection.ellipsoid; - // Get the 2D Center - const cartographic = ellipsoid.cartesianToCartographic( - rtcCenter, - scratchCartographic - ); - const projectedPosition = projection.project( - cartographic, - scratchCartesian3Projection - ); - Cartesian3.fromElements( - projectedPosition.z, - projectedPosition.x, - projectedPosition.y, - projectedPosition - ); + let projectedPosition; + if (Cartesian3.equals(rtcCenter, Cartesian3.ZERO)) { + projectedPosition = Cartesian3.clone( + Cartesian3.ZERO, + scratchCartesian3Projection + ); + } else { + // Get the 2D Center + const cartographic = ellipsoid.cartesianToCartographic( + rtcCenter, + scratchCartographic + ); + + projectedPosition = projection.project( + cartographic, + scratchCartesian3Projection + ); + Cartesian3.fromElements( + projectedPosition.z, + projectedPosition.x, + projectedPosition.y, + projectedPosition + ); + } // Assuming the instance are positioned in WGS84, invert the WGS84 transform to get the local transform and then convert to 2D const fromENU = Transforms.eastNorthUpToFixedFrame( diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js index 16a2bb02f94f..1de4a2df8532 100644 --- a/packages/engine/Source/Scene/Model/pickModel.js +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -82,20 +82,20 @@ export default function pickModel(model, ray, frameState, result) { } } - if (frameState.mode !== SceneMode.SCENE3D) { - modelMatrix = Transforms.basisTo2D( - frameState.mapProjection, - modelMatrix, - modelMatrix - ); - } - - const computedModelMatrix = Matrix4.multiplyTransformation( + 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; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 47bf3c38011e..458da821bbfe 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -3584,7 +3584,7 @@ function getGlobeHeight(scene) { const length = scene.primitives.length; for (let i = 0; i < length; ++i) { const primitive = scene.primitives.get(i); - if (!primitive.isCesium3DTileset || !primitive.enableCameraCollision) { + if (!primitive.isCesium3DTileset || primitive.disableCollision) { continue; } From 7a563de35a3259586aa22f01451d0a9034fa2bf1 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 14 Dec 2023 15:27:32 -0500 Subject: [PATCH 18/23] Fix unit tests --- packages/engine/Source/Scene/Cesium3DTileset.js | 4 ++++ packages/engine/Source/Scene/Cesium3DTilesetTraversal.js | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index bc9620546268..57791c5c02bc 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3471,6 +3471,10 @@ const scratchPickIntersection = new Cartesian3(); * @private */ Cesium3DTileset.prototype.pick = function (ray, frameState, result) { + if (!frameState.context.webgl2 && !this._enablePick) { + return; + } + const selectedTiles = this._selectedTiles; const selectedLength = selectedTiles.length; diff --git a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js index 12561ba541eb..d8ad154f3dd8 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js +++ b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js @@ -79,6 +79,7 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { return; } + tile._wasSelectedLastFrame = true; const { content, tileset } = tile; if (content.featurePropertiesDirty) { // A feature's property in this tile changed, the tile needs to be re-styled. @@ -88,6 +89,7 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { } else if (tile._selectedFrame < frameState.frameNumber - 1) { // Tile is newly selected; it is selected this frame, but was not selected last frame. tileset._selectedTilesToStyle.push(tile); + tile._wasSelectedLastFrame = false; } tile._selectedFrame = frameState.frameNumber; tileset._selectedTiles.push(tile); From 613c7eaac506a095f69dbbc9c4f8fcbac93cdc0c Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Thu, 4 Jan 2024 11:27:11 -0500 Subject: [PATCH 19/23] Vertical exaggeration --- .../engine/Source/Scene/Cesium3DTileset.js | 2 +- packages/engine/Source/Scene/Model/Model.js | 19 ++++++- .../Source/Scene/Model/Model3DTileContent.js | 14 ++++- .../engine/Source/Scene/Model/pickModel.js | 55 ++++++++++++++++++- .../engine/Specs/Scene/Model/pickModelSpec.js | 38 ++++++++++++- 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 57791c5c02bc..82320ffe1638 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -3491,7 +3491,7 @@ Cesium3DTileset.prototype.pick = function (ray, frameState, result) { continue; } - const candidate = tile.content.pick( + const candidate = tile.content?.pick( ray, frameState, scratchPickIntersection diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index aae4bc7bdc7c..f951b3d000a7 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -2500,13 +2500,28 @@ Model.prototype.isClippingEnabled = function () { * * @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, result) { - return pickModel(this, ray, frameState, result); +Model.prototype.pick = function ( + ray, + frameState, + verticalExaggeration, + relativeHeight, + result +) { + return pickModel( + this, + ray, + frameState, + verticalExaggeration, + relativeHeight, + result + ); }; /** diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index 3273c81146d6..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"; @@ -431,7 +432,18 @@ Model3DTileContent.prototype.pick = function (ray, frameState, result) { return undefined; } - return this._model.pick(ray, frameState, result); + 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) { diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js index 1de4a2df8532..0a35a0865fe4 100644 --- a/packages/engine/Source/Scene/Model/pickModel.js +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -6,11 +6,13 @@ 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"; @@ -19,11 +21,13 @@ import ModelUtility from "./ModelUtility.js"; const scratchV0 = new Cartesian3(); const scratchV1 = new Cartesian3(); const scratchV2 = new Cartesian3(); +const scratchNormal = new Cartesian3(); const scratchNodeComputedTransform = new Matrix4(); const scratchModelMatrix = new Matrix4(); const scratchcomputedModelMatrix = new Matrix4(); const scratchPickCartographic = new Cartographic(); const scratchBoundingSphere = new BoundingSphere(); +const scratchHeightCartographic = new Cartographic(); /** * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates. @@ -31,12 +35,23 @@ const scratchBoundingSphere = new BoundingSphere(); * @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, result) { +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); @@ -253,6 +268,10 @@ export default function pickModel(model, ray, frameState, result) { 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]; @@ -266,6 +285,9 @@ export default function pickModel(model, ray, frameState, result) { numComponents, quantization, instanceTransform, + verticalExaggeration, + relativeHeight, + ellipsoid, scratchV0 ); const v1 = getVertexPosition( @@ -274,6 +296,9 @@ export default function pickModel(model, ray, frameState, result) { numComponents, quantization, instanceTransform, + verticalExaggeration, + relativeHeight, + ellipsoid, scratchV1 ); const v2 = getVertexPosition( @@ -282,6 +307,9 @@ export default function pickModel(model, ray, frameState, result) { numComponents, quantization, instanceTransform, + verticalExaggeration, + relativeHeight, + ellipsoid, scratchV2 ); @@ -327,6 +355,9 @@ function getVertexPosition( numComponents, quantization, instanceTransform, + verticalExaggeration, + relativeHeight, + ellipsoid, result ) { const i = index * numComponents; @@ -365,5 +396,27 @@ function getVertexPosition( result = Matrix4.multiplyByPoint(instanceTransform, result, result); + if (verticalExaggeration !== 1.0) { + const geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal( + result, + scratchNormal + ); + const rawHeight = ellipsoid.cartesianToCartographic( + result, + scratchHeightCartographic + ).height; + const heightDifference = + VerticalExaggeration.getHeight( + rawHeight, + verticalExaggeration, + relativeHeight + ) - rawHeight; + + // some math is unrolled for better performance + result.x += geodeticSurfaceNormal.x * heightDifference; + result.y += geodeticSurfaceNormal.y * heightDifference; + result.z += geodeticSurfaceNormal.z * heightDifference; + } + return result; } diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js index af89fd06907d..b00f967ac087 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -2,6 +2,7 @@ import { pickModel, Cartesian2, Cartesian3, + Ellipsoid, HeadingPitchRange, Math as CesiumMath, Model, @@ -339,7 +340,15 @@ describe("Scene/Model/pickModel", function () { const result = new Cartesian3(); const expected = new Cartesian3(0.5, 0, 0.5); - const returned = pickModel(model, ray, scene.frameState, result); + const returned = pickModel( + model, + ray, + scene.frameState, + undefined, + undefined, + undefined, + result + ); expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON12); expect(returned).toBe(result); }); @@ -362,4 +371,31 @@ describe("Scene/Model/pickModel", function () { 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(-8197.676413311, 0, -8197.676413312); + expect( + pickModel( + model, + ray, + scene.frameState, + 2.0, + -Ellipsoid.WGS84.minimumRadius + ) + ).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); }); From dace85b15ce6032b0fe87af7ad82e4ad8a31a959 Mon Sep 17 00:00:00 2001 From: Jeshurun Hembd Date: Sat, 6 Jan 2024 23:18:37 -0500 Subject: [PATCH 20/23] Fix docs typos --- CHANGES.md | 2 +- packages/engine/Source/Scene/Model/Model.js | 2 +- packages/engine/Specs/Scene/Model/pickModelSpec.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8f9b201060aa..c02febd28ab7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ ##### Breaking Changes :mega: -- By default, the screen space camera controller will no longer go inside or under instaces of `Cesium3DTileset`. [#11581](https://github.com/CesiumGS/cesium/pull/11581) +- By default, the screen space camera controller will no longer go inside or under instances of `Cesium3DTileset`. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - This behavior can be disabled by setting `Cesium3DTileset.disableCameraCollision` to true. - This feature is enabled by default only for WebGL 2 and above, but can be enabled for WebGL 1 by setting the `enablePick` option to true when creating the `Cesium3DTileset`. diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index f951b3d000a7..f9385ed410ac 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -151,7 +151,7 @@ import pickModel from "./pickModel.js"; * @privateParam {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen. * @privateParam {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this model. * @privateParam {boolean} [options.projectTo2D=false] Whether to accurately project the model's positions in 2D. If this is true, the model will be projected accurately to 2D, but it will use more memory to do so. If this is false, the model will use less memory and will still render in 2D / CV mode, but its positions may be inaccurate. This disables minimumPixelSize and prevents future modification to the model matrix. This also cannot be set after the model has loaded. - * @privateParam {boolean} [options.enablePick=false] Whether to allow with CPU picking with 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 {boolean} [options.enablePick=false] Whether to allow CPU picking with 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. diff --git a/packages/engine/Specs/Scene/Model/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js index b00f967ac087..9b9a200c0187 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -294,7 +294,7 @@ describe("Scene/Model/pickModel", function () { expect(pickModel(model, ray, scene.frameState)).toBeUndefined(); }); - it("includes back faces results when model disbales backface culling", async function () { + it("includes back faces results when model disables backface culling", async function () { const model = await loadAndZoomToModelAsync( { url: boxBackFaceCullingUrl, From 816ba492065c8cd0b2b8115dd2562afa0050e9c2 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Tue, 9 Jan 2024 12:21:11 -0500 Subject: [PATCH 21/23] Fix documentation --- CHANGES.md | 4 ++-- packages/engine/Source/Scene/Cesium3DTileset.js | 5 +++-- .../Source/Scene/createGooglePhotorealistic3DTileset.js | 4 ---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 49ce27726103..7b75da4bf990 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,13 +7,13 @@ ##### Breaking Changes :mega: - By default, the screen space camera controller will no longer go inside or under instances of `Cesium3DTileset`. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - - This behavior can be disabled by setting `Cesium3DTileset.disableCameraCollision` to true. + - This behavior can be disabled by setting `Cesium3DTileset.disableCollision` to true. - This feature is enabled by default only for WebGL 2 and above, but can be enabled for WebGL 1 by setting the `enablePick` option to true when creating the `Cesium3DTileset`. ##### Additions :tada: - Added `Cesium3DTileset.getHeight` to sample height values of the loaded tiles. If using WebGL 1, the `enablePick` option must be set to true to use this function. [#11581](https://github.com/CesiumGS/cesium/pull/11581) -- Added `Cesium3DTileset.disableCameraCollision` to allow the camera from to go inside or below a 3D tileset, for instance, to be used with 3D Tiles interiors. [#11581](https://github.com/CesiumGS/cesium/pull/11581) +- Added `Cesium3DTileset.disableCollision` to allow the camera from to go inside or below a 3D tileset, for instance, to be used with 3D Tiles interiors. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - The `Cesium3DTileset.dynamicScreenSpaceError` optimization is now enabled by default, as this improves performance for street-level horizon views. Furthermore, the default settings of this feature were tuned for improved performance. `Cesium3DTileset.dynamicScreenSpaceErrorDensity` was changed from 0.00278 to 0.0002. `Cesium3DTileset.dynamicScreenSpaceErrorFactor` was changed from 4 to 24. [#11718](https://github.com/CesiumGS/cesium/pull/11718) ##### Fixes :wrench: diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 50fbaa6296df..bafb85770c01 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -110,7 +110,8 @@ import Ray from "../Core/Ray.js"; * @property {string|number} [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. * @property {string|number} [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. * @property {boolean} [showCreditsOnScreen=false] Whether to display the credits of this tileset on screen. - * @property {boolean} [disableCollision=false] When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, allows the camera to go in or below the tileset surface. + * @property {SplitDirection} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset. + * @property {boolean} [disableCollision=false] Whether to turn off collisions for camera collisions or picking. While this is `false` 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. @@ -860,7 +861,7 @@ function Cesium3DTileset(options) { ); /** - * When {@link ScreenSpaceCameraController#enableCollisionDetection} is true, allows the camera to go inside or below the tileset surface. + * Whether to turn off collisions for camera collisions or picking. While this is `false` the camera will be allowed to go in or below the tileset surface if {@link ScreenSpaceCameraController#enableCollisionDetection} is true. * * @type {boolean} * @default false diff --git a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js index 7877b3ac852b..aa678f6a0161 100644 --- a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js +++ b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js @@ -46,10 +46,6 @@ async function createGooglePhotorealistic3DTileset(key, options) { options.maximumCacheOverflowBytes, 1024 * 1024 * 1024 ); - options.enableCameraCollision = defaultValue( - options.enableCameraCollision, - true - ); key = defaultValue(key, GoogleMaps.defaultApiKey); if (!defined(key)) { From c47fa02ccc4289fb8cd72401c31bc01ce4634964 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Tue, 9 Jan 2024 13:19:39 -0500 Subject: [PATCH 22/23] Tweak how vertical exaggeration is handled --- .../Source/Scene/Cesium3DTilesetTraversal.js | 2 -- .../engine/Source/Scene/Model/pickModel.js | 24 ++++--------------- .../engine/Specs/Scene/Cesium3DTilesetSpec.js | 16 +++++++++++++ .../engine/Specs/Scene/Model/pickModelSpec.js | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js index d8ad154f3dd8..12561ba541eb 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js +++ b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js @@ -79,7 +79,6 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { return; } - tile._wasSelectedLastFrame = true; const { content, tileset } = tile; if (content.featurePropertiesDirty) { // A feature's property in this tile changed, the tile needs to be re-styled. @@ -89,7 +88,6 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { } else if (tile._selectedFrame < frameState.frameNumber - 1) { // Tile is newly selected; it is selected this frame, but was not selected last frame. tileset._selectedTilesToStyle.push(tile); - tile._wasSelectedLastFrame = false; } tile._selectedFrame = frameState.frameNumber; tileset._selectedTiles.push(tile); diff --git a/packages/engine/Source/Scene/Model/pickModel.js b/packages/engine/Source/Scene/Model/pickModel.js index 0a35a0865fe4..5ce238f2237d 100644 --- a/packages/engine/Source/Scene/Model/pickModel.js +++ b/packages/engine/Source/Scene/Model/pickModel.js @@ -21,13 +21,11 @@ import ModelUtility from "./ModelUtility.js"; const scratchV0 = new Cartesian3(); const scratchV1 = new Cartesian3(); const scratchV2 = new Cartesian3(); -const scratchNormal = new Cartesian3(); const scratchNodeComputedTransform = new Matrix4(); const scratchModelMatrix = new Matrix4(); const scratchcomputedModelMatrix = new Matrix4(); const scratchPickCartographic = new Cartographic(); const scratchBoundingSphere = new BoundingSphere(); -const scratchHeightCartographic = new Cartographic(); /** * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates. @@ -397,25 +395,13 @@ function getVertexPosition( result = Matrix4.multiplyByPoint(instanceTransform, result, result); if (verticalExaggeration !== 1.0) { - const geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal( + VerticalExaggeration.getPosition( result, - scratchNormal + ellipsoid, + verticalExaggeration, + relativeHeight, + result ); - const rawHeight = ellipsoid.cartesianToCartographic( - result, - scratchHeightCartographic - ).height; - const heightDifference = - VerticalExaggeration.getHeight( - rawHeight, - verticalExaggeration, - relativeHeight - ) - rawHeight; - - // some math is unrolled for better performance - result.x += geodeticSurfaceNormal.x * heightDifference; - result.y += geodeticSurfaceNormal.y * heightDifference; - result.z += geodeticSurfaceNormal.z * heightDifference; } return result; diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 4d6fe613a4f0..4e60e5dad2e6 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -2622,6 +2622,22 @@ describe( 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/pickModelSpec.js b/packages/engine/Specs/Scene/Model/pickModelSpec.js index 9b9a200c0187..4cf4ee77670d 100644 --- a/packages/engine/Specs/Scene/Model/pickModelSpec.js +++ b/packages/engine/Specs/Scene/Model/pickModelSpec.js @@ -387,7 +387,7 @@ describe("Scene/Model/pickModel", function () { ) ); - const expected = new Cartesian3(-8197.676413311, 0, -8197.676413312); + const expected = new Cartesian3(-65.51341504, 0, -65.51341504); expect( pickModel( model, From 41daf91ae9398a5fb3966e7e0ce4c2cb434211a2 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 10 Jan 2024 11:09:15 -0500 Subject: [PATCH 23/23] Doc fix, test fix --- packages/engine/Source/Scene/Cesium3DTileset.js | 4 ++-- packages/engine/Specs/Scene/Cesium3DTilesetSpec.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index bafb85770c01..86d9d0e2403e 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -111,7 +111,7 @@ import Ray from "../Core/Ray.js"; * @property {string|number} [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. * @property {boolean} [showCreditsOnScreen=false] Whether to display the credits of this tileset on screen. * @property {SplitDirection} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this tileset. - * @property {boolean} [disableCollision=false] Whether to turn off collisions for camera collisions or picking. While this is `false` the camera will be allowed to go in or below the tileset surface if {@link ScreenSpaceCameraController#enableCollisionDetection} is true. + * @property {boolean} [disableCollision=false] 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. * @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. @@ -861,7 +861,7 @@ function Cesium3DTileset(options) { ); /** - * Whether to turn off collisions for camera collisions or picking. While this is `false` the camera will be allowed to go in or below the tileset surface if {@link ScreenSpaceCameraController#enableCollisionDetection} is true. + * 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 diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 4e60e5dad2e6..6653ca4e04f7 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -212,6 +212,7 @@ describe( }); afterEach(function () { + scene.verticalExaggeration = 1.0; scene.primitives.removeAll(); ResourceCache.clearForSpecs(); });