diff --git a/src/three/plugins/mvt/MVTGlyphMaterial.js b/src/three/plugins/mvt/MVTGlyphMaterial.js index 27b1d769c..ef078e8d7 100644 --- a/src/three/plugins/mvt/MVTGlyphMaterial.js +++ b/src/three/plugins/mvt/MVTGlyphMaterial.js @@ -1,6 +1,8 @@ -import { PointsMaterial, Vector2 } from 'three'; +import { PointsMaterial, Vector2, Vector4 } from 'three'; import { MVTGlyphAtlasTexture } from './MVTGlyphAtlasTexture.js'; +const _viewport = /* @__PURE__ */ new Vector4(); + /** * A `PointsMaterial` that draws each point sprite as a glyph from an `MVTGlyphAtlasTexture` with fading. * @private @@ -63,6 +65,7 @@ export class MVTGlyphMaterial extends PointsMaterial { this.transparent = true; this.depthTest = false; this.depthWrite = false; + this.resolution = new Vector2(); // owns the glyph atlas ( unless one is provided ); the cell size is kept in sync with it // and pushed to the uniforms after compile @@ -103,6 +106,7 @@ export class MVTGlyphMaterial extends PointsMaterial { uniform sampler2D glyphAtlas; uniform vec2 glyphCellSize; + uniform float opacity; varying vec2 vGlyphUV; varying float vAlpha; varying float vAngle; @@ -125,7 +129,7 @@ export class MVTGlyphMaterial extends PointsMaterial { } - diffuseColor.a *= vAlpha; + diffuseColor.a *= vAlpha * opacity; gl_FragColor = diffuseColor; #include @@ -142,10 +146,14 @@ export class MVTGlyphMaterial extends PointsMaterial { } - onBeforeRender() { + onBeforeRender( renderer ) { this._glyphAtlas.getSlotSize( this._glyphCellSize ); + // viewport size in pixels, refreshed each frame in onBeforeRender for screen-space raycasting + renderer.getViewport( _viewport ); + this.resolution.set( _viewport.z, _viewport.w ); + } } diff --git a/src/three/plugins/mvt/MVTGlyphs.d.ts b/src/three/plugins/mvt/MVTGlyphs.d.ts index de03bf245..f6a032f22 100644 --- a/src/three/plugins/mvt/MVTGlyphs.d.ts +++ b/src/three/plugins/mvt/MVTGlyphs.d.ts @@ -1,14 +1,21 @@ -import { Points, Raycaster, Vector2 } from 'three'; +import { Group, Raycaster } from 'three'; import { MVTGlyphMaterial } from './MVTGlyphMaterial.js'; import { MVTGlyphAtlasTexture } from './MVTGlyphAtlasTexture.js'; -export class MVTGlyphs extends Points { +export class MVTGlyphs extends Group { + + static readonly DrawMode: { + OBSCURED: string; + DRAW_THROUGH: string; + OVERLAY: string; + }; size: number; readonly glyphAtlas: MVTGlyphAtlasTexture; - resolution: Vector2; fadeInDuration: number; fadeOutDuration: number; + obscuredOpacity: number; + drawMode: string; constructor( material: MVTGlyphMaterial ); diff --git a/src/three/plugins/mvt/MVTGlyphs.js b/src/three/plugins/mvt/MVTGlyphs.js index bd76a1948..dd3b1d79a 100644 --- a/src/three/plugins/mvt/MVTGlyphs.js +++ b/src/three/plugins/mvt/MVTGlyphs.js @@ -1,24 +1,45 @@ -/** @import { MVTGlyphAtlasTexture } from './MVTGlyphAtlasTexture.js' */ -/** @import { MVTGlyphMaterial } from './MVTGlyphMaterial.js' */ -import { BufferAttribute, BufferGeometry, Matrix4, Points, Vector2, Vector3, Vector4 } from 'three'; +/** @import { MVTGlyphAtlasTexture } from './MVTGlyphAtlasTexture.js'; */ +import { BufferAttribute, BufferGeometry, GreaterDepth, Group, Matrix4, Points, Vector2, Vector3, Vector4 } from 'three'; +import { MVTGlyphMaterial } from './MVTGlyphMaterial.js'; const _mvMatrix = /* @__PURE__ */ new Matrix4(); const _uvTarget = {}; // scratch reused across raycasts -const _viewport = /* @__PURE__ */ new Vector4(); const _point4 = /* @__PURE__ */ new Vector4(); const _ssOrigin = /* @__PURE__ */ new Vector4(); const _ssRay = /* @__PURE__ */ new Vector2(); const _ssPoint = /* @__PURE__ */ new Vector2(); const _worldPoint = /* @__PURE__ */ new Vector3(); +const DRAW_MODE = /* @__PURE__ */ Object.freeze( { + // depth tested, hidden behind terrain + OBSCURED: 0, + + // render hidden portions as partially transparent + DRAW_THROUGH: 1, + + // draw full opaque on top of everything + OVERLAY: 2, +} ); + /** - * Base `Points` object that renders a batch of glyphs from a shared `MVTGlyphAtlasTexture`, fading - * each item in and out. - * @extends Points + * Base object that renders a batch of glyphs from a shared `MVTGlyphAtlasTexture`, fading each item + * in and out. Manages the geometry and two child draws sharing it: an opaque pass and a transparent + * "obscured" pass, combined per `drawMode`. + * @extends Group */ -export class MVTGlyphs extends Points { +export class MVTGlyphs extends Group { + + /** + * Draw modes for `drawMode`: `OBSCURED`, `DRAW_THROUGH`, and `OVERLAY`. + * @type {{ OBSCURED: string, DRAW_THROUGH: string, OVERLAY: string }} + */ + static get DrawMode() { + + return DRAW_MODE; + + } /** * Glyph size in pixels. @@ -26,39 +47,56 @@ export class MVTGlyphs extends Points { */ get size() { - return this.material.size; + return this._opaque.material.size; } set size( v ) { - this.material.size = v; + this._opaque.material.size = v; + this._obscured.material.size = v; } /** - * The glyph atlas this object samples from. + * The texture atlas used for rendering glyphs. * @type {MVTGlyphAtlasTexture} */ get glyphAtlas() { - return this.material.glyphAtlas; + return this._opaque.material.glyphAtlas; } /** - * @param {MVTGlyphMaterial} material - Material used to draw the glyphs. + * How glyphs interact with the depth buffer; one of `MVTGlyphs.DrawMode`. + * @type {string} */ + get drawMode() { + + return this._drawMode; + + } + + set drawMode( mode ) { + + this._drawMode = mode; + this._applyDrawMode(); + + } + + get geometry() { + + return this._opaque.geometry; + + } + constructor( material ) { - super( new BufferGeometry(), material ); + super(); - this.renderOrder = 1000; this.frustumCulled = false; - // viewport size in pixels, refreshed each frame in onBeforeRender for screen-space raycasting - this.resolution = new Vector2(); - /** * Seconds a glyph takes to fade in. * @type {number} @@ -71,23 +109,56 @@ export class MVTGlyphs extends Points { */ this.fadeOutDuration = 0.3; + /** + * Opacity of the ghosted, obscured glyphs in the `DRAW_THROUGH` draw mode. + * @type {number} + */ + this.obscuredOpacity = 0.5; + // Map keyed by stable id, // plus an insertion-ordered list of the same entries for stable geometry layout this._entryMap = new Map(); this._orderedEntries = []; this._lastUpdateTime = - 1; + // create children for draw through + const geometry = new BufferGeometry(); + const opaque = new Points( geometry, new MVTGlyphMaterial() ); + opaque.frustumCulled = false; + opaque.renderOrder = 1000; + opaque.onAfterRender = ( renderer, scene, camera ) => { + + this._recenter( camera ); + + }; + + const obscured = new Points( geometry, new MVTGlyphMaterial() ); + obscured.frustumCulled = false; + obscured.material.glyphAtlas = opaque.material.glyphAtlas; + obscured.material.depthTest = true; + obscured.material.depthFunc = GreaterDepth; + obscured.renderOrder = 1001; + + // add the children + this.add( obscured, opaque ); + + // store references, update draw mode + this._opaque = opaque; + this._obscured = obscured; + this.drawMode = DRAW_MODE.OVERLAY; + } /** - * Disposes the glyph atlas, geometry, and material. + * Disposes the glyph atlas, geometry, and materials. * @returns {void} */ dispose() { this.glyphAtlas.dispose(); this.geometry.dispose(); - this.material.dispose(); + this._opaque.material.dispose(); + this._obscured.material.dispose(); } @@ -173,21 +244,14 @@ export class MVTGlyphs extends Points { } - onBeforeRender( renderer, scene, camera ) { - - // use the active viewport (not getDrawingBufferSize) so raycasting matches the GPU's - // NDC→pixel mapping in sub-viewport scenarios - renderer.getViewport( _viewport ); - this.resolution.set( _viewport.z, _viewport.w ); - - } - raycast( raycaster, intersects ) { const camera = raycaster.camera; if ( ! camera ) return; - const { geometry, material, matrixWorld, resolution } = this; + const { geometry, matrixWorld } = this; + const { material } = this._opaque; + const { resolution } = material; const posAttr = geometry.getAttribute( 'position' ); if ( ! posAttr || posAttr.count === 0 ) return; @@ -206,7 +270,7 @@ export class MVTGlyphs extends Points { _mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld ); - for ( let i = 0, l = this.geometry.drawRange.count; i < l; i ++ ) { + for ( let i = 0, l = geometry.drawRange.count; i < l; i ++ ) { _point4.fromBufferAttribute( posAttr, i ); _point4.w = 1; @@ -246,11 +310,47 @@ export class MVTGlyphs extends Points { } + // end traversal + return false; + + } + + // configure the two child draws for the current draw mode + _applyDrawMode() { + + const { _opaque, _obscured, obscuredOpacity, _drawMode } = this; + switch ( _drawMode ) { + + case DRAW_MODE.OVERLAY: + _opaque.visible = true; + _opaque.material.depthTest = false; + + _obscured.visible = false; + break; + + case DRAW_MODE.DRAW_THROUGH: + _opaque.visible = true; + _opaque.material.depthTest = true; + + _obscured.visible = true; + _obscured.material.opacity = obscuredOpacity; + break; + + case DRAW_MODE.OBSCURED: + default: + _opaque.visible = true; + _opaque.material.depthTest = true; + + _obscured.visible = false; + break; + + } + } - onAfterRender( renderer, scene, camera ) { + // keep the root near the camera to avoid gpu jitter at globe scale + _recenter( camera ) { - // keep the root near the camera to avoid gpu jitter at globe scale const { parent } = this; if ( parent ) { diff --git a/src/three/plugins/mvt/MVTIconGlyphs.js b/src/three/plugins/mvt/MVTIconGlyphs.js index 7f2eb6456..b6fcd246d 100644 --- a/src/three/plugins/mvt/MVTIconGlyphs.js +++ b/src/three/plugins/mvt/MVTIconGlyphs.js @@ -1,4 +1,3 @@ -import { MVTGlyphMaterial } from './MVTGlyphMaterial.js'; import { MVTGlyphs } from './MVTGlyphs.js'; /** @@ -28,7 +27,7 @@ export class MVTIconGlyphs extends MVTGlyphs { slotCount = 64, } = options; - super( new MVTGlyphMaterial() ); + super(); /** * Returns the atlas key for a given item, or null for none. diff --git a/src/three/plugins/mvt/MVTLabelGlyphs.js b/src/three/plugins/mvt/MVTLabelGlyphs.js index 91302dade..2586c4405 100644 --- a/src/three/plugins/mvt/MVTLabelGlyphs.js +++ b/src/three/plugins/mvt/MVTLabelGlyphs.js @@ -1,4 +1,3 @@ -import { MVTGlyphMaterial } from './MVTGlyphMaterial.js'; import { MVTGlyphs } from './MVTGlyphs.js'; /** @@ -31,7 +30,7 @@ export class MVTLabelGlyphs extends MVTGlyphs { strokeWidth = 0, } = options; - super( new MVTGlyphMaterial( { size } ) ); + super(); // CSS font used to rasterize glyphs, sized to fit the atlas slot const fontSize = Math.round( glyphSize * 0.7 ); @@ -51,6 +50,7 @@ export class MVTLabelGlyphs extends MVTGlyphs { this._needed = new Set(); this.glyphAtlas.resize( slotCount, glyphSize ); + this.size = size; } @@ -61,10 +61,10 @@ export class MVTLabelGlyphs extends MVTGlyphs { */ measureChar( char ) { - const { _advanceCache, material, glyphAtlas, _font } = this; + const { _advanceCache, glyphAtlas, _font } = this; if ( ! _advanceCache.has( char ) ) { - const multiplier = material.size / glyphAtlas.slotSize; + const multiplier = this.size / glyphAtlas.slotSize; const info = glyphAtlas.measureChar( char, _font ); const advance = info.width + 2;