@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
636 lines (492 loc) • 17.3 kB
JavaScript
/**
*
* @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;