/**
* @file TerrainViewer.js
* @author Emma Gould and Cody Smith
* @date 2022
*
* @description An adaptive level of detail terrain viewer. It uses a given Digital Elevation Model in slippy tiles to render a terrain view with minimal wasted resources.
*
*/
import * as THREE from 'three'
import { Evented } from '../ModelingCore/Evented.js'
import { debounced } from '../utils/debounced.js'
import { latLonFromAltAzDistance } from '../camera/latLong.js'
import { XYZTileNode } from './XYZTileNode.js'
import { GlobeReference } from './utils/GlobeReference.js'
import { CalibratedCamera } from './objects/CalibratedCamera.js'
import { getQuaternion } from './utils/utils.js'
import { latLonToSlippyXYZ } from './utils/utils.js'
import { a, ecef_lla, lla_ecef } from './utils/ECEF.js'
import { rotateQuaternionToECEF } from './utils/geoTools.js'
import {
unpackPixel,
renderToUint8Array,
DepthShaderMaterial,
TilePickingMaterial,
TileIndexColorMaterial,
ElevationShaderMaterial,
RidgeLineShaderMaterial,
TilesNeedUpdateMaterial,
DepthColorShaderMaterial,
RadialDistortionMaterial,
} from './utils/materialUtils.js'
/**
* @typedef {Object} TerrainViewerOptions Describe the camera.
* @property {number} s - Shear
* @property {number} x0 - Principal point
* @property {number} y0 - Principal point
* @property {number} fov - Field of View in degrees
* @property {number} width - width of the viewport in pixels
* @property {number} height - height of the viewport in pixels
* @property {number} Latitude - Latitude of the camera
* @property {number} Longitude - Longitude of the camera
* @property {number} Elevation - Elevation of the camera
* @property {number} roll - Roll of the camera
* @property {number} azimuth - Azimuth of the camera
* @property {number} altitude - Altitude of the camera
* @property {number} fov - Field of view in degrees
*/
/**
* @class TerrainiViewer
* Terrain viewer with adaptive level of detail.
* <br/>
* <a href="../test/test2.html">DEMO</a>
*
*
*
* @fires @event doneSplitting When the viewer is done splitting tiles. This usually means that the level of detail is good.
* @fires @event donePruning When the viewer is done pruning tiles.
*
* @param {TerrainViewerOptions} options
*/
export class TerrainViewer extends Evented {
constructor(options = {}) {
super()
this.far = 100
this.near = 0.001
this.zoom = 0
this.scale = 3000
this.martiniError = 0.1
this.debouncedSplitFire = debounced((argument) => {
this.fire('doneSplitting', argument)
}, 500)
this.debouncedPruneFire = debounced((argument) => {
this.fire('donePruning', argument)
}, 500)
// set up scenes
this.scene = new THREE.Scene()
this.rdScene = new THREE.Scene()
// set up bounding box group
//this.bboxGroup = new THREE.Group()
//this.scene.add(this.bboxGroup)
// set up renderer
this.renderer = new THREE.WebGLRenderer({ alpha: true })
this.renderer.setSize(this.width, this.height)
this.renderer.domElement.onWheel = function (e) {
e.preventDefault()
}
// set up radial distortion render target
this.rdTarget = new THREE.WebGLRenderTarget(this.width, this.height)
// set up depth render target
this.depthTarget = new THREE.WebGLRenderTarget(this.width, this.height)
// set up camera, globeReference, tileTree, etc.
let rotationOptions = {}
if (
options.roll !== undefined ||
options.azimuth !== undefined ||
options.altitude !== undefined
) {
rotationOptions.roll = options.roll !== undefined ? options.roll : 0
rotationOptions.azimuth =
options.azimuth !== undefined ? options.azimuth : 0
rotationOptions.altitude =
options.altitude !== undefined ? options.altitude : 0
} else {
rotationOptions.alpha =
options.alpha !== undefined ? options.alpha : 270
rotationOptions.beta = options.beta !== undefined ? options.beta : 0
rotationOptions.gamma =
options.gamma !== undefined ? options.gamma : 90
}
this.update({
s: options.s || 0,
r0: options.r0 || 0,
r1: options.r1 || 0,
r2: options.r2 || 0,
r3: options.r3 || 0,
x0: options.x0 || 0,
y0: options.y0 || 0,
fov: options.fov || 60,
color: options.color || 0xffaa00,
width: options.width || 640,
height: options.height || 480,
Latitude: options.Latitude || 35.210147,
Longitude: options.Longitude || -106.449822,
Elevation: options.Elevation || 3252,
...rotationOptions,
})
}
/**
*
* @returns {Canvas} Return the DOM element of the renderer
*/
getView() {
return this.renderer.domElement
}
/**
* Update the viewer's parameters. Basically updates everything...eventually.
*
* @param {TerrainViewerOptions} options
*/
update(options = {}) {
this.needsUpdate(options)
//this.updateZoom()
this.updateGlobeReference()
this.updateTileTree()
this.updateTileMeshes()
this.updateMaterials()
this.updateCamera()
}
/**
* @private
* @param {TerrainViewerOptions} options
*/
needsUpdate(options = {}) {
this.setRadialDistortion(options.r0, options.r1, options.r2, options.r3)
this.setFOV(options.fov)
this.setColor(options.color)
this.setShear(options.s)
this.setPosition(options.Latitude, options.Longitude, options.Elevation)
this.setDimensions(options.width, options.height)
this.setPrincipalPoint(options.x0, options.y0)
this.setDeviceOrientation(options.alpha, options.beta, options.gamma)
this.setAltitudeAzimuthRoll(
options.altitude,
options.azimuth,
options.roll
)
}
/**
*
* @param {Number} fov Field of View in degrees
*/
setFOV(fov) {
if (fov && fov > 0) {
this.originalFOV = fov
}
this.fov =
(180 / Math.PI) *
2 *
Math.atan(
this.textureScale *
Math.tan((this.originalFOV * (Math.PI / 180)) / 2)
)
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
/**
*
* @param {String} color A color used by THREE.Color
*/
setColor(color) {
if (color) {
this.color = color
this.materialsNeedUpdate = true
}
}
/**
*
* @param {Number} s Shear value.
*/
setShear(s) {
if (s !== undefined) {
this.s = s
this.cameraNeedsUpdate = true
}
}
/**
*
* @param {Number} Latitude Degrees
* @param {Number} Longitude Degrees
* @param {Number} Elevation Elevatioin above the elpsoid in meters
*/
setPosition(Latitude, Longitude, Elevation) {
if (Latitude !== undefined && Latitude <= 90 && Latitude >= -90) {
this.Latitude = Latitude
this.cameraNeedsUpdate = true
this.positionNeedsUpdate = true
}
if (Longitude !== undefined && Longitude >= -180 && Longitude <= 180) {
this.Longitude = Longitude
this.cameraNeedsUpdate = true
this.positionNeedsUpdate = true
}
if (Elevation !== undefined && Elevation >= 0) {
// TODO TODO TODO check that elevation value is not below terrain
this.Elevation = Elevation
//this.zoomNeedsUpdate = true
this.cameraNeedsUpdate = true
this.positionNeedsUpdate = true
}
}
/**
* Set the dimensions of the view port
*
* @param {Number} width Width in pixels
* @param {Number} height Height in pixels
*/
setDimensions(width, height) {
if (width !== undefined && width > 0) {
this.width = width
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (height !== undefined && height > 0) {
this.height = height
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (this.cameraNeedsUpdate) {
this.resizeRenderer()
}
}
/**
* @private
*/
resizeRenderer() {
// resize renderer
//this.renderer.setSize(this.width * 1.5, this.height * 1.5)
// resize render targets
this.rdTarget.setSize(
this.width * this.textureScale,
this.height * this.textureScale
)
this.depthTarget.setSize(
this.width * this.textureScale,
this.height * this.textureScale
)
}
/**
*
* @param {Number} x0
* @param {Number} y0
*/
setPrincipalPoint(x0, y0) {
if (x0 !== undefined && x0 <= this.width && x0 >= -this.width) {
this.x0_0 = x0
}
if (y0 !== undefined && y0 <= this.width && y0 >= -this.width) {
this.y0_0 = y0
}
this.x0 = this.x0_0 / this.textureScale
this.y0 = this.y0_0 / this.textureScale
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
/**
* Radial distortion coefficients.
* let radius = distance from uv to principal point
* uvᵣ = (1 + r0*radius + r1*radius² + r2*radius³ + r3*radius⁴) * uv
*
* @param {Number} r0
* @param {Number} r1
* @param {Number} r2
* @param {Number} r3
*/
setRadialDistortion(r0, r1, r2, r3) {
if (r0 !== undefined) {
this.r0 = r0
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (r1 !== undefined) {
this.r1 = r1
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (r2 !== undefined) {
this.r2 = r2
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (r3 !== undefined) {
this.r3 = r3
this.cameraNeedsUpdate = true
this.materialsNeedUpdate = true
}
if (this.materialsNeedUpdate) {
this.updateTextureScale()
}
}
/**
* Eularian rotations.In devices coordinate system.
* https://www.w3.org/TR/orientation-event/
*
* @param {Number} alpha
* @param {Number} beta
* @param {Number} gamma
*/
setDeviceOrientation(alpha, beta, gamma) {
if (alpha !== undefined) {
this.alpha = alpha
this.cameraNeedsUpdate = true
this.orientationNeedsUpdate = true
}
if (beta !== undefined) {
this.beta = beta
this.cameraNeedsUpdate = true
this.orientationNeedsUpdate = true
}
if (gamma !== undefined) {
this.gamma = gamma
this.cameraNeedsUpdate = true
this.orientationNeedsUpdate = true
}
}
/**
* Rotation in the world reference frame.
*
* @param {Degrees} altitude
* @param {Degrees} azimuth
* @param {Degrees} roll
*/
setAltitudeAzimuthRoll(altitude, azimuth, roll) {
if (altitude !== undefined && altitude >= -90 && altitude <= 90) {
this.altitude = altitude
this.cameraNeedsUpdate = true
this.navOrientationNeedsUpdate = true
}
if (azimuth !== undefined && azimuth >= 0 && azimuth <= 360) {
this.azimuth = azimuth
this.cameraNeedsUpdate = true
this.navOrientationNeedsUpdate = true
}
if (roll !== undefined && roll >= -90 && roll <= 90) {
this.roll = roll
this.cameraNeedsUpdate = true
this.navOrientationNeedsUpdate = true
}
}
/**
* Called internally
* @private
*/
updateZoom() {
/**
* We compute the area of interest by simplifying the Earth
* to a sphere (circle in two dimensions) and finding the
* tangent lines from the camera's elevation above the earth
* to the circle approximating mean sea level
*
* (longer ellipse radius = a = 6378137 meters)
* r - the radius of the circle
* O - the center/origin of the circle
* P₀ - the camera's position
* P₁ and P₂ - the intersection with the circle
*
* Because we are approximating a cross section of the Earth
* as a circle rather than an ellipse, we can assume that P₁
* and P₂ will be the same distance regardless of what point
* we choose. Therefore, we can simplify further by setting
* P₀ along the x-axis, making the y coordinate 0
*
* This gives us:
* P₀ = ( x₀, y₀ ) = ( x₀, 0 ) = ( elevation_in_ecef_coords, 0 )
* P₁ = ( x₁, y₁ ) = ( r² / x₀², (r / x₀) * √(x₀² - r₀²) )
* P₂ is P₁ reflected across the x axis, so:
* P₂ = ( x₂, y₂ ) = ( x₁, -y₁ )
*/
if (this.zoomNeedsUpdate) {
let [x, y, z] = lla_ecef(0, 0, this.Elevation)
let P1 = new THREE.Vector2(
a ** 2 / x ** 2,
(a / x) * Math.sqrt(x ** 2 - a ** 2)
)
let P2 = new THREE.Vector2(P1.x, -P1.y)
// we can now compute the straight-line distance between P1 and P2 and
// use it to calculate the zoom level of the initial tiles to bring in
let distance = P1.distanceTo(P2)
this.zoom = Math.floor(Math.log2((2 * Math.PI * a) / distance))
// make sure tile zoom level brings in area around center
if (!this.zoomLevelCoversAllTiles(distance / 2)) {
if (this.zoom > 0) {
this.zoom -= 1
if (!this.zoomLevelCoversAllTiles(distance / 2)) {
if (this.zoom > 0) {
this.zoom -= 1
}
}
}
}
}
if (this.globeReference && this.globeReference.zoom !== this.zoom + 2) {
this.tileTreeNeedsUpdate = true
this.globeReferenceNeedsUpdate = true
}
this.zoomNeedsUpdate = false
}
zoomLevelCoversAllTiles(distance) {
let tileX = []
let tileY = []
for (let az = 0; az < 2 * Math.PI; az += Math.PI / 2) {
let [lat, lon, elev] = latLonFromAltAzDistance(
[this.Latitude, this.Longitude, this.Elevation],
0,
az,
distance
)
let [x, y, z] = latLonToSlippyXYZ(lat, lon, this.zoom)
tileX.push(x)
tileY.push(y)
}
if (
tileX.every((v) => {
v === tileX[0]
}) &&
tileY.every((v) => {
v === tileY[0]
})
) {
return true
}
return false
}
updateCamera() {
if (!this.camera) {
this.camera = new CalibratedCamera({
near: this.near,
far: this.far,
})
this.camera.up.set(0, 0, 1)
}
if (this.cameraNeedsUpdate) {
this.camera.s = this.s
let f =
this.width / (2 * Math.tan((this.fov / 2) * (Math.PI / 180)))
this.camera.fx = f
this.camera.fy = f
this.camera.x0 = this.x0
this.camera.y0 = this.y0
this.camera.width = this.width
this.camera.height = this.height
if (this.orientationNeedsUpdate) {
// camera orientation variables
// georeference with respect to globeReference matrix
// position of the camera should be at origin (0, 0, 0)
let { x, y, z, w } = getQuaternion(
-this.alpha,
this.beta + 90,
this.gamma
)
this.camera.setRotationFromQuaternion(
new THREE.Quaternion(x, y, z, w)
)
// rotate camera with respect to its position on the Earth
rotateQuaternionToECEF(
this.camera.quaternion,
this.globeReference.object3D,
this.Latitude,
this.Longitude
)
} else if (this.navOrientationNeedsUpdate) {
// orientation in altitude, azimuth, and roll
let lookVec = new THREE.Vector3(0, 1, 0)
.applyAxisAngle(
new THREE.Vector3(1, 0, 0),
this.altitude * (Math.PI / 180)
)
.applyAxisAngle(
new THREE.Vector3(0, 0, 1),
(90 - this.azimuth) * (Math.PI / 180)
)
this.camera.lookAt(
new THREE.Vector3().addVectors(
this.camera.position,
lookVec
)
)
this.camera.updateMatrix()
this.camera.updateProjectionMatrix()
this.camera.matrix.multiply(
new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
this.roll * (Math.PI / 180)
)
)
this.camera.rotation.setFromRotationMatrix(this.camera.matrix)
// rotate camera with respect to its position on the Earth
rotateQuaternionToECEF(
this.camera.quaternion,
this.globeReference.object3D,
this.Latitude,
this.Longitude
)
}
if (this.positionNeedsUpdate) {
let pos = new THREE.Vector3(
...lla_ecef(this.Latitude, this.Longitude, this.Elevation)
).applyMatrix4(this.globeReference.getMatrix())
this.camera.position.set(pos.x, pos.y, pos.z)
}
this.camera.updateMatrix()
this.camera.updateProjectionMatrix()
this._tilesNeedUpdate = true
}
this.cameraNeedsUpdate = false
this.positionNeedsUpdate = false
this.orientationNeedsUpdate = false
this.navOrientationNeedsUpdate = false
}
updateTileTree() {
if (!this.tileTree) {
this.tileTree = new XYZTileNode(
...latLonToSlippyXYZ(this.Latitude, this.Longitude, this.zoom),
null
)
this.tileTree
.getThreeGroup(
this.martiniError,
this.globeReference.getMatrix(),
this.elevationMaterial
)
.then((threeGroup) => {
this.elevationMaterial.setMin(
threeGroup.children[0].geometry.attributes.elevation.min
)
this.elevationMaterial.setMax(
threeGroup.children[0].geometry.attributes.elevation.max
)
this.scene.add(threeGroup)
//this.bboxGroup.add(this.tileTree.bbox)
this._tilesNeedUpdate = true
})
} else if (this.tileTreeNeedsUpdate) {
// tile tree root needs to get parent if new zoom is smaller than root zoom
if (this.zoom < this.tileTree.getRoot().z) {
this.tileTree.getRoot().createParent()
this.tileTree = this.tileTree.getRoot()
for (let c of this.tileTree.getChildren()) {
c.getThreeGroup(
this.martiniError,
this.globeReference.getMatrix(),
this.elevationMaterial
).then(() => {
if (c.isLeaf() && !c.threeGroup.parent) {
this.scene.add(c.threeGroup)
//this.bboxGroup.add(c.bbox)
}
})
}
}
}
this.tileTreeNeedsUpdate = false
}
updateMaterials() {
if (this.materialsNeedUpdate) {
if (!this.depthMaterial) {
this.depthMaterial = new DepthShaderMaterial()
}
if (!this.tileIndexMaterial) {
this.tileIndexMaterial = new TilePickingMaterial()
}
if (!this.elevationMaterial) {
this.elevationMaterial = new ElevationShaderMaterial()
}
if (!this.ridgeLineMaterial) {
this.ridgeLineMaterial = new RidgeLineShaderMaterial({
width: this.width * this.textureScale,
height: this.height * this.textureScale,
depthTexture: this.depthTarget.texture,
})
this.overrideMaterial = this.ridgeLineMaterial
}
if (!this.depthColorMaterial) {
this.depthColorMaterial = new DepthColorShaderMaterial({
depthTexture: this.depthTarget.texture,
})
}
if (!this.tileIndexColorMaterial) {
this.tileIndexColorMaterial = new TileIndexColorMaterial()
}
if (!this.radialDistortionMaterial) {
// set up radial distortion 'lens'
this.radialDistortionMaterial = new RadialDistortionMaterial({
r0: this.r0,
r1: this.r1,
r2: this.r2,
r3: this.r3,
texture: this.rdTarget.texture,
textureScale: this.textureScale,
})
let geometry = new THREE.BufferGeometry()
geometry.setIndex(
new THREE.BufferAttribute(
new Uint8Array([0, 1, 2, 2, 3, 0]),
1
)
)
geometry.setAttribute(
'position',
new THREE.BufferAttribute(
new Float32Array([
-1, 1, 1, 1, 1, 1, 1, -1, 1, -1, -1, 1,
]),
3
)
)
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(
new Float32Array([0, 1, 1, 1, 1, 0, 0, 0]),
2
)
)
this.rdLens = new THREE.Mesh(
geometry,
this.radialDistortionMaterial
)
this.rdLens.frustumCulled = false
this.rdLens.renderOrder = 1000
this.rdLens.material.depthTest = false
this.rdLens.material.depthWrite = false
this.rdScene.add(this.rdLens)
}
if (!this.tilesNeedUpdateMaterial) {
this.tilesNeedUpdateMaterial = new TilesNeedUpdateMaterial({
fov: this.fov * (Math.PI / 180),
scale: 1 / this.globeReference.getScale(),
height: this.height * this.textureScale,
depthTexture: this.depthTarget.texture,
})
}
this.elevationMaterial.setColor(this.color)
this.ridgeLineMaterial.setWidth(this.width * this.textureScale)
this.ridgeLineMaterial.setHeight(this.height * this.textureScale)
this.tilesNeedUpdateMaterial.setFov(this.fov * (Math.PI / 180))
this.tilesNeedUpdateMaterial.setScale(
1 / this.globeReference.getScale()
)
this.tilesNeedUpdateMaterial.setHeight(
this.height * this.textureScale
)
this.radialDistortionMaterial.setTextureScale(this.textureScale)
this.radialDistortionMaterial.setPrincipalPoint(
new THREE.Vector2(this.x0 / this.width, this.y0 / this.height)
)
this.radialDistortionMaterial.setRadialDistortionCoefficients(
this.r0,
this.r1,
this.r2,
this.r3
)
this._tilesNeedUpdate = true
}
this.materialsNeedUpdate = false
}
updateTileMeshes() {
if (this.meshesNeedUpdate) {
for (let t of this.tileTree.getLeafNodes()) {
t.getThreeGroup(
this.martiniError,
this.globeReference.getMatrix(),
this.elevationMaterial
)
}
}
this.meshesNeedUpdate = false
}
updateTextureScale() {
if (this.r0 > 0 || this.r1 > 0 || this.r2 > 0 || this.r3 > 0) {
this.textureScale = 1.5
} else {
this.textureScale = 1
}
}
updateGlobeReference() {
if (!this.globeReference) {
this.globeReference = new GlobeReference({
zoom: this.zoom + 2,
scale: this.scale,
Latitude: this.Latitude,
Longitude: this.Longitude,
Elevation: this.Elevation,
})
} else if (this.globeReferenceNeedsUpdate) {
this.globeReference.setPosition(
this.Latitude,
this.Longitude,
this.Elevation,
this.zoom + 2,
this.scale
)
this.meshesNeedUpdate = true
}
this.globeReferenceNeedsUpdate = false
}
updateTiles(indexData, zoomData) {
if (!this._busySplitting && !this._busyPruning) {
const tileData = this.readTileData(indexData, zoomData)
const isSplitDone = Object.entries(tileData.tooLow).every(
([key, value]) => {
return !value.tile.canSplit()
}
)
if (Object.keys(tileData.tooLow).length === 0 || isSplitDone) {
this.debouncedSplitFire(tileData.tooLow)
}
this.splitTiles(tileData)
this.pruneTiles(tileData)
}
}
readTileData(indexData, zoomData) {
let tiles = {}
for (let j = this.height - 1; j >= 0; j--) {
for (let i = this.width - 1; i >= 0; i--) {
let index = unpackPixel(
i / this.width,
j / this.height,
indexData,
this.width,
this.height
)
let zoom = unpackPixel(
i / this.width,
j / this.height,
zoomData,
this.width,
this.height
)
if (zoom !== 256 * 255 && index !== 255 * 256) {
if (tiles[index] !== undefined) {
if (tiles[index] < zoom) {
tiles[index] = zoom
}
} else {
tiles[index] = zoom
}
}
}
}
const tileResults = { ids: [], tooHigh: {}, tooLow: {}, justRight: {} }
for (let i in tiles) {
let tile = this.tileTree.getNodeByID(i)
if (!tile) {
// console.log('tile not found', i)
} else if (!tile.isLeaf()) {
console.log('not a leaf', tile.toString())
} else if (tile.z > tiles[i]) {
tileResults.ids.push(Number(i))
tileResults.tooHigh[i] = { tile, zoom: tiles[i] }
} else if (tile.z < tiles[i]) {
tileResults.ids.push(Number(i))
tileResults.tooLow[i] = { tile, zoom: tiles[i] }
} else {
tileResults.ids.push(Number(i))
tileResults.justRight[i] = { tile }
}
}
return tileResults
}
/**
* @private
* @param {Object} tileData - pased from updateTiles
*/
splitTiles(tileData) {
if (!this._busySplitting) {
this._busySplitting = true
const promises = []
// split
const tooLow = tileData.tooLow
for (let t in tooLow) {
let result = tooLow[t]
if (!result.tile.isBusy() && result.tile.canSplit()) {
promises.push(
this.splitNode(result.tile).then(() => {
this._tilesNeedUpdate = true
})
)
}
}
Promise.all(promises).then(() => {
this._busySplitting = false
this.fire('split', this.tileTree.getAllNodesBelow().length)
})
}
}
/**
* @private
* @param {Object} tileData - passed from updateTiles
* @param {number} leafNodeThreshold - number of leaf leaf nodes to keep before pruning. This helps with the thrashing; which is when a node is added then removed immediatly over and over.
*/
pruneTiles(tileData) {
if (!this._busyPruning) {
this._busyPruning = true
const promises = []
const leaves = this.tileTree.getLeafNodes()
const tilesThatAreTooLow = tileData.tooLow
const tilesThatAreJustRight = tileData.justRight
const tilesWithTooMuchDetail = tileData.tooHigh
// prune
for (let t of leaves) {
// if all siblings have too much detail then they are prunable
const siblings = t.getSiblings()
const canBePruned =
siblings.length > 0 &&
siblings.every((s) => {
return (
s.isLeaf() &&
!tilesThatAreTooLow[s.id] &&
!tilesThatAreJustRight[s.id] &&
(!tileData.ids.includes(s.id) ||
tilesWithTooMuchDetail[s.id] !== undefined)
)
})
// make sure no siblings are busy
const siblingsNotBusy = siblings.every((s) => !s.isBusy())
if (canBePruned && siblingsNotBusy) {
// remove siblings and add parent's mesh
promises.push(
this.combineNode(t.id).then(() => {
this._tilesNeedUpdate = true
})
)
}
}
if (promises.length > 1) {
Promise.all(promises).then(() => {
this._busyPruning = false
this.fire('prune', this.tileTree.getAllNodesBelow().length)
if (
Object.keys(tilesThatAreTooLow).length === 0 ||
Object.entries(tileData.tooLow).every(
([key, value]) => {
return !value.tile.canSplit()
}
)
) {
this.debouncedPruneFire('donePruning')
}
})
} else if (promises.length === 1) {
console.log('flickering?')
} else {
this._busyPruning = false
}
}
}
async splitNode(node) {
node.split()
const promises = node.getChildren().map(async (child) => {
await child.getThreeGroup(
this.martiniError,
this.globeReference.getMatrix(),
this.elevationMaterial
)
return child
})
try {
const results = await Promise.all(promises)
this.scene.remove(node.threeGroup)
//this.bboxGroup.remove(node.bbox)
for (let i in results) {
this.scene.add(results[i].threeGroup)
//this.bboxGroup.add(results[i].bbox)
}
return results
} catch (err) {
node.MAX_ZOOM = node.z
node.getChildren().forEach((child) => {
child.removeNode()
})
console.warn(err)
}
}
async combineNode(id) {
// make sure that node exists, and that parent exists, because
// we don't want to remove the parent if it is the first tile
let node = this.tileTree.getNodeByID(id)
let parent = node && node.parent
if (node && parent) {
await parent.getThreeGroup(
this.martiniError,
this.globeReference.getMatrix(),
this.elevationMaterial
)
let siblings = node.getSiblings()
for (let i in siblings) {
this.scene.remove(siblings[i].threeGroup)
//this.bboxGroup.remove(siblings[i].bbox)
this.tileTree.removeNode(siblings[i])
}
this.scene.add(parent.threeGroup)
//this.bboxGroup.add(parent.bbox)
}
}
/**
* <b> Update the scene.</b>
*
* This is usually called in an animation render loop.
*/
render() {
if (
//this.zoomNeedsUpdate ||
this.meshesNeedUpdate ||
this.cameraNeedsUpdate ||
this.materialsNeedUpdate ||
this.positionNeedsUpdate ||
this.tileTreeNeedsUpdate ||
this.orientationNeedsUpdate ||
this.globeReferenceNeedsUpdate
) {
this.update()
}
// don't show bounding boxes during render
/*let reset
if (this.bboxGroup.visible) {
this.bboxGroup.visible = false
reset = true
}*/
this.renderer.setSize(
this.textureScale * this.width,
this.textureScale * this.height
)
if (this._tilesNeedUpdate) {
// render to depth
this.scene.overrideMaterial = this.depthMaterial
this.renderer.setRenderTarget(this.depthTarget)
this.renderer.render(this.scene, this.camera)
// reset render target
this.renderer.setRenderTarget(null)
// render tile indices
this.scene.overrideMaterial = this.tileIndexMaterial
let indexData = renderToUint8Array(
this.renderer,
this.scene,
this.camera
)
// render zoom correction data
this.scene.overrideMaterial = this.tilesNeedUpdateMaterial
let zoomData = renderToUint8Array(
this.renderer,
this.scene,
this.camera
)
// reset override material
this.scene.overrideMaterial = null
// test
this.updateTiles(indexData, zoomData)
// set flag to false
this._tilesNeedUpdate = false
}
// regular render without radial distortion
this.scene.overrideMaterial = this.overrideMaterial
this.renderer.setRenderTarget(this.rdTarget)
this.renderer.render(this.scene, this.camera)
this.renderer.setRenderTarget(null)
this.scene.overrideMaterial = null
this.renderer.setSize(this.width, this.height)
// regular render with radial distortion
this.renderer.render(this.rdScene, this.camera)
/*if (reset) {
this.bboxGroup.visible = true
}*/
}
}
Source