Skip to content

Commit 694c72e

Browse files
mournergithub-actions[bot]
authored andcommitted
Extract more model code out of ESM Core
GitOrigin-RevId: f007f6ade8383e6a12ff21d8d20c02a973e5752d
1 parent fb55448 commit 694c72e

21 files changed

Lines changed: 576 additions & 268 deletions

3d-style/data/bucket/tiled_3d_model_bucket.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export class Tiled3dModelFeature {
137137

138138
class Tiled3dModelBucket implements Bucket {
139139
requiresStandardRuntime = true;
140+
// Discriminator tag so core code (feature_index) can identify this bucket type without a
141+
// static value import of the class. Set as an own enumerable field, so it survives transfer.
142+
readonly isTiled3dModelBucket = true;
140143

141144
id: OverscaledTileID;
142145
uploaded: boolean;

3d-style/render/draw_model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {Texture3D} from '../../src/render/texture';
2020
import {pointInFootprint} from '../../3d-style/source/replacement_source';
2121
import Point from '@mapbox/point-geometry';
2222
import LngLat from '../../src/geo/lng_lat';
23-
import {tileToLngLat} from '../style/style_layer/model_style_layer';
23+
import {tileToLngLat} from '../style/style_layer/model_layer_query';
2424
import SegmentVector from '../../src/data/segment';
2525
import {PosArray, TriangleIndexArray} from '../../src/data/array_types';
2626
import posAttributes from '../../src/data/pos_attributes';

3d-style/render/model_manager.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {Event, ErrorEvent, Evented} from '../../src/util/evented';
2-
import Model from '../data/model';
3-
import convertModel from '../source/model_loader';
42
import {ResourceType} from '../../src/util/ajax';
5-
import {loadGLTF} from '../util/loaders';
63
import {isValidUrl} from '../../src/style-spec/validate/validate_model';
4+
import {Standard, prepareStandard} from '../../modules/standard_main';
75

6+
import type Model from '../data/model';
87
import type {RequestManager} from '../../src/util/mapbox';
98
import type Painter from '../../src/render/painter';
109
import type {ModelsSpecification} from '../../src/style-spec/types';
@@ -50,11 +49,9 @@ class ModelManager extends Evented {
5049
const request = await this.requestManager.transformRequest(url, ResourceType.Model);
5150
if (!this.modelByURL[url]) return null;
5251

53-
const gltf = await loadGLTF(request.url);
54-
const nodes = convertModel(gltf);
55-
const model = new Model(id, url, undefined, undefined, nodes);
56-
model.computeBoundsAndApplyParent();
57-
return model;
52+
await prepareStandard();
53+
if (!Standard.loadModel) return null;
54+
return await Standard.loadModel(request.url, id, url);
5855
} catch (e) {
5956
if ((e as {status?: number}).status === 404) return null;
6057
this.fire(new ErrorEvent(new Error(`Could not load model ${id} from ${url}`, {cause: e})));

3d-style/source/model_loader.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {TriangleIndexArray,
1111
Color3fLayoutArray,
1212
Color4fLayoutArray
1313
} from '../../src/data/array_types';
14-
import {GLTF_TO_ARRAY_TYPE, GLTF_COMPONENTS} from '../util/loaders';
14+
import {loadGLTF, GLTF_TO_ARRAY_TYPE, GLTF_COMPONENTS} from '../util/loaders';
1515
import {base64DecToArr} from '../../src/util/util';
1616
import TriangleGridIndex from '../../src/util/triangle_grid_index';
17-
import {HEIGHTMAP_DIM} from '../data/model';
17+
import Model, {HEIGHTMAP_DIM} from '../data/model';
1818
import {ModelBVH} from './model_bvh';
1919

2020
import type {vec2} from 'gl-matrix';
@@ -666,6 +666,15 @@ export default function convertModel(gltf: GLTF): Array<ModelNode> {
666666
return resultNodes;
667667
}
668668

669+
// Fetches a glTF, converts it, and builds a Model. Reached from core `ModelManager` through the
670+
// `Standard` facade so that the glTF/draco/meshopt loaders stay out of core.
671+
export async function loadModel(requestUrl: string, id: string, url: string): Promise<Model> {
672+
const gltf = await loadGLTF(requestUrl);
673+
const model = new Model(id, url, undefined, undefined, convertModel(gltf));
674+
model.computeBoundsAndApplyParent();
675+
return model;
676+
}
677+
669678
export function process3DTile(gltf: GLTF, zScale: number): Array<ModelNode> {
670679
// If the tile uses the mbx_bvh extension, all nodes will have a BVH picking mesh
671680
// so we can skip the expensive heightmap generation.
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import {mat4} from 'gl-matrix';
2+
import {calculateModelMatrix} from '../../data/model';
3+
import LngLat from '../../../src/geo/lng_lat';
4+
import {latFromMercatorY, lngFromMercatorX} from '../../../src/geo/mercator_coordinate';
5+
import EXTENT from '../../../src/style-spec/data/extent';
6+
import {convertModelMatrixForGlobe, queryGeometryIntersectsProjectedAabb} from '../../util/model_util';
7+
import Feature from '../../../src/util/vectortile_to_geojson';
8+
import ModelBucket from '../../data/bucket/model_bucket';
9+
10+
import type ModelSource from '../../source/model_source';
11+
import type Tiled3dModelBucket from '../../data/bucket/tiled_3d_model_bucket';
12+
import type ModelStyleLayer from './model_style_layer';
13+
import type Transform from '../../../src/geo/transform';
14+
import type SourceCache from '../../../src/source/source_cache';
15+
import type {QueryGeometry, TilespaceQueryGeometry} from '../../../src/style/query_geometry';
16+
import type {QueryResult} from '../../../src/source/query_features';
17+
import type {Feature as ExpressionEvalFeature, FeatureState} from '../../../src/style-spec/expression/index';
18+
import type {EvaluationFeature} from '../../../src/data/evaluation_feature';
19+
import type {ModelNode} from '../../data/model';
20+
import type {VectorTileFeature} from '@mapbox/vector-tile';
21+
import type {CanonicalTileID} from '../../../src/source/tile_id';
22+
23+
export function tileToLngLat(id: CanonicalTileID, position: LngLat, pointX: number, pointY: number) {
24+
const tileCount = 1 << id.z;
25+
position.lat = latFromMercatorY((pointY / EXTENT + id.y) / tileCount);
26+
position.lng = lngFromMercatorX((pointX / EXTENT + id.x) / tileCount);
27+
}
28+
29+
export function queryModelLayerRendered(
30+
layer: ModelStyleLayer,
31+
queryGeometry: QueryGeometry,
32+
sourceCache: SourceCache,
33+
transform: Transform
34+
): QueryResult {
35+
const source = sourceCache.getSource<ModelSource>();
36+
if (!source || source.type !== 'model') return {};
37+
const modelSource = source;
38+
39+
const result: QueryResult = {};
40+
result[layer.id] = [];
41+
const layerResult = result[layer.id];
42+
43+
let modelFeatureIndex = 0;
44+
for (const model of modelSource.models) {
45+
const modelFeatureState = sourceCache.getFeatureState(layer.sourceLayer, model.id);
46+
47+
const modelFeatureForEval: ExpressionEvalFeature = {
48+
type: 'Unknown',
49+
id: model.id,
50+
properties: model.featureProperties
51+
};
52+
const rotation = layer.paint.get('model-rotation').evaluate(modelFeatureForEval, modelFeatureState);
53+
const scale = layer.paint.get('model-scale').evaluate(modelFeatureForEval, modelFeatureState);
54+
const translation = layer.paint.get('model-translation').evaluate(modelFeatureForEval, modelFeatureState);
55+
const elevationReference = layer.paint.get('model-elevation-reference');
56+
const shouldFollowTerrainSlope = elevationReference === 'ground';
57+
const shouldApplyElevation = elevationReference === 'ground';
58+
59+
let matrix: mat4 = [];
60+
calculateModelMatrix(matrix,
61+
model,
62+
transform,
63+
model.position,
64+
rotation,
65+
scale,
66+
translation,
67+
shouldApplyElevation,
68+
shouldFollowTerrainSlope,
69+
false);
70+
71+
if (transform.projection.name === 'globe') {
72+
matrix = convertModelMatrixForGlobe(matrix, transform);
73+
}
74+
const worldViewProjection = mat4.multiply([], transform.projMatrix, matrix);
75+
76+
const projectedQueryGeometry = queryGeometry.isPointQuery() ? queryGeometry.screenBounds : queryGeometry.screenGeometry;
77+
78+
const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjection, model.aabb);
79+
if (depth != null) {
80+
const modelFeature: Feature = new Feature(undefined, 0, 0, 0, model.id);
81+
modelFeature.layer = layer.layer;
82+
// Use unsafe assignment for now, due to restriction of GeoJSON/Feature properties to number, string and boolean.
83+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
84+
modelFeature.properties = structuredClone(model.featureProperties) as any;
85+
modelFeature.properties['layer'] = layer.id;
86+
modelFeature.properties['uri'] = model.uri;
87+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
88+
modelFeature.properties['orientation'] = model.orientation as any;
89+
modelFeature.sourceLayer = layer.sourceLayer;
90+
modelFeature.geometry = {
91+
type: 'Point',
92+
coordinates: [model.position.lng, model.position.lat]
93+
};
94+
modelFeature.state = modelFeatureState;
95+
modelFeature.source = layer.source;
96+
layerResult.push({featureIndex: modelFeatureIndex, feature: modelFeature, intersectionZ: depth});
97+
}
98+
99+
++modelFeatureIndex;
100+
}
101+
102+
return result;
103+
}
104+
105+
export function queryModelLayerIntersectsFeature(
106+
layer: ModelStyleLayer,
107+
queryGeometry: TilespaceQueryGeometry,
108+
feature: VectorTileFeature,
109+
featureState: FeatureState,
110+
transform: Transform,
111+
scope: string | undefined
112+
): number | boolean {
113+
if (!layer.modelManager) return false;
114+
const modelManager = layer.modelManager;
115+
const bucket = queryGeometry.tile.getBucket(layer);
116+
if (!bucket || !(bucket instanceof ModelBucket)) return false;
117+
118+
for (const modelId in bucket.instancesPerModel) {
119+
const instances = bucket.instancesPerModel[modelId];
120+
const featureId = feature.id !== undefined ? feature.id :
121+
(feature.properties && Object.hasOwn(feature.properties, "id")) ? (feature.properties["id"] as string | number) : undefined;
122+
if (Object.hasOwn(instances.idToFeaturesIndex, featureId)) {
123+
const modelFeature = instances.features[instances.idToFeaturesIndex[featureId]];
124+
const model = modelManager.getModel(modelId, scope || layer.scope);
125+
if (!model) return false;
126+
127+
let matrix: mat4 = [];
128+
const position = new LngLat(0, 0);
129+
const id = bucket.canonical;
130+
let minDepth = Number.MAX_VALUE;
131+
for (let i = 0; i < modelFeature.instancedDataCount; ++i) {
132+
const instanceOffset = modelFeature.instancedDataOffset + i;
133+
const offset = instanceOffset * 16;
134+
135+
const va = instances.instancedDataArray.float32;
136+
const translation: [number, number, number] = [va[offset + 4], va[offset + 5], va[offset + 6]];
137+
const pointX = Math.floor(va[offset]); // point.x stored in integer part
138+
const pointY = Math.floor(va[offset + 1]); // point.y stored in integer part
139+
140+
tileToLngLat(id, position, pointX, pointY);
141+
142+
calculateModelMatrix(matrix,
143+
model,
144+
transform,
145+
position,
146+
modelFeature.rotation,
147+
modelFeature.scale,
148+
translation,
149+
false,
150+
false,
151+
false);
152+
if (transform.projection.name === 'globe') {
153+
matrix = convertModelMatrixForGlobe(matrix, transform);
154+
}
155+
const worldViewProjection = mat4.multiply([], transform.projMatrix, matrix);
156+
// Collision checks are performed in screen space. Corners are in ndc space.
157+
const screenQuery = queryGeometry.queryGeometry;
158+
const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry;
159+
const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjection, model.aabb);
160+
if (depth != null) {
161+
minDepth = Math.min(depth, minDepth);
162+
}
163+
}
164+
if (minDepth !== Number.MAX_VALUE) {
165+
return minDepth;
166+
}
167+
return false;
168+
}
169+
}
170+
return false;
171+
}
172+
173+
export function loadMatchingModelFeature(bucket: Tiled3dModelBucket, featureIndex: number, tilespaceGeometry: TilespaceQueryGeometry, transform: Transform): {feature: EvaluationFeature, intersectionZ: number, position: LngLat} | undefined {
174+
const nodeInfo = bucket.getNodesInfo()[featureIndex];
175+
176+
if (!nodeInfo || nodeInfo.hiddenByReplacement || !nodeInfo.node.meshes) return;
177+
178+
let intersectionZ = Number.MAX_VALUE;
179+
180+
// AABB check
181+
const node = nodeInfo.node;
182+
const tile = tilespaceGeometry.tile;
183+
const tileMatrix = transform.calculatePosMatrix(tile.tileID.toUnwrapped(), transform.worldSize);
184+
const modelMatrix = tileMatrix;
185+
const scale = nodeInfo.evaluatedScale;
186+
let elevation = 0;
187+
if (transform.elevation && node.elevation) {
188+
elevation = node.elevation * transform.elevation.exaggeration();
189+
}
190+
const anchorX = node.anchor ? node.anchor[0] : 0;
191+
const anchorY = node.anchor ? node.anchor[1] : 0;
192+
193+
mat4.translate(modelMatrix, modelMatrix, [anchorX * (scale[0] - 1), anchorY * (scale[1] - 1), elevation]);
194+
mat4.scale(modelMatrix, modelMatrix, scale);
195+
196+
// Collision checks are performed in screen space. Corners are in ndc space.
197+
const screenQuery = tilespaceGeometry.queryGeometry;
198+
const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry;
199+
200+
const checkNode = function (n: ModelNode) {
201+
const worldViewProjectionForNode = mat4.multiply([], modelMatrix, n.globalMatrix);
202+
mat4.multiply(worldViewProjectionForNode, transform.expandedFarZProjMatrix, worldViewProjectionForNode);
203+
for (let i = 0; i < n.meshes.length; ++i) {
204+
const mesh = n.meshes[i];
205+
if (i === n.lightMeshIndex) {
206+
continue;
207+
}
208+
const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjectionForNode, mesh.aabb);
209+
if (depth != null) {
210+
intersectionZ = Math.min(depth, intersectionZ);
211+
}
212+
}
213+
if (n.children) {
214+
for (const child of n.children) {
215+
checkNode(child);
216+
}
217+
}
218+
};
219+
220+
checkNode(node);
221+
if (intersectionZ === Number.MAX_VALUE) return;
222+
223+
const position = new LngLat(0, 0);
224+
tileToLngLat(tile.tileID.canonical, position, nodeInfo.node.anchor[0], nodeInfo.node.anchor[1]);
225+
226+
return {intersectionZ, position, feature: nodeInfo.feature};
227+
}

0 commit comments

Comments
 (0)