Skip to content

Commit 3ca4028

Browse files
aleksprogergithub-actions[bot]
authored andcommitted
MAPS3D-2082: Debug 3D Model Footprints in GL JS (internal-9681)
GitOrigin-RevId: a919932c8eea91b685cb1c9de54e8c026edfb5ba
1 parent 2b13636 commit 3ca4028

5 files changed

Lines changed: 164 additions & 14 deletions

File tree

3d-style/data/model.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ export type ModelNode = {
133133
anchor: vec2;
134134
hidden: boolean;
135135
isGeometryBloom: boolean;
136+
footprintDebugMesh?: {
137+
vertexBuffer: VertexBuffer;
138+
indexBuffer: IndexBuffer;
139+
segments: SegmentVector;
140+
color: Color;
141+
};
136142
};
137143

138144
export const ModelTraits = {
@@ -496,6 +502,11 @@ export function destroyBuffers(node: ModelNode) {
496502
}
497503
}
498504
}
505+
if (node.footprintDebugMesh) {
506+
node.footprintDebugMesh.vertexBuffer.destroy();
507+
node.footprintDebugMesh.indexBuffer.destroy();
508+
node.footprintDebugMesh.segments.destroy();
509+
}
499510
if (node.children) {
500511
for (const child of node.children) {
501512
destroyBuffers(child);

3d-style/render/draw_model.ts

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {mat4, quat, vec3, vec4} from 'gl-matrix';
99
import {getMetersPerPixelAtLatitude, mercatorZfromAltitude, tileToMeter} from '../../src/geo/mercator_coordinate';
1010
import TextureSlots from './texture_slots';
1111
import {convertModelMatrixForGlobe} from '../util/model_util';
12-
import {clamp, warnOnce} from '../../src/util/util';
12+
import {clamp, warnOnce, esgtsaHash} from '../../src/util/util';
1313
import assert from 'assert';
1414
import {DEMSampler} from '../../src/terrain/elevation';
1515
import {Aabb} from '../../src/util/primitives';
@@ -21,6 +21,11 @@ import {pointInFootprint} from '../../3d-style/source/replacement_source';
2121
import Point from '@mapbox/point-geometry';
2222
import LngLat from '../../src/geo/lng_lat';
2323
import {tileToLngLat} from '../style/style_layer/model_style_layer';
24+
import SegmentVector from '../../src/data/segment';
25+
import {PosArray, TriangleIndexArray} from '../../src/data/array_types';
26+
import posAttributes from '../../src/data/pos_attributes';
27+
import {debugUniformValues} from '../../src/render/program/debug_program';
28+
import Color from '../../src/style-spec/util/color';
2429

2530
import type Program from '../../src/render/program';
2631
import type Transform from '../../src/geo/transform';
@@ -57,6 +62,8 @@ type SortedMesh = {
5762
modelOpacity: number;
5863
materialOverride?: MaterialOverride;
5964
modelColor?: [number, number, number, number];
65+
node: ModelNode;
66+
modelMatrix: mat4;
6067
};
6168

6269
type SortedNode = {
@@ -313,15 +320,15 @@ function prepareMeshes(painter: Painter, node: ModelNode, modelMatrix: mat4, pro
313320
if (materialOverride && materialOverride.opacity <= 0) continue;
314321

315322
if (mesh.material.alphaMode !== 'BLEND') {
316-
const opaqueMesh: SortedMesh = {mesh, depth: 0.0, modelIndex, worldViewProjection, nodeModelMatrix, isLightMesh, materialOverride, modelOpacity, modelColor: modelColorMix};
323+
const opaqueMesh: SortedMesh = {mesh, depth: 0.0, modelIndex, worldViewProjection, nodeModelMatrix, isLightMesh, materialOverride, modelOpacity, modelColor: modelColorMix, node, modelMatrix};
317324
opaqueMeshes.push(opaqueMesh);
318325
continue;
319326
}
320327

321328
const centroidPos = vec3.transformMat4([], mesh.centroid, worldViewProjection);
322329
// Filter meshes behind the camera if in perspective mode
323330
if (!transform.isOrthographic && centroidPos[2] <= 0.0) continue;
324-
const transparentMesh: SortedMesh = {mesh, depth: centroidPos[2], modelIndex, worldViewProjection, nodeModelMatrix, isLightMesh, materialOverride, modelOpacity, modelColor: modelColorMix};
331+
const transparentMesh: SortedMesh = {mesh, depth: centroidPos[2], modelIndex, worldViewProjection, nodeModelMatrix, isLightMesh, materialOverride, modelOpacity, modelColor: modelColorMix, node, modelMatrix};
325332
transparentMeshes.push(transparentMesh);
326333
}
327334
}
@@ -347,6 +354,82 @@ function drawShadowCaster(mesh: Mesh, matrix: mat4, painter: Painter, layer: Mod
347354
undefined, undefined);
348355
}
349356

357+
function getOrCreateFootprintMesh(painter: Painter, node: ModelNode) {
358+
if (node.footprintDebugMesh) return node.footprintDebugMesh;
359+
if (!node.footprint) return null;
360+
361+
const context = painter.context;
362+
const vertices = node.footprint.vertices;
363+
const indices = node.footprint.indices;
364+
365+
const vertexArray = new PosArray();
366+
vertexArray.reserve(vertices.length);
367+
for (const v of vertices) {
368+
vertexArray.emplaceBack(v.x, v.y);
369+
}
370+
371+
const indexArray = new TriangleIndexArray();
372+
indexArray.reserve(indices.length);
373+
for (let i = 0; i < indices.length; i += 3) {
374+
indexArray.emplaceBack(indices[i], indices[i + 1], indices[i + 2]);
375+
}
376+
377+
const vertexBuffer = context.createVertexBuffer(vertexArray, posAttributes.members);
378+
const indexBuffer = context.createIndexBuffer(indexArray);
379+
const segments = SegmentVector.simpleSegment(0, 0, vertices.length, indices.length);
380+
381+
// Generate a deterministic color based on the node ID or name
382+
const idStr = node.id || node.name || 'footprint';
383+
let seed: number;
384+
385+
const numericId = parseInt(idStr, 10);
386+
if (!isNaN(numericId)) {
387+
seed = numericId;
388+
} else {
389+
seed = stringHash(idStr);
390+
}
391+
392+
const r = esgtsaHash(seed);
393+
const g = esgtsaHash(seed + 1);
394+
const b = esgtsaHash(seed + 2);
395+
396+
node.footprintDebugMesh = {
397+
vertexBuffer,
398+
indexBuffer,
399+
segments,
400+
color: new Color(r, g, b, 0.5)
401+
};
402+
403+
return node.footprintDebugMesh;
404+
}
405+
406+
function stringHash(str: string): number {
407+
let hash = 0;
408+
for (let i = 0; i < str.length; i++) {
409+
hash = (((hash << 5) - hash) + str.charCodeAt(i)) | 0;
410+
}
411+
return hash;
412+
}
413+
414+
function drawFootprint(painter: Painter, layer: ModelStyleLayer, node: ModelNode, mvpMatrix: mat4) {
415+
const mesh = getOrCreateFootprintMesh(painter, node);
416+
if (!mesh) return;
417+
418+
const context = painter.context;
419+
const gl = context.gl;
420+
421+
const program = painter.getOrCreateProgram('debug');
422+
423+
const color = mesh.color;
424+
const depthMode = DepthMode.disabled;
425+
// We bind the empty (transparent) texture to ensure no overlay is drawn.
426+
context.activeTexture.set(gl.TEXTURE0);
427+
painter.emptyTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
428+
program.draw(painter, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.alphaBlended, CullFaceMode.disabled,
429+
debugUniformValues(mvpMatrix, color.toPremultipliedRenderColor(null)), '$debug',
430+
mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
431+
}
432+
350433
// Evaluate feature state for node names
351434
function evaluateFeatureStateForNodeOverrides(layer: ModelStyleLayer, featureId: string | number, featureState: FeatureState, featureProperties: Record<string, unknown>, nodeNamesToEvaluate: string[], nodeOverrides: ModelNodeOverrides) {
352435
for (const nodeId of nodeNamesToEvaluate) {
@@ -523,6 +606,35 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
523606
return;
524607
}
525608

609+
if (painter._debugParams.show3DModelFootprints) {
610+
const proj = painter.transform.projMatrix;
611+
const footprints = new Map<string, {node: ModelNode, mvp: mat4}>();
612+
613+
const addFootprint = (node: ModelNode, matrix: mat4) => {
614+
if (node.footprint) {
615+
const id = node.id || node.name || 'footprint';
616+
if (!footprints.has(id)) {
617+
const mvp = mat4.multiply([] as unknown as mat4, proj, matrix);
618+
footprints.set(id, {node, mvp});
619+
}
620+
}
621+
};
622+
623+
for (const opaqueMesh of opaqueMeshes) {
624+
addFootprint(opaqueMesh.node, opaqueMesh.modelMatrix);
625+
}
626+
for (const transparentMesh of transparentMeshes) {
627+
addFootprint(transparentMesh.node, transparentMesh.modelMatrix);
628+
}
629+
630+
const sortedIds = Array.from(footprints.keys()).sort();
631+
632+
for (const id of sortedIds) {
633+
const {node, mvp} = footprints.get(id);
634+
drawFootprint(painter, layer, node, mvp);
635+
}
636+
}
637+
526638
drawSortedMeshes(painter, layer, transparentMeshes, opaqueMeshes, modelParametersVector);
527639

528640
cleanup();
@@ -981,6 +1093,9 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt
9811093

9821094
const stats = layer.getLayerRenderingStats();
9831095
const drawTiles = function () {
1096+
// Keyed by ID to deduplicate footprints across tiles
1097+
const footprints = new Map<string, {node: ModelNode, mvp: mat4}>();
1098+
9841099
let start, end, step;
9851100
// When front cutoff is enabled the tiles are iterated in back to front order
9861101
if (frontCutoffEnabled) {
@@ -1136,6 +1251,13 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt
11361251
const nodeInfo = sortedNode.nodeInfo;
11371252
const node = nodeInfo.node;
11381253

1254+
if (painter._debugParams.show3DModelFootprints && node.footprint) {
1255+
const id = node.id || node.name || 'footprint';
1256+
if (!footprints.has(id)) {
1257+
footprints.set(id, {node, mvp: sortedNode.wvpForTile});
1258+
}
1259+
}
1260+
11391261
let lightingMatrix = mat4.multiply([], zScaleMatrix, sortedNode.tileModelMatrix);
11401262
mat4.multiply(lightingMatrix, negCameraPosMatrix, lightingMatrix);
11411263
const normalMatrix = mat4.invert([], lightingMatrix);
@@ -1274,6 +1396,14 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt
12741396
}
12751397
}
12761398
}
1399+
1400+
if (painter._debugParams.show3DModelFootprints && footprints.size > 0) {
1401+
const sortedIds = Array.from(footprints.keys()).sort();
1402+
for (const id of sortedIds) {
1403+
const {node, mvp} = footprints.get(id);
1404+
drawFootprint(painter, layer, node, mvp);
1405+
}
1406+
}
12771407
};
12781408

12791409
// Evaluate bucket and prepare for rendering

src/render/draw_raster_particle.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate';
2727
import rasterFade from './raster_fade';
2828
import assert from 'assert';
2929
import {RGBAImage} from '../util/image';
30-
import {smoothstep} from '../util/util';
30+
import {smoothstep, esgtsaHash} from '../util/util';
3131
import {GLOBE_ZOOM_THRESHOLD_MAX} from '../geo/projection/globe_constants';
3232

3333
import type Transform from '../geo/transform';
@@ -58,20 +58,12 @@ function drawRasterParticle(painter: Painter, sourceCache: SourceCache, layer: R
5858
function createPositionRGBAData(textureDimension: number): Uint8Array {
5959
const numParticles = textureDimension * textureDimension;
6060
const RGBAPositions = new Uint8Array(4 * numParticles);
61-
// Hash function from https://www.shadertoy.com/view/XlGcRh
62-
const esgtsa = function (s: number): number {
63-
s |= 0;
64-
s = Math.imul(s ^ 2747636419, 2654435769);
65-
s = Math.imul(s ^ (s >>> 16), 2654435769);
66-
s = Math.imul(s ^ (s >>> 16), 2654435769);
67-
return (s >>> 0) / 4294967296;
68-
};
6961
// Pack random positions in [0, 1] into RGBA pixels. Matches the GLSL
7062
// `pack_pos_to_rgba` behavior.
7163
const invScale = 1.0 / RASTER_PARTICLE_POS_SCALE;
7264
for (let i = 0; i < numParticles; i++) {
73-
const x = invScale * (esgtsa(2 * i + 0) + RASTER_PARTICLE_POS_OFFSET);
74-
const y = invScale * (esgtsa(2 * i + 1) + RASTER_PARTICLE_POS_OFFSET);
65+
const x = invScale * (esgtsaHash(2 * i + 0) + RASTER_PARTICLE_POS_OFFSET);
66+
const y = invScale * (esgtsaHash(2 * i + 1) + RASTER_PARTICLE_POS_OFFSET);
7567

7668
const rx = x;
7769
const ry = (x * 255.0) % 1;

src/render/painter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ class Painter {
250250
continousRedraw: boolean;
251251
// eslint-disable-next-line @typescript-eslint/no-explicit-any
252252
enabledLayers: any;
253+
show3DModelFootprints: boolean;
253254
};
254255

255256
_timeStamp: number;
@@ -294,6 +295,7 @@ class Painter {
294295
showTerrainProxyTiles: false,
295296
fpsWindow: 30,
296297
continousRedraw: false,
298+
show3DModelFootprints: false,
297299
enabledLayers: {
298300
}
299301
};
@@ -308,6 +310,9 @@ class Painter {
308310
DevTools.addParameter(this._debugParams, 'showTerrainProxyTiles', 'Terrain', {}, () => {
309311
this.style.map.triggerRepaint();
310312
});
313+
DevTools.addParameter(this._debugParams, 'show3DModelFootprints', 'Debug', {}, () => {
314+
this.style.map.triggerRepaint();
315+
});
311316
DevTools.addParameter(this._debugParams, 'forceEnablePrecipitation', 'Precipitation');
312317
DevTools.addParameter(this._debugParams, 'fpsWindow', 'FPS', {min: 1, max: 100, step: 1});
313318
DevTools.addBinding(this._debugParams, 'continousRedraw', 'FPS', {

src/util/util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,4 +898,16 @@ export function easeIn(x: number) {
898898
return x * x * x * x * x;
899899
}
900900

901+
/**
902+
* Hash function from https://www.shadertoy.com/view/XlGcRh
903+
* Matches gl-native's esgtsaHash in mbgl/util/random.hpp
904+
*/
905+
export function esgtsaHash(s: number): number {
906+
s = s >>> 0; // Ensure unsigned 32-bit integer
907+
s = Math.imul(s ^ 2747636419, 2654435769) >>> 0;
908+
s = Math.imul(s ^ (s >>> 16), 2654435769) >>> 0;
909+
s = Math.imul(s ^ (s >>> 16), 2654435769) >>> 0;
910+
return s / 4294967296;
911+
}
912+
901913
export {deepEqual};

0 commit comments

Comments
 (0)