viewer/geometryloader.js
import * as vec4 from "./glmatrix/vec4.js";
import * as mat4 from "./glmatrix/mat4.js";
import * as mat3 from "./glmatrix/mat3.js";
import {Utils} from "./utils.js";
import {DataInputStream} from "./datainputstream.js";
const PROTOCOL_VERSION = 17;
// temporary for emergency quantization
const v4 = vec4.create();
/**
* This class is supposed to be and stay BIMserver-free.
*/
export class GeometryLoader {
constructor(loaderId, renderLayer, loaderSettings, vertexQuantizationMatrices, stats, settings, geometryCache, gpuBufferManager, usePreparedBuffers) {
this.renderLayer = renderLayer;
this.settings = settings;
this.loaderSettings = loaderSettings;
this.loaderId = loaderId;
this.vertexQuantizationMatrices = vertexQuantizationMatrices;
this.stats = stats;
this.geometryCache = geometryCache;
this.state = {};
this.objectAddedListeners = [];
this.geometryIds = new Map();
this.dataToInfo = new Map();
this.gpuBufferManager = gpuBufferManager;
if (usePreparedBuffers) {
this.createdTransparentObjects = new Map(); // object id -> object info
this.createdOpaqueObjects = new Map(); // object id -> object info
}
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
});
}
processPreparedBufferInit(stream, hasTransparancy) {
this.preparedBuffer = {};
this.preparedBuffer.nrObjects = stream.readInt();
this.preparedBuffer.nrIndices = stream.readInt();
this.preparedBuffer.positionsIndex = stream.readInt();
this.preparedBuffer.normalsIndex = stream.readInt();
this.preparedBuffer.colorsIndex = stream.readInt();
this.preparedBuffer.nrObjectsRead = 0;
this.preparedBuffer.nrColors = this.preparedBuffer.positionsIndex * 4 / 3;
this.preparedBuffer.indices = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrIndices, this.renderLayer.gl.ELEMENT_ARRAY_BUFFER, 3, WebGL2RenderingContext.UNSIGNED_INT, "Uint32Array");
this.preparedBuffer.colors = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrColors, this.renderLayer.gl.ARRAY_BUFFER, 4, WebGL2RenderingContext.UNSIGNED_BYTE, "Uint8Array");
this.preparedBuffer.vertices = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.positionsIndex, this.renderLayer.gl.ARRAY_BUFFER, 3, this.loaderSettings.quantizeVertices ? WebGL2RenderingContext.SHORT : WebGL2RenderingContext.FLOAT, this.loaderSettings.quantizeVertices ? "Int16Array" : "Float32Array");
this.preparedBuffer.normals = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.normalsIndex, this.renderLayer.gl.ARRAY_BUFFER, 3, WebGL2RenderingContext.BYTE, "Int8Array");
this.preparedBuffer.pickColors = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrColors, this.renderLayer.gl.ARRAY_BUFFER, 4, WebGL2RenderingContext.UNSIGNED_BYTE, "Uint8Array");
this.preparedBuffer.geometryIdToIndex = new Map();
this.preparedBuffer.loaderId = this.loaderId;
this.preparedBuffer.hasTransparency = hasTransparancy;
if (this.loaderSettings.quantizeVertices) {
this.preparedBuffer.unquantizationMatrix = this.unquantizationMatrix;
}
this.preparedBuffer.bytes = Utils.calculateBytesUsed(this.settings, this.preparedBuffer.positionsIndex, this.preparedBuffer.nrColors, this.preparedBuffer.nrIndices, this.preparedBuffer.normalsIndex);
stream.align8();
}
processPreparedBuffer(stream, hasTransparancy, forceUnquantized) {
const nrObjects = stream.readInt();
const totalNrIndices = stream.readInt();
const positionsIndex = stream.readInt();
const normalsIndex = stream.readInt();
const colorsIndex = stream.readInt();
if (this.preparedBuffer.nrIndices == 0) {
return;
}
const previousStartIndex = this.preparedBuffer.indices.writePosition / 4;
const previousColorIndex = this.preparedBuffer.colors.writePosition;
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.indices, stream.dataView, stream.pos, totalNrIndices);
stream.pos += totalNrIndices * 4;
var nrColors = positionsIndex * 4 / 3;
var colors = new Uint8Array(nrColors);
var colors32 = new Uint32Array(colors.buffer);
var createdObjects = null;
if (hasTransparancy) {
createdObjects = this.createdTransparentObjects;
} else {
createdObjects = this.createdOpaqueObjects;
}
var pickColors = new Uint8Array(positionsIndex * 4);
var pickColors32 = new Uint32Array(pickColors.buffer);
pickColors.i = 0;
var currentColorIndex = 0;
var tmpOids = new Set();
for (var i = 0; i < nrObjects; i++) {
var oid = stream.readLong();
tmpOids.add(oid);
var startIndex = stream.readInt();
var nrIndices = stream.readInt();
var nrVertices = stream.readInt();
var minIndex = stream.readInt();
var maxIndex = stream.readInt();
var nrObjectColors = nrVertices / 3 * 4;
const density = stream.readFloat();
var colorPackSize = stream.readInt();
if (createdObjects) {
var object = createdObjects.get(oid);
object.density = density;
} else {
this.renderLayer.viewer.addViewObject(oid, {pickId: oid});
var pickColor = this.renderLayer.viewer.getPickColor(oid);
var color32 = pickColor[0] + pickColor[1] * 256 + pickColor[2] * 256 * 256 + pickColor[3] * 256 * 256 * 256;
pickColors32.fill(color32, pickColors.i / 4, (pickColors.i + nrObjectColors) / 4);
pickColors.i += nrObjectColors;
}
const meta = {
start: previousStartIndex + startIndex,
length: nrIndices,
color: previousColorIndex + currentColorIndex,
colorLength: nrObjectColors,
minIndex: minIndex,
maxIndex: maxIndex
};
this.preparedBuffer.geometryIdToIndex.set(oid, [meta]);
if (colorPackSize == 0) {
// Generate default colors for this object
var defaultColor = this.renderLayer.viewer.defaultColors[object.type];
if (defaultColor == null) {
defaultColor = this.renderLayer.viewer.defaultColors.DEFAULT;
}
if (defaultColor.asInt == null) {
// Cache the integer version
var color = new Uint8Array(4);
color[0] = defaultColor.r * 255;
color[1] = defaultColor.g * 255;
color[2] = defaultColor.b * 255;
color[3] = defaultColor.a * 255;
defaultColor.asInt = color[0] + color[1] * 256 + color[2] * 65536 + color[3] * 16777216;
}
colors32.fill(defaultColor.asInt, currentColorIndex / 4, (currentColorIndex + nrObjectColors) / 4);
currentColorIndex += nrObjectColors;
}
for (var j = 0; j < colorPackSize; j++) {
var count = stream.readInt();
var color = stream.readUnsignedByteArray(4);
var color32 = color[0] + color[1] * 256 + color[2] * 65536 + color[3] * 16777216;
colors32.fill(color32, (currentColorIndex / 4), (currentColorIndex + count) / 4);
currentColorIndex += count;
}
}
if (currentColorIndex != nrColors) {
console.error(currentColorIndex, nrColors);
}
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.colors, colors, 0, nrColors);
if (this.loaderSettings.quantizeVertices) {
if (forceUnquantized) {
// we need to do some alignment here
const floats = new Float32Array(positionsIndex);
const aligned_u8 = new Uint8Array(floats.buffer);
const unaligned_u8 = new Uint8Array(stream.dataView.buffer, stream.pos, aligned_u8.length);
stream.pos += aligned_u8.length;
aligned_u8.set(unaligned_u8);
// now we can quantize the input
// admittedly there was code for this in BufferTransformer, but it was commented out?
const m4 = this.vertexQuantization.vertexQuantizationMatrix;
for (var i = 0; i < floats.length; i += 3) {
v4.set(floats.subarray(i, i + 3));
v4[3] = 1.0;
vec4.transformMat4(v4, v4, m4);
floats.set(v4.subarray(0, 3), i);
}
const quantized = new Int16Array(floats);
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, quantized, 0, quantized.length);
} else {
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, stream.dataView, stream.pos, positionsIndex);
stream.pos += positionsIndex * 2;
}
} else {
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, stream.dataView, stream.pos, positionsIndex);
stream.pos += positionsIndex * 4;
}
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.normals, stream.dataView, stream.pos, normalsIndex);
stream.pos += normalsIndex;
if (createdObjects) {
for (var [oid, objectInfo] of createdObjects) {
if (tmpOids.has(oid)) {
var pickColor = this.renderLayer.viewer.getPickColor(oid);
var color32 = pickColor[0] + pickColor[1] * 256 + pickColor[2] * 256 * 256 + pickColor[3] * 256 * 256 * 256;
var lenObjectPickColors = objectInfo.nrColors;
pickColors32.fill(color32, pickColors.i / 4, (pickColors.i + lenObjectPickColors) / 4);
pickColors.i += lenObjectPickColors;
}
}
}
Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.pickColors, pickColors, 0, pickColors.i);
this.preparedBuffer.nrObjectsRead += nrObjects;
if (this.preparedBuffer.nrObjectsRead == this.preparedBuffer.nrObjects) {
// Making a copy of the map, making sure it's sorted by oid, which will make other things much faster later on
this.preparedBuffer.geometryIdToIndex = Utils.sortMapKeys(this.preparedBuffer.geometryIdToIndex);
this.renderLayer.addCompleteBuffer(this.preparedBuffer, this.gpuBufferManager);
}
stream.align8();
}
processMessage(stream) {
var messageType = stream.readByte();
if (messageType == 0) {
if (!this.readStart(stream)) {
// An error occured, usually version mismatch or missing serializer
return false;
}
} else if (messageType == 6) {
this.readEnd(stream);
} else {
this.readObject(stream, messageType);
}
stream.align8();
return stream.remaining() > 0;
}
binaryDataListener(data) {
this.stats.inc("Network", "Bytes OTL", data.byteLength);
var stream = new DataInputStream(data);
var channel = stream.readLong();
var type = stream.readLong();
if (type == 0) {
while (this.processMessage(stream)) {
}
} else if (type == 1) {
// End of stream
}
}
geometryDataIdResolved(geometryDataId) {
this.dataToInfo.delete(geometryDataId);
if (this.dataToInfo.size == 0) {
// Only resolve (and cleanup this loader) when all has been loaded
this.resolve();
}
}
readObject(stream, geometryType) {
var geometryId;
var numGeometries;
var numParts;
var objectBounds;
var numIndices;
var indices;
var numPositions;
var positions;
var numNormals;
var normals;
var numColors;
var colors = null;
if (geometryType == 1) {
// Geometry
var reused = stream.readInt();
var type = stream.readUTF8();
stream.align8();
var roid = stream.readLong();
var croid = stream.readLong();
var hasTransparency = stream.readLong() == 1;
var geometryDataId = stream.readLong();
this.readGeometry(stream, roid, croid, geometryDataId, geometryDataId, hasTransparency, reused, type, true);
if (this.dataToInfo.has(geometryDataId)) {
// There are objects that have already been loaded, that are waiting for this GeometryData
var oids = this.dataToInfo.get(geometryDataId);
for (var oid of oids) {
var ob = this.renderLayer.getObject(this.loaderId, oid);
if (ob == null) {
console.error("Object with oid not found", oid)
} else {
ob.add(geometryDataId, oid);
}
}
// Now we can clean it up, nobody is waiting anymore
this.dataToInfo.delete(geometryDataId);
}
} else if (geometryType == 5) {
// Object
var inPreparedBuffer = stream.readByte() == 1;
var oid = stream.readLong();
var type = stream.readUTF8();
var nrColors = stream.readInt();
stream.align8();
var roid = stream.readLong();
var geometryInfoOid = stream.readLong();
var hasTransparency = stream.readLong() == 1;
// Making copies here because otherwise we are potentially referring to very big buffers for as long as the viewer lives
var objectBounds = stream.readDoubleArrayCopy(6);
var matrix = stream.readDoubleArrayCopy(16);
var geometryDataOid = stream.readLong();
var geometryDataOidFound = geometryDataOid;
if (inPreparedBuffer) {
if (hasTransparency) {
this.createdTransparentObjects.set(oid, {
nrColors: nrColors,
type: type
});
} else {
this.createdOpaqueObjects.set(oid, {
nrColors: nrColors,
type: type
});
}
}
if (!inPreparedBuffer) {
if (!this.geometryIds.has(geometryDataOid)) {
if (this.geometryCache != null && this.geometryCache.has(geometryDataOid)) {
// We know it's cached
} else {
geometryDataOidFound = null;
// We don't have the data yet, it might come in this stream, or maybe in a later stream
var list = this.dataToInfo.get(geometryDataOid);
if (list == null) {
list = [oid];
this.dataToInfo.set(geometryDataOid, list);
} else {
list.push(oid);
}
}
}
} else {
geometryDataOidFound = null;
}
this.createObject(roid, oid, oid, geometryDataOidFound == null ? [] : [geometryDataOidFound], matrix, hasTransparency, type, objectBounds, inPreparedBuffer);
} else if (geometryType == 9) {
// Minimal object
var oid = stream.readLong();
var type = stream.readUTF8();
var nrColors = stream.readInt();
var roid = stream.readLong();
var geometryInfoOid = stream.readLong();
var hasTransparency = stream.readLong() == 1;
stream.align8();
var objectBounds = stream.readDoubleArrayCopy(6);
var geometryDataOid = stream.readLong();
var geometryDataOidFound = geometryDataOid;
if (hasTransparency) {
this.createdTransparentObjects.set(oid, {
nrColors: nrColors,
type: type
});
} else {
this.createdOpaqueObjects.set(oid, {
nrColors: nrColors,
type: type
});
}
this.createObject(roid, oid, oid, [], null, hasTransparency, type, objectBounds, true);
} else if (geometryType == 7) {
this.processPreparedBuffer(stream, true);
} else if (geometryType == 8) {
this.processPreparedBuffer(stream, false);
} else if (geometryType == 10) {
this.processPreparedBufferInit(stream, true);
} else if (geometryType == 11) {
this.processPreparedBufferInit(stream, false);
} else {
console.error("Unsupported geometry type: " + geometryType);
return;
}
this.state.nrObjectsRead++;
}
readGeometry(stream, roid, croid, geometryId, geometryDataOid, hasTransparency, reused, type, useIntForIndices) {
var numIndices = stream.readInt();
if (useIntForIndices) {
var indices = stream.readIntArray(numIndices);
} else {
var indices = stream.readShortArray(numIndices);
}
var color = this.readColors(stream, type);
var numPositions = stream.readInt();
if (this.loaderSettings.quantizeVertices) {
var positions = stream.readShortArray(numPositions);
stream.align8();
} else {
var positions = stream.readFloatArray(numPositions);
}
var numNormals = stream.readInt();
if (this.loaderSettings.quantizeNormals) {
var normals = stream.readByteArray(numNormals);
stream.align8();
} else {
var normals = stream.readFloatArray(numNormals);
}
var numColors = stream.readInt();
if (numColors > 0) {
if (this.loaderSettings.quantizeColors) {
var colors = stream.readUnsignedByteArray(numColors);
} else {
var colors = stream.readFloatArray(numColors);
}
} else if (color != null && !this.settings.useObjectColors) {
// When we are generating this data anyways, we might as well make sure it ends up in the format required by the GPU
if (this.settings.quantizeColors) {
var size = (4 * numPositions) / 3;
var colors = new Uint8Array(size);
var quantizedColor = new Uint8Array(4);
quantizedColor[0] = color.r * 255;
quantizedColor[1] = color.g * 255;
quantizedColor[2] = color.b * 255;
quantizedColor[3] = color.a * 255;
for (var i=0; i < size / 4; i++) {
colors.set(quantizedColor, i * 4);
}
} else {
var size = (4 * numPositions) / 3;
var colors = new Float32Array(size);
var nonQuantizedColor = new Float32Array(4);
nonQuantizedColor[0] = color.r;
nonQuantizedColor[1] = color.g;
nonQuantizedColor[2] = color.b;
nonQuantizedColor[3] = color.a;
for (var i=0; i < size / 4; i++) {
colors.set(nonQuantizedColor, i * 4);
}
}
}
if (this.settings.useObjectColors) {
colors = null;
}
if (!this.geometryIds.has(geometryDataOid)) {
this.geometryIds.set(geometryDataOid, true);
}
if (colors.length == 0) {
debugger;
}
this.renderLayer.createGeometry(this.loaderId, roid, croid, geometryDataOid, positions, normals, colors, color, indices, hasTransparency, reused);
}
readColors(stream, type) {
var b = stream.readInt();
if (b == 1) {
var color = {r: stream.readFloat(), g: stream.readFloat(), b: stream.readFloat(), a: stream.readFloat()};
} else {
var defaultColor = this.renderLayer.viewer.defaultColors[type];
if (defaultColor == null) {
var color = {
r: 0,
g: 1,
b: 0,
a: 1
};
} else {
color = defaultColor;
}
}
stream.align4();
return color;
}
createObject(roid, oid, objectId, geometryIds, matrix, hasTransparency, type, aabb, inCompleteBuffer) {
if (this.state.mode == 0) {
console.log("Mode is still 0, should be 1");
return;
}
if (!inCompleteBuffer) {
if (this.multiplierToMm < 0.99 || this.multiplierToMm > 1.01) {
// We need to change the matrix because the operations in the matrix
// are based on the original geometry (e.a. not scaled to the right
// unit: mm)
var scaleMatrix = mat4.create();
mat4.scale(scaleMatrix, scaleMatrix, [this.multiplierToMm, this.multiplierToMm, this.multiplierToMm]);
var invertedScaleMatrix = mat4.create();
mat4.invert(invertedScaleMatrix, scaleMatrix);
var totalMatrix = mat4.create();
// Read} from bottom to top
// 3. Apply the scaling again
mat4.multiply(totalMatrix, totalMatrix, scaleMatrix);
// 2. Apply the matrix
mat4.multiply(totalMatrix, totalMatrix, matrix);
// 1. Server has already scaled the vertices, scale it back to the
// original values
mat4.multiply(totalMatrix, totalMatrix, invertedScaleMatrix);
matrix = totalMatrix;
}
var normalMatrix = mat3.create();
mat3.fromMat4(normalMatrix, matrix);
mat3.invert(normalMatrix, normalMatrix);
mat3.transpose(normalMatrix, normalMatrix);
}
this.renderLayer.createObject(this.loaderId, roid, oid, objectId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb);
}
readStart(data) {
var start = data.readUTF8();
if (start != "BGS") {
console.error("Data does not start with BGS (" + start + ")");
return false;
}
this.protocolVersion = data.readByte();
if (this.protocolVersion != PROTOCOL_VERSION) {
console.error("Unimplemented version (protocol: " + this.protocolVersion + ", implemented: " + PROTOCOL_VERSION + ").\nUsually this means you need to either:\n\t- Update the BinarySerializers plugin bundle in BIMserver\n\t- Update your version of BIMsurfer 3");
return false;
}
this.multiplierToMm = data.readFloat();
data.align8();
var boundary = data.readDoubleArray(6);
this.state.mode = 1;
return true;
}
initiateDownload() {
this.state = {
mode: 0,
nrObjectsRead: 0,
nrObjects: 0
};
}
start() {
if (this.onStart != null) {
this.onStart();
}
var obj = [];
this.initiateDownload();
var loaderSettings = JSON.parse(JSON.stringify(this.loaderSettings));
return this.promise;
}
readEnd(data) {
if (this.dataToInfo.size > 0) {
// We need to tell the renderlayer that not all data has been loaded
this.renderLayer.storeMissingGeometry(this, this.dataToInfo);
}
if (this.dataToInfo.size == 0) {
// Only resolve (and cleanup this loader) when all has been loaded
this.resolve();
}
}
// This promise is fired as soon as the GeometryLoader is done
getPromise() {
return this.promise;
}
}