Home Reference Source

viewer/octree.js

import * as mat4 from "./glmatrix/mat4.js";
import * as mat3 from "./glmatrix/mat3.js";
import * as vec3 from "./glmatrix/vec3.js";
import * as vec4 from "./glmatrix/vec4.js";

import {Utils} from "./utils.js";

/**
 * Octree implementation targeted towards being used in the TilingLayer, could possibly be retrofitted to be a generic Octree to be used in other contexts
 */
class OctreeNode {
	constructor(viewer, parent, id, x, y, z, width, height, depth, level, globalTransformation) {
		this.viewer = viewer;
		this.parent = parent;
		this.globalTransformation = globalTransformation;
		if (parent != null) {
			if (level > this.parent.deepestLevel) {
				this.parent.deepestLevel = level;
			}
		}
		this.id = id;
		
		this.leaf = true;
		
		this.x = x;
		this.y = y;
		this.z = z;
		
		this.nrObjects = 0;
		
		this.width = width;
		this.height = height;
		this.depth = depth;
		
		this.level = level;
		
		this.center = vec4.fromValues(this.x + this.width / 2, this.y + this.height / 2, this.z + this.depth / 2, 1);
		this.normalizedCenter = vec4.create();
		vec3.transformMat3(this.normalizedCenter, this.center, this.globalTransformation);
		
		this.radius = (Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2) + Math.pow(this.depth, 2))) / 2;
		
		this.matrix = mat4.create();
		mat4.translate(this.matrix, this.matrix, [this.x + this.width / 2, this.y + this.height / 2, this.z + this.depth / 2]);
		mat4.scale(this.matrix, this.matrix, [this.width, this.height, this.depth]);

		this.normalizedMatrix = mat4.create();
		mat4.multiply(this.normalizedMatrix, this.normalizedMatrix, this.globalTransformation);
		mat4.translate(this.normalizedMatrix, this.normalizedMatrix, [this.x + this.width / 2, this.y + this.height / 2, this.z + this.depth / 2]);
		mat4.scale(this.normalizedMatrix, this.normalizedMatrix, [this.width, this.height, this.depth]);
		
		this.bounds = [this.x, this.y, this.z, this.width, this.height, this.depth];
		
		var minVector = vec3.create();
		var maxVector = vec3.create();
		vec3.set(minVector, this.bounds[0], this.bounds[1], this.bounds[2]);
		vec3.set(maxVector, this.bounds[0] + this.bounds[3], this.bounds[1] + this.bounds[4], this.bounds[2] + this.bounds[5]);
		this.boundsVectors = [minVector, maxVector];
		
		var normalizedMinVector = vec3.clone(minVector);
		var normalizedMaxVector = vec3.clone(maxVector);
		vec3.transformMat4(normalizedMinVector, normalizedMinVector, globalTransformation);
		vec3.transformMat4(normalizedMaxVector, normalizedMaxVector, globalTransformation);
		this.normalizedBoundsVectors = [normalizedMinVector, normalizedMaxVector];
		
		// TODO also keep track of the minimal bounds (usually smaller than the "static" bounds of the node), which can be used for (frustum) occlusion culling
		// TODO also keep track of the minimal bounds inc. children (useful for hyrachical culling)
		
		this.quadrants = [];
		
		if (this.viewer.vertexQuantization) {
			this.vertexQuantizationMatrix = Utils.toArray(this.viewer.vertexQuantization.getTransformedQuantizationMatrix(this.bounds));
			this.vertexUnquantizationMatrix = Utils.toArray(this.viewer.vertexQuantization.getTransformedInverseQuantizationMatrix(this.bounds));
		}
		
		this.largestFaceArea = width * height;
		if (width * depth > this.largestFaceArea) {
			this.largestFaceArea = width * depth;
		}
		if (depth * height > this.largestFaceArea) {
			this.largestFaceArea = depth * height;
		}
		this.largestEdge = width;
		if (height > this.largestEdge) {
			this.largestEdge = height;
		}
		if (depth > this.largestEdge) {
			this.largestEdge = depth;
		}
	}
	
	getBounds() {
		return this.bounds;
	}
	
	getMatrix() {
		return this.matrix;
	}

	traverseBreathFirstInternal(fn, level) {
		if (this.level == level) {
			var result = fn(this);
		}
		if (this.level >= level) {
			return;
		}
		for (var node of this.quadrants) {
			if (node != null) {
				node.traverseBreathFirstInternal(fn, level);
			}
		}
	}
	
	traverseBreathFirst(fn) {
		if (this.levelLists == null) {
			return;
		}
		for (var l=0; l<=this.maxDepth; l++) {
			for (var node of this.levelLists[l]) {
				fn(node);
			}
		}
	}
	
	traverse(fn, onlyLeafs, level) {
		if (!onlyLeafs || this.leaf == true) {
			if (fn(this, level || 0) === false) {
				return;
			}
		}
		if (this.quadrants == null) {
			return;
		}
		for (var node of this.quadrants) {
			if (node != null) {
				node.traverse(fn, onlyLeafs, (level || 0) + 1);
			}
		}
	}
	
	getCenter() {
		return this.center;
	}
	
	getBoundingSphereRadius() {
		return this.radius;
	}
	
	getQuadrant(localId) {
		if (localId < 0 || localId > 7) {
			throw "Invalid local id: " + localId;
		}
		var quadrant = this.quadrants[localId];
		if (quadrant == null) {
			var newLevel = this.level + 1;
			var newId = this.id * 8 + localId + 1;
			switch (localId) {
				case 0: quadrant = new OctreeNode(this.viewer, this, newId, this.x, this.y, this.z, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 1: quadrant = new OctreeNode(this.viewer, this, newId, this.x, this.y, this.z + this.depth / 2, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 2: quadrant = new OctreeNode(this.viewer, this, newId, this.x, this.y + this.height / 2, this.z, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 3: quadrant = new OctreeNode(this.viewer, this, newId, this.x, this.y + this.height / 2, this.z + this.depth / 2, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 4: quadrant = new OctreeNode(this.viewer, this, newId, this.x + this.width / 2, this.y, this.z, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 5: quadrant = new OctreeNode(this.viewer, this, newId, this.x + this.width / 2, this.y, this.z + this.depth / 2, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 6: quadrant = new OctreeNode(this.viewer, this, newId, this.x + this.width / 2, this.y + this.height / 2, this.z, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
				case 7: quadrant = new OctreeNode(this.viewer, this, newId, this.x + this.width / 2, this.y + this.height / 2, this.z + this.depth / 2, this.width / 2, this.height / 2, this.depth / 2, newLevel, this.globalTransformation); break;
			}
			this.quadrants[localId] = quadrant;
		}
		this.leaf = false;
		return quadrant;
	}
	
	getNodeById(id) {
		if (id == this.id) {
			return this;
		}
		var localId = (id - 1) % 8;
		var parentId = (id - 1 - localId) / 8;
		var list = [];
		list.push((id - 1) % 8);
		while (parentId > 0) {
			list.push((parentId - 1) % 8);
			parentId = Math.floor((parentId - 1) / 8);
		}
		var node = this;
		for (var i=list.length-1; i>=0; i--) {
			node = node.getQuadrant(list[i]);
		}
		if (node.id != id) {
			throw "Ids do not match " + node.id + " / " + id;
		}
		return node;
	}
	
	prepareLevelListsInternal(level, levelList) {
		if (this.level == level) {
			levelList.push(this);
		}
		if (this.level >= level) {
			return;
		}
		for (var node of this.quadrants) {
			if (node != null) {
				node.prepareLevelListsInternal(level, levelList);
			}
		}
	}
	
	prepareBreathFirstInternal(breathFirstList, fn, level) {
		if (this.level == level) {
			if (fn(this)) {
				breathFirstList.push(this);
			}
		}
		if (this.level > level) {
			return;
		}
		if (this.quadrants == null) {
			return;
		}
		for (var node of this.quadrants) {
			if (node != null) {
				node.prepareBreathFirstInternal(breathFirstList, fn, level);
			}
		}
	}

	prepareFullListInternal(fullList, level) {
		if (this.level == level) {
			fullList.push(this);
		}
		if (this.level > level) {
			return;
		}
		if (this.quadrants == null) {
			return;
		}
		for (var node of this.quadrants) {
			node.prepareFullListInternal(fullList, level);
		}
	}
}

export class Octree extends OctreeNode {
	constructor(viewer, realBounds, globalTransformation, maxDepth) {
		super(viewer, null, 0, realBounds[0], realBounds[1], realBounds[2], realBounds[3] - realBounds[0], realBounds[4] - realBounds[1], realBounds[5] - realBounds[2], 0, globalTransformation);
		this.maxDepth = maxDepth;
		this.actualMaxLevel;
		this.level = 0;
		this.breathFirstList = [];
	}
	
	extractBreathFirstList(fn) {
		var list = [];
		this.traverseBreathFirst((node) => {
			if (fn(node)) {
				list.push(node);
				return true;
			} else {
				return false;
			}
		});
		return list;
	}
	
	traverseBreathFirstCached(fn) {
		for (var node of this.breathFirstList) {
			fn(node);
		}
	}
	
	prepareLevelLists() {
		this.levelLists = [];
		for (var l=0; l<=this.maxDepth; l++) {
			this.levelLists[l] = [];
			this.prepareLevelListsInternal(l, this.levelLists[l]);
		}
	}
}