-
Notifications
You must be signed in to change notification settings - Fork 409
Expand file tree
/
Copy pathQuantizedMeshPlugin.js
More file actions
490 lines (347 loc) · 13.5 KB
/
Copy pathQuantizedMeshPlugin.js
File metadata and controls
490 lines (347 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
import { Vector3 } from 'three';
import { QuantizedMeshLoader } from './loaders/QuantizedMeshLoader.js';
import { TilingScheme } from './images/utils/TilingScheme.js';
import { ProjectionScheme } from './images/utils/ProjectionScheme.js';
import { QuantizedMeshClipper } from './utilities/QuantizedMeshClipper.js';
const TILE_X = Symbol( 'TILE_X' );
const TILE_Y = Symbol( 'TILE_Y' );
const TILE_LEVEL = Symbol( 'TILE_LEVEL' );
const TILE_AVAILABLE = Symbol( 'TILE_AVAILABLE' );
const TILE_SPLIT_SOURCE_SCENE = Symbol( 'TILE_SPLIT_SOURCE_SCENE' );
// We don't know the height ranges for the tileset on load so assume a large range and
// adjust it once the tiles have actually loaded based on the min and max height
const INITIAL_HEIGHT_RANGE = 1e4;
const _vec = /* @__PURE__ */ new Vector3();
// Checks if the given tile is available
function isTileAvailable( available, level, x, y ) {
if ( available && level < available.length ) {
// TODO: consider a binary search
const availableSet = available[ level ];
for ( let i = 0, l = availableSet.length; i < l; i ++ ) {
const { startX, startY, endX, endY } = availableSet[ i ];
if ( x >= startX && x <= endX && y >= startY && y <= endY ) {
return true;
}
}
}
return false;
}
// Calculates the max level that can be loaded.
function getMaxLevel( layer ) {
const { available = null, maxzoom = null } = layer;
return maxzoom === null ? available.length - 1 : maxzoom;
}
// Calculates whether metadata availability is present - returns -1 if not.
function getMetadataAvailability( layer ) {
const { metadataAvailability = - 1 } = layer;
return metadataAvailability;
}
// Calculates whether the given tile should have metadata availability
function getTileHasMetadata( tile, layer ) {
const level = tile[ TILE_LEVEL ];
const metadataAvailability = getMetadataAvailability( layer );
const maxLevel = getMaxLevel( layer );
return level < maxLevel && metadataAvailability !== - 1 && ( level % metadataAvailability ) === 0;
}
// Constructs the url for the given tile content
function getContentUrl( x, y, level, version, layer ) {
return layer.tiles[ 0 ]
.replace( /{\s*z\s*}/g, level )
.replace( /{\s*x\s*}/g, x )
.replace( /{\s*y\s*}/g, y )
.replace( /{\s*version\s*}/g, version );
}
/**
* Plugin that adds support for the Cesium quantized-mesh terrain format. Fetches the
* `layer.json` descriptor from the tileset root and dynamically generates 3D Tiles
* tile content from quantized-mesh buffers.
* @param {Object} [options]
* @param {boolean} [options.useRecommendedSettings=true] Apply recommended error and fetch settings for terrain.
* @param {number|null} [options.skirtLength=null] Override skirt length in metres; defaults to tile geometric error.
* @param {boolean} [options.smoothSkirtNormals=true] Blend skirt normals with tile surface for smoother edges.
* @param {boolean} [options.generateNormals=true] Compute vertex normals for the terrain mesh.
* @param {boolean} [options.solid=false] Generate a solid closed mesh (adds a bottom cap).
*/
export class QuantizedMeshPlugin {
constructor( options = {} ) {
const {
useRecommendedSettings = true,
skirtLength = null,
smoothSkirtNormals = true,
generateNormals = true,
solid = false,
} = options;
// plugin needs to run before other plugins that fetch data since content
// is handled and loaded in a custom way
this.name = 'QUANTIZED_MESH_PLUGIN';
this.priority = - 1000;
this.tiles = null;
this.layer = null;
this.useRecommendedSettings = useRecommendedSettings;
this.skirtLength = skirtLength;
this.smoothSkirtNormals = smoothSkirtNormals;
this.solid = solid;
this.generateNormals = generateNormals;
this.attribution = null;
this.tiling = new TilingScheme();
this.projection = new ProjectionScheme();
}
// Plugin function
init( tiles ) {
// TODO: should we avoid setting this globally?
tiles.fetchOptions.headers = tiles.fetchOptions.headers || {};
tiles.fetchOptions.headers.Accept = 'application/vnd.quantized-mesh,application/octet-stream;q=0.9';
if ( this.useRecommendedSettings ) {
tiles.errorTarget = 2;
}
this.tiles = tiles;
}
loadRootTileset() {
const { tiles } = this;
// initialize href to resolve the root in case it's specified as a relative url
let url = new URL( 'layer.json', new URL( tiles.rootURL, location.href ) );
tiles.invokeAllPlugins( plugin => url = plugin.preprocessURL ? plugin.preprocessURL( url, null ) : url );
return tiles
.invokeOnePlugin( plugin => plugin.fetchData && plugin.fetchData( url, this.tiles.fetchOptions ) )
.then( res => res.json() )
.then( json => {
this.layer = json;
const {
projection: layerProjection = 'EPSG:4326',
extensions = [],
attribution = '',
available = null,
} = json;
const {
tiling,
tiles,
projection,
} = this;
// attribution
if ( attribution ) {
this.attribution = {
value: attribution,
type: 'string',
collapsible: true,
};
}
// extensions
if ( extensions.length > 0 ) {
tiles.fetchOptions.headers[ 'Accept' ] += `;extensions=${ extensions.join( '-' ) }`;
}
// initialize tiling, projection
projection.setScheme( layerProjection );
const { tileCountX, tileCountY } = projection;
tiling.setProjection( projection );
tiling.generateLevels( getMaxLevel( json ) + 1, tileCountX, tileCountY );
// initialize children
const children = [];
for ( let x = 0; x < tileCountX; x ++ ) {
const child = this.createChild( 0, x, 0, available );
if ( child ) {
children.push( child );
}
}
// produce the tileset root
const tileset = {
asset: {
version: '1.1'
},
geometricError: Infinity,
root: {
refine: 'REPLACE',
geometricError: Infinity,
boundingVolume: {
region: [ ...this.tiling.getContentBounds(), - INITIAL_HEIGHT_RANGE, INITIAL_HEIGHT_RANGE ],
},
children: children,
[ TILE_AVAILABLE ]: available,
[ TILE_LEVEL ]: - 1,
},
};
let baseUrl = tiles.rootURL;
tiles.invokeAllPlugins( plugin => baseUrl = plugin.preprocessURL ? plugin.preprocessURL( baseUrl, null ) : baseUrl );
tiles.preprocessTileset( tileset, baseUrl );
return tileset;
} );
}
parseToMesh( buffer, tile, extension, url ) {
const {
skirtLength,
solid,
smoothSkirtNormals,
generateNormals,
tiles,
} = this;
// set up loader
const ellipsoid = tiles.ellipsoid;
// split the parent tile if needed
let result;
if ( extension === 'quantized_tile_split' ) {
// split the parent tile
const searchParams = new URL( url ).searchParams;
const left = searchParams.get( 'left' ) === 'true';
const bottom = searchParams.get( 'bottom' ) === 'true';
// parse the tile data
const clipper = new QuantizedMeshClipper();
clipper.ellipsoid.copy( ellipsoid );
clipper.solid = solid;
clipper.smoothSkirtNormals = smoothSkirtNormals;
clipper.skirtLength = skirtLength === null ? tile.geometricError : skirtLength;
const [ west, south, east, north ] = tile.parent.boundingVolume.region;
clipper.minLat = south;
clipper.maxLat = north;
clipper.minLon = west;
clipper.maxLon = east;
// There is a short async gap between the parent tile finishing parse and TilesRenderer
// assigning engineData.scene. Split children can be processed during that gap, so keep a
// temporary reference to the parsed parent scene for clipping.
const scene = tile.parent.engineData.scene || tile.parent[ TILE_SPLIT_SOURCE_SCENE ];
result = clipper.clipToQuadrant( scene, left, bottom );
} else if ( extension === 'terrain' ) {
const loader = new QuantizedMeshLoader( tiles.manager );
loader.ellipsoid.copy( ellipsoid );
loader.solid = solid;
loader.smoothSkirtNormals = smoothSkirtNormals;
loader.generateNormals = generateNormals;
loader.skirtLength = skirtLength === null ? tile.geometricError : skirtLength;
const [ west, south, east, north ] = tile.boundingVolume.region;
loader.minLat = south;
loader.maxLat = north;
loader.minLon = west;
loader.maxLon = east;
result = loader.parse( buffer );
} else {
return;
}
// adjust the bounding region to be more accurate based on the contents of the terrain file
// NOTE: The debug region bounds are only created after the tile is first shown so the debug
// region bounding volume will have the correct dimensions.
const { minHeight, maxHeight, metadata } = result.userData;
tile.boundingVolume.region[ 4 ] = minHeight;
tile.boundingVolume.region[ 5 ] = maxHeight;
tile.engineData.boundingVolume.setRegionData( ellipsoid, ...tile.boundingVolume.region );
// use the geometric error value if it's present
if ( metadata ) {
if ( 'geometricerror' in metadata ) {
tile.geometricError = metadata.geometricerror;
}
// if the tile hasn't been expanded yet and isn't in the queue to do so then
// mark it for expansion again
const hasMetadata = getTileHasMetadata( tile, this.layer );
if ( hasMetadata && 'available' in metadata && tile.children.length === 0 ) {
// add an offset to account for the current and previous layers
tile[ TILE_AVAILABLE ] = [
...new Array( tile[ TILE_LEVEL ] + 1 ).fill( null ),
...metadata.available,
];
}
}
// Store the parsed scene separately until TilesRenderer finishes registering the tile model.
// Because parseTile is async, split children may be processed before the parent scene has
// been assigned on engineData and still need access to the parsed parent scene for clipping.
tile[ TILE_SPLIT_SOURCE_SCENE ] = result;
this.expandChildren( tile );
return result;
}
getAttributions( target ) {
if ( this.attribution ) {
target.push( this.attribution );
}
}
// Local functions
createChild( level, x, y, available ) {
const { tiles, layer, tiling, projection } = this;
const ellipsoid = tiles.ellipsoid;
// metadata availability will return "null" if there are no more children but we
// have to always load the root tile data.
const isAvailable = available === null && level === 0 || isTileAvailable( available, level, x, y );
const url = getContentUrl( x, y, level, 1, layer );
const region = [ ...tiling.getTileBounds( x, y, level ), - INITIAL_HEIGHT_RANGE, INITIAL_HEIGHT_RANGE ];
const [ /* west */, south, /* east */, north, /* minHeight */, maxHeight ] = region;
const midLat = ( south > 0 ) !== ( north > 0 ) ? 0 : Math.min( Math.abs( south ), Math.abs( north ) );
// get the projected perimeter
ellipsoid.getCartographicToPosition( midLat, 0, maxHeight, _vec );
_vec.z = 0;
// https://github.com/CesiumGS/cesium/blob/53889cbed2a91d38e0fae4b6f2dcf6783632fc92/packages/engine/Source/Scene/QuadtreeTileProvider.js#L24-L31
// Implicit quantized mesh tile error halves with every layer
const tileCountX = projection.tileCountX;
const maxRadius = Math.max( ...ellipsoid.radius );
const rootGeometricError = maxRadius * 2 * Math.PI * 0.25 / ( 65 * tileCountX );
const geometricError = rootGeometricError / ( 2 ** level );
// Create the child
const tile = {
[ TILE_AVAILABLE ]: null,
[ TILE_LEVEL ]: level,
[ TILE_X ]: x,
[ TILE_Y ]: y,
refine: 'REPLACE',
geometricError: geometricError,
boundingVolume: { region },
content: isAvailable ? { uri: url } : null,
children: []
};
// if we're relying on tile metadata availability then skip storing the tile metadata
if ( ! getTileHasMetadata( tile, layer ) ) {
tile[ TILE_AVAILABLE ] = available;
}
return tile;
}
expandChildren( tile ) {
const level = tile[ TILE_LEVEL ];
const x = tile[ TILE_X ];
const y = tile[ TILE_Y ];
const available = tile[ TILE_AVAILABLE ];
// only expand down to the highest level
if ( level >= this.tiling.maxLevel ) {
return;
}
let hasChildren = false;
for ( let cx = 0; cx < 2; cx ++ ) {
for ( let cy = 0; cy < 2; cy ++ ) {
const child = this.createChild( level + 1, 2 * x + cx, 2 * y + cy, available );
if ( child.content !== null ) {
tile.children.push( child );
hasChildren = true;
} else {
// mark the child as "virtual" since it relies on the parent geometry
child.content = { uri: `tile.quantized_tile_split?bottom=${ cy === 0 }&left=${ cx === 0 }` };
child.internal = { isVirtual: true };
tile.internal.virtualChildCount ++;
tile.children.push( child );
}
}
}
if ( ! hasChildren ) {
tile.children.length -= tile.internal.virtualChildCount;
tile.internal.virtualChildCount = 0;
}
}
fetchData( url, options ) {
// if this is our custom url indicating a tile split then return fake response
if ( /quantized_tile_split/.test( url ) ) {
return new ArrayBuffer();
}
}
disposeTile( tile ) {
const { tiles, layer } = this;
delete tile[ TILE_SPLIT_SOURCE_SCENE ];
// dispose of the available array since we will get it again if this tile is loaded
if ( getTileHasMetadata( tile, layer ) ) {
tile[ TILE_AVAILABLE ] = null;
}
// Remove virtual children when the parent is disposed since they depend on the parent's
// loaded scene for clipping and cannot be rendered or re-generated without it. They will
// be re-created once the parent is loaded again.
if ( TILE_AVAILABLE in tile ) {
const { virtualChildCount } = tile.internal;
const len = tile.children.length;
const start = len - virtualChildCount;
for ( let i = start; i < len; i ++ ) {
tiles.processNodeQueue.remove( tile.children[ i ] );
}
tile.children.length = 0;
tile.internal.virtualChildCount = 0;
}
}
}