Home Reference Source

viewer/tilingrenderlayer.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 {RenderLayer} from "./renderlayer.js";
import {Octree} from "./octree.js";
import {Frustum} from "./frustum.js";
import {LineBoxGeometry} from "./lineboxgeometry.js";
import {BufferManagerTransparencyOnly} from "./buffermanagertransparencyonly.js";
import {BufferManagerPerColor} from "./buffermanagerpercolor.js";
import {Utils} from "./utils.js";
import {TileLoader} from "./tileloader.js";
import {ReuseLoader} from "./reuseloader.js";

const RED = [1, 0, 0, 1];
const GREEN = [0, 1, 0, 1];
const BLUE = [0, 0, 1, 1];
const GRAY = [0.5, 0.5, 0.5, 1];
const PURPLE = [1, 0, 1, 1];

/**
 * A specific type of RenderLayer, which uses Tiling to achieve better render performance, but also minimizes the amount of data that needs to be loaded of the line.
 * 
 */
export class TilingRenderLayer extends RenderLayer {
	constructor(viewer, geometryDataToReuse, bounds) {
		super(viewer, geometryDataToReuse);

		this.octree = new Octree(viewer, bounds, viewer.globalTranslationVector, viewer.settings.maxOctreeDepth);
		this.lineBoxGeometry = new LineBoxGeometry(viewer, viewer.gl);

		this.loaderToNode = {};

		this.drawTileBorders = this.viewer.settings.realtimeSettings.drawTileBorders;

		this._frustum = new Frustum();
		
		window.tilingRenderLayer = this;
		
		this.enabled = false;
		
		this.show = "none";
		this.initialLoad = "none";
		
		// TODO unregister
		this.viewer.camera.listeners.push(() => {
			this._frustum.init(this.viewer.camera.viewMatrix, this.viewer.camera.projMatrix);
		});
	}
	
	showAll() {
		this.show = "all";
		this.viewer.dirty = 2;
	}

	load(bimServerApi, densityThreshold, roids, fieldsToInclude, progressListener) {
		var reuseLowerThreshold = this.settings.loaderSettings.reuseThreshold;
		if (!this.settings.loaderSettings.tilingLayerReuse) {
			reuseLowerThreshold = -1;
		}
		this.tileLoader = new TileLoader(this, this.viewer, bimServerApi, densityThreshold, reuseLowerThreshold, this.geometryDataToReuse, roids, fieldsToInclude);
		if (this.settings.loaderSettings.tilingLayerReuse) {
			this.reuseLoader = new ReuseLoader(this.viewer, reuseLowerThreshold, bimServerApi, fieldsToInclude, roids, this.tileLoader.quantizationMap, this.geometryCache, this.geometryDataToReuse);
		}

		var promise = new Promise((resolve, reject) => {
			var init = this.tileLoader.initialize().then(() => {
				this.enabled = true;
				// Ugly way of triggering an octree-visibility update
				this.lastViewMatrix = null;
				if (this.initialLoad == "all") {
					return this.tileLoader.loadAll(progressListener);
				}
				resolve();
			});
		});
		return promise;
	}

