diff --git a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts index e2d57f883..8f1a4f638 100644 --- a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts @@ -127,7 +127,10 @@ export class BoxSelection extends Extension { /** Get the renderer */ const renderer = this.viewer.getRenderer() /** Get the mesh batches */ - const batches = renderer.batcher.getBatches(undefined, GeometryType.MESH) + const batches = renderer.batcher.getBatches(undefined, [ + GeometryType.MESH, + GeometryType.TEXT + ]) /** Compute the clip matrix */ const clipMatrix = new Matrix4() if (renderer.renderingCamera) { diff --git a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts index 7c74db07e..88a4cee0d 100644 --- a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts +++ b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts @@ -51,7 +51,7 @@ export class CameraPlanes extends Extension { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) + .batcher.getBatches(undefined, [GeometryType.MESH, GeometryType.TEXT]) let minDist = Number.POSITIVE_INFINITY const minPoint = new Vector3() for (let b = 0; b < batches.length; b++) { diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index cb0b7cc4e..000459f69 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { @@ -53,7 +51,7 @@ import Mild2 from '../assets/hdri/Mild2.png' import Sharp from '../assets/hdri/Sharp.png' import Bright from '../assets/hdri/Bright.png' -import { Euler, Vector3, Box3, Color, LinearFilter } from 'three' +import { Euler, Vector3, Box3, LinearFilter } from 'three' import { GeometryType } from '@speckle/viewer' import { MeshBatch } from '@speckle/viewer' import { ObjectLoader2Factory } from '@speckle/objectloader2' @@ -636,38 +634,6 @@ export default class Sandbox { }) this.tabs.pages[0].addSeparator() - const colors = this.tabs.pages[0].addButton({ - title: `PM's Colors` - }) - colors.on('click', async () => { - const colorNodes = this.viewer.getWorldTree().findAll( - (node: TreeNode) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - node.model.renderView && - node.model.renderView.renderData.colorMaterial && - node.model.renderView.geometryType === GeometryType.MESH - ) - const colorMap: { [color: number]: Array } = {} - for (let k = 0; k < colorNodes.length; k++) { - const node = colorNodes[k] - - const color: number = node.model.renderView.renderData.colorMaterial.color - if (!colorMap[color]) colorMap[color] = [] - - colorMap[color].push(node.model.id) - } - const colorGroups = [] - - for (const color in colorMap) { - colorGroups.push({ - objectIds: colorMap[color], - color: '#' + new Color(Number.parseInt(color)).getHexString() - }) - } - console.log(colorGroups) - this.viewer.getExtension(FilteringExtension).setUserObjectColors(colorGroups) - }) - this.tabs.pages[0] .addInput({ dampening: 30 }, 'dampening', { label: 'Dampening', diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 8d73951ee..30ff53142 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -84,6 +84,29 @@ const createViewer = async (containerName: string, _stream: string) => { } }) + // const label = new TextLabel({ + // text: 'y: 1.00m', + // textColor: new Color(0xffffff), + // fontSize: 1, + // billboard: 'world', + // anchorX: 'left', + // anchorY: 'middle', + // backgroundColor: new Color(0xfb0404), + // backgroundMargins: new Vector2(0.75, 0.1), + // backgroundCornerRadius: 0.5, + // objectLayer: ObjectLayers.OVERLAY + // }) as unknown as Mesh + // label.rotateX(Math.PI * 0.5) + // label.position.set(2.5, 0, 0) + // viewer.getRenderer().scene.add(label) + + // const raycaster = new Raycaster() + // raycaster.layers.set(ObjectLayers.OVERLAY) + // viewer.getRenderer().input.on(InputEvent.Click, (arg) => { + // raycaster.setFromCamera(arg, viewer.getRenderer().renderingCamera) + // console.log(raycaster.intersectObject(label)) + // }) + viewer.on(ViewerEvent.UnloadComplete, () => { Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize) Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin) @@ -119,8 +142,8 @@ const getStream = () => { // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d' - //bad commit! not all items uploaded to server - //'https://app.speckle.systems/projects/8e4347e65d/models/39bea37d69' + // bad commit! not all items uploaded to server + // 'https://app.speckle.systems/projects/8e4347e65d/models/39bea37d69' // 'Super' heavy revit shit //'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5' @@ -191,7 +214,7 @@ const getStream = () => { // 'https://latest.speckle.systems/streams/85bc4f61c6/commits/8575fe2978' // Alex cubes // 'https://latest.speckle.systems/streams/4658eb53b9/commits/d8ec9cccf7' - // Alex more cubes + // // Alex more cubes // 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b' // Tekla // 'https://latest.speckle.systems/streams/caec6d6676/commits/588c731104' @@ -558,9 +581,24 @@ const getStream = () => { // Small (microscopic) building // 'https://app.speckle.systems/projects/26e4c4aab5/models/7d5ff72f5b' + // Text grid + // 'https://app.speckle.systems/projects/dcab71b3de/models/5ff99aa4e1' + // Text grid with a LOT of text + // 'https://app.speckle.systems/projects/dcab71b3de/models/5f02df011d' + // Instances with far away transform // 'https://app.speckle.systems/projects/9d0ce16ba8/models/3c079572ea' + // Far away text screen + // 'https://latest.speckle.systems/projects/d46f6cdc80/models/3a67170b04@c6622b474a' + // Far away text, world + // 'https://latest.speckle.systems/projects/d46f6cdc80/models/3a67170b04@fac9360249' + + // Text test stream + // 'https://latest.speckle.systems/projects/109e01c8c0/models/1eee4edbe6' + + // Single billboaded text + // 'https://latest.speckle.systems/projects/f28ad5b38a/models/b63ebcd807' // Duplicate display values // 'https://app.speckle.systems/projects/1466fe31c6/models/2eaf0f0571' ) diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 5b90c293d..e2a9386c1 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -64,7 +64,7 @@ "three": "^0.140.0", "three-mesh-bvh": "0.5.17", "tree-model": "1.0.7", - "troika-three-text": "0.47.2", + "troika-three-text": "0.52.4", "type-fest": "^4.15.0" }, "devDependencies": { diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 83abd8d0a..dd2c7ccbd 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -78,7 +78,7 @@ import { GeometryType } from './modules/batching/Batch.js' import { MeshBatch } from './modules/batching/MeshBatch.js' import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial.js' import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial.js' -import { SpeckleText } from './modules/objects/SpeckleText.js' +import { TextLabel } from './modules/objects/TextLabel.js' import { NodeRenderView } from './modules/tree/NodeRenderView.js' import { CONTAINED, @@ -234,7 +234,7 @@ export { SpeckleStandardMaterial, SpeckleBasicMaterial, SpeckleTextMaterial, - SpeckleText, + TextLabel, NodeRenderView, SpeckleGeometryConverter, Assets, diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index 775d8f29f..cc583bd66 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -23,7 +23,12 @@ import { PerspectiveCamera, OrthographicCamera } from 'three' -import { type Batch, type BatchUpdateRange, GeometryType } from './batching/Batch.js' +import { + type Batch, + type BatchUpdateRange, + GeometryType, + isAcceleratedBatchType +} from './batching/Batch.js' import Batcher from './batching/Batcher.js' import { Geometry } from './converter/Geometry.js' import Input, { InputEvent } from './input/Input.js' @@ -67,6 +72,8 @@ import Logger from './utils/Logger.js' /* TO DO: Not sure where to best import these */ import '../type-augmentations/three-extensions.js' +import { TextBatch } from '../index.js' +import { SpeckleBatchedText } from './objects/SpeckleBatchedText.js' export class RenderingStats { private renderTimeAcc = 0 @@ -518,13 +525,16 @@ export default class SpeckleRenderer { } private updateTransforms() { - const meshBatches: MeshBatch[] = this.batcher.getBatches( - undefined, - GeometryType.MESH - ) + const meshBatches: (MeshBatch | TextBatch)[] = this.batcher.getBatches(undefined, [ + GeometryType.MESH, + GeometryType.TEXT + ]) for (let k = 0; k < meshBatches.length; k++) { - const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh + const meshBatch: SpeckleMesh | SpeckleInstancedMesh | SpeckleBatchedText = + meshBatches[k].mesh meshBatch.updateTransformsUniform() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore meshBatch.traverse((obj: Object3D) => { const depthMaterial: SpeckleDepthMaterial = obj.customDepthMaterial as SpeckleDepthMaterial @@ -630,9 +640,16 @@ export default class SpeckleRenderer { let useRTE = false if ( batchRenderable instanceof SpeckleMesh || - batchRenderable instanceof SpeckleInstancedMesh + batchRenderable instanceof SpeckleInstancedMesh || + batchRenderable instanceof SpeckleBatchedText ) { if (batchRenderable.TAS.bvhHelper) parent.add(batchRenderable.TAS.bvhHelper) + } + + if ( + batchRenderable instanceof SpeckleMesh || + batchRenderable instanceof SpeckleInstancedMesh + ) { useRTE = batchRenderable.needsRTE } if (batch.geometryType === GeometryType.MESH) { @@ -651,6 +668,7 @@ export default class SpeckleRenderer { } }) } + this.viewer.World.expandWorld(batch.bounds) } @@ -1265,15 +1283,18 @@ export default class SpeckleRenderer { } public getObjects(): BatchObject[] { - const batches = this.batcher.getBatches(undefined, GeometryType.MESH) - const meshes = batches.map((batch: MeshBatch) => batch.mesh) + const batches = this.batcher.getBatches(undefined, [ + GeometryType.MESH, + GeometryType.TEXT + ]) + const meshes = batches.map((batch: MeshBatch | TextBatch) => batch.mesh) const objects = meshes.flatMap((mesh) => mesh.batchObjects) return objects } public getObject(rv: NodeRenderView): BatchObject | null { - const batch = this.batcher.getBatch(rv) as MeshBatch - if (!batch || batch.geometryType !== GeometryType.MESH) { + const batch = this.batcher.getBatch(rv) + if (!batch || !isAcceleratedBatchType(batch)) { // Logger.error('Render view is not of mesh type. No batch object found') return null } diff --git a/packages/viewer/src/modules/batching/Batch.ts b/packages/viewer/src/modules/batching/Batch.ts index ae9cd1d28..e0b4ba006 100644 --- a/packages/viewer/src/modules/batching/Batch.ts +++ b/packages/viewer/src/modules/batching/Batch.ts @@ -1,6 +1,9 @@ import { Box3, Material, Object3D, WebGLRenderer } from 'three' import { type FilterMaterialOptions } from '../materials/Materials.js' import { NodeRenderView } from '../tree/NodeRenderView.js' +import { MeshBatch } from './MeshBatch.js' +import { InstancedMeshBatch } from './InstancedMeshBatch.js' +import TextBatch from './TextBatch.js' export enum GeometryType { MESH, @@ -78,3 +81,13 @@ let BATCH_INDEX_COUNTER = 0 export const getNextBatchIndex = () => { return ++BATCH_INDEX_COUNTER } + +export type AcceleratedBatchTypes = MeshBatch | InstancedMeshBatch | TextBatch + +export function isAcceleratedBatchType(batch: Batch): batch is AcceleratedBatchTypes { + return ( + batch && + (batch.geometryType === GeometryType.MESH || + batch.geometryType === GeometryType.TEXT) + ) +} diff --git a/packages/viewer/src/modules/batching/BatchObject.ts b/packages/viewer/src/modules/batching/BatchObject.ts index aeff9d74e..5a61cb28d 100644 --- a/packages/viewer/src/modules/batching/BatchObject.ts +++ b/packages/viewer/src/modules/batching/BatchObject.ts @@ -23,8 +23,8 @@ export class BatchObject { public transform: Matrix4 public transformInv: Matrix4 - public tasVertIndexStart!: number - public tasVertIndexEnd!: number + public tasVertIndexStart: number + public tasVertIndexEnd: number public quaternion: Quaternion = new Quaternion() public eulerValue: Euler = new Euler() diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index a7e2bcbd8..89597fa54 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -40,6 +40,7 @@ export default class Batcher { private maxBatchObjects = 0 private maxBatchVertices = 500000 private minInstancedBatchVertices = 10000 + private maxBatchTextObjects = 5000 public materials: Materials public batches: { [id: string]: Batch } = {} @@ -284,25 +285,35 @@ export default class Batcher { } /** Finally we're splitting again based on the batch's max object count */ const geometryType = renderViews[0].geometryType - if (geometryType === GeometryType.MESH) { - const oSplit = [] - for (let i = 0; i < vSplit.length; i++) { - const objCount = vSplit[i].length - const div = Math.floor(objCount / this.maxBatchObjects) - const mod = objCount % this.maxBatchObjects - let index = 0 - for (let k = 0; k < div; k++) { - oSplit.push(vSplit[i].slice(index, index + this.maxBatchObjects)) - index += this.maxBatchObjects - } - if (mod > 0) { - oSplit.push(vSplit[i].slice(index, index + mod)) - } - } - return oSplit - } + const maxCount = this.getMaxObjectCount(geometryType) + if (!maxCount) return vSplit - return vSplit + const oSplit = [] + for (let i = 0; i < vSplit.length; i++) { + const objCount = vSplit[i].length + const div = Math.floor(objCount / maxCount) + const mod = objCount % maxCount + let index = 0 + for (let k = 0; k < div; k++) { + oSplit.push(vSplit[i].slice(index, index + maxCount)) + index += maxCount + } + if (mod > 0) { + oSplit.push(vSplit[i].slice(index, index + mod)) + } + } + return oSplit + } + + private getMaxObjectCount(geometryType: GeometryType) { + switch (geometryType) { + case GeometryType.MESH: + return this.maxBatchObjects + case GeometryType.TEXT: + return this.maxBatchTextObjects + default: + return 0 + } } private async buildInstancedBatch( @@ -539,6 +550,15 @@ export default class Batcher { public getBatches( subtreeId?: string, geometryType?: K + ): BatchTypeMap[K][] + public getBatches( + subtreeId?: string, + geometryType?: Array + ): BatchTypeMap[K][] + + public getBatches( + subtreeId?: string, + geometryType?: K | Array ): BatchTypeMap[K][] { const batches: Batch[] = Object.values(this.batches) return batches.filter((value: Batch) => { @@ -551,23 +571,34 @@ export default class Batcher { private isBatchType( batch: Batch, - geometryType?: K + geometryType?: K | Array ): batch is BatchTypeMap[K] { if (geometryType === undefined) return true - switch (geometryType) { - case GeometryType.MESH: - return batch instanceof MeshBatch || batch instanceof InstancedMeshBatch - case GeometryType.LINE: - return batch instanceof LineBatch - case GeometryType.POINT: - return batch instanceof PointBatch - case GeometryType.POINT_CLOUD: - return batch instanceof PointBatch - case GeometryType.TEXT: - return batch instanceof TextBatch - default: - return false - } + let isBatchType = false + const array = Array.isArray(geometryType) ? geometryType : [geometryType] + array.forEach((value: K) => { + switch (value) { + case GeometryType.MESH: + isBatchType ||= + batch instanceof MeshBatch || batch instanceof InstancedMeshBatch + break + case GeometryType.LINE: + isBatchType ||= batch instanceof LineBatch + break + case GeometryType.POINT: + isBatchType ||= batch instanceof PointBatch + break + case GeometryType.POINT_CLOUD: + isBatchType ||= batch instanceof PointBatch + break + case GeometryType.TEXT: + isBatchType ||= batch instanceof TextBatch + break + default: + isBatchType = false + } + }) + return isBatchType } public getBatch(rv: NodeRenderView): Batch | undefined { diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index e0ba03ec2..286c19ea8 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -1,4 +1,5 @@ -import { Box3, Material, Object3D, WebGLRenderer } from 'three' +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Box3, Material, Matrix4, Object3D, WebGLRenderer } from 'three' import { NodeRenderView } from '../tree/NodeRenderView.js' import { @@ -10,48 +11,63 @@ import { NoneBatchUpdateRange } from './Batch.js' -import { SpeckleText } from '../objects/SpeckleText.js' -import { ObjectLayers } from '../../IViewer.js' import Materials from '../materials/Materials.js' +import { SpeckleBatchedText } from '../objects/SpeckleBatchedText.js' +//@ts-ignore +import { AnchorX, AnchorY, Text } from 'troika-three-text' +import { + AccelerationStructure, + BatchObject, + Geometry, + ObjectLayers, + SpeckleTextMaterial +} from '../../index.js' +import { DefaultBVHOptions } from '../objects/AccelerationStructure.js' +import { TextBatchObject } from './TextBatchObject.js' +import { DrawRanges } from './DrawRanges.js' +import Logger from '../utils/Logger.js' +import SpeckleTextColoredMaterial from '../materials/SpeckleTextColoredMaterial.js' + +const INSTANCE_TEXT_TRIS_COUNT = 2 +const INSTANCE_TEXT_VERT_COUNT = 4 export default class TextBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] - public batchMaterial!: Material - public mesh: SpeckleText + public batchMaterial: Material + public mesh: SpeckleBatchedText + protected drawRanges: DrawRanges = new DrawRanges() public get bounds(): Box3 { - return new Box3().setFromObject(this.mesh) + return this.mesh.TAS.getBoundingBox(new Box3()) } public get drawCalls(): number { - return 1 + return this.groups.length } public get minDrawCalls(): number { + return [...Array.from(new Set(this.groups.map((value) => value.materialIndex)))] + .length + } + + public get maxDrawCalls(): number { return 1 } public get triCount(): number { - return this.getCount() + return INSTANCE_TEXT_TRIS_COUNT * this.renderViews.length } public get vertCount(): number { - return ( - this.mesh.textMesh.geometry.attributes.position.count + - this.mesh.backgroundMesh?.geometry.attributes.position.count - ) + return INSTANCE_TEXT_VERT_COUNT * this.renderViews.length } - public constructor(id: string, subtreeId: string, renderViews: NodeRenderView[]) { - this.id = id - this.subtreeId = subtreeId - this.renderViews = renderViews - } public get pointCount(): number { return 0 } + public get lineCount(): number { return 0 } @@ -61,22 +77,25 @@ export default class TextBatch implements Batch { } public get renderObject(): Object3D { - return this.mesh + return this.mesh as unknown as Object3D } public getCount(): number { - return ( - this.mesh.textMesh.geometry.index.count + - this.mesh.backgroundMesh?.geometry.index?.count - ) + return this.renderViews.length } public get materials(): Material[] { - return this.mesh.material as Material[] + return this.mesh.materials } - public get groups(): DrawGroup[] { - return [] + public get groups(): Array { + return this.mesh.groups + } + + public constructor(id: string, subtreeId: string, renderViews: NodeRenderView[]) { + this.id = id + this.subtreeId = subtreeId + this.renderViews = renderViews } public setBatchMaterial(material: Material) { @@ -116,65 +135,348 @@ export default class TextBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(range: BatchUpdateRange[]): void { - range - throw new Error('Method not implemented.') + /** Text batches are mix between how mesh and line batches work. + * - They still keep track of various draw groups each with it's material + * - However that material is not really being used, bur rather the properies are copied over to the batch fp32 data texture + * - For filtering we cheat and use `SpeckleTextColoredMaterial` only to store the gradient/ramp texture + gradient indices for each text in the batch + * - The color from the gradient/ramp texture will be used only if the gradient index > 0, otherwise the regular color will be used + * - The gradient index is stored in each text object in it's `userData` and written to the 27'th float in the batch data texture, where the shader reads if from + * - 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) + const splitRanges: BatchUpdateRange[] = [] + ranges.forEach((range: BatchUpdateRange) => { + for (let k = 0; k < range.count; k++) { + splitRanges.push({ + offset: range.offset + k, + count: 1, + material: range.material, + materialOptions: range.materialOptions + }) + } + }) + //@ts-ignore + this.mesh._members.forEach((packingInfo, text) => { + const range = splitRanges.find((val) => val.offset === packingInfo.index) + if (!range) return + + //@ts-ignore + text.color = range.material?.color + //@ts-ignore + text.material.color = range.material?.color + //@ts-ignore + text.material.opacity = range.material?.visible ? range.material?.opacity : 0 + + if (range.material instanceof SpeckleTextColoredMaterial) { + // Group has gradient/ramp texture color source + if (range.materialOptions) { + if ( + range.materialOptions.rampIndex !== undefined && + range.materialOptions.rampWidth !== undefined + ) { + /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) + * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with + * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent + * sampling can occur. + */ + const shiftedIndex = + range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth + /** Update the gradient indices for the individual texts + * The colored material is singular, as provided by Materials + */ + range.material.updateGradientIndexMap(packingInfo.index, shiftedIndex) + text.userData.gradientIndex = shiftedIndex + } + if (range.materialOptions.rampTexture !== undefined) { + ;(range.material as SpeckleTextMaterial).setGradientTexture( + range.materialOptions.rampTexture + ) + this.mesh.setGradientTexture(range.materialOptions.rampTexture) + } + } else { + text.userData.gradientIndex = + range.material.gradientIndexMap[packingInfo.index] + this.mesh.setGradientTexture(range.material.userData.gradientRamp.value) + } + } else { + // No gradient or ramp color source + text.userData.gradientIndex = -1 + } + + packingInfo.needsUpdate = true + }) + //@ts-ignore + this.mesh.dirty = true + //@ts-ignore + this.mesh.sync() } public setDrawRanges(ranges: BatchUpdateRange[]) { - this.mesh.textMesh.material = ranges[0].material - if (ranges[0].materialOptions && ranges[0].materialOptions.rampIndexColor) { - this.mesh.textMesh.material.color.copy(ranges[0].materialOptions.rampIndexColor) + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material + }) + const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] + + for (let k = 0; k < uniqueMaterials.length; k++) { + if (!this.materials.includes(uniqueMaterials[k])) + this.materials.push(uniqueMaterials[k]) + } + + this.mesh.groups = this.drawRanges.integrateRanges( + this.groups, + this.materials, + ranges + ) + + let count = 0 + this.groups.forEach((value) => (count += value.count)) + if (count !== this.renderViews.length) { + Logger.error(`Draw groups invalid on ${this.id}`) + } + this.setBatchBuffers(ranges) + this.cleanMaterials() + } + + private cleanMaterials() { + const materialsInUse = [ + ...Array.from( + new Set(this.groups.map((value) => this.materials[value.materialIndex])) + ) + ] + let k = 0 + while (this.materials.length > materialsInUse.length) { + if (!materialsInUse.includes(this.materials[k])) { + this.materials.splice(k, 1) + this.groups.forEach((value: DrawGroup) => { + if (value.materialIndex > k) value.materialIndex-- + }) + k = 0 + continue + } + k++ } } public resetDrawRanges() { - this.mesh.textMesh.material = this.batchMaterial - this.mesh.textMesh.visible = true + this.groups.length = 0 + this.materials.length = 0 + + this.materials.push(this.batchMaterial) + this.setVisibleRange([AllBatchUpdateRange]) + this.setDrawRanges([ + { + offset: 0, + count: this.renderViews.length, + material: this.batchMaterial + } + ]) + } + + protected alignmentXToAnchorX(value: number): AnchorX { + switch (value) { + case 0: + return 'left' + case 1: + return 'center' + case 2: + return 'right' + default: + return 'center' + } + } + + protected alignmentYToAnchorY(value: number): AnchorY { + switch (value) { + case 0: + return 'top' + case 1: + return 'middle' + case 2: + return 'bottom' + default: + return 'middle' + } } public async buildBatch(): Promise { - /** Catering to typescript - * There is no unniverse where there is no metadata - */ - if (!this.renderViews[0].renderData.geometry.metaData) { - throw new Error(`Cannot build batch ${this.id}. Metadata`) - } - this.mesh = new SpeckleText(this.id, ObjectLayers.STREAM_CONTENT_TEXT) - this.mesh.matrixAutoUpdate = false - await this.mesh.update( - SpeckleText.SpeckleTextParamsFromMetadata( - this.renderViews[0].renderData.geometry.metaData + return new Promise((resolve) => { + this.mesh = new SpeckleBatchedText() + const textMap = new Map() + const batchObjects: BatchObject[] = [] + const textObjects: Text[] = [] + const box = new Box3() + let needsRTE = false + let needsBillboard = false + let textSynced = this.renderViews.length + for (let k = 0; k < this.renderViews.length; k++) { + const textMeta = this.renderViews[k].renderData.geometry + .metaData as unknown as { + value: string + height: number + maxWidth: number + alignmentH: number + alignmentV: number + screenOriented: boolean + } + const text = new Text() + this.renderViews[k].renderData.geometry.bakeTransform?.decompose( + text.position, + text.quaternion, + text.scale + ) + text.updateMatrixWorld(true) + + if (textMeta) { + text.text = textMeta.value + text.fontSize = textMeta.height + text.maxWidth = + textMeta.maxWidth !== null ? textMeta.maxWidth : Number.POSITIVE_INFINITY + text.anchorX = this.alignmentXToAnchorX(textMeta.alignmentH) + text.anchorY = this.alignmentYToAnchorY(textMeta.alignmentV) + } + needsBillboard ||= textMeta !== undefined ? textMeta.screenOriented : false + + text.material = new SpeckleTextMaterial({ + color: 0xff0000 // control color + }).getDerivedMaterial() + + textMap.set(text, this.renderViews[k]) + + text.sync(() => { + const { textRenderInfo } = text + /** 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 + ) + box.setFromArray(vertices) + box.applyMatrix4( + this.renderViews[k].renderData.geometry.bakeTransform || new Matrix4() + ) + + needsRTE ||= Geometry.needsRTE(box) + + const geometry = text.geometry + geometry.computeBoundingBox() + const textBvh = AccelerationStructure.buildBVH( + geometry.index?.array as number[], + vertices, + DefaultBVHOptions + ) + /** The bounds bug. it needs a refit to report the correct bounds */ + textBvh.refit() + + const batchObject = new TextBatchObject(this.renderViews[k], k) + batchObject.buildAccelerationStructure(textBvh) + batchObjects.push(batchObject) + textObjects.push(text) + //@ts-ignore + this.mesh.addText(text) + textSynced-- + if (!textSynced) { + if (!this.batchMaterial.defines) this.batchMaterial.defines = {} + if (needsRTE) { + this.batchMaterial.defines['USE_RTE'] = ' ' + } + if (needsBillboard) this.batchMaterial.defines['BILLBOARD'] = ' ' + this.mesh.setBatchObjects(batchObjects, textObjects) + this.mesh.setBatchMaterial(this.batchMaterial) + this.mesh.buildTAS() + + //@ts-ignore + this.mesh.uuid = this.id + //@ts-ignore + this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_TEXT) + //@ts-ignore + this.mesh.frustumCulled = false + + this.mesh.dirty = true + + this.groups.push({ + start: 0, + count: this.renderViews.length, + materialIndex: 0 + }) + //@ts-ignore + this.mesh.sync(() => { + /** We assign the allocated packing info to the text render views as we'll be using the same batch indices for simplicity */ + //@ts-ignore + this.mesh._members.forEach((packingInfo, text) => { + textMap.get(text).setBatchData(this.id, packingInfo.index, 1) + packingInfo.needsUpdate = true + }) + this.setBatchBuffers([ + { + offset: 0, + count: this.renderViews.length, + material: this.batchMaterial + } + ]) + resolve() + }) + } + }) + } + }) + } + + public getRenderView(index: number): NodeRenderView | null { + index + Logger.warn('Deprecated! Use InstancedBatchObject') + return null + } + + public getMaterialAtIndex(index: number): Material | null { + index + Logger.warn('Deprecated! Use InstancedBatchObject') + return null + } + + public getMaterial(rv: NodeRenderView): Material | null { + const group = this.groups.find((value) => { + return ( + rv.batchStart >= value.start && + rv.batchStart + rv.batchCount <= value.count + value.start ) - ) - if (this.renderViews[0].renderData.geometry.bakeTransform) - this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform) - this.renderViews[0].setBatchData( - this.id, - 0, - this.mesh.textMesh.geometry.index.count / 3 - ) - this.mesh.textMesh.material = this.batchMaterial - } + }) + if (!group) { + Logger.warn(`Could not get material for ${rv.renderData.id}`) + return null + } + return this.materials[group.materialIndex] - public getRenderView(index: number): NodeRenderView { - index - return this.renderViews[0] - } + // /** Just like for lines, this isn't ideal but it's quicker */ + // const material = this.materials[group.materialIndex].clone() as SpeckleTextMaterial + // //@ts-ignore + // this.mesh._members.forEach((packingInfo, text) => { + // if (group.start === packingInfo.index) { + // material.color.copy(text.material.color) + // material.opacity = text.material.opacity + // } + // }) - public getMaterialAtIndex(index: number): Material { - index - return this.batchMaterial - } - - public getMaterial(rv: NodeRenderView): Material { - rv - return this.batchMaterial + // return material } public purge() { this.renderViews.length = 0 this.batchMaterial.dispose() - this.mesh.geometry.dispose() + //@ts-ignore + this.mesh.dispose() } } diff --git a/packages/viewer/src/modules/batching/TextBatchObject.ts b/packages/viewer/src/modules/batching/TextBatchObject.ts new file mode 100644 index 000000000..413ce1f4d --- /dev/null +++ b/packages/viewer/src/modules/batching/TextBatchObject.ts @@ -0,0 +1,33 @@ +import { Box3, Matrix4 } from 'three' +import { BatchObject, Vector3Like } from './BatchObject.js' +import { NodeRenderView } from '../tree/NodeRenderView.js' + +export class TextBatchObject extends BatchObject { + public textTransform: Matrix4 = new Matrix4() + + public constructor(renderView: NodeRenderView, batchIndex: number) { + super(renderView, batchIndex) + if (renderView.renderData.geometry.bakeTransform) + this.textTransform.copy(renderView.renderData.geometry.bakeTransform) + /** TO DO: Not sure we should do this */ + this.transform.copy(this.textTransform) + this.transformInv.copy(new Matrix4().copy(this.textTransform).invert()) + this.transformDirty = false + } + + public get aabb(): Box3 { + return this._accelerationStructure.getBoundingBox(new Box3()) + } + + public transformTRS( + translation: Vector3Like, + euler: Vector3Like, + scale: Vector3Like, + pivot: Vector3Like + ) { + super.transformTRS(translation, euler, scale, pivot) + this.transform.multiply(this.textTransform) + this.transformInv.copy(this.transform) + this.transformInv.invert() + } +} diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 73ff522ab..530c6292b 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -478,7 +478,7 @@ export class CameraController extends Extension implements SpeckleCamera { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) + .batcher.getBatches(undefined, [GeometryType.MESH, GeometryType.TEXT]) let minDist = Number.POSITIVE_INFINITY for (let b = 0; b < batches.length; b++) { const result = batches[b].mesh.TAS.closestPointToPointHalfplane( diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index e96307fee..5b2801f95 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -232,14 +232,17 @@ export class SmoothOrbitControls extends SpeckleControls { this.setDamperDecayTime(this._options.damperDecay) const billboardMaterial = new SpeckleBasicMaterial({ color: 0x047efb }, [ - 'BILLBOARD_FIXED' + 'BILLBOARD_SCREEN' ]) billboardMaterial.opacity = 0.75 billboardMaterial.transparent = true billboardMaterial.color.convertSRGBToLinear() billboardMaterial.toneMapped = false billboardMaterial.depthTest = false - billboardMaterial.billboardPixelHeight = 15 * window.devicePixelRatio + billboardMaterial.billboardPixelSize = new Vector2( + 15 * window.devicePixelRatio, + 15 * window.devicePixelRatio + ) this.orbitSphere = new Mesh(new SphereGeometry(0.5, 32, 16), billboardMaterial) this.orbitSphere.layers.set(ObjectLayers.OVERLAY) @@ -863,9 +866,7 @@ export class SmoothOrbitControls extends SpeckleControls { this._options.orbitAroundCursor && this.usePivotal ? this.pivotPoint : new Vector3().copy(this.origin).applyMatrix4(this._basisTransform) - /** TO DO: Revisit and set by writing to it's position */ - const mat = this.orbitSphere.material as SpeckleBasicMaterial - mat.userData.billboardPos.value.copy(spherePos) + this.orbitSphere.position.copy(spherePos) /** We'd rather have a palpable epsilon for regular sized streams, but also * compute a custom one for microscopic ones diff --git a/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts index 57cf0be54..a35bda07e 100644 --- a/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/AreaMeasurement.ts @@ -1,17 +1,21 @@ import { + AlwaysStencilFunc, Box3, BufferAttribute, BufferGeometry, Camera, DoubleSide, DynamicDrawUsage, + KeepStencilOp, Material, Mesh, + NotEqualStencilFunc, OrthographicCamera, PerspectiveCamera, Plane, Quaternion, Raycaster, + ReplaceStencilOp, Vector2, Vector3, type Intersection @@ -72,8 +76,16 @@ export class AreaMeasurement extends Measurement { super() this.type = 'AreaMeasurement' - /** We create the initial gizmo */ + /** We create the initial gizmo which will always display the area value text label*/ const gizmo = new MeasurementPointGizmo() + /** The gizmo's TextLabel will write `1` to the stencil buffer */ + gizmo.text.backgroundMaterial.stencilWrite = true + gizmo.text.backgroundMaterial.depthWrite = false + gizmo.text.backgroundMaterial.depthTest = false + gizmo.text.backgroundMaterial.stencilFunc = AlwaysStencilFunc + gizmo.text.backgroundMaterial.stencilRef = 1 + gizmo.text.backgroundMaterial.stencilZPass = ReplaceStencilOp + gizmo.enable(false, true, true, false) this.pointGizmos.push(gizmo) this.add(this.pointGizmos[0]) @@ -136,6 +148,7 @@ export class AreaMeasurement extends Measurement { /** Add a new gizmo */ const gizmo = new MeasurementPointGizmo() + gizmo.enable(false, true, true, false) this.pointGizmos.push(gizmo) this.add(gizmo) @@ -310,8 +323,16 @@ export class AreaMeasurement extends Measurement { toneMapped: false }) material.color.convertSRGBToLinear() - this.fillPolygon = new Mesh(new BufferGeometry(), material) + /** The transparent area plane will only draw were the stencil buffer is **NOT** `1`, effectively not overdrawing the text label */ + material.depthWrite = false + material.depthTest = false + material.stencilWrite = true + material.stencilFunc = NotEqualStencilFunc + material.stencilRef = 1 + material.stencilZPass = KeepStencilOp + this.fillPolygon = new Mesh(new BufferGeometry(), material) + this.fillPolygon.renderOrder = 100 this.fillPolygon.frustumCulled = false this.fillPolygon.layers.set(ObjectLayers.MEASUREMENTS) this.add(this.fillPolygon) diff --git a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts index 406401eb4..12a20748a 100644 --- a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts +++ b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts @@ -2,7 +2,6 @@ import { Camera, CircleGeometry, Color, - DoubleSide, DynamicDrawUsage, Group, InterleavedBufferAttribute, @@ -22,11 +21,9 @@ import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js' import { Geometry } from '../../converter/Geometry.js' import SpeckleLineMaterial from '../../materials/SpeckleLineMaterial.js' -import { SpeckleText } from '../../objects/SpeckleText.js' -import SpeckleTextMaterial from '../../materials/SpeckleTextMaterial.js' +import { TextLabel, TextLabelParams } from '../../objects/TextLabel.js' import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js' import { ObjectLayers } from '../../../IViewer.js' -import Logger from '../../utils/Logger.js' export interface MeasurementPointGizmoStyle { dashedLine?: boolean @@ -55,19 +52,21 @@ const DefaultMeasurementPointGizmoStyle = { pointOpacity: 1, textColor: 0xffffff, textOpacity: 1, - textPixelHeight: 17, + textPixelHeight: 11, pointPixelHeight: 5 } export class MeasurementPointGizmo extends Group { - private normalIndicator: LineSegments2 + public normalIndicator: LineSegments2 private normalIndicatorBuffer: Float64Array = new Float64Array(24) private normalIndicatorNormal: Vector3 = new Vector3() private normalIndicatorTangent: Vector3 = new Vector3() private normalIndicatorBitangent: Vector3 = new Vector3() - private line: LineSegments2 - private point: Mesh - private text: SpeckleText + + public line: LineSegments2 + public point: Mesh + public text: TextLabel + private _style: MeasurementPointGizmoStyle = Object.assign( {}, DefaultMeasurementPointGizmoStyle @@ -80,14 +79,10 @@ export class MeasurementPointGizmo extends Group { public set highlight(value: boolean) { if (value) { - ;(this.normalIndicator.material as SpeckleLineMaterial).color = new Color( - 0xff0000 - ) - ;(this.line.material as SpeckleLineMaterial).color = new Color(0xff0000) - ;(this.point.material as SpeckleBasicMaterial).color = new Color(0xff0000) - ;(this.text.textMesh.material as SpeckleTextMaterial).color.copy( - new Color(0xff0000) - ) + this.normalIndicator.material.color = new Color(0xff0000) + this.line.material.color = new Color(0xff0000) + this.point.material.color = new Color(0xff0000) + this.text.material.color.copy(new Color(0xff0000)) } else this.updateStyle() } @@ -153,7 +148,7 @@ export class MeasurementPointGizmo extends Group { private getPointMaterial(color?: number) { const material = new SpeckleBasicMaterial( { color: color ? color : this._style.pointColor }, - ['BILLBOARD_FIXED'] + ['BILLBOARD_SCREEN'] ) material.opacity = this._style.pointOpacity !== undefined @@ -163,40 +158,15 @@ export class MeasurementPointGizmo extends Group { material.color.convertSRGBToLinear() material.toneMapped = false material.depthTest = false - material.billboardPixelHeight = + const billboardSize = (this._style.pointPixelHeight !== undefined ? this._style.pointPixelHeight : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * window.devicePixelRatio - material.userData.billboardPos.value.copy(this.point.position) + material.billboardPixelSize = new Vector2(billboardSize, billboardSize) + return material } - private getTextMaterial() { - const material = new SpeckleTextMaterial( - { - color: this._style.textColor, - opacity: 1, - side: DoubleSide - }, - ['BILLBOARD_FIXED'] - ) - material.toneMapped = false - material.color.convertSRGBToLinear() - material.opacity = - this._style.textOpacity !== undefined - ? this._style.textOpacity - : DefaultMeasurementPointGizmoStyle.textOpacity - material.transparent = material.opacity < 1 - material.depthTest = false - material.billboardPixelHeight = - (this._style.textPixelHeight !== undefined - ? this._style.textPixelHeight - : DefaultMeasurementPointGizmoStyle.textPixelHeight) * window.devicePixelRatio - material.userData.billboardPos.value.copy(this.text.position) - - return material.getDerivedMaterial() - } - public constructor(style?: MeasurementPointGizmoStyle) { super() this.layers.set(ObjectLayers.MEASUREMENTS) @@ -235,24 +205,43 @@ export class MeasurementPointGizmo extends Group { const sphereGeometry = new CircleGeometry(1, 16) - this.point = new Mesh(sphereGeometry, undefined) + this.point = new Mesh(sphereGeometry) this.point.layers.set(ObjectLayers.MEASUREMENTS) this.point.visible = false this.point.renderOrder = 1 const point2 = new Mesh(sphereGeometry, this.getPointMaterial(0xffffff)) point2.renderOrder = 2 - point2.material.billboardPixelHeight = + const pixelSize = (this._style.pointPixelHeight !== undefined ? this._style.pointPixelHeight : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * window.devicePixelRatio - 2 * window.devicePixelRatio + point2.material.billboardPixelSize = new Vector2(pixelSize, pixelSize) point2.layers.set(ObjectLayers.MEASUREMENTS) this.point.add(point2) - this.text = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS) - this.text.textMesh.material = null + this.text = new TextLabel({ + textColor: new Color(this._style.textColor), + fontSize: + this._style.textPixelHeight !== undefined + ? this._style.textPixelHeight + : DefaultMeasurementPointGizmoStyle.textPixelHeight, + textOpacity: + this._style.textOpacity !== undefined + ? this._style.textOpacity + : DefaultMeasurementPointGizmoStyle.textOpacity, + billboard: 'screen', + anchorX: 'center', + anchorY: 'middle', + backgroundColor: new Color(0x047efb), + backgroundCornerRadius: 0.3, + backgroundMargins: new Vector2(30, 10), + objectLayer: ObjectLayers.MEASUREMENTS + }) + this.text.material.depthTest = false + this.text.depthOffset = -0.1 this.add(this.point) this.add(this.normalIndicator) @@ -380,12 +369,6 @@ export class MeasurementPointGizmo extends Group { public updatePoint(position: Vector3) { this.point.position.copy(position) - ;(this.point.material as SpeckleBasicMaterial).userData.billboardPos.value.copy( - this.point.position - ) - ;( - (this.point.children[0] as Mesh).material as SpeckleBasicMaterial - ).userData.billboardPos.value.copy(this.point.position) } public updateLine(points: Vector3[]) { @@ -425,33 +408,23 @@ export class MeasurementPointGizmo extends Group { quaternion?: Quaternion, scale?: Vector3 ): Promise { - return this.text - .update({ - textValue: value, - height: 1, - anchorX: '50%', - anchorY: '50%' - }) - .then(() => { - this.text.style = { - backgroundColor: new Color(0x047efb), - billboard: true, - backgroundPixelHeight: 20 - } - this.text.setTransform(position, quaternion, scale) - if (this.text.backgroundMesh) this.text.backgroundMesh.renderOrder = 3 - this.text.textMesh.renderOrder = 4 - }) - .catch((reason) => { - Logger.log(`Could not update text: ${reason}`) - }) + const params = { + text: value + } as TextLabelParams + + if (position) this.text.position.copy(position) + if (quaternion) this.text.quaternion.copy(quaternion) + if (scale) this.text.scale.copy(scale) + this.text.updateMatrixWorld(true) + + return this.text.updateParams(params) } public updateStyle() { this.normalIndicator.material = this.getNormalIndicatorMaterial() this.line.material = this.getLineMaterial() this.point.material = this.getPointMaterial() - this.text.textMesh.material = this.getTextMaterial() + void this.text.updateParams({ textColor: new Color(this._style.textColor) }) } public raycast(raycaster: Raycaster, intersects: Array) { diff --git a/packages/viewer/src/modules/extensions/measurements/PointMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/PointMeasurement.ts index f738d134c..cd24ee7b7 100644 --- a/packages/viewer/src/modules/extensions/measurements/PointMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/PointMeasurement.ts @@ -2,9 +2,7 @@ import { Box3, Camera, Color, - DoubleSide, Material, - MathUtils, Matrix4, OrthographicCamera, PerspectiveCamera, @@ -18,8 +16,7 @@ import { import { getConversionFactor } from '../../converter/Units.js' import { Measurement, MeasurementState } from './Measurement.js' import { ObjectLayers } from '../../../IViewer.js' -import { SpeckleText } from '../../objects/SpeckleText.js' -import SpeckleTextMaterial from '../../materials/SpeckleTextMaterial.js' +import { TextLabel } from '../../objects/TextLabel.js' import { MeasurementPointGizmo } from './MeasurementPointGizmo.js' const _vec40 = new Vector4() @@ -31,14 +28,14 @@ const _mat41 = new Matrix4() export class PointMeasurement extends Measurement { protected gizmo: MeasurementPointGizmo - protected xLabel: SpeckleText - protected yLabel: SpeckleText - protected zLabel: SpeckleText + protected xLabel: TextLabel + protected yLabel: TextLabel + protected zLabel: TextLabel protected xLabelPosition: Vector3 = new Vector3() protected yLabelPosition: Vector3 = new Vector3() protected zLabelPosition: Vector3 = new Vector3() protected readonly pixelsOffX = 50 * window.devicePixelRatio - protected readonly pixelsOffY = 27 * window.devicePixelRatio + protected readonly pixelsOffY = 25 * window.devicePixelRatio public set isVisible(value: boolean) { this.gizmo.visible = value @@ -52,65 +49,55 @@ export class PointMeasurement extends Measurement { this.type = 'PointMeasurement' this.gizmo = new MeasurementPointGizmo() this.add(this.gizmo) - this.xLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS) - const xLabelMaterial = new SpeckleTextMaterial( - { - color: 0xffffff, - opacity: 1, - side: DoubleSide - }, - ['USE_RTE', 'BILLBOARD_FIXED'] - ) - xLabelMaterial.toneMapped = false - xLabelMaterial.color.convertSRGBToLinear() - xLabelMaterial.opacity = 1 - xLabelMaterial.transparent = false - xLabelMaterial.depthTest = false - xLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio - xLabelMaterial.userData.billboardPos.value.copy(this.position) - this.xLabel.textMesh.material = xLabelMaterial.getDerivedMaterial() + this.xLabel = new TextLabel({ + text: 'sample', + textColor: new Color(0xffffff), + fontSize: 11, + billboard: 'screen', + anchorX: 'left', + anchorY: 'middle', + backgroundColor: new Color(0xfb0404), + backgroundMargins: new Vector2(30, 10), + backgroundCornerRadius: 0.3, + objectLayer: ObjectLayers.MEASUREMENTS + }) + this.xLabel.name = 'XLabel' + this.xLabel.material.depthTest = false this.add(this.xLabel) - this.yLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS) - const yLabelMaterial = new SpeckleTextMaterial( - { - color: 0xffffff, - opacity: 1, - side: DoubleSide - }, - ['USE_RTE', 'BILLBOARD_FIXED'] - ) - yLabelMaterial.toneMapped = false - yLabelMaterial.color.convertSRGBToLinear() - yLabelMaterial.opacity = 1 - yLabelMaterial.transparent = false - yLabelMaterial.depthTest = false - yLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio - yLabelMaterial.userData.billboardPos.value.copy(this.position) - - this.yLabel.textMesh.material = yLabelMaterial.getDerivedMaterial() + this.yLabel = new TextLabel({ + text: 'sample', + textColor: new Color(0xffffff), + fontSize: 11, + anchorX: 'left', + anchorY: 'middle', + billboard: 'screen', + backgroundColor: new Color(0x03c903), + backgroundMargins: new Vector2(30, 10), + backgroundCornerRadius: 0.3, + objectLayer: ObjectLayers.MEASUREMENTS + }) + this.yLabel.name = 'YLabel' + this.yLabel.material.depthTest = false this.add(this.yLabel) - this.zLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS) - const zLabelMaterial = new SpeckleTextMaterial( - { - color: 0xffffff, - opacity: 1, - side: DoubleSide - }, - ['USE_RTE', 'BILLBOARD_FIXED'] - ) - zLabelMaterial.toneMapped = false - zLabelMaterial.color.convertSRGBToLinear() - zLabelMaterial.opacity = 1 - zLabelMaterial.transparent = false - zLabelMaterial.depthTest = false - zLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio - zLabelMaterial.userData.billboardPos.value.copy(this.position) - - this.zLabel.textMesh.material = zLabelMaterial.getDerivedMaterial() + this.zLabel = new TextLabel({ + text: 'sample', + textColor: new Color(0xffffff), + fontSize: 11, + billboard: 'screen', + anchorX: 'left', + anchorY: 'middle', + backgroundColor: new Color(0x047efb), + backgroundMargins: new Vector2(30, 10), + backgroundCornerRadius: 0.3, + objectLayer: ObjectLayers.MEASUREMENTS + }) + this.zLabel.name = 'ZLabel' + this.zLabel.material.depthTest = false this.add(this.zLabel) + this.layers.set(ObjectLayers.MEASUREMENTS) } @@ -118,9 +105,9 @@ export class PointMeasurement extends Measurement { super.frameUpdate(camera, size, bounds) this.updateLabelPositions() - this.xLabel.setTransform(this.xLabelPosition) - this.yLabel.setTransform(this.yLabelPosition) - this.zLabel.setTransform(this.zLabelPosition) + this.xLabel.position.copy(this.xLabelPosition) + this.yLabel.position.copy(this.yLabelPosition) + this.zLabel.position.copy(this.zLabelPosition) this.gizmo.frameUpdate(camera, size) } @@ -171,64 +158,26 @@ export class PointMeasurement extends Measurement { } public async update(): Promise { - const xP = this.xLabel - .update({ - textValue: `x : ${( - this.startPoint.x * getConversionFactor('m', this.units) - ).toFixed(this.precision)} ${this.units}`, - height: 1, - anchorX: '0%', - anchorY: '50%' - }) - .then(() => { - this.xLabel.style = { - backgroundColor: new Color(0xfb0404), - billboard: true, - backgroundPixelHeight: 20 - } - this.xLabel.setTransform(this.xLabelPosition) - if (this.xLabel.backgroundMesh) this.xLabel.backgroundMesh.renderOrder = 3 - this.xLabel.textMesh.renderOrder = 4 - }) - const yP = this.yLabel - .update({ - textValue: `y : ${( - this.startPoint.y * getConversionFactor('m', this.units) - ).toFixed(this.precision)} ${this.units}`, - height: 1, - anchorX: '0%', - anchorY: '50%' - }) - .then(() => { - this.yLabel.style = { - backgroundColor: new Color(0x03c903), - billboard: true, - backgroundPixelHeight: 20 - } - this.yLabel.setTransform(this.yLabelPosition) - if (this.yLabel.backgroundMesh) this.yLabel.backgroundMesh.renderOrder = 3 - this.yLabel.textMesh.renderOrder = 4 - }) + this.xLabel.position.copy(this.xLabelPosition) + this.yLabel.position.copy(this.yLabelPosition) + this.zLabel.position.copy(this.zLabelPosition) + const xP = this.xLabel.updateParams({ + text: `X : ${(this.startPoint.x * getConversionFactor('m', this.units)).toFixed( + this.precision + )} ${this.units}` + }) - const zP = this.zLabel - .update({ - textValue: `z : ${( - this.startPoint.z * getConversionFactor('m', this.units) - ).toFixed(this.precision)} ${this.units}`, - height: 1, - anchorX: '0%', - anchorY: '50%' - }) - .then(() => { - this.zLabel.style = { - backgroundColor: new Color(0x047efb), - billboard: true, - backgroundPixelHeight: 20 - } - this.zLabel.setTransform(this.zLabelPosition) - if (this.zLabel.backgroundMesh) this.zLabel.backgroundMesh.renderOrder = 3 - this.zLabel.textMesh.renderOrder = 4 - }) + const yP = this.yLabel.updateParams({ + text: `Y : ${(this.startPoint.y * getConversionFactor('m', this.units)).toFixed( + this.precision + )} ${this.units}` + }) + + const zP = this.zLabel.updateParams({ + text: `Z : ${(this.startPoint.z * getConversionFactor('m', this.units)).toFixed( + this.precision + )} ${this.units}` + }) this.gizmo.updateNormalIndicator(this.startPoint, this.startNormal) this.gizmo.updatePoint(this.startPoint) diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index 050b6b520..87d861a20 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -336,20 +336,23 @@ export class SpeckleGeometryConverter extends GeometryConverter { */ protected TextToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) - /** TEMPORARY UNTIL PROPER IMPLEMENTATION FOR TEXT V3 */ const plane = node.raw.plane || { origin: node.raw.origin, xdir: new Vector3(1, 0, 0), ydir: new Vector3(0, 1, 0), normal: new Vector3(0, 0, 1) } + const billboard = node.raw.screenOriented || false const position = new Vector3(plane.origin.x, plane.origin.y, plane.origin.z) const scale = new Matrix4().makeScale( conversionFactor, conversionFactor, conversionFactor ) - const mat = new Matrix4().makeBasis(plane.xdir, plane.ydir, plane.normal) + /** We ignore rotation if screen oriented */ + const mat = billboard + ? new Matrix4() + : new Matrix4().makeBasis(plane.xdir, plane.ydir, plane.normal) mat.setPosition(position) mat.premultiply(scale) return { diff --git a/packages/viewer/src/modules/materials/Materials.ts b/packages/viewer/src/modules/materials/Materials.ts index f205c3de0..6e8c9a54a 100644 --- a/packages/viewer/src/modules/materials/Materials.ts +++ b/packages/viewer/src/modules/materials/Materials.ts @@ -14,6 +14,7 @@ import SpeckleTextMaterial from './SpeckleTextMaterial.js' import { SpeckleMaterial } from './SpeckleMaterial.js' import SpecklePointColouredMaterial from './SpecklePointColouredMaterial.js' import { type Asset, AssetType, type MaterialOptions } from '../../IViewer.js' +import SpeckleTextColoredMaterial from './SpeckleTextColoredMaterial.js' const defaultGradient: Asset = { id: 'defaultGradient', @@ -83,6 +84,7 @@ export default class Materials { private textGhostMaterial: Material private textColoredMaterial: Material + private textGradientMaterial: Material private textHiddenMaterial: Material private defaultGradientTextureData!: ImageData @@ -314,7 +316,12 @@ export default class Materials { renderView.geometryType.toString() + geometry + mat + - (renderView.geometryType === GeometryType.TEXT ? renderView.renderData.id : '') + + (renderView.geometryType === GeometryType.TEXT && + renderView.renderData.geometry.metaData?.screenOriented !== undefined + ? ( + renderView.renderData.geometry.metaData?.screenOriented as boolean + ).toString() + : '') + (renderView.renderData.geometry.instanced ? 'instanced' : '') return Materials.hashCode(s) } @@ -490,9 +497,9 @@ export default class Materials { this.textGhostMaterial = ( this.textGhostMaterial as SpeckleTextMaterial - ).getDerivedMaterial() + ).getDerivedBatchedMaterial() - this.textColoredMaterial = new SpeckleTextMaterial({ + this.textColoredMaterial = new SpeckleTextColoredMaterial({ color: 0xffffff, opacity: 1, side: DoubleSide @@ -503,11 +510,35 @@ export default class Materials { ? false : true this.textColoredMaterial.toneMapped = false - ;(this.textColoredMaterial as SpeckleTextMaterial).color.convertSRGBToLinear() + ;( + this.textColoredMaterial as SpeckleTextColoredMaterial + ).color.convertSRGBToLinear() this.textColoredMaterial = ( - this.textColoredMaterial as SpeckleTextMaterial - ).getDerivedMaterial() + this.textColoredMaterial as SpeckleTextColoredMaterial + ).getDerivedBatchedMaterial() + + this.textGradientMaterial = new SpeckleTextColoredMaterial({ + color: 0xffffff, + opacity: 1, + side: DoubleSide + }) + this.textGradientMaterial.transparent = + this.textGradientMaterial.opacity < 1 ? true : false + this.textGradientMaterial.depthWrite = this.textGradientMaterial.transparent + ? false + : true + this.textGradientMaterial.toneMapped = false + ;( + this.textGradientMaterial as SpeckleTextColoredMaterial + ).color.convertSRGBToLinear() + ;(this.textGradientMaterial as SpeckleTextColoredMaterial).setGradientTexture( + await Assets.getTexture(defaultGradient) + ) + + this.textGradientMaterial = ( + this.textGradientMaterial as SpeckleTextColoredMaterial + ).getDerivedBatchedMaterial() this.textHiddenMaterial = new SpeckleTextMaterial({ color: 0xffffff, @@ -520,7 +551,7 @@ export default class Materials { this.textHiddenMaterial = ( this.textHiddenMaterial as SpeckleTextMaterial - ).getDerivedMaterial() + ).getDerivedBatchedMaterial() } private async createDefaultNullMaterials() { @@ -796,7 +827,7 @@ export default class Materials { if (!this.materialMap[hash]) { this.materialMap[hash] = this.makeTextMaterial(material as DisplayStyle) } - return (this.materialMap[hash] as SpeckleTextMaterial).getDerivedMaterial() + return (this.materialMap[hash] as SpeckleTextMaterial).getDerivedBatchedMaterial() } public getGhostMaterial( @@ -852,7 +883,12 @@ export default class Materials { return material } case GeometryType.TEXT: - return this.textColoredMaterial + const material = this.textGradientMaterial + if (filterMaterial?.rampTexture) + (material as SpeckleStandardColoredMaterial).setGradientTexture( + filterMaterial.rampTexture + ) + return material } } @@ -890,7 +926,12 @@ export default class Materials { return material } case GeometryType.TEXT: - return this.textColoredMaterial + const material = this.textColoredMaterial + if (filterMaterial?.rampTexture) + (material as SpeckleStandardColoredMaterial).setGradientTexture( + filterMaterial.rampTexture + ) + return material } } diff --git a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts index eba718940..dfb6ed9ce 100644 --- a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts @@ -11,19 +11,24 @@ import { Scene, Camera, BufferGeometry, - Object3D + Object3D, + Vector4 } from 'three' import { Matrix4 } from 'three' import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial.js' import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer.js' -class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { - protected static readonly matBuff: Matrix4 = new Matrix4() - protected static readonly vecBuff: Vector2 = new Vector2() +const matBuff: Matrix4 = new Matrix4() +const vec2Buff0: Vector2 = new Vector2() +const vec2Buff1: Vector2 = new Vector2() +const vec2Buff2: Vector2 = new Vector2() - private _billboardPixelHeight: number - private _billboardOffset: Vector2 = new Vector2() +export type BillboardingType = 'world' | 'screen' + +class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { + protected _billboardPixelSize: Vector2 = new Vector2() + protected _billboardPixelOffset: Vector2 = new Vector2() protected get vertexProgram(): string { return speckleBasicVert @@ -43,20 +48,26 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { uViewer_low: new Vector3(), uTransforms: [new Matrix4()], tTransforms: null, - billboardPos: new Vector3(), - billboardSize: new Vector2(), - billboardOffset: new Vector2(), + objCount: 1, invProjection: new Matrix4(), - objCount: 1 + billboardPixelOffsetSize: new Vector4() } } - public set billboardPixelHeight(value: number) { - this._billboardPixelHeight = value + public get billboardPixelSize(): Vector2 { + return this._billboardPixelSize } - public set billboardOffset(value: Vector2) { - this._billboardOffset.copy(value) + public set billboardPixelSize(value: Vector2) { + this._billboardPixelSize.copy(value) + } + + public get billboardPixeOffset(): Vector2 { + return this._billboardPixelOffset + } + + public set billboardPixelOffset(value: Vector2) { + this._billboardPixelOffset.copy(value) } constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) { @@ -80,8 +91,22 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { const toStandard = to as SpeckleBasicMaterial const fromStandard = from as SpeckleBasicMaterial toStandard.color.copy(fromStandard.color) - toStandard.refractionRatio = fromStandard.refractionRatio - to.userData.billboardPos.value.copy(from.userData.billboardPos.value) + to.userData.billboardPixelOffsetSize.value.copy( + from.userData.billboardPixelOffsetSize.value + ) + } + + public setBillboarding(type: BillboardingType | null) { + /** Create the define object if not there */ + if (!this.defines) this.defines = {} + /** Clear all billboarding defines */ + delete this.defines['BILLBOARD_SCREEN'] + delete this.defines['BILLBOARD'] + + if (!type) return + + if (type === 'world') this.defines['BILLBOARD'] = ' ' + if (type === 'screen') this.defines['BILLBOARD_SCREEN'] = ' ' } /** Called by three.js render loop */ @@ -92,18 +117,33 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { _geometry: BufferGeometry, object: Object3D ) { - if (this.defines && this.defines['BILLBOARD_FIXED']) { - const resolution = _this.getDrawingBufferSize(SpeckleBasicMaterial.vecBuff) - SpeckleBasicMaterial.vecBuff.set( - (this._billboardPixelHeight / resolution.x) * 2, - (this._billboardPixelHeight / resolution.y) * 2 + if ( + this.defines && + (this.defines['BILLBOARD'] || this.defines['BILLBOARD_SCREEN']) + ) { + matBuff.copy(camera.projectionMatrix).invert() + this.userData.invProjection.value.copy(matBuff) + this.needsUpdate = true + } + + if (this.defines && this.defines['BILLBOARD_SCREEN']) { + _this.getDrawingBufferSize(vec2Buff0) + const billboardPixelOffsetNDC = vec2Buff1.set( + this._billboardPixelOffset.x, + this._billboardPixelOffset.y + ) + const billboardPixelSizeNDC = vec2Buff2.set( + this._billboardPixelSize.x, + this._billboardPixelSize.y + ) + billboardPixelOffsetNDC.divide(vec2Buff0) + billboardPixelSizeNDC.divide(vec2Buff0) + this.userData.billboardPixelOffsetSize.value.set( + billboardPixelOffsetNDC.x, + billboardPixelOffsetNDC.y, + billboardPixelSizeNDC.x, + billboardPixelSizeNDC.y ) - this.userData.billboardSize.value.copy(SpeckleBasicMaterial.vecBuff) - this.userData.billboardOffset.value.copy(this._billboardOffset) - SpeckleBasicMaterial.matBuff.copy(camera.projectionMatrix).invert() - this.userData.invProjection.value.copy(SpeckleBasicMaterial.matBuff) - /** TO DO: Revisit and Enable this */ - // this.userData.billboardPos.value.copy(object.position) this.needsUpdate = true } diff --git a/packages/viewer/src/modules/materials/SpeckleTextColoredMaterial.ts b/packages/viewer/src/modules/materials/SpeckleTextColoredMaterial.ts new file mode 100644 index 000000000..c54b2af0b --- /dev/null +++ b/packages/viewer/src/modules/materials/SpeckleTextColoredMaterial.ts @@ -0,0 +1,11 @@ +import SpeckleTextMaterial from './SpeckleTextMaterial.js' + +class SpeckleTextColoredMaterial extends SpeckleTextMaterial { + public gradientIndexMap: { [index: number]: number } = {} + + public updateGradientIndexMap(index: number, value: number) { + this.gradientIndexMap[index] = value + } +} + +export default SpeckleTextColoredMaterial diff --git a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts index 7a2e7190b..d28d27d4e 100644 --- a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts @@ -1,32 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable camelcase */ import { speckleTextVert } from './shaders/speckle-text-vert.js' import { speckleTextFrag } from './shaders/speckle-text-frag.js' -import { - ShaderLib, - Vector3, - type IUniform, - Vector2, - Material, - type MeshBasicMaterialParameters, - Scene, - Camera, - BufferGeometry, - Object3D -} from 'three' -import { Matrix4 } from 'three' +import { Vector2, Material, Texture, NearestFilter } from 'three' -import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial.js' +import { type Uniforms } from './SpeckleMaterial.js' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import { createDerivedMaterial } from 'troika-three-utils' // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore import { createTextDerivedMaterial } from 'troika-three-text' -import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer.js' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import { uniformToVarying } from 'troika-three-text/src/BatchedText.js' +import SpeckleBasicMaterial from './SpeckleBasicMaterial.js' -class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { - protected static readonly matBuff: Matrix4 = new Matrix4() - protected static readonly vecBuff: Vector2 = new Vector2() - - private _billboardPixelHeight: number +class SpeckleTextMaterial extends SpeckleBasicMaterial { + public setMatrixTexture: (texture: Texture) => void protected get vertexProgram(): string { return speckleTextVert @@ -36,34 +26,8 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return speckleTextFrag } - protected get baseUniforms(): { [uniform: string]: IUniform } { - return ShaderLib.basic.uniforms - } - protected get uniformsDef(): Uniforms { - return { - uViewer_high: new Vector3(), - uViewer_low: new Vector3(), - uTransforms: [new Matrix4()], - tTransforms: null, - objCount: 1, - billboardPos: new Vector3(), - billboardSize: new Vector2(), - invProjection: new Matrix4() - } - } - - public set billboardPixelHeight(value: number) { - this._billboardPixelHeight = value - } - - public get billboardPixelHeight() { - return this._billboardPixelHeight - } - - constructor(parameters: MeshBasicMaterialParameters, defines: Array = []) { - super(parameters) - this.init(defines) + return { ...super.uniformsDef, gradientRamp: null } } /** We need a unique key per program */ @@ -71,55 +35,459 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return this.constructor.name } - public copy(source: Material) { - super.copy(source) - this.copyFrom(source) - return this - } - - public getDerivedMaterial() { - const derived = createTextDerivedMaterial(this) + protected copyCustomUniforms(material: Material) { /** We rebind the uniforms */ for (const k in this.userData) { - derived.uniforms[k] = this.userData[k] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + material.uniforms[k] = this.userData[k] } - + } + public getDerivedMaterial() { + const derived = createTextDerivedMaterial(this) + this.copyCustomUniforms(derived) return derived } + /* + Data texture packing strategy: + + # Common: + 0-15: matrix + 16-19: uTroikaTotalBounds + 20-23: uTroikaClipRect + 24: diffuse (color/outlineColor) + 25: uTroikaFillOpacity (fillOpacity/outlineOpacity) + 26: uTroikaCurveRadius + 27: + + # Main: + 28: uTroikaStrokeWidth + 29: uTroikaStrokeColor + 30: uTroikaStrokeOpacity + + # Outline: + 28-29: uTroikaPositionOffset + 30: uTroikaEdgeOffset + 31: uTroikaBlurRadius + */ + /** Sadly, troika does not export this for no good reason so we neee to copy it over */ + public getDerivedBatchedMaterial() { + const texUniformName = 'uTroikaMatricesTexture' + const texSizeUniformName = 'uTroikaMatricesTextureSize' + const memberIndexAttrName = 'aTroikaTextBatchMemberIndex' + const floatsPerMember = 32 + // Due to how vertexTransform gets injected, the matrix transforms must happen + // in the base material of TextDerivedMaterial, but other transforms to its + // shader must come after, so we sandwich it between two derivations. + + // Transform the vertex position + let batchMaterial = createDerivedMaterial(this, { + chained: true, + uniforms: { + [texSizeUniformName]: { value: new Vector2() }, + [texUniformName]: { value: null } + }, + // language=GLSL + vertexDefs: ` + uniform highp sampler2D ${texUniformName}; + uniform vec2 ${texSizeUniformName}; + attribute float ${memberIndexAttrName}; + + vec4 troikaBatchTexel(float offset) { + offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0; + float w = ${texSizeUniformName}.x; + vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName}; + return texture2D(${texUniformName}, uv); + } + `, + // language=GLSL prefix="void main() {" suffix="}" + vertexTransform: ` + /** We don't need this. We're transforming ourselves in our shader to allow for RTE*/ + // mat4 matrix = mat4( + // troikaBatchTexel(0.0), + // troikaBatchTexel(1.0), + // troikaBatchTexel(2.0), + // troikaBatchTexel(3.0) + // ); + // position.xyz = (matrix * vec4(position, 1.0)).xyz; + ` + }) + + // Add the text shaders + batchMaterial = createTextDerivedMaterial(batchMaterial) + + // Now make other changes to the derived text shader code + batchMaterial = createDerivedMaterial(batchMaterial, { + chained: true, + uniforms: { + uTroikaIsOutline: { value: false } + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + customRewriter(shaders) { + // Convert some text shader uniforms to varyings + const varyingUniforms = [ + 'uTroikaTotalBounds', + 'uTroikaClipRect', + 'uTroikaPositionOffset', + 'uTroikaEdgeOffset', + 'uTroikaBlurRadius', + 'uTroikaStrokeWidth', + 'uTroikaStrokeColor', + 'uTroikaStrokeOpacity', + 'uTroikaFillOpacity', + 'uTroikaCurveRadius', + 'diffuse' + ] + varyingUniforms.forEach((uniformName) => { + shaders = uniformToVarying(shaders, uniformName) + }) + return shaders + }, + // language=GLSL + vertexDefs: ` + uniform bool uTroikaIsOutline; + vec3 troikaFloatToColor(float v) { + return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0; + } + `, + // language=GLSL prefix="void main() {" suffix="}" + vertexTransform: ` + uTroikaTotalBounds = troikaBatchTexel(4.0); + uTroikaClipRect = troikaBatchTexel(5.0); + + vec4 data = troikaBatchTexel(6.0); + diffuse = troikaFloatToColor(data.x); + uTroikaFillOpacity = data.y; + uTroikaCurveRadius = data.z; + + data = troikaBatchTexel(7.0); + if (uTroikaIsOutline) { + if (data == vec4(0.0)) { // degenerate if zero outline + position = vec3(0.0); + } else { + uTroikaPositionOffset = data.xy; + uTroikaEdgeOffset = data.z; + uTroikaBlurRadius = data.w; + } + } else { + uTroikaStrokeWidth = data.x; + uTroikaStrokeColor = troikaFloatToColor(data.y); + uTroikaStrokeOpacity = data.z; + } + ` + }) + + batchMaterial.setMatrixTexture = (texture: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + image: { width: any; height: any } + }) => { + batchMaterial.uniforms[texUniformName].value = texture + batchMaterial.uniforms[texSizeUniformName].value.set( + texture.image.width, + texture.image.height + ) + } + this.copyCustomUniforms(batchMaterial) + ;(batchMaterial.defines ??= {})['BATCHED_TEXT'] = ' ' + return batchMaterial + } + public fastCopy(from: Material, to: Material) { super.fastCopy(from, to) - const toStandard = to as SpeckleTextMaterial - const fromStandard = from as SpeckleTextMaterial - toStandard.color.copy(fromStandard.color) - toStandard.refractionRatio = fromStandard.refractionRatio - to.userData.billboardPos.value.copy(from.userData.billboardPos.value) + to.userData.gradientRamp.value = from.userData.gradientRamp.value } - /** Called by three.js render loop */ - public onBeforeRender( - _this: SpeckleWebGLRenderer, - _scene: Scene, - camera: Camera, - _geometry: BufferGeometry, - _object: Object3D - ) { - if (this.defines && this.defines['BILLBOARD_FIXED']) { - const resolution = _this.getDrawingBufferSize(SpeckleTextMaterial.vecBuff) - SpeckleTextMaterial.vecBuff.set( - (this._billboardPixelHeight / resolution.x) * 2, - (this._billboardPixelHeight / resolution.y) * 2 - ) - this.userData.billboardSize.value.copy(SpeckleTextMaterial.vecBuff) - SpeckleTextMaterial.matBuff.copy(camera.projectionMatrix).invert() - this.userData.invProjection.value.copy(SpeckleTextMaterial.matBuff) - } - /** TO ENABLE */ - // object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) - // this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) - // this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) + public setGradientTexture(texture: Texture) { + this.userData.gradientRamp.value = texture + this.userData.gradientRamp.value.generateMipmaps = false + this.userData.gradientRamp.value.minFilter = NearestFilter + this.userData.gradientRamp.value.magFilter = NearestFilter this.needsUpdate = true } } export default SpeckleTextMaterial + +// const matBuff: Matrix4 = new Matrix4() +// const vec2Buff: Vector2 = new Vector2() + +// export interface SpeckleTextMaterialParameters extends MeshBasicMaterialParameters { +// billboardPixelHeight?: number +// } + +// export type BillboardingType = 'world' | 'screen' + +// class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { +// private _billboardPixelHeight: number + +// protected get vertexProgram(): string { +// return speckleTextVert +// } + +// protected get fragmentProgram(): string { +// return speckleTextFrag +// } + +// protected get baseUniforms(): { [uniform: string]: IUniform } { +// return ShaderLib.basic.uniforms +// } + +// protected get uniformsDef(): Uniforms { +// return { +// uViewer_high: new Vector3(), +// uViewer_low: new Vector3(), +// invProjection: new Matrix4(), +// billboardPixelHeight: 0, +// screenSize: new Vector2(), +// gradientRamp: null +// } +// } + +// public get billboardPixelHeight() { +// return this._billboardPixelHeight +// } + +// public set billboardPixelHeight(value: number) { +// this._billboardPixelHeight = value +// } + +// constructor(parameters: SpeckleTextMaterialParameters, defines: Array = []) { +// super(parameters) +// this.init(defines) +// } + +// /** We need a unique key per program */ +// public customProgramCacheKey() { +// return this.constructor.name +// } + +// public copy(source: Material) { +// super.copy(source) +// this.copyFrom(source) +// return this +// } + +// protected copyCustomUniforms(material: Material) { +// /** We rebind the uniforms */ +// for (const k in this.userData) { +// // eslint-disable-next-line @typescript-eslint/ban-ts-comment +// //@ts-ignore +// material.uniforms[k] = this.userData[k] +// } +// } +// public getDerivedMaterial() { +// const derived = createTextDerivedMaterial(this) +// this.copyCustomUniforms(derived) +// return derived +// } + +// /* +// Data texture packing strategy: + +// # Common: +// 0-15: matrix +// 16-19: uTroikaTotalBounds +// 20-23: uTroikaClipRect +// 24: diffuse (color/outlineColor) +// 25: uTroikaFillOpacity (fillOpacity/outlineOpacity) +// 26: uTroikaCurveRadius +// 27: + +// # Main: +// 28: uTroikaStrokeWidth +// 29: uTroikaStrokeColor +// 30: uTroikaStrokeOpacity + +// # Outline: +// 28-29: uTroikaPositionOffset +// 30: uTroikaEdgeOffset +// 31: uTroikaBlurRadius +// */ +// /** Sadly, troika does not export this for no good reason so we neee to copy it over */ +// public getDerivedBatchedMaterial() { +// const texUniformName = 'uTroikaMatricesTexture' +// const texSizeUniformName = 'uTroikaMatricesTextureSize' +// const memberIndexAttrName = 'aTroikaTextBatchMemberIndex' +// const floatsPerMember = 32 +// // Due to how vertexTransform gets injected, the matrix transforms must happen +// // in the base material of TextDerivedMaterial, but other transforms to its +// // shader must come after, so we sandwich it between two derivations. + +// // Transform the vertex position +// let batchMaterial = createDerivedMaterial(this, { +// chained: true, +// uniforms: { +// [texSizeUniformName]: { value: new Vector2() }, +// [texUniformName]: { value: null } +// }, +// // language=GLSL +// vertexDefs: ` +// uniform highp sampler2D ${texUniformName}; +// uniform vec2 ${texSizeUniformName}; +// attribute float ${memberIndexAttrName}; + +// vec4 troikaBatchTexel(float offset) { +// offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0; +// float w = ${texSizeUniformName}.x; +// vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName}; +// return texture2D(${texUniformName}, uv); +// } +// `, +// // language=GLSL prefix="void main() {" suffix="}" +// vertexTransform: ` +// /** We don't need this. We're transforming ourselves in our shader to allow for RTE*/ +// // mat4 matrix = mat4( +// // troikaBatchTexel(0.0), +// // troikaBatchTexel(1.0), +// // troikaBatchTexel(2.0), +// // troikaBatchTexel(3.0) +// // ); +// // position.xyz = (matrix * vec4(position, 1.0)).xyz; +// ` +// }) + +// // Add the text shaders +// batchMaterial = createTextDerivedMaterial(batchMaterial) + +// // Now make other changes to the derived text shader code +// batchMaterial = createDerivedMaterial(batchMaterial, { +// chained: true, +// uniforms: { +// uTroikaIsOutline: { value: false } +// }, +// // eslint-disable-next-line @typescript-eslint/ban-ts-comment +// //@ts-ignore +// customRewriter(shaders) { +// // Convert some text shader uniforms to varyings +// const varyingUniforms = [ +// 'uTroikaTotalBounds', +// 'uTroikaClipRect', +// 'uTroikaPositionOffset', +// 'uTroikaEdgeOffset', +// 'uTroikaBlurRadius', +// 'uTroikaStrokeWidth', +// 'uTroikaStrokeColor', +// 'uTroikaStrokeOpacity', +// 'uTroikaFillOpacity', +// 'uTroikaCurveRadius', +// 'diffuse' +// ] +// varyingUniforms.forEach((uniformName) => { +// shaders = uniformToVarying(shaders, uniformName) +// }) +// return shaders +// }, +// // language=GLSL +// vertexDefs: ` +// uniform bool uTroikaIsOutline; +// vec3 troikaFloatToColor(float v) { +// return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0; +// } +// `, +// // language=GLSL prefix="void main() {" suffix="}" +// vertexTransform: ` +// uTroikaTotalBounds = troikaBatchTexel(4.0); +// uTroikaClipRect = troikaBatchTexel(5.0); + +// vec4 data = troikaBatchTexel(6.0); +// diffuse = troikaFloatToColor(data.x); +// uTroikaFillOpacity = data.y; +// uTroikaCurveRadius = data.z; + +// data = troikaBatchTexel(7.0); +// if (uTroikaIsOutline) { +// if (data == vec4(0.0)) { // degenerate if zero outline +// position = vec3(0.0); +// } else { +// uTroikaPositionOffset = data.xy; +// uTroikaEdgeOffset = data.z; +// uTroikaBlurRadius = data.w; +// } +// } else { +// uTroikaStrokeWidth = data.x; +// uTroikaStrokeColor = troikaFloatToColor(data.y); +// uTroikaStrokeOpacity = data.z; +// } +// ` +// }) + +// batchMaterial.setMatrixTexture = (texture: { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// image: { width: any; height: any } +// }) => { +// batchMaterial.uniforms[texUniformName].value = texture +// batchMaterial.uniforms[texSizeUniformName].value.set( +// texture.image.width, +// texture.image.height +// ) +// } +// this.copyCustomUniforms(batchMaterial) +// ;(batchMaterial.defines ??= {})['BATCHED_TEXT'] = ' ' +// return batchMaterial +// } + +// public fastCopy(from: Material, to: Material) { +// super.fastCopy(from, to) +// const toStandard = to as SpeckleTextMaterial +// const fromStandard = from as SpeckleTextMaterial +// toStandard.color.copy(fromStandard.color) +// toStandard.refractionRatio = fromStandard.refractionRatio +// to.userData.gradientRamp.value = from.userData.gradientRamp.value +// } + +// public setGradientTexture(texture: Texture) { +// this.userData.gradientRamp.value = texture +// this.userData.gradientRamp.value.generateMipmaps = false +// this.userData.gradientRamp.value.minFilter = NearestFilter +// this.userData.gradientRamp.value.magFilter = NearestFilter +// this.needsUpdate = true +// } + +// public setBillboarding(type: BillboardingType | null) { +// /** Create the define object if not there */ +// if (!this.defines) this.defines = {} +// /** Clear all billboarding defines */ +// delete this.defines['BILLBOARD_SCREEN'] +// delete this.defines['BILLBOARD'] + +// if (!type) return + +// if (type === 'world') this.defines['BILLBOARD'] = ' ' +// if (type === 'screen') this.defines['BILLBOARD_SCREEN'] = ' ' +// } + +// /** Called by three.js render loop */ +// public onBeforeRender( +// _this: SpeckleWebGLRenderer, +// _scene: Scene, +// camera: Camera, +// _geometry: BufferGeometry, +// _object: Object3D +// ) { +// if ( +// this.defines && +// (this.defines['BILLBOARD'] || this.defines['BILLBOARD_SCREEN']) +// ) { +// matBuff.copy(camera.projectionMatrix).invert() +// this.userData.invProjection.value.copy(matBuff) +// this.needsUpdate = true +// } + +// if (this.defines && this.defines['BILLBOARD_SCREEN']) { +// this.userData.billboardPixelHeight.value = this.billboardPixelHeight +// this.userData.screenSize.value.copy(_this.getDrawingBufferSize(vec2Buff)) +// this.needsUpdate = true +// } + +// if (this.defines && this.defines['USE_RTE']) { +// _object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) +// this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) +// this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) +// this.needsUpdate = true +// } +// } +// } + +// export default SpeckleTextMaterial diff --git a/packages/viewer/src/modules/materials/shaders/speckle-basic-vert.ts b/packages/viewer/src/modules/materials/shaders/speckle-basic-vert.ts index 5aeeb7e4b..82d93b2bd 100644 --- a/packages/viewer/src/modules/materials/shaders/speckle-basic-vert.ts +++ b/packages/viewer/src/modules/materials/shaders/speckle-basic-vert.ts @@ -131,13 +131,12 @@ export const speckleBasicVert = /* glsl */ ` #endif -#if defined(BILLBOARD) || defined(BILLBOARD_FIXED) - uniform vec3 billboardPos; +#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN) uniform mat4 invProjection; #endif -#ifdef BILLBOARD_FIXED - uniform vec2 billboardSize; - uniform vec2 billboardOffset; + +#ifdef BILLBOARD_SCREEN + uniform vec4 billboardPixelOffsetSize; #endif void main() { @@ -192,12 +191,12 @@ void main() { #if defined(BILLBOARD) float div = 1.; - gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0) + vec4(position.x, position.y, 0., 0.0)); - #elif defined(BILLBOARD_FIXED) - gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0)); + gl_Position = projectionMatrix * (viewMatrix * vec4(modelMatrix[3].xyz, 1.0) + vec4(position.x, position.y, 0., 0.0)); + #elif defined(BILLBOARD_SCREEN) + gl_Position = projectionMatrix * (viewMatrix * vec4(modelMatrix[3].xyz, 1.0)); float div = gl_Position.w; gl_Position /= gl_Position.w; - gl_Position.xy += (position.xy + billboardOffset) * billboardSize; + gl_Position.xy += position.xy * billboardPixelOffsetSize.zw * 2. + billboardPixelOffsetSize.xy * 2.; #else gl_Position = projectionMatrix * mvPosition; #endif @@ -206,7 +205,7 @@ void main() { #include // #include COMMENTED CHUNK #if NUM_CLIPPING_PLANES > 0 - #if defined(BILLBOARD) || defined(BILLBOARD_FIXED) + #if defined(BILLBOARD) || defined(BILLBOARD_SCREEN) vec4 movelViewProjection = gl_Position * div; vClipPosition = - (invProjection * movelViewProjection).xyz; #else diff --git a/packages/viewer/src/modules/materials/shaders/speckle-text-frag.ts b/packages/viewer/src/modules/materials/shaders/speckle-text-frag.ts index 61e3131da..2483e7038 100644 --- a/packages/viewer/src/modules/materials/shaders/speckle-text-frag.ts +++ b/packages/viewer/src/modules/materials/shaders/speckle-text-frag.ts @@ -21,9 +21,20 @@ uniform float opacity; #include #include #include + +#ifdef BATCHED_TEXT + uniform sampler2D gradientRamp; + varying float vGradientIndex; +#endif + void main() { #include - vec4 diffuseColor = vec4( diffuse, opacity ); + vec4 diffuseColor_RGB = vec4(diffuse, opacity); + vec4 diffuseColor = diffuseColor_RGB; + #ifdef BATCHED_TEXT + vec4 diffuseColor_Tex = vec4( texture2D(gradientRamp, vec2(vGradientIndex, 0.)).rgb, opacity ); + diffuseColor = mix(diffuseColor_RGB, diffuseColor_Tex, float(vGradientIndex > 0.)); + #endif #include #include #include diff --git a/packages/viewer/src/modules/materials/shaders/speckle-text-vert.ts b/packages/viewer/src/modules/materials/shaders/speckle-text-vert.ts index 00770e501..90fdcb2e4 100644 --- a/packages/viewer/src/modules/materials/shaders/speckle-text-vert.ts +++ b/packages/viewer/src/modules/materials/shaders/speckle-text-vert.ts @@ -1,28 +1,20 @@ export const speckleTextVert = /* glsl */ ` #include #ifdef USE_RTE - // The high component is stored as the default 'position' attribute buffer - attribute vec3 position_low; uniform vec3 uViewer_high; uniform vec3 uViewer_low; #endif -#ifdef TRANSFORM_STORAGE - attribute float objIndex; - - #if TRANSFORM_STORAGE == 0 - #if __VERSION__ == 300 - #define TRANSFORM_STRIDE 4 - #else - #define TRANSFORM_STRIDE 4. - #endif - uniform sampler2D tTransforms; - uniform float objCount; - #elif TRANSFORM_STORAGE == 1 - uniform mat4 uTransforms[OBJ_COUNT]; - #endif +#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN) + uniform mat4 invProjection; #endif +#ifdef BILLBOARD_SCREEN + uniform vec4 billboardPixelOffsetSize; +#endif + + + #include #include #include @@ -56,87 +48,9 @@ export const speckleTextVert = /* glsl */ ` } #endif -#ifdef TRANSFORM_STORAGE - void objectTransform(out vec4 quaternion, out vec4 pivotLow, out vec4 pivotHigh, out vec4 translation, out vec4 scale){ - #if TRANSFORM_STORAGE == 0 - #if __VERSION__ == 300 - ivec2 uv = ivec2(int(objIndex) * TRANSFORM_STRIDE, 0); - vec4 v0 = texelFetch( tTransforms, uv, 0 ); - vec4 v1 = texelFetch( tTransforms, uv + ivec2(1, 0), 0); - vec4 v2 = texelFetch( tTransforms, uv + ivec2(2, 0), 0); - vec4 v3 = texelFetch( tTransforms, uv + ivec2(3, 0), 0); - quaternion = v0; - pivotLow = vec4(v1.xyz, 1.); - pivotHigh = vec4(v2.xyz, 1.); - translation = vec4(v3.xyz, 1.); - scale = vec4(v1.w, v2.w, v3.w, 1.); - #else - float size = objCount * TRANSFORM_STRIDE; - vec2 cUv = vec2(0.5/size, 0.5); - vec2 dUv = vec2(1./size, 0.); - - vec2 uv = vec2((objIndex * TRANSFORM_STRIDE)/size + cUv.x, cUv.y); - vec4 v0 = texture2D( tTransforms, uv); - vec4 v1 = texture2D( tTransforms, uv + dUv); - vec4 v2 = texture2D( tTransforms, uv + 2. * dUv); - vec4 v3 = texture2D( tTransforms, uv + 3. * dUv); - quaternion = v0; - pivotLow = vec4(v1.xyz, 1.); - pivotHigh = vec4(v2.xyz, 1.); - translation = vec4(v3.xyz, 1.); - scale = vec4(v1.w, v2.w, v3.w, 1.); - #endif - #elif TRANSFORM_STORAGE == 1 - mat4 tMatrix = uTransforms[int(objIndex)]; - quaternion = tMatrix[0]; - pivotLow = vec4(tMatrix[1].xyz, 1.); - pivotHigh = vec4(tMatrix[2].xyz, 1.); - translation = vec4(tMatrix[3].xyz, 1.); - scale = vec4(tMatrix[1][3], tMatrix[2][3], tMatrix[3][3], 1.); - #endif - } - vec3 rotate_vertex_position(vec3 position, vec4 quat) - { - return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position); - } - - /** Another workaround for Apple's stupid compiler */ - vec4 safeMul(vec4 a, vec4 b) { - // Prevents constant folding and optimization - return (a + vec4(0.0)) * (b + vec4(1.0)) - a * vec4(1.0); - } - - highp vec3 rotate_scaled_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 scale, highp vec4 quat) - { - /** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */ - /** The code below will not produce correct results in intel IrisXE integrated GPUs. - * The geometry will turn mangled, albeit stable - * I can't know for sure what is going on, but rotating the difference seems to - * force the result into a lower precision? - */ - // highp vec4 position = v0 - v1; - // return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz); - - /** Subtracting the rotated vectors works. */ - return rotate_vertex_position(safeMul(v0, scale).xyz, quat) - rotate_vertex_position(safeMul(v1, scale).xyz, quat) ; - - /** An alternate workaround is - * highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. + 1e-7)); - return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position); - - However I'm not such a fan of the (1. + 1e-7) part - */ - } - -#endif - -#if defined(BILLBOARD) || defined(BILLBOARD_FIXED) - uniform vec3 billboardPos; - uniform mat4 invProjection; -#endif -#ifdef BILLBOARD_FIXED - uniform vec2 billboardSize; +#ifdef BATCHED_TEXT + varying float vGradientIndex; #endif void main() { @@ -154,62 +68,64 @@ void main() { #include #include #include - // #include COMMENTED CHUNK - #ifdef TRANSFORM_STORAGE - vec4 tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale; - objectTransform(tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale); - #endif - #ifdef USE_RTE - vec4 position_lowT = vec4(position_low, 1.); - vec4 position_highT = vec4(position, 1.); - const vec3 ZERO3 = vec3(0., 0., 0.); - highp vec4 rteLocalPosition = computeRelativePosition(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high); - #ifdef TRANSFORM_STORAGE - highp vec4 rtePivot = computeRelativePosition(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high); - rteLocalPosition.xyz = rotate_scaled_vertex_position_delta(rteLocalPosition, rtePivot, tScale, tQuaternion) + rtePivot.xyz + tTranslation.xyz; - #endif - #ifdef USE_INSTANCING - vec4 instancePivot = computeRelativePosition(ZERO3, ZERO3, uViewer_low, uViewer_high); - rteLocalPosition.xyz = (mat3(instanceMatrix) * (rteLocalPosition - instancePivot).xyz) + instancePivot.xyz + instanceMatrix[3].xyz; - #endif + vec4 mvPosition; + mat4 matrix; + + #ifdef BATCHED_TEXT + matrix = mat4( + troikaBatchTexel(0.0), + troikaBatchTexel(1.0), + troikaBatchTexel(2.0), + troikaBatchTexel(3.0) + ); + #else + matrix = modelMatrix; #endif #ifdef USE_RTE - vec4 mvPosition = rteLocalPosition; - #else - vec4 mvPosition = vec4( transformed, 1.0 ); - #ifdef TRANSFORM_STORAGE - mvPosition.xyz = rotate_scaled_vertex_position_delta(mvPosition, tPivotHigh, tScale, tQuaternion) + tPivotHigh.xyz + tTranslation.xyz; - #endif - #ifdef USE_INSTANCING - mvPosition = instanceMatrix * mvPosition; - #endif - #endif - - mvPosition = modelViewMatrix * mvPosition; - - #if defined(BILLBOARD) - float div = 1.; - gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0) + vec4(position.x, position.y, 0., 0.0)); - #elif defined(BILLBOARD_FIXED) - gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0)); - float div = gl_Position.w; - gl_Position /= gl_Position.w; - gl_Position.xy += position.xy * billboardSize; - #else - gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); - #endif - #include - // #include COMMENTED CHUNK - #if NUM_CLIPPING_PLANES > 0 - #if defined(BILLBOARD) || defined(BILLBOARD_FIXED) - vec4 movelViewProjection = gl_Position * div; - vClipPosition = - (invProjection * movelViewProjection).xyz; + /* We store the high part normally as the translation component */ + vec3 translationHigh = matrix[3].xyz; + /** We store the low part of the translation in row4 ofthe matrix */ + vec3 translationLow = vec3(matrix[0][3], matrix[1][3], matrix[2][3]); + highp vec4 rteTranslation = computeRelativePosition(translationLow, translationHigh, uViewer_low, uViewer_high); + #if defined(BILLBOARD) + mvPosition = (modelViewMatrix * rteTranslation + vec4(position.x, position.y, 0., 0.0)); #else - vClipPosition = - mvPosition.xyz; + mvPosition = vec4(mat3(matrix) * transformed + rteTranslation.xyz, 1.); + mvPosition = modelViewMatrix * mvPosition; + #endif + #else + #if defined(BILLBOARD) || defined(BILLBOARD_SCREEN) + vec3 billboardPosition = matrix[3].xyz; + #if defined(BILLBOARD_SCREEN) + mvPosition = projectionMatrix * (viewMatrix * vec4(billboardPosition, 1.0)); + float div = mvPosition.w; + mvPosition /= mvPosition.w; + // Pixel values are computed like so + // windowX = ((ndc.x + 1) / 2) * width; + // windowY = ((ndc.y + 1) / 2) * height; + // That's why we multiply by 2. + mvPosition.xy += position.xy * billboardPixelOffsetSize.zw * 2. + billboardPixelOffsetSize.xy * 2.; + /** Back to view space for convenience */ + mvPosition *= div; + mvPosition = invProjection * mvPosition; + #else + mvPosition = (viewMatrix * vec4(billboardPosition, 1.) + vec4(position.x, position.y, 0., 0.0)); + #endif + #else + mvPosition = viewMatrix * matrix * vec4(transformed, 1.); #endif #endif + + #ifdef BATCHED_TEXT + vGradientIndex = troikaBatchTexel(6.).w; + #endif + + gl_Position = projectionMatrix * mvPosition; + + #include + #include #include #include #include diff --git a/packages/viewer/src/modules/objects/AccelerationStructure.ts b/packages/viewer/src/modules/objects/AccelerationStructure.ts index 627875b02..f730a4dba 100644 --- a/packages/viewer/src/modules/objects/AccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/AccelerationStructure.ts @@ -68,10 +68,10 @@ to get the correct values for Vectors, Rays, Boxes, etc export class AccelerationStructure { private static readonly MatBuff: Matrix4 = new Matrix4() private _bvh: MeshBVH - public inputTransform!: Matrix4 - public outputTransform!: Matrix4 - public inputOriginTransform!: Matrix4 - public outputOriginTransfom!: Matrix4 + public inputTransform: Matrix4 + public outputTransform: Matrix4 + public inputOriginTransform: Matrix4 + public outputOriginTransfom: Matrix4 public get geometry() { return this._bvh.geometry diff --git a/packages/viewer/src/modules/objects/SpeckleBatchedText.ts b/packages/viewer/src/modules/objects/SpeckleBatchedText.ts new file mode 100644 index 000000000..0580468bc --- /dev/null +++ b/packages/viewer/src/modules/objects/SpeckleBatchedText.ts @@ -0,0 +1,605 @@ +import { Text } from 'troika-three-text' +import { BatchedText } from 'troika-three-text' +import { TopLevelAccelerationStructure } from './TopLevelAccelerationStructure.js' +import { + Box3, + BufferGeometry, + Camera, + Color, + DataTexture, + Float32BufferAttribute, + FloatType, + Int16BufferAttribute, + Intersection, + Material, + Matrix4, + Mesh, + MeshBasicMaterial, + Object3D, + Ray, + Raycaster, + RGBAFormat, + Scene, + Sphere, + Texture, + Vector3 +} from 'three' +import { BatchObject } from '../batching/BatchObject.js' +import { ExtendedMeshIntersection, SpeckleRaycaster } from './SpeckleRaycaster.js' +import { DrawGroup } from '../batching/Batch.js' +import Logger from '../utils/Logger.js' +import Materials from '../materials/Materials.js' +import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js' +import { Geometry } from '../converter/Geometry.js' +import { TextBatchObject } from '../batching/TextBatchObject.js' +import { ObjectLayers, SpeckleWebGLRenderer } from '../../index.js' + +const ray = /* @__PURE__ */ new Ray() +const tmpInverseMatrix = /* @__PURE__ */ new Matrix4() +const vecBuff0 = /* @__PURE__ */ new Vector3() +const vecBuff1 = /* @__PURE__ */ new Vector3() +const vecBuff2 = /* @__PURE__ */ new Vector3() +const matBuff0 = /* @__PURE__ */ new Matrix4() +const matBuff1 = /* @__PURE__ */ new Matrix4() +const matBuff2 = /* @__PURE__ */ new Matrix4() + +export class SpeckleBatchedText extends BatchedText { + declare material: SpeckleTextMaterial + + private tas: TopLevelAccelerationStructure + private _batchMaterial: SpeckleTextMaterial + private _batchObjects: BatchObject[] + private _textObjects: { [id: string]: Text } = {} + private _dirty: boolean = false + + public groups: Array = [] + public materials: Material[] = [] + + private materialCache: { [id: string]: Material } = {} + private materialCacheLUT: { [id: string]: number } = {} + + private readonly DEBUG_BILLBOARDS = false + private debugMeshes: Mesh[] = [] + + public get TAS(): TopLevelAccelerationStructure { + return this.tas + } + + public get batchObjects(): BatchObject[] { + return this._batchObjects + } + + public get batchMaterial(): Material { + return this._batchMaterial + } + + public set dirty(value: boolean) { + this._dirty = value + } + + public get isBillboarded() { + return ( + this._batchMaterial && + this._batchMaterial.defines && + this._batchMaterial.defines['BILLBOARD'] + ) + } + + public setBatchMaterial(material: Material) { + if (!(material instanceof SpeckleTextMaterial)) { + Logger.error( + `SpeckleBatchedText requires a SpeckleTextMaterial. Found ${material.constructor.name}` + ) + return + } + this._batchMaterial = this.getCachedMaterial(material) as SpeckleTextMaterial + this.material = this._batchMaterial + this.materials.push(this._batchMaterial) + } + + public setBatchObjects(batchObjects: BatchObject[], textObjects: Text[]) { + this._batchObjects = batchObjects + for (let k = 0; k < batchObjects.length; k++) { + const id = batchObjects[k].renderView.renderData.id + this._textObjects[id] = textObjects[k] + } + } + + private lookupMaterial(material: Material) { + return ( + this.materialCache[material.id] || + this.materialCache[this.materialCacheLUT[material.id]] + ) + } + + public getCachedMaterial(material: Material, copy = false): Material { + let cachedMaterial = this.lookupMaterial(material) + if (!cachedMaterial) { + const clone = new SpeckleTextMaterial({}) + .copy(material) + .getDerivedBatchedMaterial() + this.materialCache[material.id] = clone + this.materialCacheLUT[clone.id] = material.id + cachedMaterial = clone + } else if ( + copy || + (material as never)['needsCopy'] || + (cachedMaterial as never)['needsCopy'] + ) { + Materials.fastCopy(material, cachedMaterial) + } + return cachedMaterial + } + + public buildTAS() { + this.tas = new TopLevelAccelerationStructure(this.batchObjects) + /** We do a refit here, because for some reason the bvh library incorrectly computes the total bvh bounds at creation, + * so we force a refit in order to get the proper bounds value out of it + */ + this.tas.refit() + + /** Copy computed bounds over so that three.js doesn't freak out */ + this.geometry.boundingBox = this.TAS.getBoundingBox(new Box3()) + this.geometry.boundingSphere = this.geometry.boundingBox.getBoundingSphere( + new Sphere() + ) + } + + /** This could be made faster. BUT, as this point in time it's not worth the effort */ + public updateTransformsUniform() { + let needsUpdate = false + for (let k = 0; k < this._batchObjects.length; k++) { + const batchObject = this._batchObjects[k] + if (!(needsUpdate ||= batchObject.transformDirty)) continue + const textObject = this._textObjects[batchObject.renderView.renderData.id] + batchObject.transform.decompose( + textObject.position, + textObject.quaternion, + textObject.scale + ) + textObject.updateMatrix() + // Matrix + const matrix = textObject.matrix.elements + const texture = + this._dataTextures[ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + textObject.material.isTextOutlineMaterial ? 'outline' : 'main' + ] + const packingInfo = this._members.get(textObject) + if (packingInfo) { + const startIndex = packingInfo.index * 32 + for (let i = 0; i < 16; i++) { + this.setTexData(texture, startIndex + i, matrix[i]) + } + batchObject.transformDirty = false + } + } + if (this.tas && needsUpdate) { + this.tas.refit() + this.tas.getBoundingBox(this.tas.bounds) + } + } + + public updateMaterialTransformsUniform(material: Material) { + material + } + + public setGradientTexture(texture: Texture) { + this._batchMaterial.setGradientTexture(texture) + } + + public getBatchObjectMaterial(batchObject: BatchObject) { + const rv = batchObject.renderView + const group = this.groups.find((value) => { + return ( + rv.batchStart >= value.start && + rv.batchStart + rv.batchCount <= value.count + value.start + ) + }) + if (!group) { + Logger.warn(`Could not get material for ${batchObject.renderView.renderData.id}`) + return null + } + return this.materials[group.materialIndex] + } + + // converts the given BVH raycast intersection to align with the three.js raycast + // structure (include object, world space distance and point). + private convertRaycastIntersect( + hit: Intersection | null, + object: Object3D, + raycaster: Raycaster + ) { + if (hit === null) { + return null + } + + hit.point.applyMatrix4(object.matrixWorld) + hit.distance = hit.point.distanceTo(raycaster.ray.origin) + hit.object = object + + if (hit.distance < raycaster.near || hit.distance > raycaster.far) { + return null + } else { + return hit + } + } + + private initDebugBox() { + const debugBox = new Mesh( + new BufferGeometry(), + new MeshBasicMaterial({ wireframe: true, color: 0xff0000 }) + ) + debugBox.geometry.setAttribute( + 'position', + new Float32BufferAttribute(new Array(12), 3) + ) + debugBox.geometry.setIndex( + // prettier-ignore + new Int16BufferAttribute( + [ + 0, 1, 2, // First triangle: bottom-left → bottom-right → top-right + 0, 2, 3 // Second triangle: bottom-left → top-right → top-left + ], + 1 + ) + ) + debugBox.layers.set(ObjectLayers.OVERLAY) + this.parent?.add(debugBox) + return debugBox + } + + /** Debug purposes only */ + onBeforeRender( + renderer: SpeckleWebGLRenderer, + scene: Scene, + camera: Camera, + geometry: BufferGeometry, + material: Material, + group: unknown + ) { + super.onBeforeRender(renderer, scene, camera, geometry, material, group) + if (this.DEBUG_BILLBOARDS && this.isBillboarded) { + const vertices = [new Vector3(), new Vector3(), new Vector3(), new Vector3()] + for (let k = 0; k < this._batchObjects.length; k++) { + if (!this.debugMeshes[k]) { + this.debugMeshes[k] = this.initDebugBox() + } + const textMatrix = (this._batchObjects[k] as TextBatchObject).textTransform + + const billboardPos = vecBuff0.set( + textMatrix.elements[12], + textMatrix.elements[13], + textMatrix.elements[14] + ) + + const box = new Box3().copy(this._batchObjects[k].aabb) + const min = vecBuff1.copy(box.min) + const max = vecBuff2.copy(box.max) + vertices[0].set(min.x, min.y, 0) + vertices[1].set(max.x, min.y, 0) + vertices[2].set(max.x, max.y, 0) + vertices[3].set(min.x, max.y, 0) + + const billboardMat = matBuff0.makeTranslation( + billboardPos.x, + billboardPos.y, + billboardPos.z + ) + + billboardMat.multiply(matBuff1.extractRotation(camera.matrixWorld)) + // TO DO: This is out of place. Probably happening because acceleration structure has the text transform as input transform + billboardMat.multiply(matBuff2.copy(textMatrix).invert()) + + for (let i = 0; i < vertices.length; i++) { + const debugVertex = vecBuff2.copy(vertices[i]) + debugVertex.applyMatrix4(billboardMat) + + this.debugMeshes[k].geometry.attributes.position.setXYZ( + i, + debugVertex.x, + debugVertex.y, + debugVertex.z + ) + } + this.debugMeshes[k].geometry.attributes.position.needsUpdate = true + } + } + } + + raycast(raycaster: SpeckleRaycaster, intersects: Array) { + /** We bypass the TAS for billboarded text batches, otherwise we would need to refit it each frame */ + if (this.isBillboarded) { + const rayBuff = new Ray() + for (let k = 0; k < this._batchObjects.length; k++) { + const textMatrix = (this._batchObjects[k] as TextBatchObject).textTransform + /** The billboard position is the text object's position stored in it's world matrix */ + const billboardPos = vecBuff0.set( + textMatrix.elements[12], + textMatrix.elements[13], + textMatrix.elements[14] + ) + /** We compute the matrix that billboards the text */ + const billboardMat = matBuff0.makeTranslation( + billboardPos.x, + billboardPos.y, + billboardPos.z + ) + billboardMat.multiply(matBuff1.extractRotation(raycaster.camera.matrixWorld)) + billboardMat.multiply(matBuff2.copy(textMatrix).invert()) + /** We invert it in order to apply to the ray instead of the geometry */ + const invBillboardMat = matBuff0.copy(billboardMat).invert() + rayBuff.copy(raycaster.ray) + rayBuff.applyMatrix4(invBillboardMat) + + /** Regular intersecting from here on out on a per batch object level */ + if (raycaster.firstHitOnly === true) { + const hit = this.convertRaycastIntersect( + this._batchObjects[k].accelerationStructure.raycastFirst( + rayBuff, + this._batchMaterial + ), + this as unknown as Object3D, + raycaster + ) as ExtendedMeshIntersection + if (hit) { + hit.batchObject = this._batchObjects[k] + intersects.push(hit) + break // We break here as we only want the first hit + } + } else { + const hits = this._batchObjects[k].accelerationStructure.raycast( + rayBuff, + this._batchMaterial + ) + for (let i = 0, l = hits.length; i < l; i++) { + const hit = this.convertRaycastIntersect( + hits[i], + this as unknown as Object3D, + raycaster + ) as ExtendedMeshIntersection + if (hit) { + hit.batchObject = this._batchObjects[k] + intersects.push(hit) + } + } + } + } + } else { + if (this.tas) { + if (this._batchMaterial === undefined) return + + tmpInverseMatrix.copy(this.matrixWorld).invert() + ray.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix) + /** Texts are all quads. Intersecting their BAS is redundant */ + const tasOnly = raycaster.intersectTASOnly || true + + if (raycaster.firstHitOnly === true) { + const hit = this.convertRaycastIntersect( + this.tas.raycastFirst(ray, tasOnly, this._batchMaterial), + this as unknown as Object3D, + raycaster + ) + if (hit) { + intersects.push(hit) + } + } else { + const hits = this.tas.raycast(ray, tasOnly, this._batchMaterial) + for (let i = 0, l = hits.length; i < l; i++) { + const hit = this.convertRaycastIntersect( + hits[i], + this as unknown as Object3D, + raycaster + ) + if (hit) { + intersects.push(hit) + } + } + } + } else { + super.raycast(raycaster, intersects) + } + } + } + + /** + * Update the batched geometry bounds to hold all members + */ + updateBounds() { + if (!this._dirty) return + // Update member local matrices and the overall bounds + const tempBox3 = new Box3() + const bbox = (this.geometry.boundingBox ?? new Box3()).makeEmpty() + this._members.forEach((_, text) => { + if (text.matrixAutoUpdate) text.updateMatrix() // ignore world matrix + tempBox3.copy(text.geometry.boundingBox ?? new Box3()).applyMatrix4(text.matrix) + bbox.union(tempBox3) + }) + bbox.getBoundingSphere(this.geometry.boundingSphere ?? new Sphere()) + } + + /** + * @param {Text} text + */ + addText(text: Text) { + if (!this._members.has(text)) { + this._members.set(text, { + index: -1, + glyphCount: -1, + dirty: true, + needsUpdate: true + }) + text.addEventListener('synccomplete', this._onMemberSynced) + } + } + + private setTexData(texture: DataTexture, index: number, value: number) { + const texData = texture.image.data + if (value !== texData[index]) { + texData[index] = value + texture.needsUpdate = true + } + } + + /* + Data texture packing strategy: + + # Common: + 0-15: matrix + 16-19: uTroikaTotalBounds + 20-23: uTroikaClipRect + 24: diffuse (color/outlineColor) + 25: uTroikaFillOpacity (fillOpacity/outlineOpacity) + 26: uTroikaCurveRadius + 27: + + # Main: + 28: uTroikaStrokeWidth + 29: uTroikaStrokeColor + 30: uTroikaStrokeOpacity + + # Outline: + 28-29: uTroikaPositionOffset + 30: uTroikaEdgeOffset + 31: uTroikaBlurRadius + */ + + /** + * @override + * Patched version that allows: + * - Individual text opacities + * - Coordinate inside gradient/ramp texture <27> + */ + _prepareForRender(material: SpeckleTextMaterial) { + if (!this._dirty) return + + this._dirty = false + + const floatsPerMember = 32 + const tempColor = new Color() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const isOutline = material.isTextOutlineMaterial + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + material.uniforms.uTroikaIsOutline.value = isOutline + + // Resize the texture to fit in powers of 2 + let texture = this._dataTextures[isOutline ? 'outline' : 'main'] + const dataLength = Math.pow( + 2, + Math.ceil(Math.log2(this._members.size * floatsPerMember)) + ) + if (!texture || dataLength !== texture.image.data.length) { + // console.log(`resizing: ${dataLength}`); + if (texture) texture.dispose() + const width = Math.min(dataLength / 4, 1024) + texture = this._dataTextures[isOutline ? 'outline' : 'main'] = new DataTexture( + new Float32Array(dataLength), + width, + dataLength / 4 / width, + RGBAFormat, + FloatType + ) + } + + this._members.forEach((packingInfo, text) => { + if (packingInfo.index > -1 && packingInfo.needsUpdate) { + packingInfo.needsUpdate = false + const startIndex = packingInfo.index * floatsPerMember + + // Matrix + const matrix = text.matrix.elements + + for (let i = 0; i < 16; i++) { + this.setTexData(texture, startIndex + i, matrix[i]) + } + if (material.defines && material.defines['USE_RTE'] !== undefined) { + const translation = new Vector3(matrix[12], matrix[13], matrix[14]) + const translationLow = new Vector3() + const translationHigh = new Vector3() + Geometry.DoubleToHighLowVector(translation, translationLow, translationHigh) + this.setTexData(texture, startIndex + 3, translationLow.x) + this.setTexData(texture, startIndex + 7, translationLow.y) + this.setTexData(texture, startIndex + 11, translationLow.z) + } + // Let the member populate the uniforms, since that does all the appropriate + // logic and handling of defaults, and we'll just grab the results from there + text._prepareForRender(material) + const { + uTroikaTotalBounds, + uTroikaClipRect, + uTroikaPositionOffset, + uTroikaEdgeOffset, + uTroikaBlurRadius, + uTroikaStrokeWidth, + uTroikaStrokeColor, + uTroikaStrokeOpacity, + uTroikaFillOpacity, + uTroikaCurveRadius + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + } = material.uniforms + + // Total bounds for uv + for (let i = 0; i < 4; i++) { + this.setTexData( + texture, + startIndex + 16 + i, + uTroikaTotalBounds.value.getComponent(i) + ) + } + + // Clip rect + for (let i = 0; i < 4; i++) { + this.setTexData( + texture, + startIndex + 20 + i, + uTroikaClipRect.value.getComponent(i) + ) + } + + // Color + let color = isOutline ? text.outlineColor || 0 : text.color + if (color === null) color = this.color + if (color === null) color = this.material.color + if (color === null) color = 0xffffff + this.setTexData(texture, startIndex + 24, tempColor.set(color).getHex()) + + // Fill opacity / outline opacity + this.setTexData( + texture, + startIndex + 25, + (text.material as Material).opacity ?? uTroikaFillOpacity.value + ) + + // Curve radius + this.setTexData(texture, startIndex + 26, uTroikaCurveRadius.value) + // Billboard height + this.setTexData(texture, startIndex + 27, text.userData.gradientIndex) + + if (isOutline) { + // Outline properties + this.setTexData(texture, startIndex + 28, uTroikaPositionOffset.value.x) + this.setTexData(texture, startIndex + 29, uTroikaPositionOffset.value.y) + this.setTexData(texture, startIndex + 30, uTroikaEdgeOffset.value) + this.setTexData(texture, startIndex + 31, uTroikaBlurRadius.value) + } else { + // Stroke properties + this.setTexData(texture, startIndex + 28, uTroikaStrokeWidth.value) + this.setTexData( + texture, + startIndex + 29, + tempColor.set(uTroikaStrokeColor.value).getHex() + ) + this.setTexData(texture, startIndex + 30, uTroikaStrokeOpacity.value) + } + } + }) + material.setMatrixTexture(texture) + + // For the non-member-specific uniforms: + Text.prototype._prepareForRender.call(this, material) + } +} diff --git a/packages/viewer/src/modules/objects/SpeckleText.ts b/packages/viewer/src/modules/objects/SpeckleText.ts deleted file mode 100644 index bc197ac9c..000000000 --- a/packages/viewer/src/modules/objects/SpeckleText.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { - BufferAttribute, - BufferGeometry, - Color, - DoubleSide, - Matrix4, - Mesh, - MeshBasicMaterial, - PlaneGeometry, - Quaternion, - Raycaster, - Vector2, - Vector3, - Vector4, - type Intersection -} from 'three' -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -//@ts-ignore -import { Text } from 'troika-three-text' -import SpeckleBasicMaterial from '../materials/SpeckleBasicMaterial.js' -import { ObjectLayers, type SpeckleObject } from '../../IViewer.js' - -export interface SpeckleTextParams { - textValue?: string - richTextValue?: string - height?: number - anchorX?: string - anchorY?: string -} - -export interface SpeckleTextStyle { - backgroundColor?: Color | null - backgroundCornerRadius?: number - backgroundPixelHeight?: number - textColor?: Color - billboard?: boolean -} - -const DefaultSpeckleTextStyle: SpeckleTextStyle = { - backgroundColor: null, - backgroundCornerRadius: 1, - backgroundPixelHeight: 50, - textColor: new Color(0xffffff), - billboard: false -} - -/** TO DO: This is weird because in the billboarded scenario, background size is - * specified in pixels inside the style, yet we still rely on the actual world space - * background rectangle to be a factor larger than the text itself - * This needs to be looked and probably eliminated, but it does not currently break functionality - */ -const BACKGROUND_OVERSIZE = 1.2 - -export class SpeckleText extends Mesh { - private _layer: ObjectLayers = ObjectLayers.NONE - private _text: Text = null - private _background: Mesh | null = null - private _backgroundSize: Vector3 = new Vector3() - private _style: SpeckleTextStyle = Object.assign({}, DefaultSpeckleTextStyle) - private _resolution: Vector2 = new Vector2() - - private defaultMaterial = /*#__PURE__*/ new MeshBasicMaterial({ - color: 0xffffff, - side: DoubleSide, - transparent: true - }) - private getFlatRaycastMesh = () => { - const mesh = new Mesh(new PlaneGeometry(1, 1), this.defaultMaterial) - this.getFlatRaycastMesh = () => mesh - return mesh - } - private getCurvedRaycastMesh = () => { - const mesh = new Mesh(new PlaneGeometry(1, 1, 32, 1), this.defaultMaterial) - this.getCurvedRaycastMesh = () => mesh - return mesh - } - - public static SpeckleTextParamsFromMetadata(metadata: SpeckleObject) { - return { - textValue: metadata.value ? metadata.value : 'N/A', - height: metadata.height - } as SpeckleTextParams - } - - public get textMesh() { - return this._text - } - - public get backgroundMesh() { - return this._background - } - - public set style(value: SpeckleTextStyle) { - Object.assign(this._style, value) - this.updateStyle() - } - - public constructor(uuid: string, layer: ObjectLayers) { - super() - this.uuid = uuid - this._layer = layer - this._text = new Text() - this._text.depthOffset = -0.1 - this._text.raycast = () => { - /** We're erasing the child's raycast so we don't raycast twice - * Not the best approach but until we figure out text batching it will have to suffice - */ - } - this.layers.set(this._layer) - this._text.layers.set(this._layer) - this.add(this._text) - - this.onBeforeRender = (renderer) => { - renderer.getDrawingBufferSize(this._resolution) - } - /** Otherwise three.js is inconsistent in calling our 'onBeforeRender' */ - this.frustumCulled = false - } - - public async update(params: SpeckleTextParams, updateFinished?: () => void) { - return new Promise((resolve) => { - if (params.textValue) { - this._text.text = params.textValue - } - if (params.richTextValue) { - //TO DO - } - if (params.height) { - this._text.fontSize = params.height - } - this._text.anchorX = params.anchorX - this._text.anchorY = params.anchorY - if (this._text._needsSync) { - this._text.sync(() => { - resolve() - if (updateFinished) updateFinished() - }) - } else { - resolve() - if (updateFinished) updateFinished() - } - }) - } - - public setTransform(position?: Vector3, quaternion?: Quaternion, scale?: Vector3) { - if (position) { - if (this._style.billboard) { - this.textMesh.material.userData.billboardPos.value.copy(position) - if (this._background) { - const textSize = this.textMesh.geometry.boundingBox.getSize(new Vector3()) - const textCenter = this.textMesh.geometry.boundingBox.getCenter(new Vector3()) - const offset = new Vector3() - .copy(textCenter) - .multiplyScalar(BACKGROUND_OVERSIZE) - const sizeOffset = new Vector3() - .copy(textSize) - .multiplyScalar(BACKGROUND_OVERSIZE) - .sub(textSize) - offset.x += - textCenter.x < 0 ? sizeOffset.x : textCenter.x > 0 ? -sizeOffset.x : 0 - offset.y += - textCenter.y < 0 ? sizeOffset.y : textCenter.y > 0 ? -sizeOffset.y : 0 - ;(this._background.material as SpeckleBasicMaterial).billboardOffset = - new Vector2(offset.x, offset.y) - ;( - this._background.material as SpeckleBasicMaterial - ).userData.billboardPos.value.copy(position) - } - } - this.position.copy(position) - } - if (quaternion) this.quaternion.copy(quaternion) - if (scale) this.scale.copy(scale) - } - - public raycast(raycaster: Raycaster, intersects: Array) { - const { textRenderInfo, curveRadius } = this.textMesh - if (textRenderInfo) { - const bounds = textRenderInfo.blockBounds - const raycastMesh = curveRadius - ? this.getCurvedRaycastMesh() - : this.getFlatRaycastMesh() - const geom = raycastMesh.geometry - const { position, uv } = geom.attributes - for (let i = 0; i < uv.count; i++) { - let x = bounds[0] + uv.getX(i) * (bounds[2] - bounds[0]) - const y = bounds[1] + uv.getY(i) * (bounds[3] - bounds[1]) - let z = 0 - if (curveRadius) { - z = curveRadius - Math.cos(x / curveRadius) * curveRadius - x = Math.sin(x / curveRadius) * curveRadius - } - if (this.textMesh.material.defines['BILLBOARD_FIXED']) { - if (this._resolution.length() === 0) return - const backgroundSizeIncrease = this._background ? BACKGROUND_OVERSIZE : 1 - const billboardSize = new Vector2().set( - (this.textMesh.material.billboardPixelHeight / this._resolution.x) * - 2 * - backgroundSizeIncrease, - (this.textMesh.material.billboardPixelHeight / this._resolution.y) * - 2 * - backgroundSizeIncrease - ) - - const invProjection = new Matrix4() - .copy(raycaster.camera.projectionMatrix) - .invert() - const invView = new Matrix4() - .copy(raycaster.camera.matrixWorldInverse) - .invert() - - const clip = new Vector4( - this.position.x, - this.position.y, - this.position.z, - 1.0 - ) - .applyMatrix4(raycaster.camera.matrixWorldInverse) - .applyMatrix4(raycaster.camera.projectionMatrix) - const pDiv = clip.w - clip.multiplyScalar(1 / pDiv) - clip.add(new Vector4(x * billboardSize.x, y * billboardSize.y, 0, 0)) - clip.multiplyScalar(pDiv) - clip.applyMatrix4(invProjection) - clip.applyMatrix4(invView) - position.setXYZ(i, clip.x, clip.y, clip.z) - } else { - position.setXYZ(i, x, y, z) - } - } - if (this.textMesh.material.defines['BILLBOARD_FIXED']) { - geom.computeBoundingBox() - geom.computeBoundingSphere() - raycastMesh.matrixWorld.identity() - } else { - geom.boundingSphere = this.textMesh.geometry.boundingSphere - geom.boundingBox = this.textMesh.geometry.boundingBox - raycastMesh.matrixWorld = this.textMesh.matrixWorld - } - raycastMesh.material.side = this.textMesh.material.side - const tempArray: Array = [] - raycastMesh.raycast(raycaster, tempArray) - for (let i = 0; i < tempArray.length; i++) { - tempArray[i].object = this - intersects.push(tempArray[i]) - } - } - } - - private updateStyle() { - this.updateBackground() - } - - private updateBackground() { - if (!this._style.backgroundColor) { - if (this._background) this.remove(this._background) - this._background = null - return - } - - this._text.geometry.computeBoundingBox() - const sizeBox = this._text.geometry.boundingBox.getSize(new Vector3()) - const sizeDelta = sizeBox.distanceTo(this._backgroundSize) - let geometry = this._background?.geometry - if (sizeDelta > 0.1) { - /** BACKGROUND_OVERSIZE should not be required for billboarded backgrounds. Weird */ - geometry = this.RectangleRounded( - sizeBox.x * BACKGROUND_OVERSIZE, - sizeBox.y * BACKGROUND_OVERSIZE, - 0.5, - 5 - ) - geometry.computeBoundingBox() - this._backgroundSize.copy(sizeBox) - if (this._background) this._background.geometry = geometry - } - if (this._background === null) { - const material = new SpeckleBasicMaterial({}, ['BILLBOARD_FIXED']) - material.toneMapped = false - material.side = DoubleSide - material.depthTest = false - - this._background = new Mesh(geometry, material) - this._background.layers.set(this._layer) - this._background.frustumCulled = false - this._background.renderOrder = 1 - this.add(this._background) - } - const color = new Color(this._style.backgroundColor).convertSRGBToLinear() - ;(this._background.material as SpeckleBasicMaterial).color = color - ;(this._background.material as SpeckleBasicMaterial).billboardPixelHeight = - (this._style.backgroundPixelHeight !== undefined - ? this._style.backgroundPixelHeight - : DefaultSpeckleTextStyle.backgroundPixelHeight || 0) * window.devicePixelRatio - } - - /** From https://discourse.threejs.org/t/roundedrectangle-squircle/28645 */ - // width, height, radiusCorner, smoothness - private RectangleRounded(w: number, h: number, r: number, s: number) { - // width, height, radiusCorner, smoothness - - const pi2 = Math.PI * 2 - const n = (s + 1) * 4 // number of segments - const indices = [] - const positions = [] - const uvs = [] - let qu, sgx, sgy, x, y - - for (let j = 1; j < n; j++) indices.push(0, j, j + 1) // 0 is center - indices.push(0, n, 1) - positions.push(0, 0, 0) // rectangle center - uvs.push(0.5, 0.5) - for (let j = 0; j < n; j++) contour(j) - - 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.computeBoundingBox() - return geometry - - function contour(j: number) { - qu = Math.trunc((4 * j) / n) + 1 // quadrant qu: 1..4 - sgx = qu === 1 || qu === 4 ? 1 : -1 // signum left/right - sgy = qu < 3 ? 1 : -1 // signum top / bottom - x = sgx * (w / 2 - r) + r * Math.cos((pi2 * (j - qu + 1)) / (n - 4)) // corner center + circle - y = sgy * (h / 2 - r) + r * Math.sin((pi2 * (j - qu + 1)) / (n - 4)) - - positions.push(x, y, 0) - uvs.push(0.5 + x / w, 0.5 + y / h) - } - } -} diff --git a/packages/viewer/src/modules/objects/TextLabel.ts b/packages/viewer/src/modules/objects/TextLabel.ts new file mode 100644 index 000000000..c6905c316 --- /dev/null +++ b/packages/viewer/src/modules/objects/TextLabel.ts @@ -0,0 +1,530 @@ +import { + Box3, + BufferAttribute, + BufferGeometry, + Color, + DoubleSide, + Float32BufferAttribute, + FrontSide, + Int16BufferAttribute, + Material, + Matrix4, + Mesh, + MeshBasicMaterial, + Raycaster, + Vector2, + Vector3, + Vector4, + type Intersection +} from 'three' +import { AnchorX, AnchorY, Text } from 'troika-three-text' +import SpeckleBasicMaterial, { + BillboardingType +} from '../materials/SpeckleBasicMaterial.js' +import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js' +import { ObjectLayers } from '../../index.js' +import Logger from '../utils/Logger.js' + +const _mat40: Matrix4 = new Matrix4() +const _mat41: Matrix4 = new Matrix4() +const _box3: Box3 = new Box3() +const _vec3: Vector3 = new Vector3() +const _vec4: Vector4 = new Vector4() +const quadVerts = [new Vector3(), new Vector3(), new Vector3(), new Vector3()] + +export interface TextLabelParams { + text?: string + fontSize?: number + maxWidth?: number + anchorX?: AnchorX + anchorY?: AnchorY + billboard?: BillboardingType | null + backgroundColor?: Color | null + backgroundCornerRadius?: number + backgroundMargins?: Vector2 + textColor?: Color + textOpacity?: number + objectLayer?: ObjectLayers +} + +/** Screen */ +export const DefaultTextLabelParams: Required = { + text: 'Test Text', + fontSize: 40, + maxWidth: Number.POSITIVE_INFINITY, + anchorX: 'left', + anchorY: 'middle', + billboard: 'screen', + backgroundColor: new Color(0xff0000), + backgroundCornerRadius: 0.5, + backgroundMargins: new Vector2(50, 10), + textColor: new Color(0x00ffff), + textOpacity: 1, + objectLayer: ObjectLayers.OVERLAY +} + +// /** World Billboard*/ +// export const DefaultTextLabelParams: Required = { +// text: 'Test Text', +// fontSize: 1, +// maxWidth: Number.POSITIVE_INFINITY, +// anchorX: 'left', +// anchorY: 'middle', +// billboard: 'world', +// backgroundColor: new Color(0xff0000), +// backgroundCornerRadius: 0.5, +// backgroundMargins: new Vector2(0.75, 0.1), +// textColor: new Color(0x00ffff), +// textOpacity: 1, +// objectLayer: ObjectLayers.OVERLAY +// } + +// /** World */ +// export const DefaultTextLabelParams: Required = { +// text: 'Test Text', +// fontSize: 1, +// maxWidth: Number.POSITIVE_INFINITY, +// anchorX: 'center', +// anchorY: 'middle', +// billboard: null, +// backgroundColor: new Color(0xff0000), +// backgroundCornerRadius: 0.5, +// backgroundMargins: new Vector2(0.75, 0.1), +// textColor: new Color(0x00ffff), +// textOpacity: 1, +// objectLayer: ObjectLayers.OVERLAY +// } + +export class TextLabel extends Text { + /** Needs a raycast to start rendering */ + private readonly DEBUG_BILLBOARDS = false + + declare material: SpeckleTextMaterial + + private _background: Mesh + private _backgroundMaterial: SpeckleBasicMaterial + private _params: Required = Object.assign({}, DefaultTextLabelParams) + private _textBounds: Box3 = new Box3() + private _collisionMesh: Mesh + public get textMesh() { + return this + } + + public get backgroundMesh() { + return this._background + } + + public get textBounds(): Box3 { + return this._textBounds + } + + public get backgroundMaterial(): SpeckleBasicMaterial { + return this._backgroundMaterial + } + + public constructor(params: TextLabelParams = DefaultTextLabelParams) { + super() + this.depthOffset = -0.1 + + this.material = new SpeckleTextMaterial({}).getDerivedMaterial() + this.material.toneMapped = false + + this._backgroundMaterial = new SpeckleBasicMaterial({}) + this._backgroundMaterial.toneMapped = false + + this._background = new Mesh(undefined, this._backgroundMaterial) + /** Otherwise three.js looses it's shit when rendering it billboarded */ + this._background.frustumCulled = false + /** No raycasting for the background */ + this._background.raycast = () => {} + + const geometry = new BufferGeometry() + geometry.setAttribute( + 'position', + new Float32BufferAttribute(new Array(12).fill(0), 3) + ) + geometry.setIndex( + // prettier-ignore + new Int16BufferAttribute( + [ + 0, 1, 2, // First triangle: bottom-left → bottom-right → top-right + 0, 2, 3 // Second triangle: bottom-left → top-right → top-left + ], + 1 + ) + ) + this._collisionMesh = new Mesh( + geometry, + new MeshBasicMaterial({ color: 0x00ff00, wireframe: true }) + ) + this._collisionMesh.name = 'TextLabel_Collision_Mesh' + this._collisionMesh.renderOrder = 1 + this._collisionMesh.visible = this.DEBUG_BILLBOARDS + this.add(this._collisionMesh) + + this.updateParams(params).then().catch + } + + public async updateParams(params: TextLabelParams, onUpdateComplete?: () => void) { + return new Promise((resolve) => { + if (this.material && !(this.material instanceof SpeckleTextMaterial)) { + const mat: Material = this.material + Logger.error( + `TextLabel requires a SpeckleTextMaterial instance. Found ${mat.constructor.name}` + ) + } + + /** Automatically scale with DPR */ + const transformedParams = Object.assign({}, params) + if (params.billboard === 'screen') { + if (transformedParams.backgroundMargins) + transformedParams.backgroundMargins.multiplyScalar(window.devicePixelRatio) + if (transformedParams.fontSize) { + transformedParams.fontSize *= window.devicePixelRatio + } + this.material.side = FrontSide + this._backgroundMaterial.side = FrontSide + } else { + this.material.side = DoubleSide + this._backgroundMaterial.side = DoubleSide + } + + if (transformedParams.text) this.text = transformedParams.text + if (transformedParams.fontSize) this.fontSize = transformedParams.fontSize + if (transformedParams.anchorX) this.anchorX = transformedParams.anchorX + if (transformedParams.anchorY) this.anchorY = transformedParams.anchorY + if (transformedParams.maxWidth) this.maxWidth = transformedParams.maxWidth + + if (transformedParams.textColor !== undefined) { + this.material.color.copy(transformedParams.textColor) + this.material.color.convertSRGBToLinear() + } + if (transformedParams.textOpacity !== undefined) + this.material.opacity = transformedParams.textOpacity + + if (transformedParams.objectLayer !== undefined) { + this.layers.set(transformedParams.objectLayer) + this._collisionMesh.layers.set(transformedParams.objectLayer) + this._background.layers.set(transformedParams.objectLayer) + } + + this.material.needsUpdate = true + Object.assign(this._params, transformedParams) + + if (this._needsSync) { + this.sync(() => { + this.textBoundsToBox(this._textBounds) + this.updateBackground() + this.updateBillboarding() + + if (onUpdateComplete) onUpdateComplete() + resolve() + }) + } else { + if (onUpdateComplete) onUpdateComplete() + resolve() + } + }) + } + + public raycast(raycaster: Raycaster, intersects: Array) { + /** No billboarding, default raycasting works fine */ + if (!this._params.billboard) { + super.raycast(raycaster, intersects) + return + } + /** If we have a billboard, we need to update the collision mesh */ + const textMatrix = this.matrixWorld + const textMatrixInv = _mat40.copy(textMatrix).invert() + + const billboardPos = new Vector3().set( + textMatrix.elements[12], + textMatrix.elements[13], + textMatrix.elements[14] + ) + + /** World space billboarding */ + if (this._params.billboard === 'world') { + const box = new Box3().copy( + this._params.backgroundColor !== null + ? (this._background.geometry.boundingBox as Box3) + : this._textBounds + ) + const min = new Vector3().copy(box.min) + const max = new Vector3().copy(box.max) + quadVerts[0].set(min.x, min.y, 0) + quadVerts[1].set(max.x, min.y, 0) + quadVerts[2].set(max.x, max.y, 0) + quadVerts[3].set(min.x, max.y, 0) + + const cameraRotationMatrix = _mat41.extractRotation(raycaster.camera.matrixWorld) + + const billboardMat = new Matrix4().makeTranslation( + billboardPos.x, + billboardPos.y, + billboardPos.z + ) + billboardMat.premultiply(textMatrixInv) + billboardMat.multiply(cameraRotationMatrix) + + for (let i = 0; i < quadVerts.length; i++) { + quadVerts[i].applyMatrix4(billboardMat) + + this._collisionMesh.geometry.attributes.position.setXYZ( + i, + quadVerts[i].x, + quadVerts[i].y, + quadVerts[i].z + ) + } + } + + /** Screen space billboarding */ + if (this._params.billboard === 'screen') { + const box = new Box3().copy(this._textBounds) + if (box.getSize(new Vector3()).length() === 0) return + if (box.isInfiniteBox()) return + + const min = new Vector3().copy(box.min) + const max = new Vector3().copy(box.max) + quadVerts[0].set(min.x, min.y, 0) + quadVerts[1].set(max.x, min.y, 0) + quadVerts[2].set(max.x, max.y, 0) + quadVerts[3].set(min.x, max.y, 0) + + const billboardSize = + this._params.backgroundColor !== null + ? this._backgroundMaterial.userData.billboardPixelOffsetSize.value + : this._backgroundMaterial.userData.billboardPixelOffsetSize.value + const invProjection = raycaster.camera.projectionMatrixInverse + const invView = raycaster.camera.matrixWorld + + const clip = new Vector4(billboardPos.x, billboardPos.y, billboardPos.z, 1.0) + .applyMatrix4(raycaster.camera.matrixWorldInverse) + .applyMatrix4(raycaster.camera.projectionMatrix) + const pDiv = clip.w + clip.multiplyScalar(1 / pDiv) + + for (let i = 0; i < quadVerts.length; i++) { + _vec3.copy(quadVerts[i]) + _vec3.multiply(new Vector3(billboardSize.z * 2, billboardSize.w * 2, 0)) + _vec3.add(new Vector3(billboardSize.x * 2, billboardSize.y * 2, 0)) + _vec4.set(clip.x, clip.y, clip.z, 1) + _vec4.add(new Vector4(_vec3.x, _vec3.y, 0, 0)) + _vec4.multiplyScalar(pDiv) + _vec4.applyMatrix4(invProjection) + _vec4.applyMatrix4(invView) + _vec4.applyMatrix4(textMatrixInv) + + this._collisionMesh.geometry.attributes.position.setXYZ( + i, + _vec4.x, + _vec4.y, + _vec4.z + ) + } + } + + this._collisionMesh.geometry.attributes.position.needsUpdate = true + this._collisionMesh.geometry.computeBoundingBox() + this._collisionMesh.geometry.computeBoundingSphere() + + /** No need to manually call. _collisionMesh is a child and will get automatically raycasted */ + this._collisionMesh.raycast(raycaster, intersects) + // super.raycast(raycaster, intersects) + } + + /** Gets the current bounds reported by troika taking `fontSize` into account */ + private textBoundsToBox(target: Box3 = new Box3()): Box3 { + const { textRenderInfo } = this + /** visibleBounds generally is a better fit, *however* it reports faulty on some glyphs and messes up the text size */ + const bounds = textRenderInfo.visibleBounds + + const vertices = [] + vertices.push( + bounds[0], + bounds[3], + 0, + bounds[2], + bounds[3], + 0, + bounds[0], + bounds[1], + 0, + bounds[2], + bounds[1], + 0 + ) + target.setFromArray(vertices) + + return target + } + + /** Text's blockBounds, the one we're working with bounds-wise is not a unit quad + When using BILLBOARD_SCREEN we store the desired pixel size in the text's `fontSize` property + This makes troika compute a large text since it thinks our pixels are world units. + So we divide the text bounds by the font size to get the size of the unit text bounds, or + another way of putting it, to compute the text bounds value as if fontSize = 1 + From the unit box, we get it's size and compute a world->pixel ratio which we send to the shader + */ + private updateBillboarding() { + this.material.setBillboarding(this._params.billboard) + this._backgroundMaterial.setBillboarding(this._params.billboard) + + if (this._params.billboard === 'screen') { + /** Get the current bounds */ + const bounds = _box3.copy(this._textBounds) + /** The fontSize is the pixel value so we normalize */ + bounds.min.divideScalar(this.fontSize) + bounds.max.divideScalar(this.fontSize) + /** This is the size of the quad for the particular text value */ + let unitSize = bounds.getSize(_vec3) + /** We need to keep aspect ratio for text */ + this.material.billboardPixelSize = new Vector2(1 / unitSize.y, 1 / unitSize.y) + /** Same thing for background */ + if (!this._background.geometry.boundingBox) + this._background.geometry.computeBoundingBox() + const bgBounds = new Box3().copy(this._background.geometry.boundingBox as Box3) + bgBounds.min.divideScalar(this.fontSize) + bgBounds.max.divideScalar(this.fontSize) + unitSize = bgBounds.getSize(_vec3) + + const margins = new Vector2( + this._params.backgroundMargins?.x ?? 0, + this._params.backgroundMargins?.y ?? 0 + ) + this._backgroundMaterial.billboardPixelSize = new Vector2( + 1 / unitSize.y + (margins.x * (1 / unitSize.x)) / this.fontSize, + 1 / unitSize.y + (margins.y * (1 / unitSize.y)) / this.fontSize + ) + + const billboardPixelOffset = new Vector2(0, 0) + switch (this.anchorX) { + case 'left': + billboardPixelOffset.x = -margins.x * 0.5 + break + case 'right': + billboardPixelOffset.x = margins.x * 0.5 + break + default: + break + } + + switch (this.anchorY) { + case 'top': + billboardPixelOffset.y = -margins.y * 0.5 + break + case 'bottom': + billboardPixelOffset.x = margins.y * 0.5 + break + default: + break + } + this._backgroundMaterial.billboardPixelOffset = billboardPixelOffset + } + } + + private updateBackground() { + if (!this._params.backgroundColor) { + if (this._background) { + this._background.geometry.dispose() + this.remove(this._background) + } + return + } else if (!this._background.parent) { + this.add(this._background) + } + + const box = _box3.copy(this._textBounds) + const offset = box.getCenter(new Vector3()) + const boxSize = box.getSize(new Vector3()) + const radius = this.fontSize * (this._params.backgroundCornerRadius ?? 0) + const margins = + this._params.billboard !== 'screen' + ? this._params.backgroundMargins ?? new Vector2() + : new Vector2() + + if (!box.isInfiniteBox()) { + const geometry = this.RectangleRounded( + offset, + boxSize.x + margins.x, + boxSize.y + margins.y, + radius, + 5 + ) + geometry.computeBoundingBox() + geometry.computeBoundingSphere() + this._background.geometry = geometry + } + + const color = new Color(this._params.backgroundColor).convertSRGBToLinear() + ;(this._background.material as SpeckleBasicMaterial).color = color + } + + /** Improved version of https://discourse.threejs.org/t/roundedrectangle-squircle/28645 by way of the vibe */ + private RectangleRounded( + offset: Vector3, + w: number, + h: number, + r: number, + s: number, + inset = false + ): BufferGeometry { + const positions: number[] = [] + const uvs: number[] = [] + const indices: number[] = [] + + if (inset) { + let maxInset = 0 + for (let i = 0; i <= s; i++) { + const angle = (Math.PI / 2) * (i / (s + 1)) + const x = r * Math.cos(angle) + const inset = r - x + if (inset > maxInset) maxInset = inset + } + w += 2 * maxInset + } + const radius = Math.min(r, w / 2, h / 2) + const segmentsPerCorner = s + 1 + const pointsPerCorner = segmentsPerCorner + 1 + const totalPoints = pointsPerCorner * 4 + + positions.push(offset.x, offset.y, 0) + uvs.push(0.5, 0.5) + + const corners = [ + { cx: w / 2 - radius, cy: h / 2 - radius, angleStart: 0 }, + { cx: -w / 2 + radius, cy: h / 2 - radius, angleStart: Math.PI / 2 }, + { cx: -w / 2 + radius, cy: -h / 2 + radius, angleStart: Math.PI }, + { cx: w / 2 - radius, cy: -h / 2 + radius, angleStart: (3 * Math.PI) / 2 } + ] + + for (let corner = 0; corner < 4; corner++) { + const { cx, cy, angleStart } = corners[corner] + for (let i = 0; i <= segmentsPerCorner; i++) { + const angle = angleStart + (Math.PI / 2) * (i / segmentsPerCorner) + const x = cx + radius * Math.cos(angle) + const y = cy + radius * Math.sin(angle) + + positions.push(offset.x + x, offset.y + y, 0) + uvs.push(0.5 + x / w, 0.5 + y / h) + } + } + + for (let i = 1; i <= totalPoints; i++) { + const next = i < totalPoints ? i + 1 : 1 + indices.push(0, i, next) + } + + 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.computeBoundingBox() + + return geometry + } +} diff --git a/packages/viewer/src/modules/pipeline/Pipelines/ArcticViewPipeline.ts b/packages/viewer/src/modules/pipeline/Pipelines/ArcticViewPipeline.ts index 22e71d64a..8d8d87d0f 100644 --- a/packages/viewer/src/modules/pipeline/Pipelines/ArcticViewPipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipelines/ArcticViewPipeline.ts @@ -52,7 +52,6 @@ export class ArcticViewPipeline extends ProgressivePipeline { ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_POINT_CLOUD, - ObjectLayers.STREAM_CONTENT_TEXT, ObjectLayers.PROPS ]) viewportPass.setVisibility(ObjectVisibility.OPAQUE) diff --git a/packages/viewer/src/modules/pipeline/Pipelines/DefaultPipeline.ts b/packages/viewer/src/modules/pipeline/Pipelines/DefaultPipeline.ts index 640db5edc..1178058d4 100644 --- a/packages/viewer/src/modules/pipeline/Pipelines/DefaultPipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipelines/DefaultPipeline.ts @@ -48,7 +48,6 @@ export class DefaultPipeline extends ProgressivePipeline { ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_POINT_CLOUD, - ObjectLayers.STREAM_CONTENT_TEXT, ObjectLayers.PROPS ]) opaqueColorPass.setVisibility(ObjectVisibility.OPAQUE) diff --git a/packages/viewer/src/modules/pipeline/Pipelines/SolidViewPipeline.ts b/packages/viewer/src/modules/pipeline/Pipelines/SolidViewPipeline.ts index 459edb1bb..ab57c3399 100644 --- a/packages/viewer/src/modules/pipeline/Pipelines/SolidViewPipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipelines/SolidViewPipeline.ts @@ -25,7 +25,6 @@ export class SolidViewPipeline extends ProgressivePipeline { ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_POINT_CLOUD, - ObjectLayers.STREAM_CONTENT_TEXT, ObjectLayers.PROPS ]) viewportPass.setVisibility(ObjectVisibility.OPAQUE) diff --git a/packages/viewer/src/modules/pipeline/Pipelines/TAAPipeline.ts b/packages/viewer/src/modules/pipeline/Pipelines/TAAPipeline.ts index 510c2b62d..7f2936884 100644 --- a/packages/viewer/src/modules/pipeline/Pipelines/TAAPipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipelines/TAAPipeline.ts @@ -42,7 +42,6 @@ export class TAAPipeline extends ProgressivePipeline { ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_POINT_CLOUD, - ObjectLayers.STREAM_CONTENT_TEXT, ObjectLayers.PROPS ]) opaqueColorPass.setVisibility(ObjectVisibility.OPAQUE) diff --git a/packages/viewer/src/modules/tree/RenderTree.ts b/packages/viewer/src/modules/tree/RenderTree.ts index 00fff70e5..5ca112341 100644 --- a/packages/viewer/src/modules/tree/RenderTree.ts +++ b/packages/viewer/src/modules/tree/RenderTree.ts @@ -69,6 +69,7 @@ export class RenderTree { node.model.renderView.computeAABB() } else if (node.model.renderView.hasMetadata) { node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform) + node.model.renderView.computeAABB() } } } diff --git a/packages/viewer/src/type-augmentations/three-extensions.ts b/packages/viewer/src/type-augmentations/three-extensions.ts index cfe99f585..ba5cef20c 100644 --- a/packages/viewer/src/type-augmentations/three-extensions.ts +++ b/packages/viewer/src/type-augmentations/three-extensions.ts @@ -150,3 +150,14 @@ Box3.prototype.intersectOBB = function (obb: OBB): OBB | null { // Step 9: Return the resulting OBB return new OBB(worldCentroid, halfSize, obb.rotation.clone()) } + +Box3.prototype.isInfiniteBox = function (): boolean { + return ( + this.min.x === -Infinity || + this.min.y === -Infinity || + this.min.z === -Infinity || + this.max.x === Infinity || + this.max.y === Infinity || + this.max.z === Infinity + ) +} diff --git a/packages/viewer/src/type-augmentations/three.d.ts b/packages/viewer/src/type-augmentations/three.d.ts index 379ae5d93..44a3e5ea9 100644 --- a/packages/viewer/src/type-augmentations/three.d.ts +++ b/packages/viewer/src/type-augmentations/three.d.ts @@ -31,6 +31,7 @@ declare module 'three' { interface Box3 { intersectOBB(obb: OBB): OBB | null fromOBB(obb: OBB): Box3 + isInfiniteBox(): boolean } } diff --git a/packages/viewer/src/type-augmentations/troika-three-text.d.ts b/packages/viewer/src/type-augmentations/troika-three-text.d.ts new file mode 100644 index 000000000..cb3d7d271 --- /dev/null +++ b/packages/viewer/src/type-augmentations/troika-three-text.d.ts @@ -0,0 +1,65 @@ +declare module 'troika-three-text' { + import { Mesh, Material, DataTexture, Color } from 'three' + + export type AnchorY = 'middle' | 'top' | 'bottom' + export type AnchorX = 'center' | 'left' | 'right' | 'justify' + + export class Text extends Mesh { + text: string + fontSize: number + font: string + color: string | number | Color + anchorX: AnchorX + anchorY: AnchorY + maxWidth?: number + outlineWidth?: number + outlineColor?: string | number | Color + outlineOpacity?: number + fillOpacity?: number + letterSpacing?: number + lineHeight?: number + curveRadius?: number + depthOffset?: number + direction?: 'ltr' | 'rtl' + _needsSync: boolean + get textRenderInfo() + + constructor() + + clone(recursive?: boolean): this + copy(source: this, recursive?: boolean): this + dispose(): void + sync(callback?: () => void): void + raycast(...args: unknown[]): void + onBeforeRender(...args: unknown[]): void + onAfterRender(...args: unknown[]): void + localPositionToTextCoords(...args: unknown[]): void + worldPositionToTextCoords(...args: unknown[]): void + _prepareForRender(material: Material) + + static DefaultMatrixAutoUpdate: boolean + static DefaultMatrixWorldAutoUpdate: boolean + } + + export class BatchedText extends Text { + _members: Map< + Text, + { + index: -1 + glyphCount: -1 + dirty: true + needsUpdate?: boolean + } + > + addText(text: Text): void + _dataTextures: Record<'outline' | 'main', DataTexture> + _onMemberSynced: (e) => void + removeText(text: Text): void + createDerivedMaterial(baseMaterial: Material): Material + updateMatrixWorld(force: boolean): void + updateBounds(): void + sync(callback?: () => void): void + copy(source: BatchedText): BatchedText + dispose(): void + } +} diff --git a/yarn.lock b/yarn.lock index 069da883a..1d8f3cb0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16502,7 +16502,7 @@ __metadata: three: "npm:^0.140.0" three-mesh-bvh: "npm:0.5.17" tree-model: "npm:1.0.7" - troika-three-text: "npm:0.47.2" + troika-three-text: "npm:0.52.4" type-fest: "npm:^4.15.0" typescript: "npm:^4.5.4" vitest: "npm:^1.4.0" @@ -47218,6 +47218,20 @@ __metadata: languageName: node linkType: hard +"troika-three-text@npm:0.52.4": + version: 0.52.4 + resolution: "troika-three-text@npm:0.52.4" + dependencies: + bidi-js: "npm:^1.0.2" + troika-three-utils: "npm:^0.52.4" + troika-worker-utils: "npm:^0.52.0" + webgl-sdf-generator: "npm:1.1.1" + peerDependencies: + three: ">=0.125.0" + checksum: 10/bbc0aaaed657b30240b69034543ac71451590e0b7403ae9eadc6b0891b791434185e687c6db545c23433fea0bfe93d67def3e1d120ba73dae74fe08aea95e8ca + languageName: node + linkType: hard + "troika-three-utils@npm:^0.47.2": version: 0.47.2 resolution: "troika-three-utils@npm:0.47.2" @@ -47227,6 +47241,15 @@ __metadata: languageName: node linkType: hard +"troika-three-utils@npm:^0.52.4": + version: 0.52.4 + resolution: "troika-three-utils@npm:0.52.4" + peerDependencies: + three: ">=0.125.0" + checksum: 10/cd2382b50584fdbec86c6ab9ac771c777cc937b9f23f40cb3f2fa3f401ba438ffea822171f84fddc7d6537798e4fc1cc8c2f5fe81b04b3e1e6e7bb6c2f228f5c + languageName: node + linkType: hard + "troika-worker-utils@npm:^0.47.2": version: 0.47.2 resolution: "troika-worker-utils@npm:0.47.2" @@ -47234,6 +47257,13 @@ __metadata: languageName: node linkType: hard +"troika-worker-utils@npm:^0.52.0": + version: 0.52.0 + resolution: "troika-worker-utils@npm:0.52.0" + checksum: 10/7b58418a201611f0e350534c6ab6c4fcc0121d6ed3fdaf74b04b8d873e87341122e4f344d48c144d37cce73263d9948db9a7ab61141d90eed3652ebada4b56e4 + languageName: node + linkType: hard + "true-myth@npm:^8.5.0": version: 8.5.0 resolution: "true-myth@npm:8.5.0"