Home Reference Source

viewer/renderlayer.js

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

import {BufferTransformer} from "./buffertransformer.js";
import {Utils} from "./utils.js";
import {GeometryCache} from "./geometrycache.js";
import {FrozenBufferSet} from "./frozenbufferset.js";

import {AvlTree} from "./collections/avltree.js";

const outlineColor = new Float32Array([1.0, 0.5, 0.0, 1.0]);
const false_true = [false, true];
const UINT32_MAX = (new Uint32Array((new Int32Array([-1])).buffer))[0];

// Cache the extension availability
let WEBGL_multi_draw = null;

/**
 * Abstract base class for managing and rendering buffers pertaining
 * to a render layer, ie. the base geometries always visible vs. the
 * dynamically visible tiles based on camera orientation.
 * 
 * @class RenderLayer
 */
export class RenderLayer {
	
	constructor(viewer, geometryDataToReuse) {
		this.settings = viewer.settings;
		this.viewer = viewer;
		this.gl = viewer.gl;
		WEBGL_multi_draw = this.gl.getExtension("WEBGL_multi_draw");
		this.geometryDataToReuse = geometryDataToReuse;
		this.geometryCache = new GeometryCache(this);
		this.instanceSelectionData = new Uint32Array(256);
		this.previousInstanceVisibilityState = null;

		this.selectionOutlineMatrix = mat4.create();
		
		this.lines = null;
		this.loaders = new Map();
		this.bufferTransformer = new BufferTransformer(this.settings, viewer.vertexQuantization);
		
		this.nrPrimitivesLoaded = 0;
		this.nrTrianglesLoaded = 0;
		this.nrLinesLoaded = 0;
		
		this.postProcessingTranslation = vec3.fromValues(0, 0, 0);
	}

	createGeometry(loaderId, roid, uniqueModelId, geometryId, positions, normals, colors, color, indices, lineIndices, hasTransparency, hasTwoSidedTriangles, reused) {
		var bytesUsed = Utils.calculateBytesUsed(this.settings, positions.length, colors.length, indices.length, lineIndices ? lineIndices.length : 0, normals.length);
		var geometry = {
				id: geometryId,
				roid: roid,
				uniqueModelId: uniqueModelId,
				positions: positions,
				normals: normals,
				colors: colors,
				color: color,
				indices: indices,
				lineIndices: lineIndices,
				hasTransparency: hasTransparency,
				hasTwoSidedTriangles: hasTwoSidedTriangles,
				reused: reused, // How many times this geometry is reused, this does not necessarily mean the viewer is going to utilize this reuse
				reuseMaterialized: 0, // How many times this geometry has been reused in the viewer, when this number reaches "reused" we can flush the buffer fo' sho'
				bytes: bytesUsed,
				matrices: [],
				objects: []
		};
		
		var loader = this.getLoader(loaderId);

		loader.geometries.set(geometryId, geometry);
		geometry.isReused = geometry.reused > 1 && this.geometryDataToReuse != null && this.geometryDataToReuse.has(geometry.id);
		if (geometry.isReused) {
			this.viewer.stats.inc("Models", "Geometries reused");
		} else {
			this.viewer.stats.inc("Models", "Geometries");
		}

		return geometry;
	}
	
	createObject(loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb, gpuBufferManager, node, quantFromAabb) {
		var loader = this.getLoader(loaderId);
		var object = {
			uniqueId: uniqueId,
			hasTransparency: hasTransparency,
			matrix: matrix,
            normalMatrix: normalMatrix,
			scaleMatrix: scaleMatrix,
			geometry: [],
			min: vec3.fromValues(aabb[0], aabb[1], aabb[2]),
			max: vec3.fromValues(aabb[3], aabb[4], aabb[5]),
			roid: roid,
//				object: this.viewer.model.objects[oid],
			add: (geometryId, uniqueId) => {
				this.addGeometryToObject(geometryId, uniqueId, loader, gpuBufferManager);
			}
		};

		loader.objects.set(uniqueId, object);

		var globalizedAabb = Utils.transformBounds(aabb, this.viewer.globalTranslationVector);
		
		var viewObject = {
			renderLayer: this,
            type: type,
			aabb: aabb,
			globalizedAabb: globalizedAabb,
			uniqueId: uniqueId
		};
		
		if (node) {
			viewObject.node = node;
		}
		
		this.viewer.addViewObject(uniqueId, viewObject);

		geometryIds.forEach((id) => {
			this.addGeometryToObject(id, object.uniqueId, loader, gpuBufferManager, quantFromAabb);
		});

		this.viewer.stats.inc("Models", "Objects");

		return object;
	}

