UNPKG

@pmndrs/uikit

Version:

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

254 lines (253 loc) 10.7 kB
import { InstancedBufferAttribute, DynamicDrawUsage, MeshBasicMaterial } from 'three'; import { addToSortedBuckets, removeFromSortedBuckets, updateSortedBucketsAllocation, resizeSortedBucketsSpace, } from '../allocation/sorted-buckets.js'; import { createPanelMaterial } from './panel-material.js'; import { InstancedPanelMesh } from './instanced-panel-mesh.js'; import { ElementType, setupRenderOrder } from '../order.js'; import { computed } from '@preact/signals-core'; export function computedPanelGroupDependencies(propertiesSignal) { return computed(() => { const properties = propertiesSignal.value; return { panelMaterialClass: properties.read('panelMaterialClass', MeshBasicMaterial), castShadow: properties.read('castShadow', false), receiveShadow: properties.read('receiveShadow', false), depthWrite: properties.read('depthWrite', false), depthTest: properties.read('depthTest', true), renderOrder: properties.read('renderOrder', 0), }; }); } export class PanelGroupManager { root; objectRef; map = new Map(); constructor(root, objectRef) { this.root = root; this.objectRef = objectRef; } init(abortSignal) { const onFrame = () => this.traverse((group) => group.onFrame()); 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, properties) { let groups = this.map.get(properties.panelMaterialClass); if (groups == null) { this.map.set(properties.panelMaterialClass, (groups = new Map())); } const key = [ majorIndex, properties.renderOrder, properties.depthTest, properties.depthWrite, properties.receiveShadow, properties.castShadow, ].join(','); let panelGroup = groups.get(key); if (panelGroup == null) { groups.set(key, (panelGroup = new InstancedPanelGroup(this.objectRef.current, this.root, { elementType: ElementType.Panel, majorIndex, minorIndex: 0, }, properties))); } return panelGroup; } } const nextFrame = Symbol('nextFrame'); export class InstancedPanelGroup { object; root; orderInfo; panelGroupProperties; mesh; instanceMatrix; instanceData; instanceClipping; instanceMaterial; buckets = []; elementCount = 0; bufferElementSize = 0; instanceDataOnUpdate; nextUpdateTime; nextUpdateTimeoutRef; activateElement = (element, bucket, indexInBucket) => { const index = bucket.offset + indexInBucket; this.instanceData.set(element.materialConfig.defaultData, 16 * index); this.instanceData.addUpdateRange(16 * index, 16); this.instanceData.needsUpdate = true; element.activate(bucket, indexInBucket); }; setElementIndex = (element, index) => { element.setIndexInBucket(index); }; bufferCopyWithin = (targetIndex, startIndex, endIndex) => { copyWithinAttribute(this.instanceMatrix, targetIndex, startIndex, endIndex); copyWithinAttribute(this.instanceData, targetIndex, startIndex, endIndex); copyWithinAttribute(this.instanceClipping, targetIndex, startIndex, endIndex); }; clearBufferAt = (index) => { //hiding the element by writing a 0 matrix (0 scale ...) const bufferOffset = index * 16; this.instanceMatrix.array.fill(0, bufferOffset, bufferOffset + 16); this.instanceMatrix.addUpdateRange(bufferOffset, 16); this.instanceMatrix.needsUpdate = true; }; constructor(object, root, orderInfo, panelGroupProperties) { this.object = object; this.root = root; this.orderInfo = orderInfo; this.panelGroupProperties = panelGroupProperties; this.instanceMaterial = createPanelMaterial(panelGroupProperties.panelMaterialClass, { type: 'instanced' }); this.instanceMaterial.depthTest = panelGroupProperties.depthTest; this.instanceMaterial.depthWrite = panelGroupProperties.depthWrite; } updateCount() { const lastBucket = this.buckets[this.buckets.length - 1]; const count = lastBucket.offset + lastBucket.elements.length; if (this.mesh == null) { return; } this.mesh.count = count; this.mesh.visible = count > 0; this.root.requestRender(); } requestUpdate(time) { if (this.nextUpdateTime == nextFrame) { return; } const forTime = performance.now() + time; if (this.nextUpdateTime != null && this.nextUpdateTime < forTime) { return; } this.nextUpdateTime = forTime; clearTimeout(this.nextUpdateTimeoutRef); this.nextUpdateTimeoutRef = setTimeout(this.requestUpdateNextFrame.bind(this), time); } requestUpdateNextFrame() { this.nextUpdateTime = nextFrame; clearTimeout(this.nextUpdateTimeoutRef); this.nextUpdateTimeoutRef = undefined; this.root.requestFrame(); } insert(bucketIndex, panel) { this.elementCount += 1; if (!addToSortedBuckets(this.buckets, bucketIndex, panel, this.activateElement)) { this.updateCount(); return; } this.requestUpdateNextFrame(); } delete(bucketIndex, elementIndex, panel) { this.elementCount -= 1; if (!removeFromSortedBuckets(this.buckets, bucketIndex, panel, elementIndex, this.activateElement, this.clearBufferAt, this.setElementIndex, this.bufferCopyWithin)) { //update count already requests a render this.updateCount(); return; } this.root.requestRender(); this.requestUpdate(1000); //request update in 1 second } onFrame() { if (this.nextUpdateTime != nextFrame) { return; } this.nextUpdateTime = undefined; this.update(); } update() { if (this.elementCount === 0) { if (this.mesh != null) { this.mesh.visible = false; } return; } //buffer is resized to have space for 150% of the actually needed elements if (this.elementCount > this.bufferElementSize) { //buffer is to small to host the current elements this.resize(); //we need to execute updateSortedBucketsAllocation after resize so that updateSortedBucketsAllocation has enough space to arrange all the elements updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin); } else if (this.elementCount <= this.bufferElementSize / 3) { //we need to execute updateSortedBucketsAllocation first, so we still have access to the elements in the space that will be removed by the resize //TODO: this could be improved since now we are re-arraging in place and then copying. we could rearrange while copying. Not sure if faster though? updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin); //buffer is at least 300% bigger than the needed space this.resize(); } else { updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin); } this.mesh.count = this.elementCount; this.mesh.visible = true; } resize() { const oldBufferSize = this.bufferElementSize; this.bufferElementSize = Math.ceil(this.elementCount * 1.5); if (this.mesh != null) { this.mesh.dispose(); this.object.remove(this.mesh); } resizeSortedBucketsSpace(this.buckets, oldBufferSize, this.bufferElementSize); const matrixArray = new Float32Array(this.bufferElementSize * 16); if (this.instanceMatrix != null) { matrixArray.set(this.instanceMatrix.array.subarray(0, matrixArray.length)); } this.instanceMatrix = new InstancedBufferAttribute(matrixArray, 16, false); this.instanceMatrix.setUsage(DynamicDrawUsage); const dataArray = new Float32Array(this.bufferElementSize * 16); if (this.instanceData != null) { dataArray.set(this.instanceData.array.subarray(0, dataArray.length)); } this.instanceData = new InstancedBufferAttribute(dataArray, 16, false); this.instanceDataOnUpdate = (start, count) => { this.instanceData.addUpdateRange(start, count); this.instanceData.needsUpdate = true; }; this.instanceData.setUsage(DynamicDrawUsage); const clippingArray = new Float32Array(this.bufferElementSize * 16); if (this.instanceClipping != null) { clippingArray.set(this.instanceClipping.array.subarray(0, clippingArray.length)); } this.instanceClipping = new InstancedBufferAttribute(clippingArray, 16, false); this.instanceClipping.setUsage(DynamicDrawUsage); this.mesh = new InstancedPanelMesh(this.instanceMatrix, this.instanceData, this.instanceClipping); this.mesh.renderOrder = this.panelGroupProperties.renderOrder; setupRenderOrder(this.mesh, this.root, { value: this.orderInfo }); this.mesh.material = this.instanceMaterial; this.mesh.receiveShadow = this.panelGroupProperties.receiveShadow; this.mesh.castShadow = this.panelGroupProperties.castShadow; this.object.add(this.mesh); } destroy() { clearTimeout(this.nextUpdateTimeoutRef); if (this.mesh == null) { return; } this.object.remove(this.mesh); this.mesh?.dispose(); this.instanceMaterial.dispose(); } } function copyWithinAttribute(attribute, targetIndex, startIndex, endIndex) { const itemSize = attribute.itemSize; const start = startIndex * itemSize; const end = endIndex * itemSize; const target = targetIndex * itemSize; attribute.array.copyWithin(target, start, end); const count = end - start; attribute.addUpdateRange(start, count); attribute.addUpdateRange(target, count); attribute.needsUpdate = true; }