	cull(node) {
		// 1. Are we always showing all objects?
		if (this.show === "all") {
			return false;
		}

		// 2. Is the complete Tile outside of the view frustum?
		if (this._frustum.intersectsWorldAABB(node.minimalBox.minmax) === Frustum.OUTSIDE_FRUSTUM) {
			return true;
		}
		
		// 3. Is the tile too far away?
		var cameraEye = this.viewer.camera.eye;
		var tileCenter = node.minimalBox.normalizedCenter;
		var closestPotentialDistanceMm = Math.abs(vec3.distance(cameraEye, tileCenter) - node.minimalBox.radius);
		
//		console.log(closestPotentialDistanceMm);
		
		// Project the biggest face of the node to 2D and determine it's area in pixels
		
		const vFOV = this.viewer.camera.perspective.fov * Math.PI / 180;
		const pixelWidth = 1000 * Math.tan(vFOV / 2) / closestPotentialDistanceMm; // far-plane distance

		const factor = 100000 / pixelWidth;
		
		if (node.gpuBufferManager != null) {
			// A tile is already loaded, we need to determine how much of it to show
			node.stats.trianglesDrawing = 0;
			var totalTriangles = 0;
			for (var transparent of [false, true]) {
				for (var twoSidedTriangles of [false, true]) {
					var buffers = node.gpuBufferManager.getBuffers(transparent, twoSidedTriangles, false);
					for (var buffer of buffers) {
						buffer.nrTrianglesToDraw = Math.floor(Math.min(buffer.nrIndices, Math.floor(buffer.nrIndices * factor)) / 3);
						totalTriangles += buffer.nrIndices / 3;
						node.stats.trianglesDrawing += buffer.nrTrianglesToDraw;
					}
				}
			}
			for (var transparent of [false, true]) {
				for (var twoSidedTriangles of [false, true]) {
					var buffers = node.gpuBufferManager.getBuffers(transparent, twoSidedTriangles, true);
					for (var buffer of buffers) {
						buffer.nrTrianglesToDraw = Math.floor(Math.min(buffer.nrIndices, Math.floor(buffer.nrIndices * factor)) / 3) * buffer.numInstances;
						totalTriangles += (buffer.nrIndices / 3) * buffer.numInstances;
						node.stats.trianglesDrawing += buffer.nrTrianglesToDraw;
					}
				}
			}
			node.normalizedDistanceFactor = node.stats.trianglesDrawing / totalTriangles;
			if (node.stats.trianglesDrawing == 0) {
				return true;
			}
		} else {
			// This bit determines whether a tile will be loaded or not
			if (pixelWidth > 0.004) {
				return false;
			} else {
				node.normalizedDistanceFactor = 0;
				if (node.stats != null) {
					node.stats.trianglesDrawing = 0;
				}
				return true;
			}
		}
		// Default response
		return false;
	}
	
	prepareRender(reason) {
		// This only needs to be recalculated if the camera has changed, so we keep track of the last view matrix
		
		// TODO To correctly update the stats, this also needs to run whenever new data was loaded
		if (this.lastViewMatrix == null || this.octree.size != this.lastOctreeSize || !mat4.equals(this.lastViewMatrix, this.viewer.camera.viewMatrix) || reason == 2) {
			this.lastViewMatrix = mat4.clone(this.viewer.camera.viewMatrix);

			var renderingTiles = 0;
			var renderingTriangles = 0;
			var drawCalls = 0;
			
			this.octree.traverseBreathFirst((node) => {
				if (node.parent != null && node.parent.visibilityStatus == 0) {
					node.visibilityStatus = 0;
					return false;
				}
				if (this.cull(node)) {
					node.visibilityStatus = 0;
					return false;
				} else {
					node.visibilityStatus = 1;
					if (node.loadingStatus == 0) {
						this.tileLoader.loadTile(node);
					} else {
						if (node.stats != null) {
							renderingTiles++;
							renderingTriangles += (node.stats.trianglesDrawing ? node.stats.trianglesDrawing : 0);
							drawCalls += node.stats.drawCallsPerFrame;
						}
					}
				}
			});
			
			this.viewer.stats.setParameter("Drawing", "Draw calls per frame (L2)", drawCalls);
			this.viewer.stats.setParameter("Drawing", "Triangles to draw (L2)", renderingTriangles);
			this.viewer.stats.setParameter("Tiling", "Rendering", renderingTiles);
		}
	}
	
	renderBuffers(transparency, twoSidedTriangles, reuse, lines, visibleElements) {
		// TODO when navigation is active (rotating, panning etc...), this would be the place to decide to for example not-render anything in this layer, or maybe apply more aggressive culling
		// if (this.viewer.navigationActive) {
		// 	return;
		// }
		
		// TODO would be nicer if this was passed as an integer argument, indicating the iteration count of this frame
		let picking = visibleElements.pass === 'pick';

		var programInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(reuse, picking));

		this.gl.useProgram(programInfo.program);

		if (!picking) {
			// TODO find out whether it's possible to do this binding before the program is used (possibly just once per frame, and better yet, a different location in the code)
			this.viewer.lighting.render(programInfo.uniformBlocks.LightData);
			this.gl.uniformMatrix3fv(programInfo.uniformLocations.viewNormalMatrix, false, this.viewer.camera.viewNormalMatrix);
		}

