diff --git a/common/webapp/src/js/markers/ExtrudeMarker.js b/common/webapp/src/js/markers/ExtrudeMarker.js index d4dbe7558..c7d9ef213 100644 --- a/common/webapp/src/js/markers/ExtrudeMarker.js +++ b/common/webapp/src/js/markers/ExtrudeMarker.js @@ -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 { @@ -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; } diff --git a/common/webapp/src/js/markers/LineMarker.js b/common/webapp/src/js/markers/LineMarker.js index b5fcb6923..08807b6cb 100644 --- a/common/webapp/src/js/markers/LineMarker.js +++ b/common/webapp/src/js/markers/LineMarker.js @@ -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 { @@ -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; } diff --git a/common/webapp/src/js/markers/Marker.js b/common/webapp/src/js/markers/Marker.js index 2ee07ce33..1ad35948a 100644 --- a/common/webapp/src/js/markers/Marker.js +++ b/common/webapp/src/js/markers/Marker.js @@ -24,6 +24,7 @@ */ import {MathUtils, Object3D, Vector3} from "three"; import {reactive} from "vue"; +import {isBatchable} from "./MarkerBatch"; export class Marker extends Object3D { @@ -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(); + } }); } diff --git a/common/webapp/src/js/markers/MarkerBatch.js b/common/webapp/src/js/markers/MarkerBatch.js new file mode 100644 index 000000000..37bf621c1 --- /dev/null +++ b/common/webapp/src/js/markers/MarkerBatch.js @@ -0,0 +1,210 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * 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; +} diff --git a/common/webapp/src/js/markers/MarkerSet.js b/common/webapp/src/js/markers/MarkerSet.js index d4719dcb4..d1755a7e9 100644 --- a/common/webapp/src/js/markers/MarkerSet.js +++ b/common/webapp/src/js/markers/MarkerSet.js @@ -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 { @@ -46,6 +47,9 @@ export class MarkerSet extends Scene { /** @type {Map} */ this.markers = new Map(); + this.markerBatch = new MarkerBatch(this); + this.markerBatchOutdated = false; + this.data = reactive({ id: id, label: id, @@ -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 */ @@ -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(); } } @@ -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(); } } diff --git a/common/webapp/src/js/markers/ShapeMarker.js b/common/webapp/src/js/markers/ShapeMarker.js index 499fab373..cfd093a3e 100644 --- a/common/webapp/src/js/markers/ShapeMarker.js +++ b/common/webapp/src/js/markers/ShapeMarker.js @@ -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 { @@ -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; }