	addGeometry(loaderId, geometry, object, buffer, sizes, quantFromAabb) {
		var loaderQuantizeNormals = this.settings.loaderSettings.quantizeNormals;
		var quantizeNormals = this.settings.quantizeNormals;

		var startIndex = buffer.positionsIndex / 3;

		let QM = this.viewer.vertexQuantization.vertexQuantizationMatrix;
		let IQM = this.viewer.vertexQuantization.inverseVertexQuantizationMatrixWithGlobalTranslation;

		if (quantFromAabb) {
			// @todo not really from AABB (because we want to limit the amount of quantization matrices),
			// but maybe come up with something more robust than this.
			let QM2 = mat4.create();
			QM2[ 0] = 0.01;
			QM2[ 5] = 0.01;
			QM2[10] = 0.01;
			QM2[12] = 0.01 * QM[12] / QM[0];
			QM2[13] = 0.01 * QM[13] / QM[5];
			QM2[14] = 0.01 * QM[14] / QM[10];
			QM2[15] = 1.0;
			QM = QM2;

			IQM = mat4.identity(mat4.create());
			IQM[0] *= 100.;
			IQM[5] *= 100.;
			IQM[10] *= 100.;
		}

		buffer.unquantizationMatrix = IQM;

		try {
			var vertex = vec3.create();
			let outOfBounds = new Set();
			for (var i=0; i<geometry.positions.length; i+=3) {
				// When quantizeVertices is on and we use the buffers in a combined buffer (which is what this method, addGeometry does),
				// we need to un-quantize the vertices, transform them, then quantize them again (so the shaders can again unquantize them).
				// This because order does matter (object transformation sometimes even mirror stuff)
				// Obviously quantization slows down both CPU (only initially) and GPU (all the time)
				vertex[0] = geometry.positions[i + 0];
				vertex[1] = geometry.positions[i + 1];
				vertex[2] = geometry.positions[i + 2];
				
				// If the geometry loader loads quantized data we need to unquantize first
				// TODO there is a possible performance improvement possible for all modelset where the totalBounds of the modelSet are the same as for each individual submodel (for example all projects without subprojects).
				// In that case we won't have to unquantize + quantize again
				
				if (this.settings.loaderSettings.quantizeVertices) {
					vec3.transformMat4(vertex, vertex, this.viewer.vertexQuantization.getUntransformedInverseVertexQuantizationMatrixForUniqueModelId(geometry.uniqueModelId));
				}
				vec3.transformMat4(vertex, vertex, object.matrix);				
				vec3.scale(vertex, vertex, 1000);

				if (this.settings.quantizeVertices) {
					vec3.transformMat4(vertex, vertex, QM);
					// Detect vertices going out of bounds of the quantization window.
					if (Math.abs(vertex[0]) > 16000 || Math.abs(vertex[1]) > 16000 || Math.abs(vertex[1]) > 16000) {
						outOfBounds.add(i / 3);
					}
				}
				
				buffer.positions.set(vertex, buffer.positionsIndex);
				buffer.positionsIndex += 3;
			}
			var floatNormal = vec3.create();
			var intNormal = new Int8Array(3);
			for (var i = 0; i < geometry.normals.length; i += 3) {
				if (loaderQuantizeNormals) {
					floatNormal[0] = geometry.normals[i] / 127;
					floatNormal[1] = geometry.normals[i + 1] / 127;
					floatNormal[2] = geometry.normals[i + 2] / 127;
				} else {
					floatNormal[0] = geometry.normals[i];
					floatNormal[1] = geometry.normals[i + 1];
					floatNormal[2] = geometry.normals[i + 2];
				}

				vec3.transformMat3(floatNormal, floatNormal, object.normalMatrix);
				vec3.normalize(floatNormal, floatNormal);
				// TODO this results in vectors with a negative magnitude... (at least on the unquantized data) We should probably do something with that information
				// Also the number becomes really small, resulting in all zeros when quantizing again, that can't be right				

				if (quantizeNormals) {
					intNormal[0] = floatNormal[0] * 127;
					intNormal[1] = floatNormal[1] * 127;
					intNormal[2] = floatNormal[2] * 127;

					buffer.normals.set(intNormal, buffer.normalsIndex);
				} else {
					buffer.normals.set(floatNormal, buffer.normalsIndex);
				}

				buffer.normalsIndex += 3;
			}
			let originalColorIndex = buffer.colorsIndex;
			if (geometry.colors != null) {
				if (geometry.colors instanceof Uint8Array == this.settings.quantizeColors) {
					// The same, just copy
					buffer.colors.set(geometry.colors, buffer.colorsIndex);
					buffer.colorsIndex += geometry.colors.length;
				} else {
					// Different, conversion required
					var color = new Array(4);
					for (var i=0; i<geometry.colors.length; i+=4) {
						color[0] = geometry.colors[i + 0];
						color[1] = geometry.colors[i + 1];
						color[2] = geometry.colors[i + 2];
						color[3] = geometry.colors[i + 3];
						if (this.settings.quantizeColors) {
							// Quantize
							color[0] = color[0] * 255;
							color[1] = color[1] * 255;
							color[2] = color[2] * 255;
							color[3] = color[3] * 255;
						} else {
							// Unquantize
							color[0] = color[0] / 255;
							color[1] = color[1] / 255;
							color[2] = color[2] / 255;
							color[3] = color[3] / 255;
						}
						
						buffer.colors.set(color, buffer.colorsIndex);
						buffer.colorsIndex += 4;
					}
				}
			}
			var pickColor = this.viewer.getPickColor(object.uniqueId);
			var lenObjectPickColors = (geometry.positions.length / 3);
			for (var i=0; i<lenObjectPickColors; i++) {
				buffer.pickColors.set(pickColor, buffer.pickColorsIndex);
				buffer.pickColorsIndex += 4;
			}

			var li = (buffer.uniqueIdToIndex.get(object.uniqueId) || []);
			var idx = {
				start: buffer.indicesIndex, 
				length: geometry.indices.length,
				lineIndexStart: buffer.lineIndicesIndex,
				lineIndexLength: geometry.lineIndices ? geometry.lineIndices.length : 0,
				color: originalColorIndex,
				colorLength: geometry.colors.length
			};
			li.push(idx);
			buffer.uniqueIdToIndex.set(object.uniqueId, li);
			buffer.uniqueIdSet.add(object.uniqueId);
			
			var index = Array(3);
			for (var i=0; i<geometry.indices.length; i+=3) {
				let anyOutOfBounds = false;
				for (let j = 0; j < 3; ++j) {
					if (outOfBounds.has(geometry.indices[i + j])) {
						anyOutOfBounds = true;
						break;
					}
				}
				if (anyOutOfBounds) {
					buffer.indices.set([0,0,0], buffer.indicesIndex);
					buffer.indicesIndex += 3;
					continue;
				}				

				index[0] = geometry.indices[i + 0] + startIndex;
				index[1] = geometry.indices[i + 1] + startIndex;
				index[2] = geometry.indices[i + 2] + startIndex;

				for (var j=0; j<3; j++) {
					if (idx.minIndex == null || index[j] < idx.minIndex) {
						idx.minIndex = index[j];
					}
					if (idx.maxIndex == null || index[j] > idx.maxIndex) {
						idx.maxIndex = index[j];
					}
				}
				
				buffer.indices.set(index, buffer.indicesIndex);
				buffer.indicesIndex += 3;
			}
			for (var i=0; i<(geometry.lineIndices ? geometry.lineIndices.length : 0); i+=3) {
				index[0] = geometry.lineIndices[i + 0] + startIndex;
				index[1] = geometry.lineIndices[i + 1] + startIndex;
				index[2] = geometry.lineIndices[i + 2] + startIndex;
				
				for (var j=0; j<3; j++) {
					if (idx.minLineIndex == null || index[j] < idx.minLineIndex) {
						idx.minLineIndex = index[j];
					}
					if (idx.maxLineIndex == null || index[j] > idx.maxLineIndex) {
						idx.maxLineIndex = index[j];
					}
				}
				try {
					buffer.lineIndices.set(index, buffer.lineIndicesIndex);
					buffer.lineIndicesIndex += 3;
				} catch (e) {
					debugger;
				}
			}
		} catch (e) {
			debugger;
			console.error(e);
			console.log(sizes);
			console.log(buffer);
			throw e;
		}

		buffer.nrIndices += geometry.indices.length;
		buffer.bytes += geometry.bytes;
		
		if (buffer.needsToFlush) {
			this.flushBuffer(buffer);
		}
	}
	