		this.gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, this.viewer.camera.projMatrix);
		this.gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, this.viewer.camera.viewMatrix);
		this.gl.uniform3fv(programInfo.uniformLocations.postProcessingTranslation, this.postProcessingTranslation);
		this.gl.uniform4fv(programInfo.uniformLocations.sectionPlane, this.viewer.sectionPlanes.buffer);

//		if (this.settings.quantizeVertices) {
//			this.gl.uniformMatrix4fv(programInfo.uniformLocations.vertexQuantizationMatrix, false, this.viewer.vertexQuantization.getTransformedInverseVertexQuantizationMatrix());
//		}

		programInfo.lastUnquantizationMatrixUsed = null; // This ony is used for "caching", need to reset it otherwise it won't be set
		
		this.octree.traverse((node) => {
			// TODO at the moment a list (of non-empty tiles) is used to do traverseBreathFirst, but since a big optimization is possible by automatically culling 
			// child nodes of parent nodes that are culled, we might have to reconsider this and go back to tree-traversal, where returning false would indicate to 
			// skip the remaining child nodes
			
			if (node.visibilityStatus == 1) {
				if (node.gpuBufferManager == null) {
					// Not initialized yet
					return;
				}
				var buffers = node.gpuBufferManager.getBuffers(transparency, twoSidedTriangles, reuse);
				this.renderFinalBuffers(buffers, programInfo, visibleElements);
			} else {
				return false;
			}
		});
	}

	traverseFunction(node, level, lineBoxGeometry) {
		var color = null;
		if (node.loadingStatus === 0) {
			// No visualisation, node is not empty (or parent node)
		} else if (node.loadingStatus === 1) {
			// Node is waiting to start loading
			color = RED;
		} else if (node.loadingStatus === 2) {
			// Node is loading
		} else if (node.loadingStatus === 3) {
			// Node is loaded
			if (node.visibilityStatus === 0) {
				color = GREEN;
			} else if (node.visibilityStatus === 1) {
				if (node.normalizedDistanceFactor === 1) {
					// Uncomment for debugging tile borders
//					color = PURPLE;
				} else {
					color = BLUE;
					// This changes (content of) the constant, but the constant is only used for this, so it's fine
					color[3] = 1 - node.normalizedDistanceFactor;
				}
			}
		} else if (node.loadingStatus === 4) {
			// To be documented
			color = GRAY;
		} else if (node.loadingStatus === 5) {
			// Node has been tried to load, but no objects were returned
		}
		if (color != null) {
			lineBoxGeometry.render(color, node.minimalBox.normalizedMatrix, 0.001);
		}
	}
	
	renderTileBorders() {
		if (this.drawTileBorders) {
			// The lines are rendered in the transparency-phase only
			this.lineBoxGeometry.renderStart(this.viewer, this);
			this.octree.traverse(this.traverseFunction, false, 0, this.lineBoxGeometry);
			this.lineBoxGeometry.renderStop();
		}
	}

	addGeometry(loaderId, geometry, object) {
		var sizes = {
			vertices: geometry.positions.length,
			normals: geometry.normals.length,
			indices: geometry.indices.length,
			lineIndices: geometry.lineIndices.length,
			colors: (geometry.colors != null ? geometry.colors.length : 0),
			pickColors: geometry.positions.length
		};

		var node = this.loaderToNode[loaderId];

		// TODO some of this is duplicate code, also in defaultrenderlayer.js
		if (geometry.reused > 1 && this.geometryDataToReuse != null && this.geometryDataToReuse.has(geometry.id)) {
			geometry.matrices.push(object.matrix);
			geometry.objects.push(object);

			this.viewer.stats.inc("Drawing", "Triangles to draw", geometry.indices.length / 3);

			return;
		}
		
		if (node.bufferManager == null) {
			if (this.settings.useObjectColors) {
				node.bufferManager = new BufferManagerPerColor(this.viewer, this.viewer.settings, this, this.viewer.bufferSetPool);
			} else {
				node.bufferManager = new BufferManagerTransparencyOnly(this.viewer, this.viewer.settings, this, this.viewer.bufferSetPool);
			}
		}
		var buffer = node.bufferManager.getBufferSet(geometry.hasTransparency, geometry.color, sizes);
		buffer.node = node;
		
		super.addGeometry(loaderId, geometry, object, buffer, sizes);
	}
	
	dump() {
		console.log(this.tileLoader.executor);
	}

	createObject(loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb) {
		var loader = this.getLoader(loaderId);
		var node = this.loaderToNode[loaderId];
		return super.createObject(loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb, node.gpuBufferManager, node);
	}

	addGeometryReusable(geometry, loader, gpuBufferManager) {
		super.addGeometryReusable(geometry, loader, gpuBufferManager);
		var node = this.loaderToNode[loader.loaderId];
		node.stats.triangles += ((geometry.indices.length / 3) * (geometry.matrices.length));
		node.stats.drawCallsPerFrame++;
		
		this.viewer.stats.inc("Drawing", "Draw calls per frame (L2)");
	}

	done(loaderId) {
		var loader = this.getLoader(loaderId);
		var node = this.loaderToNode[loaderId];
		
		// When a new tile has been loaded and the viewer is not moving, we need to force an update of the culling of the node
		if (this.cull(node)) {
			node.visibilityStatus = 0;
		} else {
			node.visibilityStatus = 1;
		}
		
		for (var geometry of loader.geometries.values()) {
			if (geometry.isReused) {
				this.addGeometryReusable(geometry, loader, node.gpuBufferManager);
			}
		}
		
		var bufferManager = node.bufferManager;
		if (bufferManager != null) {
			for (var buffer of bufferManager.getAllBuffers()) {
				this.flushBuffer(buffer);
			}
			bufferManager.clear();
			node.bufferManager = null;
		}

		for (var object of loader.objects.values()) {
			object.add = null;
		}

		if (this.settings.autoCombineGpuBuffers) {
			var savedBuffers = node.gpuBufferManager.combineBuffers();
			this.viewer.stats.dec("Drawing", "Draw calls per frame (L2)", savedBuffers);
			this.viewer.stats.dec("Buffers", "Buffer groups", savedBuffers);
			node.stats.drawCallsPerFrame -= savedBuffers;
		}

		this.viewer.dirty = 2;
		
		this.removeLoader(loaderId);
	}

	flushAllBuffers() {
		this.octree.traverse((node) => {
			var bufferManager = node.bufferManager;
			if (bufferManager != null) {
				for (var buffer of bufferManager.getAllBuffers()) {
					this.flushBuffer(buffer);
				}
				if (this.settings.useObjectColors) {
					// When using object colors, it makes sense to sort the buffers by color, so we can potentially skip a few uniform binds
					// It might be beneficiary to do this sorting on-the-lfy and not just when everything is loaded

					// TODO disabled for now since we are testing combining buffer, which makes this obsolete
//					this.sortBuffers(node.liveBuffers);
				}
			}
		}, false);
	}

	renderSelectionOutlines(ids, width) {
		for (var oid of ids) {
			// TODO this is already much more efficient than iterating over all octree nodes, but can be made more efficient for large selections by first 
			// organizing the oid's per node
			var viewObject = this.viewer.getViewObject(oid);
			super.renderSelectionOutlines(ids, width, viewObject.node);
		}
	}
	
	addCompleteBuffer(buffer, gpuBufferManager) {
		var newBuffer = super.addCompleteBuffer(buffer, gpuBufferManager);
		
		const node = this.loaderToNode[buffer.loaderId];
		newBuffer.node = node;
		node.stats.triangles += buffer.nrIndices / 3;
		node.stats.drawCallsPerFrame++;
		
		return newBuffer;
	}
	
	flushBuffer(buffer) {
		var node = buffer.node;
		let gpuBuffer = super.flushBuffer(buffer, node.gpuBufferManager);

		if (gpuBuffer == null) {
			debugger;
		}
		gpuBuffer.node = node;

		node.stats.triangles += buffer.nrIndices / 3;
		node.stats.drawCallsPerFrame++;

		if (node.bufferManager) {
			node.bufferManager.resetBuffer(buffer);
		}
		this.viewer.dirty = 2;

		return gpuBuffer;
	}

	completelyDone() {
		this.flushAllBuffers();
		this.viewer.dirty = 2;
	}
}