UNPKG

@pmndrs/uikit

Version:

Build performant 3D user interfaces with Three.js and yoga.

234 lines (233 loc) 9.25 kB
import { DynamicDrawUsage, InstancedBufferAttribute } from 'three'; import { InstancedGlyphMesh } from './instanced-glyph-mesh.js'; import { InstancedGlyphMaterial } from './instanced-gylph-material.js'; import { ElementType, setupRenderOrder } from '../../order.js'; export class GlyphGroupManager { root; objectRef; map = new Map(); constructor(root, objectRef) { this.root = root; this.objectRef = objectRef; } init(abortSignal) { const onFrame = (delta) => this.traverse((group) => group.onFrame(delta)); this.root.onFrameSet.add(onFrame); abortSignal.addEventListener('abort', () => { this.root.onFrameSet.delete(onFrame); this.traverse((group) => group.destroy()); }); } traverse(fn) { for (const groups of this.map.values()) { for (const group of groups.values()) { fn(group); } } } getGroup(majorIndex, depthTest, depthWrite, renderOrder, font) { let groups = this.map.get(font); if (groups == null) { this.map.set(font, (groups = new Map())); } const key = [majorIndex, depthTest, depthWrite, renderOrder].join(','); let glyphGroup = groups?.get(key); if (glyphGroup == null) { groups.set(key, (glyphGroup = new InstancedGlyphGroup(this.objectRef, font, this.root, { majorIndex, elementType: ElementType.Text, minorIndex: 0, }, depthTest, depthWrite, renderOrder))); } return glyphGroup; } } export class InstancedGlyphGroup { objectRef; root; orderInfo; renderOrder; instanceMatrix; instanceUV; instanceRGBA; instanceClipping; glyphs = []; requestedGlyphs = []; holeIndicies = []; mesh; instanceMaterial; timeTillDecimate; constructor(objectRef, font, root, orderInfo, depthTest, depthWrite, renderOrder) { this.objectRef = objectRef; this.root = root; this.orderInfo = orderInfo; this.renderOrder = renderOrder; this.instanceMaterial = new InstancedGlyphMaterial(font); this.instanceMaterial.depthTest = depthTest; this.instanceMaterial.depthWrite = depthWrite; } requestActivate(glyph) { const holeIndex = this.holeIndicies.shift(); if (holeIndex != null) { //inserting into existing hole this.glyphs[holeIndex] = glyph; glyph.activate(holeIndex); this.root.requestRender(); return; } if (this.mesh == null || this.mesh.count >= this.instanceMatrix.count) { //requesting insert because no space available this.requestedGlyphs.push(glyph); this.root.requestFrame(); return; } //inserting at the end because space available const index = this.mesh.count; this.glyphs[index] = glyph; glyph.activate(index); this.mesh.count += 1; this.root.requestRender(); return; } delete(glyph) { if (glyph.index == null) { //remove an not yet added glyph const indexInRequested = this.requestedGlyphs.indexOf(glyph); if (indexInRequested === -1) { return; } this.requestedGlyphs.splice(indexInRequested, 1); return; } //can directly request render because we don't need "onFrame" to handle delete this.root.requestRender(); const replacement = this.requestedGlyphs.shift(); if (replacement != null) { //replace replacement.activate(glyph.index); this.glyphs[glyph.index] = replacement; glyph.index = undefined; return; } if (glyph.index === this.glyphs.length - 1) { //remove at the end this.glyphs.length -= 1; this.mesh.count -= 1; glyph.index = undefined; return; } //remove in between //hiding the glyph by writing a 0 matrix (0 scale ...) const bufferOffset = glyph.index * 16; this.instanceMatrix.array.fill(0, bufferOffset, bufferOffset + 16); this.instanceMatrix.addUpdateRange(bufferOffset, 16); this.instanceMatrix.needsUpdate = true; this.holeIndicies.push(glyph.index); this.glyphs[glyph.index] = undefined; glyph.index = undefined; } onFrame(delta) { const requiredSize = this.glyphs.length - this.holeIndicies.length + this.requestedGlyphs.length; if (this.mesh != null) { this.mesh.visible = requiredSize > 0; } if (requiredSize === 0) { return; } const availableSize = this.instanceMatrix?.count ?? 0; //if the buffer is continously to small over a period of 1 second, it will be decimated if (requiredSize < availableSize / 3) { this.timeTillDecimate ??= 1; } else { this.timeTillDecimate = undefined; } if (this.timeTillDecimate != null) { this.timeTillDecimate -= delta; } if ((this.timeTillDecimate == null || this.timeTillDecimate > 0) && requiredSize <= availableSize) { return; } this.timeTillDecimate = undefined; this.resize(requiredSize); const indexOffset = this.mesh.count; const requestedGlyphsLength = this.requestedGlyphs.length; for (let i = 0; i < requestedGlyphsLength; i++) { const glyph = this.requestedGlyphs[i]; glyph.activate(indexOffset + i); this.glyphs[indexOffset + i] = glyph; } this.mesh.count += requestedGlyphsLength; this.mesh.visible = true; this.requestedGlyphs.length = 0; } resize(neededSize) { const newSize = Math.ceil(neededSize * 1.5); const matrixArray = new Float32Array(newSize * 16); const uvArray = new Float32Array(newSize * 4); const rgbaArray = new Float32Array(newSize * 4); const clippingArray = new Float32Array(newSize * 16); this.instanceMatrix = new InstancedBufferAttribute(matrixArray, 16, false); this.instanceMatrix.setUsage(DynamicDrawUsage); this.instanceUV = new InstancedBufferAttribute(uvArray, 4, false); this.instanceUV.setUsage(DynamicDrawUsage); this.instanceRGBA = new InstancedBufferAttribute(rgbaArray, 4, false); this.instanceRGBA.setUsage(DynamicDrawUsage); this.instanceClipping = new InstancedBufferAttribute(clippingArray, 16, false); this.instanceClipping.setUsage(DynamicDrawUsage); const oldMesh = this.mesh; this.mesh = new InstancedGlyphMesh(this.instanceMatrix, this.instanceRGBA, this.instanceUV, this.instanceClipping, this.instanceMaterial); this.mesh.renderOrder = this.renderOrder; //copy over old arrays and merging the holes if (oldMesh != null) { this.holeIndicies.sort((i1, i2) => i1 - i2); const holesLength = this.holeIndicies.length; let afterPrevHoleIndex = 0; let i = 0; while (i < holesLength) { const holeIndex = this.holeIndicies[i]; copyBuffer(afterPrevHoleIndex - i, afterPrevHoleIndex, holeIndex, oldMesh, this.mesh); afterPrevHoleIndex = holeIndex + 1; this.glyphs.splice(holeIndex - i, 1); i++; } copyBuffer(afterPrevHoleIndex - i, afterPrevHoleIndex, oldMesh.count, oldMesh, this.mesh); if (this.holeIndicies.length > 0) { for (let i = this.holeIndicies[0]; i < this.glyphs.length; i++) { this.glyphs[i].setIndex(i); } } this.holeIndicies.length = 0; //destroying the old mesh this.objectRef.current?.remove(oldMesh); oldMesh.dispose(); } //finalizing the new mesh setupRenderOrder(this.mesh, this.root, { value: this.orderInfo }); this.mesh.count = this.glyphs.length; this.objectRef.current?.add(this.mesh); } destroy() { if (this.mesh == null) { return; } this.objectRef.current?.remove(this.mesh); this.mesh.dispose(); this.instanceMaterial.dispose(); } } function copyBuffer(target, start, end, oldMesh, newMesh) { copy(target, start, end, oldMesh.instanceMatrix.array, newMesh.instanceMatrix.array, 16); copy(target, start, end, oldMesh.instanceUV.array, newMesh.instanceUV.array, 4); copy(target, start, end, oldMesh.instanceRGBA.array, newMesh.instanceRGBA.array, 4); copy(target, start, end, oldMesh.instanceClipping.array, newMesh.instanceClipping.array, 16); } function copy(target, start, end, from, to, itemSize) { if (start === end) { return; } const targetIndex = target * itemSize; const startIndex = start * itemSize; const endIndex = end * itemSize; to.set(from.subarray(startIndex, endIndex), targetIndex); }