3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
284 lines (197 loc) • 7.48 kB
JavaScript
import { Vector3, Matrix4, MathUtils } from 'three';
// iterates over all present tiles in the given tileset at the given level in the given range
export function forEachTileInBounds( range, level, tiling, callback ) {
// pull the bounds in a bit to avoid loading unnecessary tiles. 1e-8 was chosen since smaller values
// are not larger enough and cause extra tiles to load in cases where 1-to-1 tile-to-image should occur
let [ minLon, minLat, maxLon, maxLat ] = range;
minLat += 1e-8;
minLon += 1e-8;
maxLat -= 1e-8;
maxLon -= 1e-8;
const clampedLevel = Math.max( Math.min( level, tiling.maxLevel ), tiling.minLevel );
const [ minX, minY, maxX, maxY ] = tiling.getTilesInRange( minLon, minLat, maxLon, maxLat, clampedLevel, true );
for ( let x = minX; x <= maxX; x ++ ) {
for ( let y = minY; y <= maxY; y ++ ) {
callback( x, y, clampedLevel );
}
}
}
// functions for generating UVs for cartographic-projected UVs
function getGeometryCartographicChannel( geometry, geomToEllipsoidMatrix, ellipsoid ) {
const _vec = new Vector3();
const _cart = {};
const uv = [];
const posAttr = geometry.getAttribute( 'position' );
geometry.computeBoundingBox();
geometry.boundingBox.getCenter( _vec ).applyMatrix4( geomToEllipsoidMatrix );
// find a rough mid lat / lon point
ellipsoid.getPositionToCartographic( _vec, _cart );
// fall back to 0 because if the geometry is exactly centered at 0, 0, 0 then
// the calculated lat / lon will be NaN.
const centerLat = _cart.lat || 0;
const centerLon = _cart.lon || 0;
let minLat = Infinity;
let minLon = Infinity;
let minHeight = Infinity;
let maxLat = - Infinity;
let maxLon = - Infinity;
let maxHeight = - Infinity;
for ( let i = 0; i < posAttr.count; i ++ ) {
// get the lat / lon values per vertex
_vec.fromBufferAttribute( posAttr, i ).applyMatrix4( geomToEllipsoidMatrix );
ellipsoid.getPositionToCartographic( _vec, _cart );
// The latitude calculations are not so stable at the poles so force the lat value to
// the mid point to ensure we don't load an unnecessarily large of tiles
// NOTE: this can distort the texture a bit at the poles
if ( Math.abs( Math.abs( _cart.lat ) - Math.PI / 2 ) < 1e-5 ) {
_cart.lon = centerLon;
}
// ensure we're not wrapping on the same geometry
if ( Math.abs( centerLon - _cart.lon ) > Math.PI ) {
_cart.lon += Math.sign( centerLon - _cart.lon ) * Math.PI * 2;
}
if ( Math.abs( centerLat - _cart.lat ) > Math.PI ) {
_cart.lat += Math.sign( centerLat - _cart.lat ) * Math.PI * 2;
}
uv.push( _cart.lon, _cart.lat, _cart.height );
minLat = Math.min( minLat, _cart.lat );
maxLat = Math.max( maxLat, _cart.lat );
minLon = Math.min( minLon, _cart.lon );
maxLon = Math.max( maxLon, _cart.lon );
minHeight = Math.min( minHeight, _cart.height );
maxHeight = Math.max( maxHeight, _cart.height );
}
const range = [ minLon, minLat, maxLon, maxLat ];
const region = [ ...range, minHeight, maxHeight ];
return {
uv,
range,
region,
};
}
export function getMeshesCartographicRange( meshes, ellipsoid, meshToEllipsoidMatrix = null, projection = null, normalizedRange = null ) {
// find the lat / lon ranges
let minLat = Infinity;
let minLon = Infinity;
let minHeight = Infinity;
let maxLat = - Infinity;
let maxLon = - Infinity;
let maxHeight = - Infinity;
const uvs = [];
const _matrix = new Matrix4();
meshes.forEach( mesh => {
// multiply in the ellipsoid matrix if necessary
_matrix.copy( mesh.matrixWorld );
if ( meshToEllipsoidMatrix ) {
_matrix.premultiply( meshToEllipsoidMatrix );
}
const { uv, region } = getGeometryCartographicChannel( mesh.geometry, _matrix, ellipsoid );
uvs.push( uv );
// save the min and max values
minLat = Math.min( minLat, region[ 1 ] );
maxLat = Math.max( maxLat, region[ 3 ] );
minLon = Math.min( minLon, region[ 0 ] );
maxLon = Math.max( maxLon, region[ 2 ] );
minHeight = Math.min( minHeight, region[ 4 ] );
maxHeight = Math.max( maxHeight, region[ 5 ] );
} );
if ( projection !== null ) {
// Clamp the mesh vertex range to the projection's valid bounds (e.g. ~±85° for Mercator) to
// avoid NaN UV values for vertices that fall outside the projection range. This also stretches
// the texture at the projection boundary rather than leaving gaps.
// takes generate a normalized range if not already provided
if ( normalizedRange === null ) {
normalizedRange = projection.clampToBounds( [ minLon, minLat, maxLon, maxLat ] );
normalizedRange = projection.toNormalizedRange( normalizedRange );
}
const [ minU, minV, maxU, maxV ] = normalizedRange;
uvs.forEach( uv => {
for ( let i = 0, l = uv.length; i < l; i += 3 ) {
const lon = uv[ i + 0 ];
const lat = uv[ i + 1 ];
const h = uv[ i + 2 ];
let [ u, v ] = projection.toNormalizedPoint( lon, lat );
u = MathUtils.clamp( u, 0, 1 );
v = MathUtils.clamp( v, 0, 1 );
uv[ i + 0 ] = MathUtils.mapLinear( u, minU, maxU, 0, 1 );
uv[ i + 1 ] = MathUtils.mapLinear( v, minV, maxV, 0, 1 );
uv[ i + 2 ] = MathUtils.mapLinear( h, minHeight, maxHeight, 0, 1 );
}
} );
}
return {
uvs,
range: normalizedRange,
region: [ minLon, minLat, maxLon, maxLat, minHeight, maxHeight ],
};
}
// functions for generating UVs for planar-projected UVs
function getGeometryPlanarChannel( geometry, meshToFrame ) {
const _vec = new Vector3();
const uv = [];
const posAttr = geometry.getAttribute( 'position' );
let minU = Infinity;
let minV = Infinity;
let minW = Infinity;
let maxU = - Infinity;
let maxV = - Infinity;
let maxW = - Infinity;
for ( let i = 0; i < posAttr.count; i ++ ) {
_vec.fromBufferAttribute( posAttr, i ).applyMatrix4( meshToFrame );
uv.push( _vec.x, _vec.y, _vec.z );
minU = Math.min( minU, _vec.x );
maxU = Math.max( maxU, _vec.x );
minV = Math.min( minV, _vec.y );
maxV = Math.max( maxV, _vec.y );
minW = Math.min( minW, _vec.z );
maxW = Math.max( maxW, _vec.z );
}
// TODO: output a more complete bounds definition relative to the frame
const range = [ minU, minV, maxU, maxV ];
return {
uv,
range,
heightRange: [ minW, maxW ],
};
}
export function getMeshesPlanarRange( meshes, worldToFrame ) {
// find the U / V ranges
let minU = Infinity;
let minV = Infinity;
let minW = Infinity;
let maxU = - Infinity;
let maxV = - Infinity;
let maxW = - Infinity;
const uvs = [];
const _matrix = new Matrix4();
meshes.forEach( mesh => {
// multiply in the ellipsoid matrix if necessary
_matrix.copy( mesh.matrixWorld );
if ( worldToFrame ) {
_matrix.premultiply( worldToFrame );
}
const { uv, range, heightRange } = getGeometryPlanarChannel( mesh.geometry, _matrix );
uvs.push( uv );
// save the min and max values
minU = Math.min( minU, range[ 0 ] );
maxU = Math.max( maxU, range[ 2 ] );
minV = Math.min( minV, range[ 1 ] );
maxV = Math.max( maxV, range[ 3 ] );
minW = Math.min( minW, heightRange[ 0 ] );
maxW = Math.max( maxW, heightRange[ 1 ] );
} );
uvs.forEach( uv => {
for ( let i = 0, l = uv.length; i < l; i += 3 ) {
const u = uv[ i + 0 ];
const v = uv[ i + 1 ];
uv[ i + 0 ] = MathUtils.mapLinear( u, minU, maxU, 0, 1 );
uv[ i + 1 ] = MathUtils.mapLinear( v, minV, maxV, 0, 1 );
}
} );
// TODO: output a more complete bounds definition relative to the frame
return {
uvs,
range: [ minU, minV, maxU, maxV ],
heightRange: [ minW, maxW ],
};
}