@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
189 lines (188 loc) • 7.98 kB
JavaScript
import { DynamicDrawUsage, InstancedBufferAttribute } from 'three';
import { addToSortedBuckets, removeFromSortedBuckets, resizeSortedBucketsSpace, updateSortedBucketsAllocation, } from '../../allocation/sorted-buckets.js';
import { setupRenderOrder } from '../../order.js';
import { createPanelMaterial } from '../material/create.js';
import { resolvePanelMaterialClassProperty } from '../material/presets.js';
import { InstancedPanelMesh } from './mesh.js';
import { parseNumberValue } from '../../properties/values.js';
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.
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;
const materialClass = resolvePanelMaterialClassProperty(panelGroupProperties.panelMaterialClass);
this.instanceMaterial = createPanelMaterial(materialClass, { 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)) {
this.updateCount();
return;
}
this.root.requestRender?.();
this.requestUpdate(1000);
}
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;
}
if (this.elementCount > this.bufferElementSize) {
this.resize();
updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin);
}
else if (this.elementCount <= this.bufferElementSize / 3) {
updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin);
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.root, this.instanceMatrix, this.instanceData, this.instanceClipping);
this.mesh.renderOrder = parseNumberValue(this.panelGroupProperties.renderOrder);
setupRenderOrder(this.mesh, { peek: () => this.root }, { value: this.orderInfo });
this.mesh.material = this.instanceMaterial;
this.mesh.receiveShadow = this.panelGroupProperties.receiveShadow;
this.mesh.castShadow = this.panelGroupProperties.castShadow;
this.object.addUnsafe(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;
}