UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

636 lines (492 loc) • 17.3 kB
/** * * @enum {number} */ import { BufferAttribute, BufferGeometry, DynamicDrawUsage, InstancedBufferAttribute, InstancedBufferGeometry } from "three"; import IdPool from "../../../../../core/IdPool.js"; import ObservedValue from "../../../../../core/model/ObservedValue.js"; import { itemSizeFromAttributeType } from "./itemSizeFromAttributeType.js"; import { Operation } from "./Operation.js"; import { OperationType } from "./OperationType.js"; import { optimizeCommandQueue } from "./optimizeCommandQueue.js"; import { typedArrayConstructorFromDataType } from "./typedArrayConstructorFromDataType.js"; export class ParticleGroup { /** * * @param {ParticleSpecification} spec * @param {boolean} [instanced=false] * @constructor */ constructor(spec, instanced = false) { this.instanced = instanced; /** * * @type {ParticleSpecification} */ this.spec = spec; this.attributes = []; /** * * @type {ObservedValue<BufferGeometry>} */ this.geometry = new ObservedValue(null); /** * * @type {int} */ this.size = 0; /** * * @type {int} */ this.capacity = 100; this.growFactor = 1.1; this.shrinkFactor = 0.9; this.referenceIndexLookup = new Map(); this.indexReferenceLookup = []; /** * * @type {Array.<Operation>} */ this.commandQueue = []; this.referencePool = new IdPool(); this.build(); } /** * * @param {Float64Array|Float32Array|Uint32Array|Uint16Array|Uint8Array|Int32Array|Int16Array|Int8Array} array * @param {int} itemSize * @returns {InstancedBufferAttribute|BufferAttribute} */ buildNewBufferAttribute(array, itemSize) { if (this.instanced) { return new InstancedBufferAttribute(array, itemSize); } else { return new BufferAttribute(array, itemSize); } } /** * * @returns {InstancedBufferGeometry|BufferGeometry} */ buildNewGeometry() { if (this.instanced) { const result = new InstancedBufferGeometry(); result.maxInstanceCount = this.capacity; return result; } else { return new BufferGeometry(); } } build() { const geometry = this.buildNewGeometry(); geometry.dynamic = true; const l = this.spec.attributes.length; for (let i = 0; i < l; i++) { /** * @type {ParticleAttribute} */ const attributeSpec = this.spec.attributes[i]; const itemSize = itemSizeFromAttributeType(attributeSpec.type); const TypedArrayConstructor = typedArrayConstructorFromDataType(attributeSpec.dataType); const newArrayLength = itemSize * this.capacity; const newArray = new TypedArrayConstructor(newArrayLength); const attribute = this.attributes[i]; if (attribute !== undefined && attribute.count !== 0) { //copy old data newArray.set(attribute.array.subarray(0, Math.min(newArrayLength, attribute.array.length))); } const newAttribute = this.buildNewBufferAttribute(newArray, itemSize); newAttribute.setUsage(DynamicDrawUsage); newAttribute.name = attributeSpec.name; this.attributes[i] = newAttribute; geometry.setAttribute(attributeSpec.name, newAttribute); } this.geometry.set(geometry); } /** * attributes can not grow, this means that we need to rebuild entire geometry if we want to make that happen */ setCapacity(maxSize) { if (this.capacity === maxSize) { //do nothing return; } if (this.size > maxSize) { throw new Error(`Attempted to resize capacity to ${maxSize}. Can't set capacity below current size(=${this.size}).`); } // console.log(`capacity resized from ${this.capacity} to ${maxSize}`); this.capacity = maxSize; this.build(); } reset() { this.commandQueue = []; this.size = 0; this.referenceIndexLookup.clear(); this.indexReferenceLookup = []; this.referencePool.reset(); this.setCapacity(100); } /** * Flush command queue * @returns {boolean} true if some operations were executed, false otherwise */ update() { this.optimizeCommandQueue(); const numOperations = this.commandQueue.length; for (let i = 0; i < numOperations; i++) { const operation = this.commandQueue[i]; this.executeOperation(operation); } //clear out the queue this.commandQueue = []; return numOperations > 0; } optimizeCommandQueue() { optimizeCommandQueue(this.commandQueue); } /** * * @param {number} id */ createSpecific(id) { const reference = this.referencePool.getSpecific(id); this.commandQueue.push(new Operation(OperationType.Add, [reference])); } /** * NOTE: Deferred operation, required update before results can be observed * @returns {number} particle reference */ create() { //reserve reference const reference = this.referencePool.get(); this.commandQueue.push(new Operation(OperationType.Add, [reference])); return reference; } /** * NOTE: Deferred operation, required update before results can be observed */ remove(reference) { this.commandQueue.push(new Operation(OperationType.Remove, [reference])); } /** * NOTE: this method does not take pending operations into account * @param reference */ contains(reference) { return this.referenceIndexLookup.has(reference); } traverseReferences(visitor) { this.referenceIndexLookup.forEach(function (value, key) { visitor(key); }); } /** * NOTE: Deferred operation, required update before results can be observed * @param {int} reference * @param {int} attributeIndex Index of attribute to be written * @param {Array.<number>} value */ writeAttribute(reference, attributeIndex, value) { this.commandQueue.push(new Operation(OperationType.WriteAttribute, [reference, attributeIndex, value])); } /** * * @param {number} index * @param {number} attributeIndex * @param {Array|Float32Array|Float64Array|Uint8Array} result */ readAttributeByIndex(index, attributeIndex, result) { const attribute = this.attributes[attributeIndex]; const itemSize = attribute.itemSize; const offset = index * itemSize; for (let i = 0; i < itemSize; i++) { const element = attribute.array[offset + i]; result[i] = element; } } readAttribute(reference, attributeIndex, result) { //dereference const index = this.referenceIndexLookup.get(reference); this.readAttributeByIndex(index, attributeIndex, result); } /** * Produces reference of a particle by its index * @param {number} index * @returns {number} */ referenceByIndex(index) { return this.indexReferenceLookup[index]; } createImmediate() { const reference = this.referencePool.get(); this.executeOperationAdd([reference]); return reference; } executeOperationAdd(references) { const numAdded = references.length; for (let i = 0; i < numAdded; i++) { const reference = references[i]; const index = this.size + i; this.referenceIndexLookup.set(reference, index); this.indexReferenceLookup[index] = reference; } this.grow(numAdded); } executeOperationRemove(references) { const numRemoved = references.length; const deleteIndices = []; for (let i = 0; i < numRemoved; i++) { const reference = references[i]; const index = this.referenceIndexLookup.get(reference); this.referenceIndexLookup.delete(reference); deleteIndices.push(index); //release reference this.referencePool.release(reference); } this.deleteIndices(deleteIndices); } /** * * @param reference * @param attributeIndex * @param {number[]} value */ executeOperationWriteAttribute(reference, attributeIndex, value) { //de-reference const index = this.referenceIndexLookup.get(reference); //bind attribute const attribute = this.attributes[attributeIndex]; attribute.array.set(value, index * attribute.itemSize); attribute.needsUpdate = true; //TODO: set update range on the attribute } /** * * @param reference * @param attributeIndex * @param {number} value */ executeOperationWriteAttribute_Scalar(reference, attributeIndex, value) { //de-reference const index = this.referenceIndexLookup.get(reference); //bind attribute const attribute = this.attributes[attributeIndex]; attribute.array[index] = value; attribute.needsUpdate = true; //TODO: set update range on the attribute } /** * * @param reference * @param attributeIndex * @param x * @param y * @param z */ executeOperationWriteAttribute_Vector3(reference, attributeIndex, x, y, z) { //de-reference const index = this.referenceIndexLookup.get(reference); //bind attribute const attribute = this.attributes[attributeIndex]; const address = index * 3; const array = attribute.array; array[address] = x; array[address + 1] = y; array[address + 2] = z; attribute.needsUpdate = true; //TODO: set update range on the attribute } /** * * @param reference * @param attributeIndex * @param x * @param y * @param z * @param w */ executeOperationWriteAttribute_Vector4(reference, attributeIndex, x, y, z, w) { //de-reference const index = this.referenceIndexLookup.get(reference); //bind attribute const attribute = this.attributes[attributeIndex]; const address = index * 4; const array = attribute.array; array[address] = x; array[address + 1] = y; array[address + 2] = z; array[address + 3] = w; attribute.needsUpdate = true; //TODO: set update range on the attribute } /** * * @param {Operation} operation */ executeOperation(operation) { const operands = operation.operands; const self = this; function _writeAttribute(operands) { const reference = operands[0]; const attributeIndex = operands[1]; const value = operands[2]; self.executeOperationWriteAttribute(reference, attributeIndex, value); } switch (operation.operator) { case OperationType.Add: this.executeOperationAdd(operands); break; case OperationType.Remove: this.executeOperationRemove(operands); break; case OperationType.WriteAttribute: _writeAttribute(operands); break; default: throw new Error(`Unsupported operator: ${operation.operator}`); } } /** * * @param {Array.<int>} indices */ deleteIndices(indices) { const deleteCount = indices.length; if (deleteCount <= 0) { //nothing to do return; } const oldSize = this.size; const newSize = oldSize - deleteCount; //sort indices indices.sort(); const specAttributes = this.spec.attributes; const numAttributes = specAttributes.length; let numSwapElements = 0; const swaps = []; let swapDestination = this.size - 1; let i, j; for (i = deleteCount - 1; i >= 0; i--) { const victim = indices[i]; if (swapDestination === victim) { swapDestination--; } if (victim >= newSize) { //is beyond the end of the new array and will be removed anyway, no need to swap continue; } swaps.push(victim, swapDestination); numSwapElements += 2; } for (i = 0; i < numAttributes; i++) { const attribute = this.attributes[i]; const itemSize = attribute.itemSize; const oldArray = attribute.array; //do swaps in the old array for (j = 0; j < numSwapElements; j += 2) { const targetIndex = swaps[j] * itemSize; const sourceIndex = swaps[j + 1] * itemSize; oldArray.copyWithin(targetIndex, sourceIndex, sourceIndex + itemSize); } attribute.needsUpdate = true; } //update references for (j = 0; j < numSwapElements; j += 2) { const targetIndex = swaps[j]; const sourceIndex = swaps[j + 1]; const ref = this.indexReferenceLookup[sourceIndex]; //move reference this.indexReferenceLookup[targetIndex] = ref; //this reference will be getting a new index this.referenceIndexLookup.set(ref, targetIndex); } //cut tail of index lookup for (i = 0; i < deleteCount; i++) { this.indexReferenceLookup.pop(); } this.setSize(newSize); } /** * Swap two particles identified by index * @param {number} indexA * @param {number} indexB */ swapAttributeValues(indexA, indexB) { const attributes = this.attributes; const numAttributes = attributes.length; let i, j; for (i = 0; i < numAttributes; i++) { const attribute = attributes[i]; const itemSize = attribute.itemSize; const offsetA = indexA * itemSize; const offsetB = indexB * itemSize; const array = attribute.array; for (j = 0; j < itemSize; j++) { const addressA = offsetA + j; const addressB = offsetB + j; const elementA = array[addressA]; array[addressA] = array[addressB]; array[addressB] = elementA; } attribute.needsUpdate = true; } } /** * * @param {number} indexA * @param {number} indexB */ swap(indexA, indexB) { this.swapAttributeValues(indexA, indexB); const refA = this.indexReferenceLookup[indexA]; const refB = this.indexReferenceLookup[indexB]; this.referenceIndexLookup.set(refA, indexB); this.referenceIndexLookup.set(refB, indexA); this.indexReferenceLookup[indexA] = refB; this.indexReferenceLookup[indexB] = refA; } /** * * @param {int} numAdded */ grow(numAdded) { const oldSize = this.size; const newSize = oldSize + numAdded; this.setSize(newSize); } preAllocate(itemCount) { const newSize = this.size + itemCount; if (this.capacity < newSize) { this.updateCapacity(newSize); } } updateCapacity(newSize) { if (newSize > this.capacity) { //grow const newCapacity = Math.floor(Math.max(newSize, this.capacity * this.growFactor, this.capacity + GROW_MIN_STEP)); this.setCapacity(newCapacity); } else if (newSize < this.capacity * this.shrinkFactor && newSize < this.capacity - SHRINK_THRESHOLD) { //shrink this.setCapacity(newSize); } const geometry = this.geometry.getValue(); geometry.setDrawRange(0, newSize); } /** * * @param {int} newSize */ setSize(newSize) { this.size = newSize; this.updateCapacity(newSize); } } const SHRINK_THRESHOLD = 64; const GROW_MIN_STEP = 16;