Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/webapp/src/js/markers/ExtrudeMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {deepEquals} from "../util/Utils";
import {LineSegmentsGeometry} from "three/examples/jsm/lines/LineSegmentsGeometry";
import {ObjectMarker} from "./ObjectMarker";
import {lineShader} from "../util/LineShader";
import {invalidateMarkerBatchOnChange} from "./MarkerBatch";

export class ExtrudeMarker extends ObjectMarker {

Expand Down Expand Up @@ -137,6 +138,8 @@ export class ExtrudeMarker extends ObjectMarker {
this.fill.fadeDistanceMin = minDist;
this.fill.fadeDistanceMax = maxDist;

invalidateMarkerBatchOnChange(this, this._markerData, markerData);

// save used marker data for next update
this._markerData = markerData;
}
Expand Down
3 changes: 3 additions & 0 deletions common/webapp/src/js/markers/LineMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {Line2} from "three/examples/jsm/lines/Line2";
import {deepEquals} from "../util/Utils";
import {ObjectMarker} from "./ObjectMarker";
import {lineShader} from "../util/LineShader";
import {invalidateMarkerBatchOnChange} from "./MarkerBatch";

export class LineMarker extends ObjectMarker {

Expand Down Expand Up @@ -123,6 +124,8 @@ export class LineMarker extends ObjectMarker {
this.line.fadeDistanceMin = minDist;
this.line.fadeDistanceMax = maxDist;

invalidateMarkerBatchOnChange(this, this._markerData, markerData);

// save used marker data for next update
this._markerData = markerData;
}
Expand Down
7 changes: 6 additions & 1 deletion common/webapp/src/js/markers/Marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*/
import {MathUtils, Object3D, Vector3} from "three";
import {reactive} from "vue";
import {isBatchable} from "./MarkerBatch";

export class Marker extends Object3D {

Expand All @@ -50,7 +51,11 @@ export class Marker extends Object3D {
});
Object.defineProperty(this, "visible", {
get() { return this.data.visible },
set(value) { this.data.visible = value }
set(value) {
if (this.data.visible === value) return;
this.data.visible = value;
if (isBatchable(this) && this.parent && this.parent.isMarkerSet) this.parent.invalidateMarkerBatch();
}
});
}

Expand Down
210 changes: 210 additions & 0 deletions common/webapp/src/js/markers/MarkerBatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {BufferAttribute, BufferGeometry, Matrix4, Mesh, Vector2, Vector3} from "three";
import {Line2} from "three/examples/jsm/lines/Line2";
import {LineSegmentsGeometry} from "three/examples/jsm/lines/LineSegmentsGeometry";
const MIN_BATCHABLE_MARKERS = 64;
const HIDDEN_LAYER = 30;
const CELL_SIZE = 4096;
const DRAWN_FIELDS = [
"position", "shape", "holes", "line", "shapeY", "height", "shapeMinY", "shapeMaxY",
"depthTest", "lineWidth", "lineColor", "borderColor", "fillColor", "minDistance", "maxDistance",
"x", "y", "z", "r", "g", "b", "a"
];

export function isBatchable(marker) {
return !!(marker.isShapeMarker || marker.isExtrudeMarker || marker.isLineMarker);
}

export function invalidateMarkerBatchOnChange(marker, oldData, newData) {
if (!marker.parent || !marker.parent.isMarkerSet) return;
if (JSON.stringify(oldData, DRAWN_FIELDS) === JSON.stringify(newData, DRAWN_FIELDS)) return;
marker.parent.invalidateMarkerBatch();
}

function batchableParts(marker) {
if (marker.isLineMarker) return [marker.line];
return [marker.fill, marker.border];
}

export class MarkerBatch {

/**
* @param markerSet {MarkerSet}
*/
constructor(markerSet) {
this.markerSet = markerSet;
this.objects = [];
this.parts = [];
}

update() {
this.clear();

let markers = [...this.markerSet.markers.values()].filter(marker => isBatchable(marker) && marker.visible);
if (markers.length < MIN_BATCHABLE_MARKERS) return;

this.markerSet.updateWorldMatrix(true, true);
let toSet = new Matrix4().copy(this.markerSet.matrixWorld).invert();

let groups = new Map();
let position = new Vector3();
markers.forEach(marker => {
position.setFromMatrixPosition(marker.matrixWorld).applyMatrix4(toSet);
let cellX = Math.floor(position.x / CELL_SIZE);
let cellZ = Math.floor(position.z / CELL_SIZE);
batchableParts(marker).forEach(part => {
if (!part.visible) return;
let key = batchKey(part) + "|" + cellX + "|" + cellZ;
let group = groups.get(key);
if (group) group.parts.push(part);
else groups.set(key, {cellX, cellZ, parts: [part]});
});
});

groups.forEach(({cellX, cellZ, parts}) => {
let origin = new Vector3((cellX + 0.5) * CELL_SIZE, 0, (cellZ + 0.5) * CELL_SIZE);
let toBatch = new Matrix4().makeTranslation(-origin.x, -origin.y, -origin.z).multiply(toSet);
let object = parts[0].isLine2 ? createLineBatch(parts, toBatch) : createMeshBatch(parts, toBatch);
object.position.copy(origin);
this.objects.push(object);
this.markerSet.add(object);
parts.forEach(part => {
part.layers.set(HIDDEN_LAYER);
this.parts.push(part);
});
});
}

clear() {
this.objects.forEach(object => {
this.markerSet.remove(object);
object.dispose();
});
this.parts.forEach(part => part.layers.set(0));
this.objects = [];
this.parts = [];
}

}

function batchKey(part) {
let color = part.color;
return [
part.isLine2 ? "line" : "mesh",
part.renderOrder,
part.depthTest,
color.r, color.g, color.b,
part.opacity,
part.isLine2 ? part.linewidth : 0,
part.fadeDistanceMin,
part.fadeDistanceMax
].join("|");
}

function createMeshBatch(parts, toSet) {
let vertexCount = 0;
let indexCount = 0;
parts.forEach(part => {
let position = part.geometry.attributes.position;
vertexCount += position.count;
indexCount += part.geometry.index ? part.geometry.index.count : position.count;
});

let positions = new Float32Array(vertexCount * 3);
let indices = new Uint32Array(indexCount);
let matrix = new Matrix4();
let point = new Vector3();
let v = 0;
let i = 0;
parts.forEach(part => {
matrix.multiplyMatrices(toSet, part.matrixWorld);
let position = part.geometry.attributes.position;
for (let k = 0; k < position.count; k++) {
point.fromBufferAttribute(position, k).applyMatrix4(matrix).toArray(positions, (v + k) * 3);
}

let index = part.geometry.index;
if (index) {
for (let k = 0; k < index.count; k++) indices[i++] = v + index.getX(k);
} else {
for (let k = 0; k < position.count; k++) indices[i++] = v + k;
}

v += position.count;
});

let geometry = new BufferGeometry();
geometry.setAttribute("position", new BufferAttribute(positions, 3));
geometry.setIndex(new BufferAttribute(indices, 1));

let material = parts[0].material.clone();
material.depthWrite = false;

return createBatchObject(new Mesh(geometry, material), parts[0]);
}

function createLineBatch(parts, toSet) {
let segmentCount = parts.reduce((sum, part) => sum + part.geometry.attributes.instanceStart.count, 0);

let positions = new Float32Array(segmentCount * 6);
let matrix = new Matrix4();
let point = new Vector3();
let s = 0;
parts.forEach(part => {
matrix.multiplyMatrices(toSet, part.matrixWorld);
let start = part.geometry.attributes.instanceStart;
let end = part.geometry.attributes.instanceEnd;
for (let k = 0; k < start.count; k++, s++) {
point.fromBufferAttribute(start, k).applyMatrix4(matrix).toArray(positions, s * 6);
point.fromBufferAttribute(end, k).applyMatrix4(matrix).toArray(positions, s * 6 + 3);
}
});

let geometry = new LineSegmentsGeometry();
geometry.setPositions(positions);

let line = createBatchObject(new Line2(geometry, parts[0].material.clone()), parts[0]);
let partsResolution = new Vector2(-1, -1);
line.onBeforeRender = renderer => {
let resolution = line.material.resolution;
renderer.getSize(resolution);
if (partsResolution.equals(resolution)) return;
partsResolution.copy(resolution);
parts.forEach(part => part.material.resolution.copy(resolution));
};

return line;
}

function createBatchObject(object, part) {
object.renderOrder = part.renderOrder;
object.raycast = () => {};
object.dispose = () => {
object.geometry.dispose();
object.material.dispose();
};
return object;
}
16 changes: 16 additions & 0 deletions common/webapp/src/js/markers/MarkerSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {HtmlMarker} from "./HtmlMarker";
import {PoiMarker} from "./PoiMarker";
import {reactive} from "vue";
import {getLocalStorage, setLocalStorage} from "../Utils";
import {isBatchable, MarkerBatch} from "./MarkerBatch";

export class MarkerSet extends Scene {

Expand All @@ -46,6 +47,9 @@ export class MarkerSet extends Scene {
/** @type {Map<string, Marker>} */
this.markers = new Map();

this.markerBatch = new MarkerBatch(this);
this.markerBatchOutdated = false;

this.data = reactive({
id: id,
label: id,
Expand Down Expand Up @@ -183,6 +187,16 @@ export class MarkerSet extends Scene {
marker.updateFromData(data);
}

invalidateMarkerBatch() {
if (this.markerBatchOutdated) return;
this.markerBatchOutdated = true;

queueMicrotask(() => {
this.markerBatchOutdated = false;
this.markerBatch.update();
});
}

/**
* Removes all markers and marker-sets
*/
Expand All @@ -201,6 +215,7 @@ export class MarkerSet extends Scene {
if (o.isMarker && !this.markers.has(o.data.id)) {
this.markers.set(o.data.id, o);
this.data.markers.push(o.data);
if (isBatchable(o)) this.invalidateMarkerBatch();
}
}

Expand All @@ -221,6 +236,7 @@ export class MarkerSet extends Scene {
if (i > -1) this.data.markers.splice(i, 1);
this.markers.delete(o.data.id);
o.dispose();
if (isBatchable(o)) this.invalidateMarkerBatch();
}
}

Expand Down
3 changes: 3 additions & 0 deletions common/webapp/src/js/markers/ShapeMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {Line2} from "three/examples/jsm/lines/Line2";
import {deepEquals} from "../util/Utils";
import {ObjectMarker} from "./ObjectMarker";
import {lineShader} from "../util/LineShader";
import {invalidateMarkerBatchOnChange} from "./MarkerBatch";
import {LineSegmentsGeometry} from "three/examples/jsm/lines/LineSegmentsGeometry";

export class ShapeMarker extends ObjectMarker {
Expand Down Expand Up @@ -134,6 +135,8 @@ export class ShapeMarker extends ObjectMarker {
this.fill.fadeDistanceMin = minDist;
this.fill.fadeDistanceMax = maxDist;

invalidateMarkerBatchOnChange(this, this._markerData, markerData);

// save used marker data for next update
this._markerData = markerData;
}
Expand Down