Skip to content

Commit c01c620

Browse files
authored
ImageOverlayPlugin: Use the original texture directly if the bounds match geometry exactly. (#1535)
* Allow for "init" to be called twice * Ensure consistent return value from "_init" * Make "init" async * Add memory efficient path to RegionImageSource, ImageOverlayPlugin * Cleanup
1 parent 8a3bc86 commit c01c620

2 files changed

Lines changed: 84 additions & 29 deletions

File tree

src/three/plugins/images/ImageOverlayPlugin.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -904,27 +904,21 @@ export class ImageOverlayPlugin {
904904

905905
const { tiles } = this;
906906

907-
if ( ! overlay.isInitialized ) {
907+
overlay.init().then( () => {
908908

909-
overlay.init();
909+
// Set resolution on the overlay
910+
overlay.setResolution( this.resolution );
910911

911-
overlay.whenReady().then( () => {
912+
const overlayFetch = overlay.fetch.bind( overlay );
913+
overlay.fetch = ( ...args ) => tiles
914+
.downloadQueue
915+
.add( { priority: - performance.now() }, () => {
912916

913-
// Set resolution on the overlay
914-
overlay.setResolution( this.resolution );
917+
return overlayFetch( ...args );
915918

916-
const overlayFetch = overlay.fetch.bind( overlay );
917-
overlay.fetch = ( ...args ) => tiles
918-
.downloadQueue
919-
.add( { priority: - performance.now() }, () => {
920-
921-
return overlayFetch( ...args );
922-
923-
} );
924-
925-
} );
919+
} );
926920

927-
}
921+
} );
928922

929923
const promises = [];
930924
const initTile = async ( scene, tile ) => {
@@ -1311,8 +1305,14 @@ class ImageOverlay {
13111305

13121306
init() {
13131307

1314-
this.isInitialized = true;
1315-
this._whenReady = this._init().then( () => this.isReady = true );
1308+
if ( ! this.isInitialized ) {
1309+
1310+
this.isInitialized = true;
1311+
this._whenReady = this._init().then( () => this.isReady = true );
1312+
1313+
}
1314+
1315+
return this._whenReady;
13161316

13171317
}
13181318

@@ -1323,7 +1323,11 @@ class ImageOverlay {
13231323
}
13241324

13251325
// overrideable
1326-
_init() {}
1326+
_init() {
1327+
1328+
return Promise.resolve();
1329+
1330+
}
13271331

13281332
fetch( url, options = {} ) {
13291333

src/three/plugins/images/sources/RegionImageSource.js

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { forEachTileInBounds } from '../overlays/utils.js';
33
import { DataCache } from '../utils/DataCache.js';
44
import { SRGBColorSpace, CanvasTexture } from 'three';
55

6+
// Epsilon for comparing normalized tile bounds to determine if a region exactly matches a single
7+
// image tile.
8+
const BOUNDS_EPSILON = 1e-10;
9+
610
export class RegionImageSource extends DataCache {
711

812
hasContent( ...tokens ) {
@@ -21,6 +25,8 @@ export class TiledRegionImageSource extends RegionImageSource {
2125
this.tiledImageSource = tiledImageSource;
2226
this.tileComposer = new TiledTextureComposer();
2327
this.resolution = 256;
28+
this.IS_DIRECT_TILE = Symbol( 'IS_DIRECT_TILE' );
29+
this.LOCK_TOKENS = Symbol( 'LOCK_TOKENS' );
2430

2531
}
2632

@@ -40,22 +46,63 @@ export class TiledRegionImageSource extends RegionImageSource {
4046

4147
async fetchItem( [ minX, minY, maxX, maxY, level ], signal ) {
4248

49+
const { tiledImageSource, tileComposer, IS_DIRECT_TILE, LOCK_TOKENS } = this;
4350
const range = [ minX, minY, maxX, maxY ];
44-
const imageSource = this.tiledImageSource;
45-
const tileComposer = this.tileComposer;
46-
const tiling = imageSource.tiling;
51+
const tiling = tiledImageSource.tiling;
52+
const tokens = [ ...range, level ];
53+
54+
// lock tiles for the requested level
55+
await this._markImages( range, level, false );
56+
57+
//
58+
59+
// Fast path: if the range maps to exactly one tile with matching bounds, use its
60+
// texture directly without compositing into an intermediate canvas to save memory.
61+
let singleTileBounds = null;
62+
let tileCount = 0;
63+
forEachTileInBounds( range, level, tiling, ( tx, ty, tl ) => {
64+
65+
tileCount ++;
66+
singleTileBounds = [ tx, ty, tl ];
67+
68+
} );
69+
70+
if ( tileCount === 1 ) {
71+
72+
const [ tx, ty, tl ] = singleTileBounds;
73+
const tileBounds = tiling.getTileBounds( tx, ty, tl, true, false );
74+
if (
75+
Math.abs( tileBounds[ 0 ] - minX ) <= BOUNDS_EPSILON &&
76+
Math.abs( tileBounds[ 1 ] - minY ) <= BOUNDS_EPSILON &&
77+
Math.abs( tileBounds[ 2 ] - maxX ) <= BOUNDS_EPSILON &&
78+
Math.abs( tileBounds[ 3 ] - maxY ) <= BOUNDS_EPSILON
79+
) {
80+
81+
// Clone rather than returning the texture directly so each region cache entry owns
82+
// a distinct object. Returning the shared texture would cause symbol properties
83+
// to be overwritten or deleted by concurrent cache entries during race conditions,
84+
// (create, delete, create) leading to errors on disposal.
85+
// Cloning shares the underlying Source so no extra GPU memory is used.
86+
const clone = tiledImageSource.get( tx, ty, tl ).clone();
87+
clone[ IS_DIRECT_TILE ] = true;
88+
clone[ LOCK_TOKENS ] = tokens;
89+
return clone;
90+
91+
}
92+
93+
}
94+
95+
//
4796

97+
// Compose path: tiles must be composed into a single texture
4898
const canvas = document.createElement( 'canvas' );
4999
canvas.width = this.resolution;
50100
canvas.height = this.resolution;
51101

52102
const target = new CanvasTexture( canvas );
53103
target.colorSpace = SRGBColorSpace;
54104
target.generateMipmaps = false;
55-
target.tokens = [ ...range, level ];
56-
57-
// Start locking tiles for the requested level
58-
await this._markImages( range, level, false );
105+
target[ LOCK_TOKENS ] = tokens;
59106

60107
// TODO: we could draw the parent tile data here if it's available just to make sure we
61108
// have something to display but the texture is not usable until it returns. Though it
@@ -69,7 +116,7 @@ export class TiledRegionImageSource extends RegionImageSource {
69116

70117
// draw using normalized bounds since the mercator bounds are non-linear
71118
const span = tiling.getTileBounds( tx, ty, tl, true, false );
72-
const tex = imageSource.get( tx, ty, tl );
119+
const tex = tiledImageSource.get( tx, ty, tl );
73120
tileComposer.draw( tex, span );
74121

75122
} );
@@ -80,10 +127,14 @@ export class TiledRegionImageSource extends RegionImageSource {
80127

81128
disposeItem( target ) {
82129

130+
const { IS_DIRECT_TILE, LOCK_TOKENS } = this;
131+
const [ minX, minY, maxX, maxY, level ] = target[ LOCK_TOKENS ];
132+
83133
target.dispose();
134+
delete target[ IS_DIRECT_TILE ];
135+
delete target[ LOCK_TOKENS ];
84136

85-
// Unlock the component tiles using the tokens stored on the target
86-
const [ minX, minY, maxX, maxY, level ] = target.tokens;
137+
// Unlock the component tiles using the stored tokens
87138
this._markImages( [ minX, minY, maxX, maxY ], level, true );
88139

89140
}

0 commit comments

Comments
 (0)