@@ -9,7 +9,7 @@ import {mat4, quat, vec3, vec4} from 'gl-matrix';
99import { getMetersPerPixelAtLatitude , mercatorZfromAltitude , tileToMeter } from '../../src/geo/mercator_coordinate' ;
1010import TextureSlots from './texture_slots' ;
1111import { convertModelMatrixForGlobe } from '../util/model_util' ;
12- import { clamp , warnOnce } from '../../src/util/util' ;
12+ import { clamp , warnOnce , esgtsaHash } from '../../src/util/util' ;
1313import assert from 'assert' ;
1414import { DEMSampler } from '../../src/terrain/elevation' ;
1515import { Aabb } from '../../src/util/primitives' ;
@@ -21,6 +21,11 @@ import {pointInFootprint} from '../../3d-style/source/replacement_source';
2121import Point from '@mapbox/point-geometry' ;
2222import LngLat from '../../src/geo/lng_lat' ;
2323import { 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
2530import type Program from '../../src/render/program' ;
2631import 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
6269type 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
351434function 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
0 commit comments