	storeMissingGeometry(geometryLoader, map) {
		// TODO this is only applicable for the TilingRenderLayer, in other layers it's usually an indication of an error
		if (this.loaderToNode != null) {
			var node = this.loaderToNode[geometryLoader.loaderId];
			for (var geometryDataId of map.keys()) {
				var geometryInfoIds = map.get(geometryDataId);
				this.geometryCache.integrate2(geometryDataId, this.getLoader(geometryLoader.loaderId), node.gpuBufferManager, geometryInfoIds, geometryLoader);
			}
			
			// We need to start loading some GeometryData at some point, and add the missing pieces
			if (!this.geometryCache.isEmpty()) {
				this.reuseLoader.load(this.geometryCache.pullToLoad());
			}
		} else {
			console.log("Missing", map);
		}
	}
	
	addGeometryToObject(geometryId, uniqueId, loader, gpuBufferManager, quantFromAabb) {
		var geometry = loader.geometries.get(geometryId);
		if (geometry == null) {
			if (this.geometryCache.has(geometryId)) {
				geometry = this.geometryCache.get(geometryId);
			} else {
				console.error("Missing geometry id", geometryId);
				return;
			}
		}
		var object = loader.objects.get(uniqueId);
		this.addGeometry(loader.loaderId, geometry, object, quantFromAabb);
		object.geometry.push(geometryId);
		if (geometry.isReused) {
			geometry.reuseMaterialized++;
			if (geometry.reuseMaterialized == geometry.reused) {
				this.addGeometryReusable(geometry, loader, gpuBufferManager);
				loader.geometries.delete(geometry.id);
			} else if (geometry.reuseMaterialized % 256 == 0) {
				// TODO this 256 is now equal to the number in the vertex shader, at some point this should become dynamic, based on hardware capabilities
				console.log("Flushing 256");
				this.addGeometryReusable(geometry, loader, gpuBufferManager);
				geometry.objects = [];
				geometry.matrices = [];
			}
		}
	}
	
