From aa17a4853347bf0c906dedd72de4a47f51ef97d8 Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Thu, 31 Jul 2025 12:10:10 +0300 Subject: [PATCH 1/2] Better Support for Large Models (#5144) * fix(viewer-lib): Transformed is no longer baked in if matrix is identity * fix(viewer-lib): Do not use uint32 indices unless we have to * fix(viewer-lib): Do not use Float64 array unless the batch needs RTE * feat(viewer-lib): Update on reducing memory allocation during startup: - Geometry data is now stored as separate DataChunks as they come - Dechunking now no longer allocates memory. It just returns the DataChunk array - Updated the SpeckleGeometryConverter to work with chunk arrays - Updated Geometry and triangulation to work with chunk arrays - All geometry type batches now work with chunk arrays instead of flattened arrays - Chunks are tracked by use and deleted after all render views are done with them. The chunks also track their usage across different render views so they aren't deleted until all render views that use tham are finished with them - In order to better support this new way of working with geometry data, VirtualArray and ChunkArray classes have been implemented. They make it easier to work with segmented data and offer a unified view on the array of array segments * chore(viewer-lib): Denormalized normals to keep parity, even though they should be normalized * feat(viewer-lib): Geometry transformation is now deferred until we build batches, and we transform the batched arrays leaving the original data chunks intact. Text and TextBatchObject now use the render view's 'transform' property to store it's final startup transformation and not 'bakeTransform' anymore * fix(viewer-lib): Fixed the issues caused by chunking geometry to the acceleration structures. * chore(viewer-lib): Made a pass on the entire viewer project and removed pointless typed array backing buffer re-allocations * feat(viewer-lib): Updates on better large model support: - Fixed an issue in LineBatach that broke building it - Improved VirtualArray performance and added some extra functionality - Already triangulated faces no longer allocate redundant memory, they get processed in place - Moved triangulation to SpeckleConverter so that processed index chunks get stored in local storage so we don't have to re-triangulate each time * feat(viewer-lib): Gave up on trying to cache triangulated indice. Too much hasle and edge cases to handle when only some chunks get saved as triangulated in a multi chunk setup * fix(viewer-lib): Fixed non triangulted geometry converter return * feat(viewer-lib): Glow-up to our triangulation implementation. Faster, zero allocation * fix(viewer-lib): Frontfacing not backfacing triangles * chore(viewer-lib): Fixed compile errors * fix(viewer-lib): Already processed chunks just copy over * fix(viewer-lib): Skip processed chunks when computing triangulation index size * fix(viewer-lib): Some fixes: - Fixed an issue where instances that will not be rendered as instanced geometry were not correctly transformed - Removed geometry duplication from instances that were de-instanced in the batcher - Fixed an issue with LineBatch and buffer type * fix(viewer-lib): Implemented box3 bounds generation from ChunkArray which takes care to respect inter-chunk bounds for vec3. Without this, box3s were incorrectly calculated by computing a box3 for each chunk * fix(viewer-lib): Fixed an issue where transformations that contain non-uniform scaling incorrectly produce node render views aabb values. So we recompute them based on the post-transform geometry when building batches * fix(viewer-lib): When mixing triangles with ngons we also need to increment total tris count for the triangle case as well * fix(viewer-lib): If geometry is invalid, clear it all * fix(viewer-lib): Instanced rvs no longer transform their aabbs when building the render tree because they don't need to * fix(viewer-lib): aabb for render views needs to be recomputed when de-instanced by the batcher --- packages/viewer-sandbox/src/Sandbox.ts | 2 +- packages/viewer-sandbox/src/main.ts | 21 +- packages/viewer/src/IViewer.ts | 7 + packages/viewer/src/modules/UrlHelper.ts | 7 +- .../src/modules/batching/BatchObject.ts | 23 +- .../viewer/src/modules/batching/Batcher.ts | 32 ++- .../modules/batching/InstancedMeshBatch.ts | 49 ++-- .../viewer/src/modules/batching/LineBatch.ts | 44 ++- .../viewer/src/modules/batching/MeshBatch.ts | 92 +++++-- .../viewer/src/modules/batching/PointBatch.ts | 56 +++- .../viewer/src/modules/batching/TextBatch.ts | 39 ++- .../src/modules/batching/TextBatchObject.ts | 4 +- .../viewer/src/modules/converter/Geometry.ts | 256 +++++++++++++----- .../converter/MeshTriangulationHelper.js | 156 ++++------- .../src/modules/converter/VirtualArray.ts | 177 ++++++++++++ .../measurements/AreaMeasurement.ts | 3 +- .../extensions/sections/SectionOutlines.ts | 3 +- .../extensions/sections/SectionTool.ts | 18 +- .../loaders/Speckle/SpeckleConverter.ts | 59 +++- .../Speckle/SpeckleGeometryConverter.ts | 248 +++++++++++++---- .../viewer/src/modules/materials/Materials.ts | 9 +- .../modules/objects/AccelerationStructure.ts | 6 +- .../viewer/src/modules/objects/TextLabel.ts | 11 +- .../objects/TopLevelAccelerationStructure.ts | 17 +- .../viewer/src/modules/tree/NodeRenderView.ts | 19 +- .../viewer/src/modules/tree/RenderTree.ts | 37 +-- 26 files changed, 981 insertions(+), 414 deletions(-) create mode 100644 packages/viewer/src/modules/converter/VirtualArray.ts diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index 11aba1a1e..17d6573f8 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -186,7 +186,7 @@ export default class Sandbox { this.addStreamControls(url) this.addViewControls() this.addBatches() - this.properties = await this.viewer.getObjectProperties() + // this.properties = await this.viewer.getObjectProperties() this.batchesParams.totalBvhSize = this.getBVHSize() this.refresh() }) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 30ff53142..ecadd625c 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -135,6 +135,7 @@ const getStream = () => { // prettier-ignore // Revit sample house (good for bim-like stuff with many display meshes) 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' + // 'https://app.speckle.systems/streams/da9e320dad/objects/ee5d160d84090822813bc74188da34a7' //large tower //'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5' @@ -162,6 +163,7 @@ const getStream = () => { // 'https://latest.speckle.systems/streams/3ed8357f29/commits/d10f2af1ce' // AutoCAD NEW // 'https://latest.speckle.systems/streams/3ed8357f29/commits/46905429f6' + // 'https://latest.speckle.systems/streams/3ed8357f29/objects/95160b8d593a0ba12dd004d5fe142257' //Blizzard world // 'https://latest.speckle.systems/streams/0c6ad366c4/commits/aa1c393aec' //Car @@ -355,7 +357,6 @@ const getStream = () => { // 'https://app.speckle.systems/streams/25d8a162af/commits/6c842a713c' // 'https://app.speckle.systems/streams/76e3acde68/commits/0ea3d47e6c' // Point cloud - // 'https://app.speckle.systems/streams/b920636274/commits/8df6496749' // 'https://multiconsult.speckle.xyz/streams/9721fe797c/objects/ff5d939a8c26bde092152d5b4a0c945d' // 'https://app.speckle.systems/streams/87a2be92c7/objects/803c3c413b133ee9a6631160ccb194c9' // 'https://latest.speckle.systems/streams/1422d91a81/commits/480d88ba68' @@ -543,7 +544,7 @@ const getStream = () => { // 'https://app.speckle.systems/projects/7591c56179/models/82b94108a3' // SUPER slow tree build time (LARGE N-GONS TRIANGULATION) - // 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c' + // 'https://app.speckle.systems/projects/0edb6ef628/models/87f3fb5e2bd681d731dd048390ae3a8f' /* ObjectLoader 2 tests */ // `https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02` @@ -601,6 +602,22 @@ const getStream = () => { // 'https://latest.speckle.systems/projects/f28ad5b38a/models/b63ebcd807' // Duplicate display values // 'https://app.speckle.systems/projects/1466fe31c6/models/2eaf0f0571' + // MEPS + // 'https://app.speckle.systems/projects/f3cee517d4/models/21f128a3ea' + // Tower + // 'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5' + + // Barbican + // 'https://app.speckle.systems/projects/32baa9291e/models/all' + // 'https://app.speckle.systems/streams/32baa9291e/objects/21a3621c0a3e6d2884e1315f02314313' + // 'https://app.speckle.systems/projects/5d723f097a/models/c05abd36b5' + + //Guggenheim + // 'https://app.speckle.systems/projects/937d78e0a5/models/a48f0274eb' + // 'https://app.speckle.systems/projects/937d78e0a5/objects/0e3c61147f3a035a85a3542c7f1c7a43' + + // heatherwick LARGE + // 'https://app.speckle.systems/projects/63a3226049/models/bdd4f553a8' ) } diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index f2f9b1729..61687c414 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -28,6 +28,13 @@ export type SpeckleObject = { applicationId?: string } +export type DataChunk = { + id: string + data: number[] + references: number + processed?: boolean +} + export interface ViewerParams { showStats: boolean environmentSrc: Asset diff --git a/packages/viewer/src/modules/UrlHelper.ts b/packages/viewer/src/modules/UrlHelper.ts index 1a1aed844..bb080fd64 100644 --- a/packages/viewer/src/modules/UrlHelper.ts +++ b/packages/viewer/src/modules/UrlHelper.ts @@ -299,9 +299,10 @@ async function runAllModelsQuery( const urls: string[] = [] data.project.models.items.forEach( (element: { versions: { items: { referencedObject: string }[] } }) => { - urls.push( - `${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}` - ) + if (element.versions.items.length) + urls.push( + `${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}` + ) } ) return urls diff --git a/packages/viewer/src/modules/batching/BatchObject.ts b/packages/viewer/src/modules/batching/BatchObject.ts index 5a61cb28d..157021bba 100644 --- a/packages/viewer/src/modules/batching/BatchObject.ts +++ b/packages/viewer/src/modules/batching/BatchObject.ts @@ -102,8 +102,16 @@ export class BatchObject { this.pivot_High ) } + public buildAccelerationStructure( + position: Float32Array | Float64Array, + indices: Uint16Array | Uint32Array + ): void + public buildAccelerationStructure(bvh: MeshBVH): void - public buildAccelerationStructure(bvh?: MeshBVH) { + public buildAccelerationStructure( + positionOrBvh: Float32Array | Float64Array | MeshBVH, + indices?: Uint16Array | Uint32Array + ): void { const transform = new Matrix4().makeTranslation( this._localOrigin.x, this._localOrigin.y, @@ -111,14 +119,15 @@ export class BatchObject { ) transform.invert() - if (!bvh) { - const indices: number[] | undefined = - this._renderView.renderData.geometry.attributes?.INDEX - const position: number[] | undefined = - this._renderView.renderData.geometry.attributes?.POSITION + let bvh = positionOrBvh + + if (!(bvh instanceof MeshBVH)) { + if (!indices) { + throw new Error(`Cannot build a BVH with only positions. Need indices too`) + } bvh = AccelerationStructure.buildBVH( indices, - position, + positionOrBvh as Float32Array | Float64Array, DefaultBVHOptions, transform ) diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index 89597fa54..53f8695ba 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -141,21 +141,23 @@ export default class Batcher { rvs.forEach((nodeRv) => { const geometry = nodeRv.renderData.geometry geometry.instanced = false - const attribs = geometry.attributes - geometry.attributes = { - POSITION: attribs.POSITION.slice(), - INDEX: attribs.INDEX.slice(), - ...(attribs.COLOR && { - COLOR: attribs.COLOR.slice() - }) - } - /** - I don't particularly like this branch - - * All instances should have a transform. But it's the easiest thing we can do - * until we figure out the viewer <-> connector object duplication inconsistency - */ - if (geometry.transform) - Geometry.transformGeometryData(geometry, geometry.transform) - nodeRv.computeAABB() + nodeRv.computeAABB(geometry.transform) + /** I don't think we need to duplicate geometry here, now that we're transforming the batch position directly */ + // const attribs = geometry.attributes + // geometry.attributes = { + // POSITION: attribs.POSITION.slice(), + // INDEX: attribs.INDEX.slice(), + // ...(attribs.COLOR && { + // COLOR: attribs.COLOR.slice() + // }) + // } + // /** - I don't particularly like this branch - + // * All instances should have a transform. But it's the easiest thing we can do + // * until we figure out the viewer <-> connector object duplication inconsistency + // */ + // if (geometry.transform) + // Geometry.transformGeometryData(geometry, geometry.transform) + // nodeRv.computeAABB() }) continue } diff --git a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts index b0d30fa1b..0dcdd2c3a 100644 --- a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts +++ b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts @@ -525,6 +525,21 @@ export class InstancedMeshBatch implements Batch { ) const targetInstanceTransformBuffer = this.getCurrentTransformBuffer() + const positions = + this.renderViews[0].renderData.geometry.attributes?.POSITION.getFloat32Array() + const indicesLength = + this.renderViews[0].renderData.geometry.attributes?.INDEX.length ?? 0 + const positionsLength = + this.renderViews[0].renderData.geometry.attributes?.POSITION.length ?? 0 + const indices = + indicesLength < 65535 && positionsLength < 65535 + ? this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint16Array() + : this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint32Array() + const colors = + this.renderViews[0].renderData.geometry.attributes?.COLOR?.getFloat32Array() + const normals = + this.renderViews[0].renderData.geometry.attributes?.NORMAL?.getFloat32Array() + for (let k = 0; k < this.renderViews.length; k++) { /** Catering to typescript * There is no unniverse where an instanced render view does not have a transform @@ -555,13 +570,10 @@ export class InstancedMeshBatch implements Batch { batchObject.localOrigin.z ) transform.invert() - const indices: number[] | undefined = - this.renderViews[k].renderData.geometry.attributes?.INDEX - const position: number[] | undefined = this.renderViews[k].renderData.geometry - .attributes?.POSITION as number[] + instanceBVH = AccelerationStructure.buildBVH( indices, - position, + positions, DefaultBVHOptions, transform ) @@ -572,32 +584,13 @@ export class InstancedMeshBatch implements Batch { batchObjects.push(batchObject) } - const indices: number[] | undefined = - this.renderViews[0].renderData.geometry.attributes?.INDEX - - const positions: number[] | undefined = - this.renderViews[0].renderData.geometry.attributes?.POSITION - - const colors: number[] | undefined = - this.renderViews[0].renderData.geometry.attributes?.COLOR - - const normals: number[] | undefined = - this.renderViews[0].renderData.geometry.attributes?.NORMAL - /** Catering to typescript * There is no unniverse where indices or positions are undefined at this point */ if (!indices || !positions) { throw new Error(`Cannot build batch ${this.id}. Undefined indices or positions`) } - this.geometry = this.makeInstancedMeshGeometry( - positions.length >= 65535 || indices.length >= 65535 - ? new Uint32Array(indices) - : new Uint16Array(indices), - new Float64Array(positions), - normals ? new Float32Array(normals) : undefined, - colors ? new Float32Array(colors) : undefined - ) + this.geometry = this.makeInstancedMeshGeometry(indices, positions, normals, colors) this.mesh = new SpeckleInstancedMesh(this.geometry) this.mesh.setBatchObjects(batchObjects) @@ -655,16 +648,12 @@ export class InstancedMeshBatch implements Batch { private makeInstancedMeshGeometry( indices: Uint32Array | Uint16Array, - position: Float64Array, + position: Float32Array, normal?: Float32Array, color?: Float32Array ): BufferGeometry { const geometry = new BufferGeometry() if (position) { - /** When RTE enabled, we'll be storing the high component of the encoding here, - * which considering our current encoding method is actually the original casted - * down float32 position! - */ geometry.setAttribute('position', new Float32BufferAttribute(position, 3)) } diff --git a/packages/viewer/src/modules/batching/LineBatch.ts b/packages/viewer/src/modules/batching/LineBatch.ts index b5822f0fb..74bfa3a2e 100644 --- a/packages/viewer/src/modules/batching/LineBatch.ts +++ b/packages/viewer/src/modules/batching/LineBatch.ts @@ -24,6 +24,7 @@ import { } from './Batch.js' import { ObjectLayers } from '../../IViewer.js' import Materials from '../materials/Materials.js' +import { ChunkArray } from '../converter/VirtualArray.js' export default class LineBatch implements Batch { public id: string @@ -231,6 +232,7 @@ export default class LineBatch implements Batch { public buildBatch() { let attributeCount = 0 + const rvAABB: Box3 = new Box3() const bounds = new Box3() this.renderViews.forEach((val: NodeRenderView) => { if (!val.renderData.geometry.attributes) { @@ -241,14 +243,18 @@ export default class LineBatch implements Batch { : val.renderData.geometry.attributes.POSITION.length bounds.union(val.aabb) }) - const position = new Float64Array(attributeCount) + const needsRTE = Geometry.needsRTE(bounds) + + const position = needsRTE + ? new Float64Array(attributeCount) + : new Float32Array(attributeCount) let offset = 0 for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry if (!geometry.attributes) { throw new Error(`Cannot build batch ${this.id}. Invalid geometry`) } - let points: Array + let points: Array | ChunkArray /** We need to make sure the line geometry has a layout of : * start(x,y,z), end(x,y,z), start(x,y,z), end(x,y,z)... etc * Some geometries have that inherent form, some don't @@ -258,19 +264,30 @@ export default class LineBatch implements Batch { points = new Array(2 * length) for (let i = 0; i < length; i += 3) { - points[2 * i] = geometry.attributes.POSITION[i] - points[2 * i + 1] = geometry.attributes.POSITION[i + 1] - points[2 * i + 2] = geometry.attributes.POSITION[i + 2] + points[2 * i] = geometry.attributes.POSITION.get(i) + points[2 * i + 1] = geometry.attributes.POSITION.get(i + 1) + points[2 * i + 2] = geometry.attributes.POSITION.get(i + 2) - points[2 * i + 3] = geometry.attributes.POSITION[i + 3] - points[2 * i + 4] = geometry.attributes.POSITION[i + 4] - points[2 * i + 5] = geometry.attributes.POSITION[i + 5] + points[2 * i + 3] = geometry.attributes.POSITION.get(i + 3) + points[2 * i + 4] = geometry.attributes.POSITION.get(i + 4) + points[2 * i + 5] = geometry.attributes.POSITION.get(i + 5) } + position.set(points, offset) } else { points = geometry.attributes.POSITION + geometry.attributes.POSITION.copyToBuffer(position, offset) } - position.set(points, offset) + const positionSubArray = position.subarray(offset, offset + points.length) + Geometry.transformArray(positionSubArray, geometry.transform, 0, points.length) + /** We re-compute the render view aabb based on transformed geometry + * We do this because some transforms like non-uniform scaling can produce incorrect results + * if we compute an aabb from original geometry then apply the transform. That's why we compute + * an aabb from the transformed geometry here and set it in the rv + */ + rvAABB.setFromArray(positionSubArray) + this.renderViews[k].aabb = rvAABB + this.renderViews[k].setBatchData(this.id, offset / 6, points.length / 6) offset += points.length @@ -319,10 +336,15 @@ export default class LineBatch implements Batch { return material } - private makeLineGeometry(position: Float64Array): LineSegmentsGeometry { + private makeLineGeometry( + position: Float64Array | Float32Array + ): LineSegmentsGeometry { const geometry = new LineSegmentsGeometry() /** This will set the instanceStart and instanceEnd attributes. These will be our high parts */ - geometry.setPositions(new Float32Array(position)) + if (position instanceof Float64Array) + /** We need to re-allocate because there is no way to cast it down to float32. If we pass in a Float64Array, three.js will do it anyway */ + geometry.setPositions(new Float32Array(position)) + else geometry.setPositions(position) const buffer = new Float32Array(position.length + position.length / 3) this.colorBuffer = new InstancedInterleavedBuffer(buffer, 8, 1) // rgba, rgba diff --git a/packages/viewer/src/modules/batching/MeshBatch.ts b/packages/viewer/src/modules/batching/MeshBatch.ts index 351cc3ebb..94c3f0b17 100644 --- a/packages/viewer/src/modules/batching/MeshBatch.ts +++ b/packages/viewer/src/modules/batching/MeshBatch.ts @@ -193,6 +193,7 @@ export class MeshBatch extends PrimitiveBatch { public buildBatch(): Promise { let indicesCount = 0 let attributeCount = 0 + const rvAABB: Box3 = new Box3() const bounds: Box3 = new Box3() for (let k = 0; k < this.renderViews.length; k++) { const ervee = this.renderViews[k] @@ -209,11 +210,17 @@ export class MeshBatch extends PrimitiveBatch { attributeCount += ervee.renderData.geometry.attributes.POSITION.length bounds.union(ervee.aabb) } + const needsRTE = Geometry.needsRTE(bounds) const hasVertexColors = this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined - const indices = new Uint32Array(indicesCount) - const position = new Float64Array(attributeCount) + const indices = + attributeCount >= 65535 || indicesCount >= 65535 + ? new Uint32Array(indicesCount) + : new Uint16Array(indicesCount) + const position = needsRTE + ? new Float64Array(attributeCount) + : new Float32Array(attributeCount) const color = new Float32Array(hasVertexColors ? attributeCount : 0) color.fill(1) const batchIndices = new Float32Array(attributeCount / 3) @@ -228,49 +235,84 @@ export class MeshBatch extends PrimitiveBatch { /** Catering to typescript * There is no unniverse where indices or positions are undefined at this point */ - if (!geometry.attributes || !geometry.attributes.INDEX) { + if (!geometry.attributes || !geometry.attributes?.INDEX) { throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) } - indices.set( - geometry.attributes.INDEX.map((val) => val + offset / 3), - arrayOffset + + geometry.attributes?.INDEX.copyToBuffer(indices, arrayOffset) + const indicesSubArray = indices.subarray( + arrayOffset, + arrayOffset + geometry.attributes?.INDEX.length ) - position.set(geometry.attributes.POSITION, offset) - if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset) + + geometry.attributes?.POSITION.copyToBuffer(position, offset) + const positionSubarray = position.subarray( + offset, + offset + + (this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0) + ) + /** We transform the copied geometry so that we do not alter original chunk data which might be shared */ + Geometry.transformArray( + positionSubarray, + geometry.transform, + 0, + geometry.attributes?.POSITION.length + ) + + if (geometry.attributes.COLOR) { + geometry.attributes?.COLOR.copyToBuffer(color, offset) + } /** We either copy over the provided vertex normals */ if (geometry.attributes.NORMAL) { - normals.set(geometry.attributes.NORMAL, offset) + geometry.attributes?.NORMAL.copyToBuffer(normals, offset) } else { /** Either we compute them ourselves */ - Geometry.computeVertexNormalsBuffer( + Geometry.computeVertexNormalsBufferVirtual( normals.subarray( offset, - offset + geometry.attributes.POSITION.length + offset + + (this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0) ) as unknown as number[], geometry.attributes.POSITION, geometry.attributes.INDEX ) } - batchIndices.fill( - k, - offset / 3, - offset / 3 + geometry.attributes.POSITION.length / 3 - ) + batchIndices.fill(k, offset / 3, offset / 3 + geometry.attributes.POSITION.length) this.renderViews[k].setBatchData( this.id, arrayOffset, geometry.attributes.INDEX.length, offset / 3, - offset / 3 + geometry.attributes.POSITION.length / 3 + offset / 3 + geometry.attributes.POSITION.length ) + /** We re-compute the render view aabb based on transformed geometry + * We do this because some transforms like non-uniform scaling can produce incorrect results + * if we compute an aabb from original geometry then apply the transform. That's why we compute + * an aabb from the transformed geometry here and set it in the rv + */ + rvAABB.setFromArray(positionSubarray) + this.renderViews[k].aabb = rvAABB + const batchObject = new BatchObject(this.renderViews[k], k) - batchObject.buildAccelerationStructure() + batchObject.buildAccelerationStructure(positionSubarray, indicesSubArray) batchObjects.push(batchObject) + indices.set( + batchObject.accelerationStructure.bvh.geometry.index?.array as number[], + arrayOffset + ) + + /** Re-index the indices inside the batch */ + for (let i = 0; i < indicesSubArray.length; i++) { + indicesSubArray[i] = indicesSubArray[i] + offset / 3 + } + offset += geometry.attributes.POSITION.length arrayOffset += geometry.attributes.INDEX.length + + this.renderViews[k].disposeGeometry() } const geometry = this.makeMeshGeometry( @@ -280,7 +322,7 @@ export class MeshBatch extends PrimitiveBatch { batchIndices, hasVertexColors ? color : undefined ) - const needsRTE = Geometry.needsRTE(bounds) + if (needsRTE) Geometry.updateRTEGeometry(geometry, position) this.primitive = new SpeckleMesh(geometry, needsRTE) @@ -296,16 +338,16 @@ export class MeshBatch extends PrimitiveBatch { this.primitive.frustumCulled = false this.primitive.geometry.addGroup(0, this.getCount(), 0) - batchObjects.forEach((element: BatchObject) => { - element.renderView.disposeGeometry() - }) + // batchObjects.forEach((element: BatchObject) => { + // element.renderView.disposeGeometry() + // }) return Promise.resolve() } protected makeMeshGeometry( indices: Uint32Array | Uint16Array, - position: Float64Array, + position: Float64Array | Float32Array, normals: Float32Array, batchIndices: Float32Array, color?: Float32Array @@ -313,10 +355,10 @@ export class MeshBatch extends PrimitiveBatch { const geometry = new BufferGeometry() if (position.length >= 65535 || indices.length >= 65535) { this.indexBuffer0 = new Uint32BufferAttribute(indices, 1) - this.indexBuffer1 = new Uint32BufferAttribute(new Uint32Array(indices.length), 1) + this.indexBuffer1 = new Uint32BufferAttribute(indices, 1) } else { this.indexBuffer0 = new Uint16BufferAttribute(indices, 1) - this.indexBuffer1 = new Uint16BufferAttribute(new Uint16Array(indices.length), 1) + this.indexBuffer1 = new Uint16BufferAttribute(indices, 1) } geometry.setIndex(this.indexBuffer0) diff --git a/packages/viewer/src/modules/batching/PointBatch.ts b/packages/viewer/src/modules/batching/PointBatch.ts index 74a45cfe3..e03c6a1c3 100644 --- a/packages/viewer/src/modules/batching/PointBatch.ts +++ b/packages/viewer/src/modules/batching/PointBatch.ts @@ -158,6 +158,7 @@ export class PointBatch extends PrimitiveBatch { public buildBatch(): Promise { let attributeCount = 0 + const rvAABB: Box3 = new Box3() const bounds = new Box3() for (let k = 0; k < this.renderViews.length; k++) { const ervee = this.renderViews[k] @@ -170,9 +171,17 @@ export class PointBatch extends PrimitiveBatch { attributeCount += ervee.renderData.geometry.attributes.POSITION.length bounds.union(ervee.aabb) } - const position = new Float64Array(attributeCount) - const color = new Float32Array(attributeCount).fill(1) - const index = new Int32Array(attributeCount / 3) + const needsRTE = Geometry.needsRTE(bounds) + const hasVertexColors = + this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined + const needsInt32Indices = attributeCount >= 65535 + const position = needsRTE + ? new Float64Array(attributeCount) + : new Float32Array(attributeCount) + const color = new Float32Array(hasVertexColors ? attributeCount : 0).fill(1) + const index = needsInt32Indices + ? new Uint32Array(attributeCount / 3) + : new Uint16Array(attributeCount / 3) let offset = 0 let indexOffset = 0 for (let k = 0; k < this.renderViews.length; k++) { @@ -180,14 +189,39 @@ export class PointBatch extends PrimitiveBatch { if (!geometry.attributes) { throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) } - position.set(geometry.attributes.POSITION, offset) - if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset) + + geometry.attributes?.POSITION.copyToBuffer(position, offset) + const positionSubarray = position.subarray( + offset, + offset + + (this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0) + ) + + Geometry.transformArray( + positionSubarray, + geometry.transform, + 0, + geometry.attributes?.POSITION.length + ) + if (geometry.attributes.COLOR) { + geometry.attributes?.COLOR.copyToBuffer(color, offset) + } index.set( - new Int32Array(geometry.attributes.POSITION.length / 3).map( - (_value, index) => index + indexOffset - ), + (needsInt32Indices + ? new Uint32Array(geometry.attributes.POSITION.length / 3) + : new Uint16Array(geometry.attributes.POSITION.length / 3) + ).map((_value, index) => index + indexOffset), indexOffset ) + + /** We re-compute the render view aabb based on transformed geometry + * We do this because some transforms like non-uniform scaling can produce incorrect results + * if we compute an aabb from original geometry then apply the transform. That's why we compute + * an aabb from the transformed geometry here and set it in the rv + */ + rvAABB.setFromArray(positionSubarray) + this.renderViews[k].aabb = rvAABB + this.renderViews[k].setBatchData( this.id, offset / 3, @@ -201,7 +235,7 @@ export class PointBatch extends PrimitiveBatch { } const geometry = this.makePointGeometry(index, position, color) - if (Geometry.needsRTE(bounds)) { + if (needsRTE) { Geometry.updateRTEGeometry(geometry, position) if (!this.batchMaterial.defines) this.batchMaterial.defines = {} this.batchMaterial.defines['USE_RTE'] = ' ' @@ -221,8 +255,8 @@ export class PointBatch extends PrimitiveBatch { } protected makePointGeometry( - index: Int32Array, - position: Float64Array, + index: Uint32Array | Uint16Array, + position: Float64Array | Float32Array, color: Float32Array ): BufferGeometry { const geometry = new BufferGeometry() diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index 286c19ea8..e9e391a71 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -144,8 +144,8 @@ export default class TextBatch implements Batch { * - Even if, the **text batch does not use the materials in it's draw groups**, it emulates the behavior as if it would */ public setBatchBuffers(ranges: BatchUpdateRange[]): void { - console.warn(' Groups -> ', this.mesh.groups) - console.warn(' Ranges -> ', ranges) + // console.warn(' Groups -> ', this.mesh.groups) + // console.warn(' Ranges -> ', ranges) const splitRanges: BatchUpdateRange[] = [] ranges.forEach((range: BatchUpdateRange) => { for (let k = 0; k < range.count; k++) { @@ -321,7 +321,7 @@ export default class TextBatch implements Batch { screenOriented: boolean } const text = new Text() - this.renderViews[k].renderData.geometry.bakeTransform?.decompose( + this.renderViews[k].renderData.geometry.transform?.decompose( text.position, text.quaternion, text.scale @@ -349,24 +349,23 @@ export default class TextBatch implements Batch { /** We're using visibleBounds for a better fit */ const bounds = textRenderInfo.visibleBounds // console.log('bounds -> ', bounds) - const vertices = [] - vertices.push( - bounds[0], - bounds[3], - 0, - bounds[2], - bounds[3], - 0, - bounds[0], - bounds[1], - 0, - bounds[2], - bounds[1], - 0 - ) + const vertices = new Float32Array(12) + vertices[0] = bounds[0] + vertices[1] = bounds[3] + vertices[2] = 0 + vertices[3] = bounds[2] + vertices[4] = bounds[3] + vertices[5] = 0 + vertices[6] = bounds[0] + vertices[7] = bounds[1] + vertices[8] = 0 + vertices[9] = bounds[2] + vertices[10] = bounds[1] + vertices[11] = 0 + box.setFromArray(vertices) box.applyMatrix4( - this.renderViews[k].renderData.geometry.bakeTransform || new Matrix4() + this.renderViews[k].renderData.geometry.transform || new Matrix4() ) needsRTE ||= Geometry.needsRTE(box) @@ -374,7 +373,7 @@ export default class TextBatch implements Batch { const geometry = text.geometry geometry.computeBoundingBox() const textBvh = AccelerationStructure.buildBVH( - geometry.index?.array as number[], + geometry.index?.array as unknown as Uint16Array | Uint32Array, vertices, DefaultBVHOptions ) diff --git a/packages/viewer/src/modules/batching/TextBatchObject.ts b/packages/viewer/src/modules/batching/TextBatchObject.ts index 413ce1f4d..df8e7527b 100644 --- a/packages/viewer/src/modules/batching/TextBatchObject.ts +++ b/packages/viewer/src/modules/batching/TextBatchObject.ts @@ -7,8 +7,8 @@ export class TextBatchObject extends BatchObject { public constructor(renderView: NodeRenderView, batchIndex: number) { super(renderView, batchIndex) - if (renderView.renderData.geometry.bakeTransform) - this.textTransform.copy(renderView.renderData.geometry.bakeTransform) + if (renderView.renderData.geometry.transform) + this.textTransform.copy(renderView.renderData.geometry.transform) /** TO DO: Not sure we should do this */ this.transform.copy(this.textTransform) this.transformInv.copy(new Matrix4().copy(this.textTransform).invert()) diff --git a/packages/viewer/src/modules/converter/Geometry.ts b/packages/viewer/src/modules/converter/Geometry.ts index a921f30dd..3450d7148 100644 --- a/packages/viewer/src/modules/converter/Geometry.ts +++ b/packages/viewer/src/modules/converter/Geometry.ts @@ -6,18 +6,19 @@ import { Float32BufferAttribute, InstancedInterleavedBuffer, InterleavedBufferAttribute, + MathUtils, Matrix4, Vector2, Vector3, Vector4 } from 'three' -import { type SpeckleObject } from '../../IViewer.js' +import { DataChunk, type SpeckleObject } from '../../IViewer.js' import { getRelativeOffset, makePerspectiveProjection } from '../Helpers.js' import earcut from 'earcut' +import { ChunkArray } from './VirtualArray.js' const vecBuff0: Vector3 = new Vector3() const floatArrayBuff: Float32Array = new Float32Array(16) -Vector3 export enum GeometryAttributes { POSITION = 'POSITION', @@ -28,14 +29,23 @@ export enum GeometryAttributes { INDEX = 'INDEX' } +type AttributeValue = ChunkArray + +// Required keys +type RequiredKeys = GeometryAttributes.POSITION | GeometryAttributes.INDEX + +// Optional keys +type OptionalKeys = Exclude + +// Final shape: required + optional keys +type GeometryAttributesShape = { + [K in RequiredKeys]: AttributeValue +} & { + [K in OptionalKeys]?: AttributeValue +} + export interface GeometryData { - attributes: - | ({ - [GeometryAttributes.POSITION]: number[] - } & Partial< - Record, number[]> - >) - | null + attributes: GeometryAttributesShape | null bakeTransform: Matrix4 | null transform: Matrix4 | null metaData?: SpeckleObject @@ -70,11 +80,7 @@ export class Geometry { Geometry.DoubleToHighLowBuffer(doublePositions, position_low, position_high) - const instanceBufferLow = new InstancedInterleavedBuffer( - new Float32Array(position_low), - 6, - 1 - ) // xyz, xyz + const instanceBufferLow = new InstancedInterleavedBuffer(position_low, 6, 1) // xyz, xyz geometry.setAttribute( 'instanceStartLow', new InterleavedBufferAttribute(instanceBufferLow, 3, 0) @@ -87,7 +93,7 @@ export class Geometry { } static mergeGeometryAttribute( - attributes: (number[] | undefined)[], + attributes: AttributeValue[], target: Float32Array | Float64Array ): ArrayLike { let offset = 0 @@ -96,15 +102,15 @@ export class Geometry { if (!attribute || !target) { throw new Error('Cannot merge geometries. Indices or positions are undefined') } - target.set(attribute, offset) + attribute.copyToBuffer(target, offset) offset += attribute.length } return target } static mergeIndexAttribute( - indexAttributes: (number[] | undefined)[], - positionAttributes: (number[] | undefined)[] + indexAttributes: AttributeValue[], + positionAttributes: AttributeValue[] ): number[] { let indexOffset = 0 const mergedIndex = [] @@ -117,7 +123,7 @@ export class Geometry { } for (let j = 0; j < index.length; ++j) { - mergedIndex.push(index[j] + indexOffset / 3) + mergedIndex.push(index.get(j) + indexOffset / 3) } indexOffset += positions.length @@ -139,53 +145,59 @@ export class Geometry { Geometry.transformGeometryData(geometries[i], geometries[i].bakeTransform) } - if (sampleAttributes && sampleAttributes[GeometryAttributes.INDEX]) { - const indexAttributes: (number[] | undefined)[] = geometries.map( - (item: GeometryData) => { - /** Catering to typescript */ - if (!item.attributes) return - return item.attributes[GeometryAttributes.INDEX] - } - ) - const positionAttributes: (number[] | undefined)[] = geometries.map((item) => { + if (sampleAttributes && sampleAttributes.INDEX) { + const indexAttributes = geometries.map((item: GeometryData) => { /** Catering to typescript */ if (!item.attributes) return - return item.attributes[GeometryAttributes.POSITION] - }) + return item.attributes.INDEX + }) as ChunkArray[] + const positionAttributes = geometries.map((item) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes.POSITION + }) as ChunkArray[] /** o_0 Catering to typescript*/ if (mergedGeometry.attributes) - mergedGeometry.attributes[GeometryAttributes.INDEX] = - Geometry.mergeIndexAttribute(indexAttributes, positionAttributes) + mergedGeometry.attributes.INDEX = new ChunkArray([ + { + data: Geometry.mergeIndexAttribute(indexAttributes, positionAttributes), + id: MathUtils.generateUUID(), + references: 1 + } + ]) } for (const k in sampleAttributes) { if (k !== GeometryAttributes.INDEX) { - const attributes: (number[] | undefined)[] = geometries.map((item) => { + const attributes: ChunkArray[] = geometries.map((item) => { /** Catering to typescript */ if (!item.attributes) return - return item.attributes[k as GeometryAttributes] as number[] - }) + return item.attributes[k as GeometryAttributes] + }) as ChunkArray[] /** Catering to typescript */ - if (mergedGeometry.attributes) - mergedGeometry.attributes[k as GeometryAttributes] = - Geometry.mergeGeometryAttribute( - attributes, - k === GeometryAttributes.POSITION - ? new Float64Array( - attributes.reduce((prev, cur) => { - /** Catering to typescript */ - if (!cur) return 0 - return prev + cur.length - }, 0) - ) - : new Float32Array( - attributes.reduce((prev, cur) => { - /** Catering to typescript */ - if (!cur) return 0 - return prev + cur.length - }, 0) - ) - ) as number[] + if (mergedGeometry.attributes) { + const mergedData = Geometry.mergeGeometryAttribute( + attributes, + k === GeometryAttributes.POSITION + ? new Float64Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + : new Float32Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + ) as number[] + mergedGeometry.attributes[k as GeometryAttributes] = new ChunkArray([ + { data: mergedData, id: MathUtils.generateUUID(), references: 1 } + ]) + } } } @@ -202,23 +214,76 @@ export class Geometry { if (!geometryData.attributes) return if (!geometryData.attributes.POSITION) return if (!m) return + if (Geometry.isMatrix4Identity(m)) return const e = m.elements + geometryData.attributes.POSITION.chunkArray.forEach((chunk: DataChunk) => { + for (let k = 0; k < chunk.data.length; k += 3) { + const x = chunk.data[k], + y = chunk.data[k + 1], + z = chunk.data[k + 2] + const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]) - for (let k = 0; k < geometryData.attributes.POSITION.length; k += 3) { - const x = geometryData.attributes.POSITION[k], - y = geometryData.attributes.POSITION[k + 1], - z = geometryData.attributes.POSITION[k + 2] + chunk.data[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w + chunk.data[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w + chunk.data[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w + } + }) + } + + public static transformArray( + array: number[] | Float32Array | Float64Array, + m: Matrix4 | null, + offset?: number, + count?: number + ) { + if (!m) return + if (Geometry.isMatrix4Identity(m)) return + + const e = m.elements + offset = offset || 0 + count = count || array.length + for (let k = offset; k < offset + count; k += 3) { + const x = array[k], + y = array[k + 1], + z = array[k + 2] const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]) - geometryData.attributes.POSITION[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w - geometryData.attributes.POSITION[k + 1] = - (e[1] * x + e[5] * y + e[9] * z + e[13]) * w - geometryData.attributes.POSITION[k + 2] = - (e[2] * x + e[6] * y + e[10] * z + e[14]) * w + array[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w + array[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w + array[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w } } + public static isMatrix4Identity(matrix: Matrix4) { + const e = matrix.elements + + // Check all off-diagonal elements first + if ( + e[1] !== 0 || + e[2] !== 0 || + e[3] !== 0 || + e[4] !== 0 || + e[6] !== 0 || + e[7] !== 0 || + e[8] !== 0 || + e[9] !== 0 || + e[11] !== 0 || + e[12] !== 0 || + e[13] !== 0 || + e[14] !== 0 + ) { + return false + } + + // Now check diagonals + if (e[0] !== 1 || e[5] !== 1 || e[10] !== 1 || e[15] !== 1) { + return false + } + + return true + } + public static unpackColors(int32Colors: number[]): number[] { const colors = new Array(int32Colors.length * 3) for (let i = 0; i < int32Colors.length; i++) { @@ -431,9 +496,58 @@ export class Geometry { } } + public static computeVertexNormalsBufferVirtual( + buffer: number[], + position: ChunkArray, + index: ChunkArray + ) { + const pA = new Vector3(), + pB = new Vector3(), + pC = new Vector3() + const nA = new Vector3(), + nB = new Vector3(), + nC = new Vector3() + const cb = new Vector3(), + ab = new Vector3() + + // indexed elements + for (let i = 0, il = index.length; i < il; i += 3) { + const vA = index.get(i + 0) + const vB = index.get(i + 1) + const vC = index.get(i + 2) + pA.set(position.get(vA * 3), position.get(vA * 3 + 1), position.get(vA * 3 + 2)) + pB.set(position.get(vB * 3), position.get(vB * 3 + 1), position.get(vB * 3 + 2)) + pC.set(position.get(vC * 3), position.get(vC * 3 + 1), position.get(vC * 3 + 2)) + + cb.subVectors(pC, pB) + ab.subVectors(pA, pB) + cb.cross(ab) + + nA.fromArray(buffer, vA * 3) + nB.fromArray(buffer, vB * 3) + nC.fromArray(buffer, vC * 3) + + nA.add(cb) + nB.add(cb) + nC.add(cb) + + buffer[vA * 3] = nA.x + buffer[vA * 3 + 1] = nA.y + buffer[vA * 3 + 2] = nA.z + + buffer[vB * 3] = nB.x + buffer[vB * 3 + 1] = nB.y + buffer[vB * 3 + 2] = nB.z + + buffer[vC * 3] = nC.x + buffer[vC * 3 + 1] = nC.y + buffer[vC * 3 + 2] = nC.z + } + } + public static computeVertexNormals( buffer: BufferGeometry, - doublePositions: Float64Array + positions: Float64Array | Float32Array ) { const index = buffer.index const positionAttribute = buffer.getAttribute('position') @@ -470,9 +584,9 @@ export class Geometry { const vB = index.getX(i + 1) const vC = index.getX(i + 2) - pA.fromArray(doublePositions, vA * 3) - pB.fromArray(doublePositions, vB * 3) - pC.fromArray(doublePositions, vC * 3) + pA.fromArray(positions, vA * 3) + pB.fromArray(positions, vB * 3) + pC.fromArray(positions, vC * 3) cb.subVectors(pC, pB) ab.subVectors(pA, pB) @@ -495,9 +609,9 @@ export class Geometry { for (let i = 0, il = positionAttribute.count; i < il; i += 3) { /** This is done blind. Don't think speckle supports non-indexed geometry */ - pA.fromArray(doublePositions, i * 3) - pB.fromArray(doublePositions, i * 3 + 1) - pC.fromArray(doublePositions, i * 3 + 2) + pA.fromArray(positions, i * 3) + pB.fromArray(positions, i * 3 + 1) + pC.fromArray(positions, i * 3 + 2) cb.subVectors(pC, pB) ab.subVectors(pA, pB) diff --git a/packages/viewer/src/modules/converter/MeshTriangulationHelper.js b/packages/viewer/src/modules/converter/MeshTriangulationHelper.js index 4c2daf13e..404b1d682 100644 --- a/packages/viewer/src/modules/converter/MeshTriangulationHelper.js +++ b/packages/viewer/src/modules/converter/MeshTriangulationHelper.js @@ -1,7 +1,17 @@ +/* eslint-disable camelcase */ +import { Triangle, Vector3 } from 'three' + /** * Set of functions to triangulate n-gon faces (i.e. polygon faces with an arbitrary (n) number of vertices). * This class is a JavaScript port of https://github.com/specklesystems/speckle-sharp/blob/main/Objects/Objects/Utils/MeshTriangulationHelper.cs */ +const _vec30 = new Vector3() +const _vec31 = new Vector3() +const _vec32 = new Vector3() +const _vec33 = new Vector3() +const _normal = new Vector3() +const _triangle = new Triangle() + export default class MeshTriangulationHelper { /** * Calculates the triangulation of the face at given faceIndex. @@ -9,30 +19,39 @@ export default class MeshTriangulationHelper { * @param {Number} faceIndex The index of the face's cardinality indicator `n` * @param {Number[]} faces The list of faces in the mesh * @param {Number[]} vertices The list of vertices in the mesh - * @return {Number[]} flat list of triangle faces (without cardinality indicators) + * @return {Number} flat list of triangle faces (without cardinality indicators) */ - static triangulateFace(faceIndex, faces, vertices) { - let n = faces[faceIndex] + static triangulateFace( + faceIndex, + faces, + vertices, + /** Purists rolling over in their graves because of this */ + _inout_targetArray, + _in_offset + ) { + let n = faces.get(faceIndex) if (n < 3) n += 3 // 0 -> 3, 1 -> 4 //Converts from relative to absolute index (returns index in mesh.vertices list) + /** Why doesn't javascript have a means to inline functions?! */ function asIndex(v) { return faceIndex + v + 1 } //Gets vertex from a relative vert index - function V(v) { - const index = faces[asIndex(v)] * 3 - return new Vector3(vertices[index], vertices[index + 1], vertices[index + 2]) + function V(v, target) { + const index = faces.get(asIndex(v)) * 3 + target.x = vertices.get(index) + target.y = vertices.get(index + 1) + target.z = vertices.get(index + 2) + return target } - const triangleFaces = Array((n - 2) * 3) - //Calculate face normal using the Newell Method - const faceNormal = new Vector3(0, 0, 0) + const faceNormal = _normal for (let ii = n - 1, jj = 0; jj < n; ii = jj, jj++) { - const iPos = V(ii) - const jPos = V(jj) + const iPos = V(ii, _vec30) + const jPos = V(jj, _vec31) faceNormal.x += (jPos.y - iPos.y) * (iPos.z + jPos.z) // projection on yz faceNormal.y += (jPos.z - iPos.z) * (iPos.x + jPos.x) // projection on xz faceNormal.z += (jPos.x - iPos.x) * (iPos.y + jPos.y) // projection on xy @@ -40,8 +59,8 @@ export default class MeshTriangulationHelper { faceNormal.normalize() //Set up previous and next links to effectively form a double-linked vertex list - const prev = Array(n) - const next = Array(n) + const prev = [] //new Array(n) + const next = [] //new Array(n) for (let j = 0; j < n; j++) { prev[j] = j - 1 next[j] = j + 1 @@ -52,20 +71,25 @@ export default class MeshTriangulationHelper { //Start clipping ears until we are left with a triangle let i = 0 let counter = 0 + let localOffset = 0 while (n >= 3) { let isEar = true //If we are the last triangle or we have exhausted our vertices, the below statement will be false if (n > 3 && counter < n) { - const prevVertex = V(prev[i]) - const earVertex = V(i) - const nextVertex = V(next[i]) + const prevVertex = V(prev[i], _vec30) + const earVertex = V(i, _vec31) + const nextVertex = V(next[i], _vec32) - if (this.triangleIsCCW(faceNormal, prevVertex, earVertex, nextVertex)) { + _triangle.a.copy(prevVertex) + _triangle.b.copy(earVertex) + _triangle.c.copy(nextVertex) + + if (_triangle.isFrontFacing(faceNormal)) { let k = next[next[i]] do { - if (this.testPointTriangle(V(k), prevVertex, earVertex, nextVertex)) { + if (_triangle.containsPoint(V(k, _vec33))) { isEar = false break } @@ -78,10 +102,13 @@ export default class MeshTriangulationHelper { } if (isEar) { - const a = faces[asIndex(i)] - const b = faces[asIndex(next[i])] - const c = faces[asIndex(prev[i])] - triangleFaces.push(a, b, c) + const a = faces.get(asIndex(i)) + const b = faces.get(asIndex(next[i])) + const c = faces.get(asIndex(prev[i])) + _inout_targetArray[_in_offset + localOffset] = a + _inout_targetArray[_in_offset + localOffset + 1] = b + _inout_targetArray[_in_offset + localOffset + 2] = c + localOffset += 3 next[prev[i]] = next[i] prev[next[i]] = prev[i] @@ -94,89 +121,6 @@ export default class MeshTriangulationHelper { } } - return triangleFaces - } - - /** - * Tests if point v is within the triangle *abc*. - * @param {Vector3} v - * @param {Vector3} a - * @param {Vector3} b - * @param {Vector3} c - * @returns {boolean} true if v is within triangle. - */ - static testPointTriangle(v, a, b, c) { - function Test(_v, _a, _b) { - const crossA = _v.cross(_a) - const crossB = _v.cross(_b) - const dotWithEpsilon = Number.EPSILON + crossA.dot(crossB) - return Math.sign(dotWithEpsilon) !== -1 - } - - return ( - Test(b.sub(a), v.sub(a), c.sub(a)) && - Test(c.sub(b), v.sub(b), a.sub(b)) && - Test(a.sub(c), v.sub(c), b.sub(c)) - ) - } - - /** - * Checks that triangle abc is clockwise with reference to referenceNormal. - * @param {Vector3} referenceNormal The normal direction of the face. - * @param {Vector3} a - * @param {Vector3} b - * @param {Vector3} c - * @returns {boolean} true if triangle is ccw - */ - static triangleIsCCW(referenceNormal, a, b, c) { - const triangleNormal = c.sub(a).cross(b.sub(a)) - triangleNormal.normalize() - return referenceNormal.dot(triangleNormal) > 0.0 - } -} - -/** - * Encapsulates vector maths operations required for polygon triangulation - */ -class Vector3 { - constructor(x, y, z) { - this.x = x - this.y = y - this.z = z - } - - add(v) { - return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z) - } - - sub(v) { - return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z) - } - - mul(n) { - return new Vector3(this.x - n, this.y - n, this.z - n) - } - - dot(v) { - return this.x * v.x + this.y * v.y + this.z * v.z - } - - cross(v) { - const nx = this.y * v.z - this.z * v.y - const ny = this.z * v.x - this.x * v.z - const nz = this.x * v.y - this.y * v.x - - return new Vector3(nx, ny, nz) - } - - squareSum() { - return this.x * this.x + this.y * this.y + this.z * this.z - } - - normalize() { - const scale = 1.0 / Math.sqrt(this.squareSum()) - this.x *= scale - this.y *= scale - this.z *= scale + return localOffset } } diff --git a/packages/viewer/src/modules/converter/VirtualArray.ts b/packages/viewer/src/modules/converter/VirtualArray.ts new file mode 100644 index 000000000..7f5a892cf --- /dev/null +++ b/packages/viewer/src/modules/converter/VirtualArray.ts @@ -0,0 +1,177 @@ +import { TypedArray } from 'type-fest' +import { DataChunk } from '../../IViewer.js' +import { Box3, MathUtils, Vector3 } from 'three' + +export class VirtualArray { + private offsets: number[] + + constructor(public chunks: Array>) { + this.updateOffsets() + } + + get length() { + if (this.chunks.length === 0) return 0 + const lastChunk = this.chunks[this.chunks.length - 1] + return this.offsets[this.offsets.length - 1] + lastChunk.length + } + + get(index: number): number { + if (this.chunks.length === 1) return this.chunks[0][index] + const chunkIndex = this.findChunkIndex(index) + const localIndex = index - this.offsets[chunkIndex] + return this.chunks[chunkIndex][localIndex] + } + + set(index: number, value: number) { + if (this.chunks.length === 1) { + this.chunks[0][index] = value + return + } + const chunkIndex = this.findChunkIndex(index) + const localIndex = index - this.offsets[chunkIndex] + this.chunks[chunkIndex][localIndex] = value + } + + public findChunkIndex(index: number): number { + let low = 0 + let high = this.offsets.length - 1 + + while (low <= high) { + const mid = (low + high) >> 1 + const start = this.offsets[mid] + const end = mid + 1 < this.offsets.length ? this.offsets[mid + 1] : this.length + if (index >= start && index < end) return mid + if (index < start) high = mid - 1 + else low = mid + 1 + } + + throw new RangeError('Index out of bounds') + } + + public updateOffsets() { + this.offsets = [] + let sum = 0 + for (const chunk of this.chunks) { + this.offsets.push(sum) + sum += chunk.length + } + } +} + +export class ChunkArray extends VirtualArray { + public chunkArray: Array + protected flatArray: TypedArray + + constructor(chunks: Array) { + super(chunks && chunks.map((c: DataChunk) => c.data)) + this.chunkArray = chunks + } + + public slice() { + const copiesArray: Array = [] + this.chunkArray.forEach((chunk: DataChunk) => { + const chunkCopy = new Array(chunk.data.length) + for (let k = 0; k < chunk.data.length; k++) { + chunkCopy[k] = chunk.data[k] + } + copiesArray.push({ data: chunkCopy, id: MathUtils.generateUUID(), references: 1 }) + }) + return new ChunkArray(copiesArray) + } + + public copyToBuffer(buffer: TypedArray, offset: number) { + let chunkOffset = 0 + + this.chunkArray.forEach((chunk: DataChunk) => { + buffer.set( + chunk.data as unknown as ArrayLike & ArrayLike, + offset + chunkOffset + ) + chunkOffset += chunk.data.length + }) + } + + public computeBox3(): Box3 { + const box = new Box3() + const vec3 = new Vector3() + let carry: number[] = [] // to hold x/y if vec3 is split + + for (let c = 0; c < this.chunks.length; c++) { + const chunk = this.chunks[c] + let i = 0 + + // Handle carry-over from previous chunk + if (carry.length > 0) { + while (carry.length < 3 && i < chunk.length) { + carry.push(chunk[i++]) + } + if (carry.length === 3) { + vec3.set(carry[0], carry[1], carry[2]) + box.expandByPoint(vec3) + carry = [] + } + } + + // Now read as many full vec3s as possible from this chunk + const fullVec3Count = Math.floor((chunk.length - i) / 3) + for (let j = 0; j < fullVec3Count; j++) { + const x = chunk[i++] + const y = chunk[i++] + const z = chunk[i++] + vec3.set(x, y, z) + box.expandByPoint(vec3) + } + + // If there's a leftover partial vec3 at the end, save it + while (i < chunk.length) { + carry.push(chunk[i++]) + } + } + + // Final sanity check + if (carry.length !== 0) { + console.warn('Virtual position buffer ended with incomplete vec3 data') + } + + return box + } + + protected getFlatArray(Type: { new (length: number): T }) { + if (!this.flatArray || !(this.flatArray instanceof Type)) { + this.flatArray = new Type(this.length) + let chunkOffset = 0 + this.chunks.forEach((chunk: number[]) => { + this.flatArray.set( + chunk as unknown as ArrayLike & ArrayLike, + chunkOffset + ) + chunkOffset += chunk.length + }) + } + return this.flatArray as T + } + + public getFloat32Array(): Float32Array { + return this.getFlatArray(Float32Array) + } + + public getFloat64Array(): Float64Array { + return this.getFlatArray(Float64Array) + } + + public getInt16Array(): Int16Array { + return this.getFlatArray(Int16Array) + } + + public getInt32Array(): Int32Array { + return this.getFlatArray(Int32Array) + } + + public getUint16Array(): Uint16Array { + return this.getFlatArray(Uint16Array) + } + + public getUint32Array(): Uint32Array { + return this.getFlatArray(Uint32Array) + } +} diff --git a/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts index a35bda07e..076ba64a8 100644 --- a/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts @@ -16,6 +16,7 @@ import { Quaternion, Raycaster, ReplaceStencilOp, + Uint16BufferAttribute, Vector2, Vector3, type Intersection @@ -370,7 +371,7 @@ export class AreaMeasurement extends Measurement { } if (!index || index.count !== indices.length) { - geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1)) + geometry.setIndex(new Uint16BufferAttribute(indices, 1)) } else { ;(index.array as Uint16Array).set(indices, 0) index.needsUpdate = true diff --git a/packages/viewer/src/modules/extensions/sections/SectionOutlines.ts b/packages/viewer/src/modules/extensions/sections/SectionOutlines.ts index 8d54c2c13..6152c040d 100644 --- a/packages/viewer/src/modules/extensions/sections/SectionOutlines.ts +++ b/packages/viewer/src/modules/extensions/sections/SectionOutlines.ts @@ -303,6 +303,7 @@ export class SectionOutlines extends Extension { private createPlaneOutline(planeId: string): PlaneOutline { const buffer = new Float64Array(SectionOutlines.INITIAL_BUFFER_SIZE) const lineGeometry = new LineSegmentsGeometry() + /** We need to re-allocate, otherwise three.js will do it anyway */ lineGeometry.setPositions(new Float32Array(buffer)) ;( lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute @@ -390,7 +391,7 @@ export class SectionOutlines extends Extension { const buffer = new Float32Array(size) outline.renderable.geometry = new LineSegmentsGeometry() - outline.renderable.geometry.setPositions(new Float32Array(buffer)) + outline.renderable.geometry.setPositions(buffer) ;( outline.renderable.geometry.attributes[ 'instanceStart' diff --git a/packages/viewer/src/modules/extensions/sections/SectionTool.ts b/packages/viewer/src/modules/extensions/sections/SectionTool.ts index 2e92f72df..0358d5b9a 100644 --- a/packages/viewer/src/modules/extensions/sections/SectionTool.ts +++ b/packages/viewer/src/modules/extensions/sections/SectionTool.ts @@ -21,6 +21,8 @@ import { Color, MeshBasicMaterial, PlaneGeometry, + Float32BufferAttribute, + Uint16BufferAttribute, Euler } from 'three' import { intersectObjectWithRay, TransformControls } from '../TransformControls.js' @@ -61,7 +63,7 @@ const _vector3 = new Vector3() const _tempEuler = new Euler() const _tempQuaternion = new Quaternion() -const unitCube = [ +const unitCube = new Float32Array([ -1 * 0.5, -1 * 0.5, -1 * 0.5, @@ -93,12 +95,12 @@ const unitCube = [ -1 * 0.5, 1 * 0.5, 1 * 0.5 -] +]) -const unitCubeIndices: number[] = [ +const unitCubeIndices: Uint16Array = new Uint16Array([ 0, 1, 3, 3, 1, 2, 1, 5, 2, 2, 5, 6, 5, 4, 6, 6, 4, 7, 4, 0, 7, 7, 0, 3, 3, 2, 7, 7, 2, 6, 4, 5, 0, 0, 5, 1 -] +]) const unitCubeEdges: number[] = [ // Bottom Face @@ -932,11 +934,11 @@ export class SectionTool extends Extension { /** Creates the geometry for the visible outline of the section tool */ protected createOutline() { /** We start from the unit cube's edges */ - const buffer = new Float32Array(unitCubeEdges.slice()) + const buffer = unitCubeEdges.slice() as unknown as Float32Array /** Create the line segments geometry */ const lineGeometry = new LineSegmentsGeometry() - lineGeometry.setPositions(new Float32Array(buffer)) + lineGeometry.setPositions(buffer) ;( lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute ).data.setUsage(DynamicDrawUsage) @@ -1015,8 +1017,8 @@ export class SectionTool extends Extension { const indexes = unitCubeIndices.slice() const g = new BufferGeometry() - g.setAttribute('position', new BufferAttribute(new Float32Array(vertices), 3)) - g.setIndex(indexes) + g.setAttribute('position', new Float32BufferAttribute(vertices, 3)) + g.setIndex(new Uint16BufferAttribute(indexes, 1)) g.computeBoundingBox() g.computeVertexNormals() return g diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index fe51fa4a7..83c4e19f2 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -6,6 +6,7 @@ import { SpeckleType, type SpeckleObject } from '../../../index.js' import Logger from '../../utils/Logger.js' import { ObjectLoader2 } from '@speckle/objectloader2' import { SpeckleTypeAllRenderables } from '../GeometryConverter.js' +import { DataChunk } from '../../../IViewer.js' export type ConverterResultDelegate = (count: number) => void export type SpeckleConverterNodeDelegate = @@ -64,7 +65,7 @@ export default class SpeckleConverter { Parameter: null } - protected readonly IgnoreNodes = ['Parameter'] + protected readonly IgnoreNodes = ['Parameter', 'RawEncoding'] constructor(objectLoader: ObjectLoader2, tree: WorldTree) { if (!objectLoader) { @@ -287,24 +288,49 @@ export default class SpeckleConverter { * @param {[type]} arr [description] * @return {[type]} [description] */ - private async dechunk(arr: Array<{ referencedId: string }>) { - if (!arr || arr.length === 0) return arr - // Handles pre-chunking objects, or arrs that have not been chunked - if (!arr[0].referencedId) return arr + private async dechunk(arr: Array<{ referencedId: string }>): Promise { + if (!arr || arr.length === 0) { + return arr as unknown as DataChunk[] + } - const chunked: unknown[] = [] + if (Array.isArray(arr[0]) && !arr[0].referencedId) { + return arr as unknown as DataChunk[] + } + // Handles pre-chunking objects, or arrs that have not been chunked + if (!arr[0].referencedId) { + if (!(arr[0] instanceof Object)) + return [ + { + data: arr, + id: MathUtils.generateUUID(), + references: 1 + } + ] as unknown as DataChunk[] + else return arr as unknown as DataChunk[] + } + + const chunked: DataChunk[] = [] for (const ref of arr) { - const real: Record = (await this.objectLoader.getObject({ + const real: DataChunk = (await this.objectLoader.getObject({ id: ref.referencedId - })) as unknown as Record - chunked.push(real.data) + })) as unknown as DataChunk + if (real.references === undefined) { + real.references = 1 + } else { + real.references++ + } + if (typeof real.data[0] !== 'number' || isNaN(real.data[0])) { + Logger.error( + `Chunk id ${real.id} used for mesh ${ref.referencedId} might not have numeric geometry data. This is not supported!` + ) + } + chunked.push(real) // await this.asyncPause() } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dechunked = [].concat(...(chunked as any)) + // const dechunked = [].concat(...(chunked as any)) - return dechunked + return chunked } /** @@ -916,14 +942,23 @@ export default class SpeckleConverter { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no vertex position data and will be ignored` ) + node.model.raw.vertices = [] + node.model.raw.faces = [] + node.model.raw.colors = [] + node.model.raw.vertexNormals = [] return } if (!obj.faces || (obj.faces as Array).length === 0) { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no face data and will be ignored` ) + node.model.raw.vertices = [] + node.model.raw.faces = [] + node.model.raw.colors = [] + node.model.raw.vertexNormals = [] return } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore node.model.raw.vertices = await this.dechunk(obj.vertices) diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index 87d861a20..e82f59fe6 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -2,9 +2,11 @@ import { Geometry, type GeometryData } from '../../converter/Geometry.js' import MeshTriangulationHelper from '../../converter/MeshTriangulationHelper.js' import { getConversionFactor } from '../../converter/Units.js' import { type NodeData } from '../../tree/WorldTree.js' -import { Box3, EllipseCurve, Matrix4, Vector2, Vector3 } from 'three' +import { Box3, EllipseCurve, MathUtils, Matrix4, Vector2, Vector3 } from 'three' import { GeometryConverter, SpeckleType } from '../GeometryConverter.js' import Logger from '../../utils/Logger.js' +import { DataChunk } from '../../../IViewer.js' +import { ChunkArray } from '../../converter/VirtualArray.js' export class SpeckleGeometryConverter extends GeometryConverter { public typeLookupTable: { [type: string]: SpeckleType } = {} @@ -93,9 +95,35 @@ export class SpeckleGeometryConverter extends GeometryConverter { node.raw.colors = [] break case SpeckleType.Mesh: + /** Raw objects will no longer hold references to chunks */ node.raw.vertices = [] node.raw.faces = [] node.raw.colors = [] + node.raw.normals = [] + + // /** We can already delete these because we don't need them after triangulation */ + // node.raw.faces.forEach((c: DataChunk) => { + // c.references-- + + // if (!c.references) { + // Logger.warn(`Deleting chunk data ${c.id}`) + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // //@ts-ignore + // delete c.data + // } + // }) + + /** We can already delete this because we've changes the colors to floats in linear space */ + node.raw.colors.forEach((c: DataChunk) => { + c.references-- + + if (!c.references) { + Logger.warn(`Deleting chunk data ${c.id}`) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete c.data + } + }) break case SpeckleType.Point: if (node.raw.value) node.raw.value = [] @@ -197,8 +225,10 @@ export class SpeckleGeometryConverter extends GeometryConverter { protected PointcloudToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) - const vertices = node.instanced ? node.raw.points.slice() : node.raw.points - const colorsRaw = node.raw.colors + const vertices = new ChunkArray( + node.instanced ? node.raw.points.slice() : node.raw.points + ) + const colorsRaw = new ChunkArray(node.raw.colors) let colors = null if (colorsRaw && colorsRaw.length !== 0) { @@ -215,6 +245,10 @@ export class SpeckleGeometryConverter extends GeometryConverter { attributes: { POSITION: vertices, COLOR: colors + ? new ChunkArray([ + { data: colors, id: MathUtils.generateUUID(), references: 1 } + ]) + : undefined }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -254,45 +288,101 @@ export class SpeckleGeometryConverter extends GeometryConverter { if (!node.raw) return null const conversionFactor = getConversionFactor(node.raw.units) - const indices = [] if (!node.raw.vertices) return null if (!node.raw.faces) return null const start = performance.now() - const vertices = node.raw.vertices - const faces = node.raw.faces - const colorsRaw = node.raw.colors + + const vertices = new ChunkArray(node.raw.vertices) + const faces = new ChunkArray(node.raw.faces) + const colorsRaw = this.chunkArrayHasData(node.raw.colors) + ? new ChunkArray(node.raw.colors) + : undefined let normals = node.raw.vertexNormals + ? new ChunkArray(node.raw.vertexNormals) + : undefined let colors = undefined let k = 0 + let triangulated = true + let triangulatedArraySize = 0 while (k < faces.length) { - let n = faces[k] + const chunkIndex = faces.findChunkIndex(k) + if (faces.chunkArray[chunkIndex].processed) { + k += faces.chunkArray[chunkIndex].data.length + continue + } + let n = faces.get(k) if (n < 3) n += 3 // 0 -> 3, 1 -> 4 + k += n + 1 if (n === 3) { - const startP = performance.now() - // Triangle face - indices.push(faces[k + 1], faces[k + 2], faces[k + 3]) - this.pushTime += performance.now() - startP + triangulatedArraySize += 3 + continue } else { - // Quad or N-gon face - const start1 = performance.now() - const triangulation = MeshTriangulationHelper.triangulateFace( - k, - faces, - vertices - ) - this.actualTriangulateTime += performance.now() - start1 - indices.push( - ...triangulation.filter((el) => { - return el !== undefined - }) - ) + triangulatedArraySize += (n - 2) * 3 + triangulated = false } - - k += n + 1 } + + const indices = + triangulatedArraySize >= 65535 || vertices.length >= 65535 + ? new Uint32Array(triangulatedArraySize) + : new Uint16Array(triangulatedArraySize) + let indicesOffset = 0 + + if (triangulated) { + /** If already triangulated modfy the faces array in place */ + faces.chunkArray.forEach((chunk: DataChunk) => { + if (chunk.processed) return + + let write = 0 + for (let read = 0; read < chunk.data.length; read++) { + if (read % 4 !== 0) { + chunk.data[write++] = chunk.data[read] + } + } + chunk.data.length = write + chunk.processed = true + }) + faces.updateOffsets() + } else { + k = 0 + while (k < faces.length) { + /** We skip to the end of triangulated chunks */ + const chunkIndex = faces.findChunkIndex(k) + if (faces.chunkArray[chunkIndex].processed) { + indices.set(faces.chunks[chunkIndex], k) + k += faces.chunkArray[chunkIndex].data.length + continue + } + let n = faces.get(k) + if (n < 3) n += 3 // 0 -> 3, 1 -> 4 + if (n === 3) { + // Triangle face + indices[indicesOffset] = faces.get(k + 1) + indices[indicesOffset + 1] = faces.get(k + 2) + indices[indicesOffset + 2] = faces.get(k + 3) + indicesOffset += 3 + } else { + const start1 = performance.now() + const indexCount = MeshTriangulationHelper.triangulateFace( + k, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + faces, + vertices, + indices, // inout + indicesOffset // in + ) + indicesOffset += indexCount + this.actualTriangulateTime += performance.now() - start1 + } + + k += n + 1 + } + } + this.meshTriangulationTime += performance.now() - start if (colorsRaw && colorsRaw.length !== 0) { @@ -302,7 +392,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { ) } else /** We want the colors in linear space */ - colors = this.unpackColors(colorsRaw, true) + colors = new ChunkArray([ + { + id: MathUtils.generateUUID(), + references: 1, + data: this.unpackColors(colorsRaw, true) + } + ]) } if (normals && normals.length !== 0) { @@ -317,7 +413,15 @@ export class SpeckleGeometryConverter extends GeometryConverter { return { attributes: { POSITION: vertices, - INDEX: indices, + INDEX: triangulated + ? faces + : new ChunkArray([ + { + data: indices as unknown as number[], + id: MathUtils.generateUUID(), + references: 1 + } + ]), ...(colors && { COLOR: colors }), ...(normals && { NORMAL: normals }) }, @@ -368,15 +472,18 @@ export class SpeckleGeometryConverter extends GeometryConverter { */ protected PointToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) + const points = this.PointToFloatArray( + node.raw as { value: Array; units: string } & { + x: number + y: number + z: number + } + ) return { attributes: { - POSITION: this.PointToFloatArray( - node.raw as { value: Array; units: string } & { - x: number - y: number - z: number - } - ) + POSITION: new ChunkArray([ + { data: points, id: MathUtils.generateUUID(), references: 1 } + ]) }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -394,9 +501,15 @@ export class SpeckleGeometryConverter extends GeometryConverter { const conversionFactor = getConversionFactor(node.raw.units) return { attributes: { - POSITION: this.PointToFloatArray(node.raw.start).concat( - this.PointToFloatArray(node.raw.end) - ) + POSITION: new ChunkArray([ + { + data: this.PointToFloatArray(node.raw.start).concat( + this.PointToFloatArray(node.raw.end) + ), + id: MathUtils.generateUUID(), + references: 1 + } + ]) }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -412,12 +525,26 @@ export class SpeckleGeometryConverter extends GeometryConverter { */ protected PolylineToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) + const chunkArray = new ChunkArray(node.raw.value) - if (node.raw.closed) - node.raw.value.push(node.raw.value[0], node.raw.value[1], node.raw.value[2]) + let outChunk = chunkArray + if (node.raw.closed) { + const complete = new Float32Array(chunkArray.length + 3) + chunkArray.copyToBuffer(complete, 0) + complete[chunkArray.length] = complete[0] + complete[chunkArray.length + 1] = complete[1] + complete[chunkArray.length + 2] = complete[2] + outChunk = new ChunkArray([ + { + data: complete as unknown as number[], + id: MathUtils.generateUUID(), + references: 1 + } + ]) + } return { attributes: { - POSITION: node.raw.value.slice(0) + POSITION: outChunk }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -504,7 +631,9 @@ export class SpeckleGeometryConverter extends GeometryConverter { return { attributes: { - POSITION: edges + POSITION: new ChunkArray([ + { data: edges, id: MathUtils.generateUUID(), references: 1 } + ]) }, bakeTransform: new Matrix4().copy(T).multiply(R), transform: null @@ -562,7 +691,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { ) return { attributes: { - POSITION: this.FlattenVector3Array(points) + POSITION: new ChunkArray([ + { + data: this.FlattenVector3Array(points), + id: MathUtils.generateUUID(), + references: 1 + } + ]) }, bakeTransform: null, transform: null @@ -664,7 +799,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { return { attributes: { - POSITION: this.FlattenVector3Array(points) + POSITION: new ChunkArray([ + { + data: this.FlattenVector3Array(points), + id: MathUtils.generateUUID(), + references: 1 + } + ]) }, bakeTransform: matrix, transform: null @@ -710,7 +851,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { return { attributes: { - POSITION: this.FlattenVector3Array(points) + POSITION: new ChunkArray([ + { + data: this.FlattenVector3Array(points), + id: MathUtils.generateUUID(), + references: 1 + } + ]) }, bakeTransform: null, transform: null @@ -818,10 +965,10 @@ export class SpeckleGeometryConverter extends GeometryConverter { return output } - protected unpackColors(int32Colors: number[], tolinear = false): number[] { + protected unpackColors(int32Colors: ChunkArray, tolinear = false): number[] { const colors = new Array(int32Colors.length * 3) for (let i = 0; i < int32Colors.length; i++) { - const color = int32Colors[i] + const color = int32Colors.get(i) const r = (color >> 16) & 0xff const g = (color >> 8) & 0xff const b = color & 0xff @@ -843,4 +990,9 @@ export class SpeckleGeometryConverter extends GeometryConverter { else if (x < 0.04045) return x / 12.92 else return Math.pow((x + 0.055) / 1.055, 2.4) } + + /** Connectors send empty chunks ಠ_ಠ */ + protected chunkArrayHasData(chunks: Array): boolean { + return chunks && chunks.filter((c: DataChunk) => c.data && c.data.length).length > 0 + } } diff --git a/packages/viewer/src/modules/materials/Materials.ts b/packages/viewer/src/modules/materials/Materials.ts index 6e8c9a54a..5569fe04d 100644 --- a/packages/viewer/src/modules/materials/Materials.ts +++ b/packages/viewer/src/modules/materials/Materials.ts @@ -15,6 +15,7 @@ import { SpeckleMaterial } from './SpeckleMaterial.js' import SpecklePointColouredMaterial from './SpecklePointColouredMaterial.js' import { type Asset, AssetType, type MaterialOptions } from '../../IViewer.js' import SpeckleTextColoredMaterial from './SpeckleTextColoredMaterial.js' +import { ChunkArray } from '../converter/VirtualArray.js' const defaultGradient: Asset = { id: 'defaultGradient', @@ -124,6 +125,10 @@ export default class Materials { if (!materialNode) return null let renderMaterial: RenderMaterial | null = null if (materialNode.model.raw.renderMaterial) { + const colorsChunkArray = geometryNode?.model.raw.colors + ? new ChunkArray(geometryNode?.model.raw.colors) + : undefined + renderMaterial = { id: materialNode.model.raw.renderMaterial.id, color: materialNode.model.raw.renderMaterial.diffuse, @@ -135,9 +140,7 @@ export default class Materials { roughness: materialNode.model.raw.renderMaterial.roughness, metalness: materialNode.model.raw.renderMaterial.metalness, vertexColors: - geometryNode && - geometryNode.model.raw.colors && - geometryNode.model.raw.colors.length > 0 + (geometryNode && colorsChunkArray && colorsChunkArray.length > 0) ?? false } } return renderMaterial diff --git a/packages/viewer/src/modules/objects/AccelerationStructure.ts b/packages/viewer/src/modules/objects/AccelerationStructure.ts index f730a4dba..44e9de2ee 100644 --- a/packages/viewer/src/modules/objects/AccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/AccelerationStructure.ts @@ -82,8 +82,8 @@ export class AccelerationStructure { } public static buildBVH( - indices: number[] | undefined, - position: number[] | undefined, + indices: Uint16Array | Uint32Array | undefined, + position: Float32Array | Float64Array | undefined, options: BVHOptions = DefaultBVHOptions, transform?: Matrix4 ): MeshBVH { @@ -92,7 +92,7 @@ export class AccelerationStructure { throw new Error('Cannot build BVH with undefined indices or position!') } - let bvhPositions = new Float32Array(position) + let bvhPositions = position if (transform) { bvhPositions = new Float32Array(position.length) const vecBuff = new Vector3() diff --git a/packages/viewer/src/modules/objects/TextLabel.ts b/packages/viewer/src/modules/objects/TextLabel.ts index c6905c316..5bd662ed7 100644 --- a/packages/viewer/src/modules/objects/TextLabel.ts +++ b/packages/viewer/src/modules/objects/TextLabel.ts @@ -1,6 +1,5 @@ import { Box3, - BufferAttribute, BufferGeometry, Color, DoubleSide, @@ -24,6 +23,7 @@ import SpeckleBasicMaterial, { import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js' import { ObjectLayers } from '../../index.js' import Logger from '../utils/Logger.js' +import { Uint16BufferAttribute } from 'three' const _mat40: Matrix4 = new Matrix4() const _mat41: Matrix4 = new Matrix4() @@ -517,12 +517,9 @@ export class TextLabel extends Text { } const geometry = new BufferGeometry() - geometry.setIndex(new BufferAttribute(new Uint32Array(indices), 1)) - geometry.setAttribute( - 'position', - new BufferAttribute(new Float32Array(positions), 3) - ) - geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2)) + geometry.setIndex(new Uint16BufferAttribute(indices, 1)) + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) + geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) geometry.computeBoundingBox() return geometry diff --git a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts index 437167c6d..a191e8c2a 100644 --- a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts @@ -67,24 +67,29 @@ export class TopLevelAccelerationStructure { } private buildBVH() { - const indices = [] - const vertices: number[] = new Array( + const indices: Uint16Array = new Uint16Array( + TopLevelAccelerationStructure.cubeIndices.length * this.batchObjects.length + ) + const vertices: Float32Array = new Float32Array( TopLevelAccelerationStructure.CUBE_VERTS * 3 * this.batchObjects.length ) let vertOffset = 0 + let indexOffset = 0 for (let k = 0; k < this.batchObjects.length; k++) { const boxBounds: Box3 = this.batchObjects[k].accelerationStructure.getBoundingBox( new Box3() ) this.updateVertArray(boxBounds, vertOffset, vertices) - indices.push( - ...TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3) + indices.set( + TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3), + indexOffset ) this.batchObjects[k].tasVertIndexStart = vertOffset / 3 this.batchObjects[k].tasVertIndexEnd = vertOffset / 3 + TopLevelAccelerationStructure.CUBE_VERTS vertOffset += TopLevelAccelerationStructure.CUBE_VERTS * 3 + indexOffset += TopLevelAccelerationStructure.cubeIndices.length } this.accelerationStructure = new AccelerationStructure( AccelerationStructure.buildBVH(indices, vertices) @@ -105,7 +110,7 @@ export class TopLevelAccelerationStructure { } } - private updateVertArray(box: Box3, offset: number, outPositions: number[]) { + private updateVertArray(box: Box3, offset: number, outPositions: Float32Array) { outPositions[offset] = box.min.x outPositions[offset + 1] = box.min.y outPositions[offset + 2] = box.max.z @@ -141,7 +146,7 @@ export class TopLevelAccelerationStructure { public refit() { const positions = this.accelerationStructure.geometry.attributes.position - .array as number[] + .array as Float32Array // const boxBuffer: Box3 = new Box3() for (let k = 0; k < this.batchObjects.length; k++) { const start = this.batchObjects[k].tasVertIndexStart diff --git a/packages/viewer/src/modules/tree/NodeRenderView.ts b/packages/viewer/src/modules/tree/NodeRenderView.ts index d0819e97d..21723e703 100644 --- a/packages/viewer/src/modules/tree/NodeRenderView.ts +++ b/packages/viewer/src/modules/tree/NodeRenderView.ts @@ -1,4 +1,4 @@ -import { Box3 } from 'three' +import { Box3, Matrix4 } from 'three' import { GeometryType } from '../batching/Batch.js' import { GeometryAttributes, type GeometryData } from '../converter/Geometry.js' import Materials, { @@ -7,6 +7,7 @@ import Materials, { type RenderMaterial } from '../materials/Materials.js' import { SpeckleType } from '../loaders/GeometryConverter.js' +import { ChunkArray } from '../converter/VirtualArray.js' export interface NodeRenderData { id: string @@ -84,6 +85,10 @@ export class NodeRenderView { return this._aabb } + public set aabb(value: Box3) { + this._aabb.copy(value) + } + public get transparent(): boolean { return ( (this._renderData.renderMaterial && @@ -150,14 +155,18 @@ export class NodeRenderView { if (vertEnd !== undefined) this._batchVertexEnd = vertEnd } - public computeAABB() { + public computeAABB(transform?: Matrix4) { if (!this._aabb) this._aabb = new Box3() if ( this._renderData.geometry.attributes && this._renderData.geometry.attributes.POSITION.length ) { - this._aabb.setFromArray(this._renderData.geometry.attributes.POSITION) + /** For transformations that contain non-uniform scaling combine with rotation the resulting + * aabb is not going to be accurate. We will re-compute and assign it when we build the batches + */ + this._aabb.copy(this._renderData.geometry.attributes.POSITION.computeBox3()) + if (transform) this._aabb.applyMatrix4(transform) } } @@ -181,7 +190,9 @@ export class NodeRenderView { public disposeGeometry() { for (const attr in this._renderData.geometry.attributes) { - this._renderData.geometry.attributes[attr as GeometryAttributes] = [] + this._renderData.geometry.attributes[attr as GeometryAttributes] = new ChunkArray( + [] + ) } } } diff --git a/packages/viewer/src/modules/tree/RenderTree.ts b/packages/viewer/src/modules/tree/RenderTree.ts index 5ca112341..54f96e4ef 100644 --- a/packages/viewer/src/modules/tree/RenderTree.ts +++ b/packages/viewer/src/modules/tree/RenderTree.ts @@ -3,7 +3,6 @@ import { type TreeNode, WorldTree } from './WorldTree.js' import Materials from '../materials/Materials.js' import { type NodeRenderData, NodeRenderView } from './NodeRenderView.js' import { GeometryConverter, SpeckleType } from '../loaders/GeometryConverter.js' -import { Geometry } from '../converter/Geometry.js' import Logger from '../utils/Logger.js' export class RenderTree { @@ -51,25 +50,29 @@ export class RenderTree { private applyTransforms(node: TreeNode) { if (node.model.renderView) { const transform = this.computeTransform(node) - if (node.model.renderView.hasGeometry) { + if (node.model.renderView.hasGeometry || node.model.renderView.hasMetadata) { if (node.model.renderView.renderData.geometry.bakeTransform) { transform.multiply(node.model.renderView.renderData.geometry.bakeTransform) } - if ( - node.model.instanced && - node.model.renderView.speckleType === SpeckleType.Mesh - ) - node.model.renderView.renderData.geometry.transform = transform - else { - Geometry.transformGeometryData( - node.model.renderView.renderData.geometry, - transform - ) - } - node.model.renderView.computeAABB() - } else if (node.model.renderView.hasMetadata) { - node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform) - node.model.renderView.computeAABB() + node.model.renderView.renderData.geometry.transform = transform + node.model.renderView.computeAABB(!node.model.instanced ? transform : undefined) + /** I like that this is gone now! */ + // if ( + // node.model.instanced && + // node.model.renderView.speckleType === SpeckleType.Mesh + // ) + // node.model.renderView.renderData.geometry.transform = transform + // else { + // Geometry.transformGeometryData( + // node.model.renderView.renderData.geometry, + // transform + // ) + // } + // node.model.renderView.computeAABB() + // } else if (node.model.renderView.hasMetadata) { + // node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform) + // node.model.renderView.computeAABB() + // } } } } From e27c38e85757ca93bdd4b98095284c387ccd60f5 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Thu, 31 Jul 2025 11:23:01 +0200 Subject: [PATCH 2/2] Models panel changes --- .../components/viewer/models/Card.vue | 3 +- .../components/viewer/models/TreeItem.vue | 154 +++++++++--------- 2 files changed, 81 insertions(+), 76 deletions(-) diff --git a/packages/frontend-2/components/viewer/models/Card.vue b/packages/frontend-2/components/viewer/models/Card.vue index ee62c9cc5..5dac7fa87 100644 --- a/packages/frontend-2/components/viewer/models/Card.vue +++ b/packages/frontend-2/components/viewer/models/Card.vue @@ -4,7 +4,8 @@
-
-
-
- - - - {{ unfold ? 'Collapse' : 'Expand' }} - - -
-
-
- - {{ header || headerAndSubheader.header }} -
-
- {{ subHeader || headerAndSubheader.subheader }} +
+
+
+
+ + + + {{ unfold ? 'Collapse' : 'Expand' }} + + +
+
+
+ + {{ header || headerAndSubheader.header }} +
+
+ {{ subHeader || headerAndSubheader.subheader }} +
-
-
- - +
+ + +
-
+
@@ -338,9 +342,9 @@ const isChildOfSelected = computed(() => { }) const getBackgroundClass = computed(() => { - if (isSelected.value) return 'bg-highlight-3' + if (isSelected.value) return 'bg-highlight-3 rounded-sm' if (isChildOfSelected.value) return 'bg-foundation-2' - return 'bg-foundation hover:bg-highlight-1' + return 'bg-foundation hover:bg-highlight-1 hover:rounded-sm' }) const highlightObject = () => {