TileSet = {
class TileSet {
constructor(){
this.url = null;
this.version = null;
this.gltfUpAxis = 'Z';
this.geometricError = null;
this.root = null;
}
load(url, styleParams) {
this.url = url;
let resourcePath = THREE.LoaderUtils.extractUrlBase(url);
let self = this;
return new Promise((resolve, reject) => {
fetch(self.url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
}
return response;
})
.then(response => response.json())
.then(json => {
self.version = json.asset.version;
self.geometricError = json.geometricError;
self.refine = json.refine ? json.refine.toUpperCase() : 'ADD';
self.root = new ThreeDeeTile(json.root, resourcePath, styleParams, self.refine, true);
})
.then(res => resolve())
.catch(error => {
console.error(error);
reject(error);
});
});
}
}
class ThreeDeeTile {
constructor(json, resourcePath, styleParams, parentRefine, isRoot) {
this.loaded = false;
this.styleParams = styleParams;
this.resourcePath = resourcePath;
this.totalContent = new THREE.Group(); // Three JS Object3D Group for this tile and all its children
this.tileContent = new THREE.Group(); // Three JS Object3D Group for this tile's content
this.childContent = new THREE.Group(); // Three JS Object3D Group for this tile's children
this.totalContent.add(this.tileContent);
this.totalContent.add(this.childContent);
this.boundingVolume = json.boundingVolume;
if (this.boundingVolume && this.boundingVolume.box) {
let b = this.boundingVolume.box;
let extent = [b[0] - b[3], b[1] - b[7], b[0] + b[3], b[1] + b[7]];
let sw = new THREE.Vector3(extent[0], extent[1], b[2] - b[11]);
let ne = new THREE.Vector3(extent[2], extent[3], b[2] + b[11]);
this.box = new THREE.Box3(sw, ne);
if (DEBUG) {
let geom = new THREE.BoxGeometry(b[3] * 2, b[7] * 2, b[11] * 2);
let edges = new THREE.EdgesGeometry( geom );
let line = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0x800000 } ) );
let trans = new THREE.Matrix4().makeTranslation(b[0], b[1], b[2]);
line.applyMatrix4(trans);
this.totalContent.add(line);
}
} else {
this.extent = null;
this.sw = null;
this.ne = null;
this.box = null;
this.center = null;
}
this.refine = json.refine ? json.refine.toUpperCase() : parentRefine;
this.geometricError = json.geometricError;
this.transform = json.transform;
if (this.transform && !isRoot) {
// if not the root tile: apply the transform to the THREE js Group
// the root tile transform is applied to the camera while rendering
this.totalContent.applyMatrix4(new THREE.Matrix4().fromArray(this.transform));
}
this.content = json.content;
this.children = [];
if (json.children) {
for (let i=0; i<json.children.length; i++){
let child = new ThreeDeeTile(json.children[i], resourcePath, styleParams, this.refine, false);
this.childContent.add(child.totalContent);
this.children.push(child);
}
}
}
load() {
this.tileContent.visible = true;
this.childContent.visible = true;
if (this.loaded) {
return;
}
this.loaded = true;
let self = this;
if (this.content) {
let url = this.content.uri ? this.content.uri : this.content.url;
if (!url) return;
if (url.substr(0, 4) != 'http')
url = this.resourcePath + url;
let type = url.slice(-4);
if (type == 'json') {
// child is a tileset json
let tileset = new TileSet();
tileset.load(url, this.styleParams).then(function(){
self.children.push(tileset.root);
if (tileset.root) {
if (tileset.root.transform) {
// the root tile transform of a tileset is normally not applied because
// it is applied by the camera while rendering. However, in case the tileset
// is a subset of another tileset, so the root tile transform must be applied
// to the THREE js group of the root tile.
tileset.root.totalContent.applyMatrix4(new THREE.Matrix4().fromArray(tileset.root.transform));
}
self.childContent.add(tileset.root.totalContent);
}
});
} else if (type == 'b3dm') {
let loader = new THREE.GLTFLoader();
let b3dm = new B3DM(url);
let rotateX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI / 2);
self.tileContent.applyMatrix4(rotateX); // convert from GLTF Y-up to Z-up
b3dm.load()
.then(d => loader.parse(d.glbData, self.resourcePath, function(gltf) {
//Add the batchtable to the userData since gltLoader doesn't deal with it
gltf.scene.children[0].userData = d.batchTableJson;
var meshMaterial = new THREE.MeshNormalMaterial({color: 0x7777ff});
gltf.scene.traverse(child => {
//child.material=meshMaterial;
});
if (self.styleParams.color != null || self.styleParams.opacity != null) {
let color = new THREE.Color(self.styleParams.color);
gltf.scene.traverse(child => {
if (child instanceof THREE.Mesh) {
if (self.styleParams.color != null)
child.material.color = color;
if (self.styleParams.opacity != null) {
child.material.opacity = self.styleParams.opacity;
child.material.transparent = self.styleParams.opacity < 1.0 ? true : false;
}
}
});
}
let children = gltf.scene.children;
/*
for (let i=0; i<children.length; i++) {
if (children[i].isObject3D)
self.tileContent.add(children[i]);
}*/
self.tileContent.add(gltf.scene);
}, function(e) {
throw new Error('error parsing gltf: ' + e);
})
)
} else if (type == 'pnts') {
let pnts = new PNTS(url);
pnts.load()
.then(d => {
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(d.points, 3));
let material = new THREE.PointsMaterial();
material.size = self.styleParams.pointsize != null ? self.styleParams.pointsize : 1.0;
if (self.styleParams.color) {
material.vertexColors = THREE.NoColors;
material.color = new THREE.Color(self.styleParams.color);
material.opacity = self.styleParams.opacity != null ? self.styleParams.opacity : 1.0;
} else if (d.rgba) {
geometry.setAttribute('color', new THREE.Float32BufferAttribute(d.rgba, 4));
material.vertexColors = THREE.VertexColors;
} else if (d.rgb) {
geometry.setAttribute('color', new THREE.Float32BufferAttribute(d.rgb, 3));
material.vertexColors = THREE.VertexColors;
}
self.tileContent.add(new THREE.Points( geometry, material ));
if (d.rtc_center) {
let c = d.rtc_center;
self.tileContent.applyMatrix4(new THREE.Matrix4().makeTranslation(c[0], c[1], c[2]));
}
self.tileContent.add(new THREE.Points( geometry, material ));
});
} else if (type == 'i3dm') {
throw new Error('i3dm tiles not yet implemented');
} else if (type == 'cmpt') {
throw new Error('cmpt tiles not yet implemented');
} else {
throw new Error('invalid tile type: ' + type);
}
}
}
unload(includeChildren) {
this.tileContent.visible = false;
if (includeChildren) {
this.childContent.visible = false;
} else {
this.childContent.visible = true;
}
// TODO: should we also free up memory?
}
checkLoad(frustum, cameraPosition) {
// is this tile visible?
if (!frustum.intersectsBox(this.box)) {
this.unload(true);
return;
}
let dist = this.box.distanceToPoint(cameraPosition);
//console.log(`dist: ${dist}, geometricError: ${this.geometricError}`);
// are we too far to render this tile?
if (this.geometricError > 0.0 && dist > this.geometricError * 50.0) {
this.unload(true);
return;
}
// should we load this tile?
if (this.refine == 'REPLACE' && dist < this.geometricError * 20.0) {
this.unload(false);
} else {
this.load();
}
// should we load its children?
for (let i=0; i<this.children.length; i++) {
if (dist < this.geometricError * 20.0) {
this.children[i].checkLoad(frustum, cameraPosition);
} else {
this.children[i].unload(true);
}
}
/*
// below code loads tiles based on screenspace instead of geometricError,
// not sure yet which algorith is better so i'm leaving this code here for now
let sw = this.box.min.clone().project(camera);
let ne = this.box.max.clone().project(camera);
let x1 = sw.x, x2 = ne.x;
let y1 = sw.y, y2 = ne.y;
let tilespace = Math.sqrt((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1)); // distance in screen space
if (tilespace < 0.2) {
this.unload();
}
// do nothing between 0.2 and 0.25 to avoid excessive tile loading/unloading
else if (tilespace > 0.25) {
this.load();
this.children.forEach(child => {
child.checkLoad(camera);
});
}*/
}
}
return TileSet;
}