Skip to content

Commit 7c3a70c

Browse files
authored
Merge pull request #13098 from CesiumGS/2d-picking-fixes
2d picking fixes
2 parents 4b1b05f + 66ceaf5 commit 7c3a70c

File tree

10 files changed

+342
-30
lines changed

10 files changed

+342
-30
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Fixed label sizing for some fonts and characters [#9767](https://github.com/CesiumGS/cesium/issues/9767)
1313
- Fixed a type error when accessing the ellipsoid of a viewer [#13123](https://github.com/CesiumGS/cesium/pull/13123)
1414
- Fixed a bug where entities have not been clustered correctly [#13064](https://github.com/CesiumGS/cesium/pull/13064)
15+
- Fixes multiple issues causing undefined pick results in 2D/CV scene modes [#13083](https://github.com/CesiumGS/cesium/issues/13083)
1516

1617
## 1.137 - 2026-01-05
1718

packages/engine/Source/Core/TerrainMesh.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ function TerrainMesh(
179179
this._transform = new Matrix4();
180180

181181
/**
182-
* True if the transform needs to be recomputed (due to changes in exaggeration or scene mode).
183-
* @type {boolean}
182+
* The scene mode used the last time a pick was performed on this terrain mesh.
183+
* @type {SceneMode}
184184
*/
185-
this._recomputeTransform = true;
185+
this._lastPickSceneMode = undefined;
186186

187187
/**
188188
* The terrain picker for this mesh, used for ray intersection tests.
@@ -199,10 +199,10 @@ function TerrainMesh(
199199
* @private
200200
*/
201201
TerrainMesh.prototype.getTransform = function (mode, projection) {
202-
if (!this._recomputeTransform) {
202+
if (this._lastPickSceneMode === mode) {
203203
return this._transform;
204204
}
205-
this._recomputeTransform = false;
205+
this._terrainPicker.needsRebuild = true;
206206

207207
if (!defined(mode) || mode === SceneMode.SCENE3D) {
208208
return computeTransform(this, this._transform);
@@ -322,13 +322,16 @@ function computeTransform2D(mesh, projection, result) {
322322
* @private
323323
*/
324324
TerrainMesh.prototype.pick = function (ray, cullBackFaces, mode, projection) {
325-
return this._terrainPicker.rayIntersect(
325+
const intersection = this._terrainPicker.rayIntersect(
326326
ray,
327327
this.getTransform(mode, projection),
328328
cullBackFaces,
329329
mode,
330330
projection,
331331
);
332+
333+
this._lastPickSceneMode = mode;
334+
return intersection;
332335
};
333336

334337
/**
@@ -345,7 +348,7 @@ TerrainMesh.prototype.updateExaggeration = function (
345348
// to trigger a rebuild on the terrain picker.
346349
this._terrainPicker._vertices = this.vertices;
347350
this._terrainPicker.needsRebuild = true;
348-
this._recomputeTransform = true;
351+
this._lastPickSceneMode = undefined;
349352
};
350353

351354
/**
@@ -355,7 +358,7 @@ TerrainMesh.prototype.updateExaggeration = function (
355358
*/
356359
TerrainMesh.prototype.updateSceneMode = function (mode) {
357360
this._terrainPicker.needsRebuild = true;
358-
this._recomputeTransform = true;
361+
this._lastPickSceneMode = undefined;
359362
};
360363

361364
export default TerrainMesh;

packages/engine/Source/Core/TerrainPicker.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import SceneMode from "../Scene/SceneMode.js";
1010
import Interval from "./Interval.js";
1111
import Check from "./Check.js";
1212
import DeveloperError from "./DeveloperError.js";
13+
import CesiumMath from "./Math.js";
1314

1415
// Terrain picker can be 4 levels deep (0-3)
1516
const MAXIMUM_TERRAIN_PICKER_LEVEL = 3;
@@ -20,7 +21,7 @@ const MAXIMUM_TERRAIN_PICKER_LEVEL = 3;
2021
* @alias TerrainPicker
2122
* @constructor
2223
*
23-
* @param {Float32Array} vertices The terrain mesh's vertex buffer.
24+
* @param {Float64Array} vertices The terrain mesh's vertex buffer.
2425
* @param {Uint8Array|Uint16Array|Uint32Array} indices The terrain mesh's index buffer.
2526
* @param {TerrainEncoding} encoding The terrain mesh's vertex encoding.
2627
*
@@ -35,7 +36,7 @@ function TerrainPicker(vertices, indices, encoding) {
3536

3637
/**
3738
* The terrain mesh's vertex buffer.
38-
* @type {Float32Array}
39+
* @type {Float64Array}
3940
*/
4041
this._vertices = vertices;
4142
/**
@@ -268,7 +269,7 @@ function createAABBForNode(x, y, level) {
268269
/**
269270
* Packs triangle vertex positions and index into provided buffers, for the worker to process.
270271
* (The worker does tests to organize triangles into child nodes of the quadtree.)
271-
* @param {Float32Array} trianglePositionsBuffer The buffer to pack triangle vertex positions into.
272+
* @param {Float64Array} trianglePositionsBuffer The buffer to pack triangle vertex positions into.
272273
* @param {Uint32Array} triangleIndicesBuffer The buffer to pack triangle indices into.
273274
* @param {Cartesian3[]} trianglePositions The triangle's vertex positions.
274275
* @param {number} triangleIndex The triangle's index in the overall tile's index buffer.
@@ -424,7 +425,7 @@ function getClosestTriangleInNode(
424425
let triangleIndices;
425426
if (shouldBuildChildren) {
426427
// If the tree can be built deeper, prepare buffers to store triangle data for child nodes
427-
trianglePositions = new Float32Array(triangleCount * 9); // 3 vertices per triangle * 3 floats per vertex
428+
trianglePositions = new Float64Array(triangleCount * 9); // 3 vertices per triangle * 3 floats per vertex
428429
triangleIndices = new Uint32Array(triangleCount);
429430
}
430431

@@ -434,6 +435,7 @@ function getClosestTriangleInNode(
434435
encoding,
435436
mode,
436437
projection,
438+
ray,
437439
vertices,
438440
indices[3 * triIndex],
439441
scratchTrianglePoints[0],
@@ -442,6 +444,7 @@ function getClosestTriangleInNode(
442444
encoding,
443445
mode,
444446
projection,
447+
ray,
445448
vertices,
446449
indices[3 * triIndex + 1],
447450
scratchTrianglePoints[1],
@@ -450,6 +453,7 @@ function getClosestTriangleInNode(
450453
encoding,
451454
mode,
452455
projection,
456+
ray,
453457
vertices,
454458
indices[3 * triIndex + 2],
455459
scratchTrianglePoints[2],
@@ -502,7 +506,8 @@ const scratchCartographic = new Cartographic();
502506
* @param {TerrainEncoding} encoding The terrain encoding.
503507
* @param {SceneMode} mode The scene mode (2D/3D/Columbus View).
504508
* @param {MapProjection} projection The map projection.
505-
* @param {Float32Array} vertices The vertex buffer of the terrain mesh.
509+
* @param {Ray} ray The pick ray being tested (used here as a reference to resolve antimeridian wrapping in 2D/Columbus View).
510+
* @param {Float64Array} vertices The terrain mesh's vertex buffer.
506511
* @param {Number} index The index of the vertex to get.
507512
* @param {Cartesian3} result The decoded, exaggerated, and possibly projected vertex position.
508513
* @returns {Cartesian3} The result vertex position.
@@ -512,6 +517,7 @@ function getVertexPosition(
512517
encoding,
513518
mode,
514519
projection,
520+
ray,
515521
vertices,
516522
index,
517523
result,
@@ -535,6 +541,14 @@ function getVertexPosition(
535541
result,
536542
);
537543

544+
// Due to wrapping in 2D/CV modes, near the antimeridian, the vertex
545+
// position may correspond to the other side of the world from the ray origin.
546+
// Compare the vertex position to the ray origin and adjust it accordingly.
547+
// A spherical approximation is sufficient for cylindrical projections,
548+
// like mercator and geographic.
549+
const worldWidth = CesiumMath.TWO_PI * projection.ellipsoid.maximumRadius;
550+
const k = Math.round((ray.origin.y - position.y) / worldWidth);
551+
position.y += k * worldWidth;
538552
return position;
539553
}
540554

@@ -544,7 +558,7 @@ function getVertexPosition(
544558
* @param {Matrix4} inverseTransform
545559
* @param {TerrainNode} node
546560
* @param {Uint32Array} triangleIndices
547-
* @param {Float32Array} trianglePositions
561+
* @param {Float64Array} trianglePositions
548562
* @returns {Promise<void>} A promise that resolves when the triangles have been added to the child nodes.
549563
* @private
550564
*/
@@ -596,7 +610,10 @@ async function addTrianglesToChildrenNodes(
596610
// Assign these to the child nodes
597611
const result = await incrementallyBuildTerrainPickerPromise;
598612
result.intersectingTrianglesArrays.forEach((buffer, index) => {
599-
node.children[index].intersectingTriangles = new Uint32Array(buffer);
613+
// Guard against case where tree is reset while waiting for worker
614+
if (defined(node.children[index])) {
615+
node.children[index].intersectingTriangles = new Uint32Array(buffer);
616+
}
600617
});
601618

602619
// The node's triangles have been distributed to its children

packages/engine/Source/Scene/Camera.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3005,6 +3005,17 @@ function getPickRayOrthographic(camera, windowPosition, result) {
30053005

30063006
Cartesian3.clone(camera.directionWC, result.direction);
30073007

3008+
// Account for wrap-around in 2D infinite scroll mode
3009+
if (
3010+
camera._mode === SceneMode.SCENE2D &&
3011+
camera._scene.mapMode2D === MapMode2D.INFINITE_SCROLL
3012+
) {
3013+
const maxHorizontal = camera._maxCoord.x;
3014+
origin.y =
3015+
CesiumMath.mod(origin.y + maxHorizontal, 2.0 * maxHorizontal) -
3016+
maxHorizontal;
3017+
}
3018+
30083019
return result;
30093020
}
30103021

packages/engine/Source/Scene/Globe.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -723,14 +723,7 @@ Globe.prototype.pickWorldCoordinates = function (
723723
const sphereIntersections = scratchArray;
724724
sphereIntersections.length = 0;
725725

726-
const tilesToRender = this._surface._tilesToRender;
727-
let length = tilesToRender.length;
728-
729-
let tile;
730-
let i;
731-
732-
for (i = 0; i < length; ++i) {
733-
tile = tilesToRender[i];
726+
for (const tile of this._surface._tilesRenderedThisFrame) {
734727
const surfaceTile = tile.data;
735728

736729
if (!defined(surfaceTile)) {
@@ -776,8 +769,8 @@ Globe.prototype.pickWorldCoordinates = function (
776769
sphereIntersections.sort(createComparePickTileFunction(ray.origin));
777770

778771
let intersection;
779-
length = sphereIntersections.length;
780-
for (i = 0; i < length; ++i) {
772+
const length = sphereIntersections.length;
773+
for (let i = 0; i < length; ++i) {
781774
intersection = sphereIntersections[i].pick(
782775
ray,
783776
scene.mode,

packages/engine/Source/Scene/QuadtreePrimitive.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function QuadtreePrimitive(options) {
7777
const tilingScheme = this._tileProvider.tilingScheme;
7878
const ellipsoid = tilingScheme.ellipsoid;
7979

80+
this._tilesRenderedThisFrame = new Set(); // collect all tiles selected to render (useful when multiple render calls are made in a single frame (as in 2D mode))
8081
this._tilesToRender = [];
8182
this._tileLoadQueueHigh = []; // high priority tiles are preventing refinement
8283
this._tileLoadQueueMedium = []; // medium priority tiles are being rendered
@@ -256,9 +257,9 @@ QuadtreePrimitive.prototype.forEachLoadedTile = function (tileFunction) {
256257
* function is passed a reference to the tile as its only parameter.
257258
*/
258259
QuadtreePrimitive.prototype.forEachRenderedTile = function (tileFunction) {
259-
const tilesRendered = this._tilesToRender;
260-
for (let i = 0, len = tilesRendered.length; i < len; ++i) {
261-
tileFunction(tilesRendered[i]);
260+
const tilesRendered = this._tilesRenderedThisFrame;
261+
for (const tile of tilesRendered) {
262+
tileFunction(tile);
262263
}
263264
};
264265

@@ -347,6 +348,7 @@ QuadtreePrimitive.prototype.beginFrame = function (frameState) {
347348
}
348349

349350
this._tileReplacementQueue.markStartOfRenderFrame();
351+
this._tilesRenderedThisFrame.clear();
350352
};
351353

352354
/**
@@ -1303,6 +1305,7 @@ function screenSpaceError2D(primitive, frameState, tile) {
13031305

13041306
function addTileToRenderList(primitive, tile) {
13051307
primitive._tilesToRender.push(tile);
1308+
primitive._tilesRenderedThisFrame.add(tile);
13061309
}
13071310

13081311
function processTileLoadQueue(primitive, frameState) {

packages/engine/Source/Workers/incrementallyBuildTerrainPicker.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const scratchTrianglePoints = [
1212
];
1313
const scratchTriangleAABB = new AxisAlignedBoundingBox();
1414

15+
const TILE_AABB_MAX = new Cartesian3(0.5, 0.5, 0.5);
16+
const TILE_AABB_MIN = new Cartesian3(-0.5, -0.5, -0.5);
17+
1518
/**
1619
* Builds the next layer of the terrain picker's quadtree by determining which triangles intersect
1720
* each of the four child nodes. (Essentially distributing the parent's triangles to its children.)
@@ -42,7 +45,7 @@ function incrementallyBuildTerrainPicker(parameters, transferableObjects) {
4245
);
4346

4447
const triangleIndices = new Uint32Array(parameters.triangleIndices);
45-
const trianglePositions = new Float32Array(parameters.trianglePositions);
48+
const trianglePositions = new Float64Array(parameters.trianglePositions);
4649
const intersectingTrianglesArrays = Array.from({ length: 4 }, () => []);
4750

4851
for (let j = 0; j < triangleIndices.length; j++) {
@@ -102,7 +105,18 @@ function createAABBFromTriangle(inverseTransform, trianglePoints) {
102105
trianglePoints[2],
103106
);
104107

105-
return AxisAlignedBoundingBox.fromPoints(trianglePoints, scratchTriangleAABB);
108+
const aabb = AxisAlignedBoundingBox.fromPoints(
109+
trianglePoints,
110+
scratchTriangleAABB,
111+
);
112+
113+
// In 2D mode, sometimes the height-scale of a tile is 0. See {@link TerrainMesh#computeTransform2D}.
114+
// This makes the inverseTransform degenerate, so we set the height-scale to 1 to be prevent that. However, this is artificial and
115+
// can lead to the triangle's AABB extending beyond the (height) bounds of the tile's AABB.
116+
// Thus, we clamp the triangle's AABB to the tile's local-space AABB.
117+
Cartesian3.clamp(aabb.minimum, TILE_AABB_MIN, TILE_AABB_MAX, aabb.minimum);
118+
Cartesian3.clamp(aabb.maximum, TILE_AABB_MIN, TILE_AABB_MAX, aabb.maximum);
119+
return aabb;
106120
}
107121

108122
export default createTaskProcessorWorker(incrementallyBuildTerrainPicker);

0 commit comments

Comments
 (0)