Home Reference Source

viewer/gpubuffermanager.js

/**
 * Responsible for managing GPU buffers. There are 4 types of buffers:
 * - Transparent batched
 * - Opaque batched
 * - Transparent reused
 * - Opaque reused
 *  
 */
const TRANSPARENCY = 1;
const REUSE = 2;
const TWO_SIDED_TRIANGLES = 4;

export class GpuBufferManager {
	constructor(viewer) {
		this.viewer = viewer;
		this.gl = this.viewer.gl;
		this.settings = this.viewer.settings;
		
		this.buffers = new Map();
		
		for (var i=0; i<8; i++) {
			this.buffers.set(i, []);
		}
	}
	
	isEmpty() {
		for (var buffer of this.buffers) {
			if (buffer.length != 0) {
				return false;
			}
		}
		return true;
	}
	
	/* 
	 * Get a buffer based on two booleans: transparency and reuse
	 */
	getBuffers(transparency, twoSidedTriangles, reuse) {
		return this.buffers.get(
			(transparency ? TRANSPARENCY : 0) + 
			(twoSidedTriangles ? TWO_SIDED_TRIANGLES : 0) + 
			(reuse ? REUSE : 0));
	}
	
	pushBuffer(buffer) {
		// TODO this can potentially become slow when there are a lot of buffers
		let buffers = this.getBuffers(buffer.hasTransparency, buffer.hasTwoSidedTriangles, buffer.reuse);
		buffers.push(buffer);
		if (buffer.uniqueModelId) {
			this.sortBuffersByUniqueModelId(buffers);
		}
	}

	deleteBuffer(buffer) {
		let arr = this.getBuffers(buffer.hasTransparency, buffer.hasTwoSidedTriangles, buffer.reuse);
		let idx = arr.indexOf(buffer);
		if (idx === -1) {
			throw "Unable to find buffer to delete";
		}
		arr.splice(idx, 1);
	}
	
	sortAllBuffersByColor() {
		// Unused/untested
		for (var buffer of this.buffers) {
			this.sortBuffersByColor(buffer);
		}
	}
	
	sortBuffersByColor(buffers) {
		buffers.sort((a, b) => {
			for (var i=0; i<4; i++) {
				if (a.color[i] == b.color[i]) {
					continue;
				}
				return a.color[i] - b.color[i];
			}
			// Colors are the same
			return 0;
		});
	}
	
	sortBuffersByUniqueModelId(buffers) {
		buffers.sort((a, b) => {
			return a.uniqueModelId - b.uniqueModelId;
		});
	}
	
	/*
	 * This method will combine buffers on the GPU. It's disabled for now, not that it doesn't work, but it seems to generate quite a bit of "stuttering". Maybe we need to use a different type of buffer.
	 */
	combineBuffers() {
		// TODO this is not working currently
		// TODO also twoSidedTriangles has not been refactored in this method
		
		for (var transparency of [false, true]) {
			var buffers = this.getBuffers(transparency, false);
			
			// This is only done when useObjectColors is false for now, probably because that's going to be the default anyways
			
			if (buffers.length > 1 && !this.viewer.settings.useObjectColors) {
				console.log("Combining buffers", buffers.length);
				
				var nrPositions = 0;
				var nrNormals = 0;
				var nrIndices = 0;
				var nrColors = 0;
				
				for (var buffer of buffers) {
					nrPositions += buffer.nrPositions;
					nrNormals += buffer.nrNormals;
					nrIndices += buffer.nrIndices;
					nrColors += buffer.nrColors;
				}
				
				const positionBuffer = this.gl.createBuffer();
				this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, positionBuffer);
				this.gl.bufferData(this.gl.COPY_WRITE_BUFFER, nrPositions * (this.settings.quantizeVertices ? 2 : 4) , this.gl.STATIC_DRAW);
				
				const normalBuffer = this.gl.createBuffer();
				this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, normalBuffer);
				this.gl.bufferData(this.gl.COPY_WRITE_BUFFER, nrNormals * (this.settings.quantizeNormals ? 1 : 4), this.gl.STATIC_DRAW);

				var colorBuffer = this.gl.createBuffer();
				this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, colorBuffer);
				this.gl.bufferData(this.gl.COPY_WRITE_BUFFER, nrColors * (this.settings.quantizeColors ? 1 : 4), this.gl.STATIC_DRAW);
				
