viewer/abstractbufferset.js
import {FatLineRenderer} from "./fatlinerenderer.js";
var counter = 1;
/**
* @ignore
*/
export class AbstractBufferSet {
constructor(viewer) {
this.viewer = viewer;
this.geometryIdToIndex = new Map();
// Unique id per bufferset, easier to use as Map key
this.id = counter++;
}
/**
* Creates new buffers, but is more efficient than joinConsecutiveRanges, funky buffer layers looks this way because we can directly send it to the GPU with multiDrawElementsWEBGL
*/
joinConsecutiveRangesAsBuffers(input) {
var result = {
offsets: new Int32Array(input.pos),
counts: new Int32Array(input.pos),
pos: 0
};
for (var i=0; i<input.pos; i++) {
var offset = input.offsets[i];
var totalCount = input.counts[i];
while (i < input.pos && input.offsets[i] + input.counts[i] == input.offsets[i + 1]) {
i++;
totalCount += input.counts[i];
}
result.offsets[result.pos] = offset;
result.counts[result.pos] = totalCount;
result.pos++;
}
// console.log("Joined", input.pos, result.pos);
return result;
}
/**
* More efficient version of complementRanges, but also creates new buffers.
*/
complementRangesAsBuffers(input) {
if (input.pos == 0) {
// Special case, inverting all
return {
counts: new Int32Array([this.nrIndices]),
offsets: new Int32Array([0]),
pos: 1
}
}
var maxNrRanges = this.geometryIdToIndex.size / 2;
var complement = {
counts: new Int32Array(maxNrRanges),
offsets: new Int32Array(maxNrRanges),
pos: 0
};
var previousIndex = 0;
for (var i=0; i<=input.pos; i++) {
if (i == input.pos) {
if (offset + count != this.nrIndices) {
// Complement the last range
complement.offsets[complement.pos] = previousIndex;
complement.counts[complement.pos] = this.nrIndices - previousIndex;
complement.pos++;
}
continue;
}
var count = input.counts[i];
var offset = input.offsets[i];
var newCount = offset - previousIndex;
if (newCount > 0) {
complement.offsets[complement.pos] = previousIndex;
complement.counts[complement.pos] = offset - previousIndex;
complement.pos++;
}
previousIndex = offset + count;
}
// TODO trim buffers?
// console.log(complement.pos, complement.counts.length);
return complement;
}
/**
* When changing colors, a lot of data is read from the GPU. It seems as though all of this reading is sync, making it a bottle-neck
* When wrapping abstractbufferset calls that read from the GPU buffer in batchGpuRead, the complete bufferset is read into memory once, and is removed afterwards
*/
batchGpuRead(gl, toCopy, bounds, fn) {
if (this.objects) {
// Reuse, no need to batch
fn();
return;
}
if (bounds == null) {
throw "Not supported anymore";
bounds = {
startIndex: 0,
endIndex: this.nrIndices,
minIndex: 0,
maxIndex: this.nrPositions
};
}
this.batchGpuBuffers = {
indices: new Uint32Array(bounds.endIndex - bounds.startIndex),
bounds: bounds
};
let restoreElementBinding = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
let restoreArrayBinding = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, bounds.startIndex * 4, this.batchGpuBuffers.indices, 0, bounds.endIndex - bounds.startIndex);
for (var name of toCopy) {
let buffer = this[name];
let bytes_per_elem = window[buffer.js_type].BYTES_PER_ELEMENT;
let gpu_data = new window[buffer.js_type]((bounds.maxIndex - bounds.minIndex) * buffer.components);
this.batchGpuBuffers[name] = gpu_data;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.getBufferSubData(gl.ARRAY_BUFFER, bounds.minIndex * buffer.components * bytes_per_elem, gpu_data, 0, gpu_data.length);
}
fn();
// Restoring after fn() because potentially fn is creating linebuffers
gl.bindBuffer(gl.ARRAY_BUFFER, restoreArrayBinding);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, restoreElementBinding);
this.batchGpuBuffers = null;
}
createLineRenderer(gl, objectId, a, b) {
const lineRenderer = new FatLineRenderer(this.viewer, gl, {
quantize: this.positionBuffer.js_type !== Float32Array.name
}, this.unquantizationMatrix);
const m = new Map();
let idx = this.geometryIdToIndex.get(objectId)[0];
let [offset, length] = [idx.start, idx.length];
let [minIndex, maxIndex] = [idx.minIndex, idx.maxIndex];
let numVertices = maxIndex - minIndex + 1;
let gpu_data = this.batchGpuBuffers["positionBuffer"];
const bounds = this.batchGpuBuffers.bounds;
var size = 0;
// A more efficient (and certainly more compact) version that used bitshifting was working fine up until 16bits, unfortunately JS only does bitshifting < 32 bits, so now we have this crappy solution
var indexOffset = offset - bounds.startIndex;
for (var i=0; i<length; i+=3) {
let abc = [
this.batchGpuBuffers.indices[indexOffset + i],
this.batchGpuBuffers.indices[indexOffset + i + 1],
this.batchGpuBuffers.indices[indexOffset + i + 2]];
for (let j = 0; j < 3; ++j) {
let a = abc[j];
let b = abc[(j+1)%3];
if (b < a) {
const c = b;
b = a;
a = c;
}
if (m.has(a)) {
var d = m.get(a);
if (Array.isArray(d)) {
if (d.includes(b)) {
d.splice(d.indexOf(b));
size--;
if (d.length == 0) {
m.delete(a);
continue;
} else if (d.length == 1) {
m.set(a, d[0]);
}
} else {
d.push(b);
size++;
}
} else if (d === b) {
m.delete(a);
continue;
} else {
m.set(a, [d, b]);
size++;
}
} else {
m.set(a, b);
size++;
}
}
}
lineRenderer.init(size, maxIndex);
const vertexOffset = -bounds.minIndex * 3;
for (let a of m.keys()) {
const bs = m.get(a);
for (var b of Array.isArray(bs) ? bs : [bs]) {
const as = vertexOffset + a * 3;
const bs = vertexOffset + b * 3;
let A = gpu_data.subarray(as, as + 3);
let B = gpu_data.subarray(bs, bs + 3);
lineRenderer.pushVertices(A, B);
}
}
lineRenderer.finalize();
return lineRenderer;
}
getBounds(id_ranges) {
var bounds = {};
for (const idRange of id_ranges) {
const oid = idRange[0];
const range = idRange[1];
let idx = this.geometryIdToIndex.get(oid)[0];
if (bounds.startIndex == null || range[0] < bounds.startIndex) {
bounds.startIndex = range[0];
}
if (bounds.endIndex == null || range[1] > bounds.endIndex) {
bounds.endIndex = range[1];
}
if (bounds.minIndex == null || idx.minIndex < bounds.minIndex) {
bounds.minIndex = idx.minIndex;
}
if (bounds.maxIndex == null || idx.maxIndex + 1 > bounds.maxIndex) {
// This one seems to be wrong
bounds.maxIndex = idx.maxIndex + 1;
}
}
return bounds;
}
computeVisibleInstances(ids_with_or_without, gl) {
var ids = Object.values(ids_with_or_without)[0];
var exclude = "without" in ids_with_or_without;
var ids_str = exclude + ':' + ids.frozen;
{
var cache_lookup;
if ((cache_lookup = this.visibleRanges.get(ids_str))) {
return cache_lookup;
}
}
let ranges = {instanceIds: [], hidden: exclude, somethingVisible: null};
this.objects.forEach((ob, i) => {
if (ids !== null && ids.has(ob.id)) {
// @todo, for large lists of objects, this is not efficient
ranges.instanceIds.push(i);
}
});
if (ranges.instanceIds.length == this.objects.length) {
ranges.instanceIds = [];
ranges.hidden = !ranges.hidden;
}
ranges.somethingVisible = ranges.hidden
? ranges.instanceIds.length < this.objects.length
: ranges.instanceIds.length > 0;
this.visibleRanges.set(ids_str, ranges);
if (!exclude && ranges.instanceIds.length && this.lineIndexBuffers.size === 0) {
var id = 0; // TODO !!
let lineRenderer = this.createLineRenderer(gl, id, 0, this.indexBuffer.N);
// This will result in a different dequantization matrix later on, not sure why
lineRenderer.croid = this.croid;
this.objects.forEach((ob) => {
lineRenderer.matrixMap.set(ob.id, ob.matrix);
this.lineIndexBuffers.set(ob.id, lineRenderer);
});
}
return ranges;
}
// generator function that yields ranges in this buffer for the selected ids
* _(geometryIdToIndex, ids) {
var oids;
for (var i of ids) {
if ((oids = geometryIdToIndex.get(i))) {
for (var j = 0; j < oids.length; ++j) {
yield [i, [oids[j].start, oids[j].start + oids[j].length]];
}
}
}
}
getIdRanges(oids) {
var iterator1 = this.geometryIdToIndex.keys();
var iterator2 = oids[Symbol.iterator]();
const id_ranges = this.geometryIdToIndex
? Array.from(this.findUnion(iterator1, iterator2)).sort((a, b) => (a[1][0] > b[1][0]) - (a[1][0] < b[1][0]))
// If we don't have this mapping, we're dealing with a dedicated
// non-instanced bufferset for one particular overriden object
: [[this.objectId & 0x8FFFFFFF, [0, this.nrIndices]]];
return id_ranges;
}
/**
* Generator function that yields ranges in this buffer for the selected ids
* This one tries to do better than _ by utilizing the fact (requirement) that both geometryIdToIndex and ids are numerically ordered beforehand
* Basically it only iterates through both iterators only once. Could be even faster with a real TreeMap, but we don't have it available
*/
* findUnion(iterator1, iterator2) {
var next1 = iterator1.next();
var next2 = iterator2.next();
while (!next1.done && !next2.done) {
if (next1.value == next2.value) {
const i = next1.value;
var oids = this.geometryIdToIndex.get(i);
for (var j = 0; j < oids.length; ++j) {
yield [i, [oids[j].start, oids[j].start + oids[j].length]];
}
next1 = iterator1.next();
next2 = iterator2.next();
} else {
if (next1.value < next2.value) {
next1 = iterator1.next();
} else {
next2 = iterator2.next();
}
}
}
}
computeVisibleRangesAsBuffers(ids_with_or_without, gl) {
var ids = Object.values(ids_with_or_without)[0];
var exclude = "without" in ids_with_or_without;
const ids_str = exclude + ':' + ids.frozen;
{
var cache_lookup;
if ((cache_lookup = this.visibleRanges.get(ids_str))) {
return cache_lookup;
}
}
if (ids === null || ids.size === 0) {
return {
counts: new Int32Array([this.nrIndices]),
offsets: new Int32Array([0]),
pos: 1
};
}
var iterator1 = this.geometryIdToIndex.keys();
var iterator2 = ids._set[Symbol.iterator]();
const id_ranges = this.geometryIdToIndex
? Array.from(this.findUnion(iterator1, iterator2)).sort((a, b) => (a[1][0] > b[1][0]) - (a[1][0] < b[1][0]))
// If we don't have this mapping, we're dealing with a dedicated
// non-instanced bufferset for one particular overriden object
: [[this.objectId & 0x8FFFFFFF, [0, this.nrIndices]]];
var result = {
counts: new Int32Array(id_ranges.length),
offsets: new Int32Array(id_ranges.length),
pos: id_ranges.length
};
var c = 0;
for (const range of id_ranges) {
const realRange = range[1];
result.offsets[c] = realRange[0];
result.counts[c] = realRange[1] - realRange[0];
c++;
}
result = this.joinConsecutiveRangesAsBuffers(result);
if (exclude) {
let complement = this.complementRangesAsBuffers(result);
// store in cache
this.visibleRanges.set(ids_str, complement);
return complement;
}
// store in cache
this.visibleRanges.set(ids_str, result);
// Create fat line renderings for these elements. This should (a)
// not in the draw loop (b) maybe in something like a web worker
let bounds = this.getBounds(id_ranges);
this.batchGpuRead(gl, ["positionBuffer"], bounds, () => {
id_ranges.forEach((range, i) => {
let [id, [a, b]] = range;
if (this.lineIndexBuffers.has(id)) {
return;
}
let lineRenderer = this.createLineRenderer(gl, id, a, b);
this.lineIndexBuffers.set(id, lineRenderer);
});
});
return result;
}
reset() {
this.positionsIndex = 0;
this.normalsIndex = 0;
this.pickColorsIndex = 0;
this.indicesIndex = 0;
this.nrIndices = 0;
this.bytes = 0;
this.visibleRanges = new Map();
this.geometryIdToIndex = new Map();
this.lineIndexBuffers = new Map();
}
copy(gl, objectId) {
let returnDictionary = {};
if (this.objects) {
return this.copyEmpty();
} else {
let idx = this.geometryIdToIndex.get(objectId)[0];
let [offset, length] = [idx.start, idx.length];
const indices = new Uint32Array(length);
let [minIndex, maxIndex] = [idx.minIndex, idx.maxIndex];
let bounds = this.batchGpuBuffers.bounds;
for (let i=0; i<length; i++) {
indices[i] = this.batchGpuBuffers.indices[-bounds.startIndex + offset + i] - minIndex;
}
let numVertices = maxIndex - minIndex + 1;
let toCopy = ["positionBuffer", "normalBuffer", "colorBuffer", "pickColorBuffer"];
for (var name of toCopy) {
let buffer = this[name];
let gpu_data = this.batchGpuBuffers[name];
let new_gpu_data = new window[buffer.js_type](numVertices * buffer.components);
// @todo this can probably be a combination of subarray() and set()
var vertexOffset = (-bounds.minIndex + minIndex) * buffer.components;
for (let j=0; j<numVertices * buffer.components; j++) {
new_gpu_data[j] = gpu_data[vertexOffset + j];
}
let shortName = name.replace("Buffer", "") + "s";
returnDictionary[shortName] = new_gpu_data;
returnDictionary["nr" + shortName.substr(0,1).toUpperCase() + shortName.substr(1)] = new_gpu_data.length;
}
returnDictionary.isCopy = true;
returnDictionary["indices"] = indices;
returnDictionary["nrIndices"] = indices.length;
}
return returnDictionary;
}
setColor(gl, objectId, clr) {
// Reusing buffer sets always results in a copy
if (this.objects) {
return false;
}
// Switching transparency states results in a copy
if (clr.length == 4 && this.hasTransparency != (clr[3] < 1.)) {
return false;
}
var oldColors, newColors, clrArray;
if (clr.length == 4) {
let factor = this.colorBuffer.js_type == Uint8Array.name ? 255. : 1.;
clrArray = new window[this.colorBuffer.js_type](4);
for (let i = 0; i < 4; ++i) {
clrArray[i] = clr[i] * factor;
}
} else {
newColors = clr;
}
for (var idx of this.geometryIdToIndex.get(objectId)) {
let [offset, length] = [idx.color, idx.colorLength];
let bytes_per_elem = window[this.colorBuffer.js_type].BYTES_PER_ELEMENT;
// Assumes there is just one index pair, this is for now always the case.
oldColors = new window[this.colorBuffer.js_type](length);
if (clr.length == 4) {
newColors = new window[this.colorBuffer.js_type](length);
for (let i = 0; i < length; i += 4) {
newColors.set(clrArray, i);
}
}
var restoreArrayBinding = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
let bounds = this.batchGpuBuffers.bounds;
let gpu_data = this.batchGpuBuffers.colorBuffer;
// @todo this can probably be a combination of subarray() and set()
for (let j=0; j<length; j++) {
oldColors[j] = gpu_data[offset - (bounds.minIndex * 4) + j];
}
gl.bufferSubData(gl.ARRAY_BUFFER, offset * bytes_per_elem, newColors, 0, length);
gl.bindBuffer(gl.ARRAY_BUFFER, restoreArrayBinding);
}
return oldColors;
}
}