	addGeometryReusable(geometry, loader, gpuBufferManager) {
		var programInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(true, false));
		var lineProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(true, false, true));
        var pickProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(true, true));

		const numInstances = geometry.objects.length;

		const positionBuffer = Utils.createBuffer(this.gl, this.bufferTransformer.convertVertices(geometry.uniqueModelId, geometry.positions));
		const normalBuffer = Utils.createBuffer(this.gl, this.bufferTransformer.convertNormals(geometry.normals), null, this.gl.ARRAY_BUFFER, this.settings.loaderSettings.octEncodeNormals ? 2 : 3);
		const colorBuffer = geometry.colors != null
			? Utils.createBuffer(this.gl, geometry.colors, null, this.gl.ARRAY_BUFFER, 4)
			: null;
		const indexBuffer = Utils.createIndexBuffer(this.gl, this.bufferTransformer.convertIndices(geometry.indices, geometry.positions.length));
		const lineIndexBuffer = geometry.lineIndices ? Utils.createLineIndexBuffer(this.gl, this.bufferTransformer.convertIndices(geometry.lineIndices, geometry.positions.length)) : null;
		
		let color, colorHash;

		if (this.settings.useObjectColors) {
			color = [geometry.color.r, geometry.color.g, geometry.color.b, geometry.color.a];
			colorHash = Utils.hash(JSON.stringify(geometry.color));
		}

		let buffer = new FrozenBufferSet(
			this.viewer,
			null,
			
			positionBuffer,
			normalBuffer,
			colorBuffer,
			null,
			indexBuffer,
			lineIndexBuffer,
			
			color,
			colorHash,
			
			geometry.indices.length,
			geometry.lineIndices ? geometry.lineIndices.length : 0,
			normalBuffer.N,
			positionBuffer.N,
			colorBuffer.N,
			
			null,
			null,
			null,

			geometry.hasTransparency,
			geometry.hasTwoSidedTriangles,
			true,
			this,
			gpuBufferManager,

			geometry.roid,
			geometry.uniqueModelId
		);

		buffer.numInstances = numInstances;
		buffer.nrTrianglesToDraw = (buffer.nrIndices / 3) * geometry.matrices.length;
		buffer.nrLinesToDraw = (buffer.nrLineIndices / 2) * geometry.matrices.length;
		
		buffer.setObjects(this.gl, geometry.objects);
		buffer.buildVao(this.gl, this.settings, programInfo, pickProgramInfo, lineProgramInfo);

		buffer.uniqueIdToIndex = new AvlTree(this.viewer.inverseUniqueIdCompareFunction);
		
		geometry.objects.forEach((obj) => {
			buffer.uniqueIdSet.add(obj.uniqueId);
			this.viewer.uniqueIdToBufferSet.set(obj.uniqueId, [buffer]);
		});

		gpuBufferManager.pushBuffer(buffer);

		this.nrTrianglesLoaded += buffer.nrTrianglesToDraw;
		this.nrLinesLoaded += buffer.nrLinesToDraw;
		this.nrPrimitivesLoaded += buffer.nrTrianglesToDraw + buffer.nrLinesToDraw;
		this.viewer.stats.inc("Primitives", "Nr primitives loaded", buffer.nrTrianglesToDraw + buffer.nrLinesToDraw);
		if (this.progressListener != null) {
			this.progressListener(this.nrTrianglesLoaded, this.nrLinesLoaded);
		}

		var toadd = 
			geometry.bytes + 
			geometry.matrices.length * 16 * 4 + // vertex matrices
			geometry.matrices.length * 9 * 4; // normal matrices
		
		this.viewer.stats.inc("Data", "GPU bytes reuse", toadd);
		this.viewer.stats.inc("Data", "GPU bytes total", toadd);

		geometry.buffer = buffer;
	}
	
	getLoader(loaderId) {
		return this.loaders.get(loaderId);
	}
	
	removeLoader(loaderId) {
		this.loaders.delete(loaderId);
	}
	
	getObject(loaderId, identifier) {
		return this.getLoader(loaderId).objects.get(identifier);
	}
	
	registerLoader(loaderId) {
		this.loaders.set(loaderId, {
			loaderId: loaderId,
			objects: new Map(),
			geometries: new Map()
		});
	}
	
	renderBuffers(transparency, twoSidedTriangles, reuse, lineRender, visibleElements) {
		console.log("Not implemented in this layer");
	}
	
	/*
	 * Prepare the rendering pass, this is called only once for each frame
	 */	
	prepareRender() {
		// this.lastUniqueModelIdRendered is used to keep track of which uniqueModelId was rendered previously, so we can skip some GPU calls, need to reset it though for each new frame
		this.lastUniqueModelIdRendered = null;
	}
	
	render(transparency, lineRender, twoSidedTriangles, visibleElements) {
		this.renderBuffers(transparency, twoSidedTriangles, false, lineRender, visibleElements);
		if (this.settings.gpuReuse) {
			this.renderBuffers(transparency, twoSidedTriangles, true, lineRender, visibleElements);
		}
	}
	
	renderFinalBuffers(buffers, programInfo, visibleElements, lines) {
		if (buffers != null && buffers.length > 0) {
			let picking = visibleElements.pass === 'pick';
			var lastUsedColorHash = null;

			for (let buffer of buffers) {
				if (!picking && this.settings.useObjectColors) {
					if (lastUsedColorHash == null || lastUsedColorHash != buffer.colorHash) {
						this.gl.uniform4fv(programInfo.uniformLocations.vertexColor, buffer.color);
						lastUsedColorHash = buffer.colorHash;
					}
				}

				if (buffer.unquantizationMatrix != null && programInfo.lastUnquantizationMatrixUsed != buffer.unquantizationMatrix) {
					this.gl.uniformMatrix4fv(programInfo.uniformLocations.vertexQuantizationMatrix, false, buffer.unquantizationMatrix);
					programInfo.lastUnquantizationMatrixUsed = buffer.unquantizationMatrix;
				}

				this.renderBuffer(buffer, programInfo, visibleElements, lines);
			}
		}
	}
	
	renderBuffer(buffer, programInfo, visibleElements, lines) {
		const gl = this.gl;
		
		// console.log(programInfo.uniformLocations);

		let picking = visibleElements.pass === 'pick';
		gl.bindVertexArray(picking ? buffer.vaoPick : (lines ? buffer.lineRenderVao : buffer.vao));
		if (buffer.reuse) {
			if (this.viewer.settings.quantizeVertices) {
				if (buffer.uniqueModelId) {
					if (this.lastUniqueModelIdRendered === buffer.uniqueModelId && false) {
						// Skip it, this needs clarification, disabling for now because that seems to fix picking for instanced rendering
					} else {
						let uqm = this.viewer.vertexQuantization.getUntransformedInverseVertexQuantizationMatrixForUniqueModelId(buffer.uniqueModelId);
						gl.uniformMatrix4fv(programInfo.uniformLocations.vertexQuantizationMatrix, false, uqm);
						this.lastUniqueModelIdRendered = buffer.uniqueModelId;
					}
				} else {
					console.log("No uniqueModelId");
				}
			}
			
			let subset = buffer.computeVisibleInstances(visibleElements, this.gl);
			if (subset.somethingVisible) {
				if (subset.instanceIds.length > this.instanceSelectionData.length) {
					console.error("Too many instances of a geometry are activated.");
				} else {
					// A bit unreadable, but much faster than concat + join
					const instanceVisibilityState = 
						(visibleElements.pass != null ? (visibleElements.pass + ",") : "") +
						(subset.hidden != null ? (subset.hidden + ",") : "") + 
						subset.instanceIds.join(",");

					// Disabled caching for now because it's does seem to hide all line renders
//					if (instanceVisibilityState !== this.previousInstanceVisibilityState) {
						this.instanceSelectionData.fill(UINT32_MAX);
						this.instanceSelectionData.set(subset.instanceIds);

						// TODO Maybe we can store this in a buffer instead of send it as a uniform?
						
						// console.log("selection", visibleElements.pass, subset.hidden ? "hide" : "show", ...this.instanceSelectionData.subarray(0, subset.instanceIds.length));
						gl.uniform1uiv(programInfo.uniformLocations.containedInstances, this.instanceSelectionData);
						gl.uniform1ui(programInfo.uniformLocations.numContainedInstances, subset.instanceIds.length);
						gl.uniform1ui(programInfo.uniformLocations.containedMeansHidden, subset.hidden ? 1 : 0);
						
						// Ruben: Is this really ok? Do we need to clear it? Can this really be stored in the renderlayer?
						this.previousInstanceVisibilityState = instanceVisibilityState;
//					}
					if (lines) {
						if (buffer.lineIndexBuffer) {
							gl.drawElementsInstanced(this.gl.LINES, buffer.lineIndexBuffer.N, buffer.indexType, 0, buffer.nrProcessedMatrices);
						}
					} else {
						gl.drawElementsInstanced(this.gl.TRIANGLES, buffer.indexBuffer.N, buffer.indexType, 0, buffer.nrProcessedMatrices);
					}
				}
			}
		} else {
			if (buffer.uniqueId) {
				// This is a buffer for one specific element, probably created when
				// a call to setColor() changed the transparency state of an element.
				let include = true;
				if (visibleElements.with && !visibleElements.with.has(buffer.uniqueId)) {
					include = false;
				} else if (visibleElements.without && visibleElements.without.has(buffer.uniqueId)) {
					include = false;
				}
				if (include) {
					if (lines) {
						this.gl.drawElements(this.gl.LINES, buffer.nrLinesToDraw * 2, this.gl.UNSIGNED_INT, 0);
					} else {
						this.gl.drawElements(this.gl.TRIANGLES, buffer.nrTrianglesToDraw * 3, this.gl.UNSIGNED_INT, 0);
					}
				}
			} else {
				const visibleRanges = buffer.computeVisibleRangesAsBuffers(visibleElements, this.gl);
				if (visibleRanges && visibleRanges.pos > 0) {
					// TODO add buffer.nrTrianglesToDraw code
					
					if (WEBGL_multi_draw) {
						// This is available on Chrome Canary 75
						if (lines) {
							WEBGL_multi_draw.multiDrawElementsWEBGL(this.gl.LINES, visibleRanges.lineRenderCounts, 0, this.gl.UNSIGNED_INT, visibleRanges.lineRenderOffsetsBytes, 0, visibleRanges.pos);
						} else {
							WEBGL_multi_draw.multiDrawElementsWEBGL(this.gl.TRIANGLES, visibleRanges.counts, 0, this.gl.UNSIGNED_INT, visibleRanges.offsetsBytes, 0, visibleRanges.pos);
						}
					} else {
						// A manual loop using the same range data
						if (lines) {
							for (let i = 0; i < visibleRanges.pos; ++i) {
								this.gl.drawElements(this.gl.LINES, visibleRanges.lineRenderCounts[i], this.gl.UNSIGNED_INT, visibleRanges.lineRenderOffsetsBytes[i]);
							}
						} else {
							for (let i = 0; i < visibleRanges.pos; ++i) {
								this.gl.drawElements(this.gl.TRIANGLES, visibleRanges.counts[i], this.gl.UNSIGNED_INT, visibleRanges.offsetsBytes[i]);
							}
						}
					}
				}
			}
		}
		// Enabled, this kind of doubles the amount of GPU calls during rendering, but disabled resulted in errors, somehow some old buffers keep being used if we don't do this
		this.gl.bindVertexArray(null);
	}	

	/**
	 * Add a buffer that is already prepared
	 */
	addCompleteBuffer(buffer, gpuBufferManager) {
		var newBuffer = null;

		if (buffer == null) {
			return newBuffer;
		}
		if (buffer.nrIndices == 0) {
			return newBuffer;
		}
		
		var programInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, false));
        var pickProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, true));
		var lineProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, false, true));

		if (!this.settings.fakeLoading) {
			newBuffer = new FrozenBufferSet(
				this.viewer,
				buffer,
				
				buffer.vertices,
				buffer.normals,
				buffer.colors,
				buffer.pickColors,
				buffer.indices,
				buffer.lineIndices,
				
				null,
				0,
				
				buffer.nrIndices,
				buffer.nrLineIndices,
				buffer.normalsIndex,
				buffer.positionsIndex,
				buffer.colorsIndex,
				
				null,
				null,
				null,

				buffer.hasTransparency,
				buffer.hasTwoSidedTriangles,
				false,
				this,
				gpuBufferManager
			);
			
			newBuffer.nrTrianglesToDraw = buffer.nrIndices / 3;
			newBuffer.nrLinesToDraw = buffer.nrLineIndices / 2;
			newBuffer.hasTwoSidedTriangles = buffer.hasTwoSidedTriangles;
			
			newBuffer.unquantizationMatrix = buffer.unquantizationMatrix;

			newBuffer.uniqueIdToIndex = buffer.uniqueIdToIndex;
			if (buffer.uniqueIdSet == null) {
				debugger;
			}
			newBuffer.uniqueIdSet = buffer.uniqueIdSet;
			
			newBuffer.buildVao(this.gl, this.settings, programInfo, pickProgramInfo, lineProgramInfo);
					
			gpuBufferManager.pushBuffer(newBuffer);
		}
		
