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 = () => { 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() + // } } } }