				const indexBuffer = this.gl.createBuffer();
				this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
				this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, nrIndices * 4, this.gl.STATIC_DRAW);

				var positionsOffset = 0;
				var normalsOffset = 0;
				var indicesOffset = 0;
				var colorsOffset = 0;

				for (var buffer of buffers) {
					this.gl.bindBuffer(this.gl.COPY_READ_BUFFER, buffer.positionBuffer);
					this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, positionBuffer);
					this.gl.copyBufferSubData(this.gl.COPY_READ_BUFFER, this.gl.COPY_WRITE_BUFFER, 0, positionsOffset * (this.settings.quantizeVertices ? 2 : 4), buffer.nrPositions * (this.settings.quantizeVertices ? 2 : 4));
					
					this.gl.bindBuffer(this.gl.COPY_READ_BUFFER, buffer.normalBuffer);
					this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, normalBuffer);
					this.gl.copyBufferSubData(this.gl.COPY_READ_BUFFER, this.gl.COPY_WRITE_BUFFER, 0, normalsOffset * (this.settings.quantizeNormals ? 1 : 4), buffer.nrNormals * (this.settings.quantizeNormals ? 1 : 4));

					this.gl.bindBuffer(this.gl.COPY_READ_BUFFER, buffer.colorBuffer);
					this.gl.bindBuffer(this.gl.COPY_WRITE_BUFFER, colorBuffer);
					this.gl.copyBufferSubData(this.gl.COPY_READ_BUFFER, this.gl.COPY_WRITE_BUFFER, 0, colorsOffset * (this.settings.quantizeColors ? 1 : 4), buffer.nrColors * (this.settings.quantizeColors ? 1 : 4));

					if (positionsOffset == 0) {
						// Minor optimization for the first buffer
						this.gl.bindBuffer(this.gl.COPY_READ_BUFFER, buffer.indexBuffer);
						this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
						this.gl.copyBufferSubData(this.gl.COPY_READ_BUFFER, this.gl.ELEMENT_ARRAY_BUFFER, 0, 0, buffer.nrIndices * 4);
					} else {
						var startIndex = positionsOffset / 3;
						
						this.gl.bindBuffer(this.gl.COPY_READ_BUFFER, buffer.indexBuffer);
						var tmpIndexBuffer = new Int32Array(buffer.nrIndices);
						this.gl.getBufferSubData(this.gl.COPY_READ_BUFFER, 0, tmpIndexBuffer, 0, buffer.nrIndices);
						
						for (var i=0; i<buffer.nrIndices; i++) {
							tmpIndexBuffer[i] = tmpIndexBuffer[i] + startIndex;
						}
						
						this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
						this.gl.bufferSubData(this.gl.ELEMENT_ARRAY_BUFFER, indicesOffset * 4, tmpIndexBuffer, 0, buffer.nrIndices);
					}

					this.gl.deleteBuffer(buffer.positionBuffer);
					this.gl.deleteBuffer(buffer.normalBuffer);
					this.gl.deleteBuffer(buffer.colorBuffer);
					this.gl.deleteBuffer(buffer.indexBuffer);
					
					this.gl.deleteVertexArray(buffer.vao);
					
					positionsOffset += buffer.nrPositions;
					normalsOffset += buffer.nrNormals;
					indicesOffset += buffer.nrIndices;
					colorsOffset += buffer.nrColors;
				}
				
				var programInfo = this.viewer.programManager.getProgram({
					picking: false,
					instancing: false,
					useObjectColors: this.settings.useObjectColors,
					quantizeNormals: this.settings.quantizeNormals,
					quantizeVertices: this.settings.quantizeVertices,
					quantizeColors: this.settings.quantizeColors
				});
				
				var vao = this.gl.createVertexArray();
				this.gl.bindVertexArray(vao);

				{
					const numComponents = 3;
					const normalize = false;
					const stride = 0;
					const offset = 0;
					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
					if (this.settings.quantizeVertices) {
						this.gl.vertexAttribIPointer(programInfo.attribLocations.vertexPosition, numComponents, this.gl.SHORT, normalize, stride, offset);
					} else {
						this.gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, this.gl.FLOAT, normalize, stride, offset);
					}
					this.gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
				}
				{
					const numComponents = 3;
					const normalize = false;
					const stride = 0;
					const offset = 0;
					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, normalBuffer);
					if (this.settings.quantizeNormals) {
						this.gl.vertexAttribIPointer(programInfo.attribLocations.vertexNormal, numComponents, this.gl.BYTE, normalize, stride, offset);
					} else {
						this.gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, this.gl.FLOAT, normalize, stride, offset);
					}
					this.gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal);
				}
				{
					const numComponents = 4;
					const normalize = false;
					const stride = 0;
					const offset = 0;
					this.gl.bindBuffer(this.gl.ARRAY_BUFFER, colorBuffer);
					if (this.settings.quantizeColors) {
						this.gl.vertexAttribIPointer(programInfo.attribLocations.vertexColor, numComponents, this.gl.UNSIGNED_BYTE, normalize, stride, offset);
					} else {
						this.gl.vertexAttribPointer(programInfo.attribLocations.vertexColor, numComponents, this.gl.FLOAT, normalize, stride, offset);
					}
					this.gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
				}

				this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

				this.gl.bindVertexArray(null);
				
				var newBuffer = {
					positionBuffer: positionBuffer,
					normalBuffer: normalBuffer,
					indexBuffer: indexBuffer,
					colorBuffer: colorBuffer,
					nrIndices: indicesOffset,
					nrPositions: positionsOffset,
					nrNormals: normalsOffset,
					nrColors: colorsOffset,
					vao: vao,
					hasTransparency: transparency,
					reuse: false
				};
				
				var previousLength = buffers.length;
				buffers.length = 0;
				buffers.push(newBuffer);
				
				return previousLength - 1;
			}
		}
		return 0;
	}
}