//		this.incLoadedTriangles(buffer.indicesRead / 3);
		this.viewer.stats.inc("Data", "GPU bytes", buffer.bytes);
		this.viewer.stats.inc("Data", "GPU bytes total", buffer.bytes);

		this.viewer.stats.inc("Models", "Geometries", buffer.nrObjects);

		return newBuffer;
	}
	
	incLoadedPrimitives(triangles, lines) {
		this.nrTrianglesLoaded += triangles;
		this.nrLinesLoaded += lines;
		this.nrPrimitivesLoaded += triangles + lines;

		this.viewer.stats.inc("Primitives", "Nr primitives loaded", triangles + lines);
		if (this.progressListener != null) {
			this.progressListener(this.nrTrianglesLoaded, this.nrLinesLoaded);
		}
	}
	
	flushBuffer(buffer, gpuBufferManager) {
		var newBuffer = null;
		if (buffer == null) {
			return newBuffer;
		}
		if (buffer.nrIndices == 0) {
			return newBuffer;
		}
		
		var programInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, false));
		var pickProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, true));
		var lineProgramInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(false, false, true));

		if (!this.settings.fakeLoading) {
			const positionBuffer = Utils.createBuffer(this.gl, buffer.positions, buffer.positionsIndex);
			const normalBuffer = Utils.createBuffer(this.gl, buffer.normals, buffer.normalsIndex, this.gl.ARRAY_BUFFER, this.settings.loaderSettings.octEncodeNormals ? 2 : 3);
			var colorBuffer = buffer.colors
				? Utils.createBuffer(this.gl, buffer.colors, buffer.colorsIndex, this.gl.ARRAY_BUFFER, 4)
				: null;
			// Per-object pick vertex colors
			var pickColorBuffer = buffer.pickColors
				? Utils.createBuffer(this.gl, buffer.pickColors, buffer.pickColorsIndex, this.gl.ARRAY_BUFFER, 4)
				: null;
			const indexBuffer = Utils.createIndexBuffer(this.gl, buffer.indices, buffer.indicesIndex);
			if (buffer.lineIndices == null) {
				debugger;
			}
			const lineIndexBuffer = buffer.lineIndices ? Utils.createLineIndexBuffer(this.gl, buffer.lineIndices, buffer.lineIndicesIndex) : null;
			
			let color, colorHash;

			if (this.settings.useObjectColors) {
				color = [buffer.color.r, buffer.color.g, buffer.color.b, buffer.color.a];
				colorHash = Utils.hash(JSON.stringify(buffer.color));
			}

			newBuffer = new FrozenBufferSet(
				this.viewer,
				buffer,

				positionBuffer,
				normalBuffer,
				colorBuffer,
				pickColorBuffer,
				indexBuffer,
				lineIndexBuffer,
				
				color,
				colorHash,
				
				buffer.nrIndices,
				buffer.nrLineIndices,
				buffer.normalsIndex,
				buffer.positionsIndex,
				buffer.colorsIndex,
				
				null,
				null,
				null,

				buffer.hasTransparency,
				buffer.hasTwoSidedTriangles,
				false,
				this,
				gpuBufferManager
			);
			
			newBuffer.unquantizationMatrix = buffer.unquantizationMatrix;
			newBuffer.nrTrianglesToDraw = buffer.nrIndices / 3;
			newBuffer.nrLinesToDraw = buffer.nrLineIndices / 2;
			
			newBuffer.buildVao(this.gl, this.settings, programInfo, pickProgramInfo, lineProgramInfo);

			if (buffer.uniqueIdToIndex) {
				for (var key of buffer.uniqueIdToIndex.keys()) {
					var li = (this.viewer.uniqueIdToBufferSet.get(key) || []);
					li.push(newBuffer);
					this.viewer.uniqueIdToBufferSet.set(key, li);
				}
			}			
			
			gpuBufferManager.pushBuffer(newBuffer);
			this.viewer.dirty = 2;
		}
		
		if (!buffer.isCopy) {
			this.nrTrianglesLoaded += buffer.nrIndices / 3;
			this.nrLinesLoaded += buffer.lineIndices / 3;
			this.nrPrimitivesLoaded += buffer.nrIndices / 3 + buffer.lineIndices / 3;
			this.viewer.stats.inc("Primitives", "Nr primitives loaded", buffer.nrIndices / 3 + buffer.lineIndices / 3);
			if (this.progressListener != null) {
				this.progressListener(this.nrTrianglesLoaded, this.nrLinesLoaded);
			}
			this.viewer.stats.inc("Data", "GPU bytes", buffer.bytes);
			this.viewer.stats.inc("Data", "GPU bytes total", buffer.bytes);
			this.viewer.stats.inc("Buffers", "Buffer groups");
		}

		return newBuffer;
	}

	renderLines() {
		if (this.lines) {
			let bufferManager = this.gpuBufferManager;
			let viewer = bufferManager.viewer;
			this.lines.renderStart(viewer, this);
			this.lines.render(this.lineColour || outlineColor, this.selectionOutlineMatrix, this.lineWidth || 0.01);
			this.lines.renderStop();
		}
	}

	renderSelectionOutlines(ids, width, node) {
		let bufferManager = (node || this).gpuBufferManager;

		if (!bufferManager) {
			// probably a tile that has not been loaded yet
			return;
		}

		let viewer = bufferManager.viewer;

		var gl = this.gl;

		for (let transparency of false_true) {
			// TODO check for reuse setting
			for (let reuse of false_true) {
				for (let twoSidedTriangles of false_true) {
					var buffers = (node || this).gpuBufferManager.getBuffers(transparency, twoSidedTriangles, reuse);
					var lastLineRenderer = null;
					for (let buffer of buffers) {
						// TODO iterate over union of buffer.uniqueIds and ids
						for (var id of ids) {
							if (buffer.has(id)) {
								if (buffer.lineIndexBuffers) {
									let lines = buffer.getLines(id, this.gl);
									if (lines) {
										if (!lastLineRenderer) {
											// Kind of a dirty hack to only do the initialization once, we know the init result is the same for all buffers in this set, this improves the render speed when a lot of objects are selected
											lines.renderStart(viewer, this);
										}
										// TODO move outlineColor to renderStart, saves us another uniform, same probably for width
										// TODO selectionOutlineMatrix most of the is an identify matrix, no need to send that to the GPU?
										lines.render(outlineColor, lines.matrixMap.get(id) || this.selectionOutlineMatrix, width || 0.01);
										lastLineRenderer = lines;
									}
								}
							}
						}
					}
					if (lastLineRenderer) {
						lastLineRenderer.renderStop();
					}
				}
			}
		}
	}
}