Skip to content

Commit eb46f8c

Browse files
stephanheiglgithub-actions[bot]
authored andcommitted
Fix 3D model query intersection inaccuracies
GitOrigin-RevId: 621eaabde48d77614f1f58350eb1d6c40c3f3c08
1 parent 9a1c893 commit eb46f8c

6 files changed

Lines changed: 206 additions & 30 deletions

File tree

3d-style/style/style_layer/model_style_layer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class ModelStyleLayer extends StyleLayer {
101101
const model = modelManager.getModel(modelId, this.scope);
102102
if (!model) return false;
103103

104-
let matrix = mat4.create();
104+
let matrix: mat4 = [];
105105
const position = new LngLat(0, 0);
106106
const id = bucket.canonical;
107107
let minDepth = Number.MAX_VALUE;
@@ -111,8 +111,8 @@ class ModelStyleLayer extends StyleLayer {
111111

112112
const va = instances.instancedDataArray.float32;
113113
const translation: [number, number, number] = [va[offset + 4], va[offset + 5], va[offset + 6]];
114-
const pointX = va[offset];
115-
const pointY = va[offset + 1] | 0; // point.y stored in integer part
114+
const pointX = Math.floor(va[offset]); // point.x stored in integer part
115+
const pointY = Math.floor(va[offset + 1]); // point.y stored in integer part
116116

117117
tileToLngLat(id, position, pointX, pointY);
118118

3d-style/util/model_util.ts

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {Aabb} from '../../src/util/primitives';
2020
import {polygonIntersectsPolygon} from '../../src/util/intersection_tests';
2121
import Point from '@mapbox/point-geometry';
2222

23+
import type {vec2} from 'gl-matrix';
2324
import type Transform from '../../src/geo/transform';
2425

2526
export function rotationScaleYZFlipMatrix(out: mat4, rotation: vec3, scale: vec3) {
@@ -214,6 +215,79 @@ export function convertModelMatrixForGlobe(matrix: mat4, transform: Transform, s
214215
return modelMatrix;
215216
}
216217

218+
/**
219+
* Computes the convex hull from points using Jarvis's algorithm (Gift Wrapping).
220+
* The algorithm finds the leftmost point first and then iterates through
221+
* outermost points until the hull is closed.
222+
*
223+
* @param points Array of 2D points to compute convex hull for
224+
* @returns Array of Points forming the convex hull as a closed linear ring,
225+
* or empty array if input has fewer than 3 points
226+
* @private
227+
*/
228+
// Ported from gl-native: src/mbgl/util/bounding_volumes.cpp - convexPolygonFromPoints
229+
export function convexPolygonFromPoints(points: Array<vec2>): Array<Point> {
230+
assert(points.length >= 3, 'Polygon must have at least 3 points');
231+
232+
const ring: Array<Point> = [];
233+
234+
// Find the leftmost and bottommost point first
235+
let leftmostIdx = 0;
236+
for (let i = 1; i < points.length; i++) {
237+
if (points[i][0] < points[leftmostIdx][0] ||
238+
(points[i][0] === points[leftmostIdx][0] && points[i][1] < points[leftmostIdx][1])) {
239+
leftmostIdx = i;
240+
}
241+
}
242+
243+
let current = leftmostIdx;
244+
let next: number;
245+
const visitedMap = new Uint8Array(points.length);
246+
247+
do {
248+
// Return if the previous point is revisited, otherwise will end up with infinite loop
249+
if (visitedMap[current]) {
250+
break;
251+
}
252+
253+
ring.push(new Point(points[current][0], points[current][1]));
254+
visitedMap[current] = 1;
255+
256+
// Find the next most outmost point
257+
next = (current + 1) % points.length;
258+
for (let i = 0; i < points.length; i++) {
259+
if ((points[i][0] === points[next][0] && points[i][1] === points[next][1]) ||
260+
(points[i][0] === points[current][0] && points[i][1] === points[current][1])) {
261+
continue;
262+
}
263+
264+
// Sign of the result of the cross product tells the direction
265+
const a: vec2 = [points[i][0] - points[current][0], points[i][1] - points[current][1]];
266+
const b: vec2 = [points[next][0] - points[current][0], points[next][1] - points[current][1]];
267+
const det = a[0] * b[1] - a[1] * b[0];
268+
const dir = a[0] * b[0] + a[1] * b[1];
269+
270+
// Iterate over the points and select the rightmost point as 'next' relative to 'current'.
271+
// If vectors a and b are collinear and point in the same direction, update 'next' to index i if i is
272+
// further than next. If vectors a and b are collinear but point in opposite directions, index i is not
273+
// considered rightmost but leftmost and is ignored. If next and current are overlapped, update 'next'
274+
// to index i.
275+
if (det > 0.0 || (det === 0.0 && dir >= 0.0 && (a[0] * a[0] + a[1] * a[1]) > (b[0] * b[0] + b[1] * b[1]))) {
276+
next = i;
277+
}
278+
}
279+
280+
current = next;
281+
} while (current !== leftmostIdx);
282+
283+
// Close the ring by adding the first point
284+
if (ring.length > 0) {
285+
ring.push(ring[0]);
286+
}
287+
288+
return ring;
289+
}
290+
217291
// In case of intersection, returns depth of the closest corner. Otherwise, returns undefined.
218292
export function queryGeometryIntersectsProjectedAabb(
219293
queryGeometry: Point[],
@@ -225,38 +299,17 @@ export function queryGeometryIntersectsProjectedAabb(
225299
const corners = Aabb.projectAabbCorners(aabb, worldViewProjection);
226300
// convert to screen points
227301
let minDepth = Number.MAX_VALUE;
228-
let closestCornerIndex = -1;
229302
for (let c = 0; c < corners.length; ++c) {
230303
const corner = corners[c];
231304
corner[0] = (0.5 * corner[0] + 0.5) * transform.width;
232305
corner[1] = (0.5 - 0.5 * corner[1]) * transform.height;
233306
if (corner[2] < minDepth) {
234-
closestCornerIndex = c;
235307
minDepth = corner[2]; // This is a rough aabb intersection check for now and no need to interpolate over aabb sides.
236308
}
237309
}
238-
const p = (i: number): Point => new Point(corners[i][0], corners[i][1]);
239-
240-
let convexPolygon;
241-
switch (closestCornerIndex) {
242-
case 0:
243-
case 6:
244-
convexPolygon = [p(1), p(5), p(4), p(7), p(3), p(2), p(1)];
245-
break;
246-
case 1:
247-
case 7:
248-
convexPolygon = [p(0), p(4), p(5), p(6), p(2), p(3), p(0)];
249-
break;
250-
case 3:
251-
case 5:
252-
convexPolygon = [p(1), p(0), p(4), p(7), p(6), p(2), p(1)];
253-
break;
254-
default:
255-
convexPolygon = [p(1), p(5), p(6), p(7), p(3), p(0), p(1)];
256-
break;
257-
}
258310

259-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
311+
const convexPolygon = convexPolygonFromPoints(corners);
312+
260313
if (polygonIntersectsPolygon(queryGeometry, convexPolygon)) {
261314
return minDepth;
262315
}

test/integration/query-tests/model/sf-trees-puck-landmarks-meshopt-terrain/expected.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{
1919
"geometry": {
2020
"coordinates": [
21-
-122.39465564489365,
21+
-122.39465564489365,
2222
37.79525622982453
2323
],
2424
"type": "Point"
@@ -37,7 +37,24 @@
3737
{
3838
"geometry": {
3939
"coordinates": [
40-
-122.39468783140182,
40+
-122.39474147558212,
41+
37.79522231786474
42+
],
43+
"type": "Point"
44+
},
45+
"id": 5832589281457790,
46+
"properties": {
47+
"tree_type": "palm"
48+
},
49+
"source": "trees",
50+
"sourceLayer": "trees",
51+
"state": {},
52+
"type": "Feature"
53+
},
54+
{
55+
"geometry": {
56+
"coordinates": [
57+
-122.39468783140182,
4158
37.79514813539846
4259
],
4360
"type": "Point"

test/integration/query-tests/model/sf-trees-puck-landmarks-terrain/expected.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{
1919
"geometry": {
2020
"coordinates": [
21-
-122.39465564489365,
21+
-122.39465564489365,
2222
37.79525622982453
2323
],
2424
"type": "Point"
@@ -37,7 +37,24 @@
3737
{
3838
"geometry": {
3939
"coordinates": [
40-
-122.39468783140182,
40+
-122.39474147558212,
41+
37.79522231786474
42+
],
43+
"type": "Point"
44+
},
45+
"id": 5832589281457790,
46+
"properties": {
47+
"tree_type": "palm"
48+
},
49+
"source": "trees",
50+
"sourceLayer": "trees",
51+
"state": {},
52+
"type": "Feature"
53+
},
54+
{
55+
"geometry": {
56+
"coordinates": [
57+
-122.39468783140182,
4158
37.79514813539846
4259
],
4360
"type": "Point"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"geometry": {
4+
"coordinates": [
5+
-74.49999991804361,
6+
39.99999996437472
7+
],
8+
"type": "Point"
9+
},
10+
"properties": {
11+
"id": "object-one"
12+
},
13+
"source": "layer-source",
14+
"state": {},
15+
"type": "Feature"
16+
}
17+
]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"version": 8,
3+
"metadata": {
4+
"test": {
5+
"height": 512,
6+
"width": 512,
7+
"operations": [
8+
[
9+
"wait"
10+
],
11+
[
12+
"wait"
13+
],
14+
[
15+
"wait"
16+
]
17+
],
18+
"queryGeometry": [
19+
165,
20+
279
21+
]
22+
}
23+
},
24+
"sources": {
25+
"layer-source": {
26+
"type": "geojson",
27+
"data": {
28+
"type": "Feature",
29+
"properties": {
30+
"id": "object-one"
31+
},
32+
"geometry": {
33+
"coordinates": [
34+
-74.5,
35+
40
36+
],
37+
"type": "Point"
38+
}
39+
}
40+
}
41+
},
42+
"transition": {
43+
"duration": 0
44+
},
45+
"models": {
46+
"model-glb": "local://models/Box.glb"
47+
},
48+
"pitch": 50,
49+
"bearing": 45,
50+
"zoom": 22,
51+
"center": [
52+
-74.5,
53+
40
54+
],
55+
"layers": [
56+
{
57+
"id": "model-layer",
58+
"source": "layer-source",
59+
"type": "model",
60+
"layout": {
61+
"model-id": "model-glb"
62+
},
63+
"paint": {
64+
"model-scale": [
65+
2,
66+
2,
67+
2
68+
]
69+
}
70+
}
71+
]
72+
}

0 commit comments

Comments
 (0)