UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

606 lines (485 loc) • 16.2 kB
import { BufferGeometry, DynamicDrawUsage, InstancedBufferAttribute, InstancedBufferGeometry, Line, LineSegments, Mesh, MeshDepthMaterial, RGBADepthPacking } from 'three'; import { Cache } from "../../../../core/cache/Cache.js"; import { array_copy } from "../../../../core/collection/array/array_copy.js"; import { typed_array_copy } from "../../../../core/collection/array/typed/typed_array_copy.js"; import { max3 } from "../../../../core/math/max3.js"; import { min2 } from "../../../../core/math/min2.js"; import { computeMaterialEquality } from "../../../asset/loaders/material/computeMaterialEquality.js"; import { computeMaterialHash } from "../../../asset/loaders/material/computeMaterialHash.js"; import { StaticMaterialCache } from "../../../asset/loaders/material/StaticMaterialCache.js"; import { DrawMode } from "../../ecs/mesh-v2/DrawMode.js"; import { composeCompile } from "../../material/composeCompile.js"; import ThreeFactory from "../../three/ThreeFactory.js"; import { geometry_copy } from "./geometry_copy.js"; import { rewriteMaterial } from "./rewriteMaterial.js"; /** * @typedef {Object} CacheKey * @property {Material} color * @property {Material} depth */ /** * @readonly * @type {Cache<Material,CacheKey>} */ const material_cache = new Cache({ maxWeight: 1024, keyHashFunction: computeMaterialHash, keyEqualityFunction: computeMaterialEquality }); export class InstancedMeshGroup { /** * * @constructor */ constructor() { /** * Instanced geometry * @type {InstancedBufferGeometry|null} * @private */ this.__threeGeometry = null; /** * Geometry of a single instance * @type {BufferGeometry|null} * @private */ this.__threeInstanceGeometry = null; /** * @private * @type {number} */ this.count = 0; /** * * @type {number} */ this.capacity = 0; /** * * @type {number} */ this.growFactor = 1.2; /** * Minimum spare capacity increase during growing * @type {number} */ this.growConstant = 16; /** * * @type {number} */ this.shrinkFactor = 0.5; /** * Minimum capacity reduction for shrinkage to occur * @type {number} */ this.shrinkConstant = 64; /** * * @type {InstancedBufferAttribute} * @private */ this.__attributeTransform = null; /** * Shortcut to buffer attribute data * @type {Float32Array} * @private */ this.__attributeTransformArray = null; /** * * TODO implement usage * @type {InstancedBufferAttribute} * @private */ this.__attributeColor = null; /** * TODO implement usage * @type {Uint8Array} * @private */ this.__attributeColorArray = null; /** * * @type {Usage} * @private */ this.__instanceUsage = DynamicDrawUsage; /** * * @type {Material} * @private */ this.__material = null; this.indices = []; this.references = []; /** * * @type {DrawMode} * @private */ this.__draw_mode = DrawMode.Triangles; this.mesh = ThreeFactory.createMesh(); this.use_color = true; } __build_mesh() { let new_object; switch (this.__draw_mode) { case DrawMode.Triangles: new_object = new Mesh(this.mesh.geometry, this.mesh.material); break; case DrawMode.Lines: new_object = new Line(this.mesh.geometry, this.mesh.material); break; case DrawMode.LineSegments: new_object = new LineSegments(this.mesh.geometry, this.mesh.material); break; default: throw new Error(`Unsupported DrawMode '${this.__draw_mode}'`); } new_object.matrixAutoUpdate = false; new_object.frustumCulled = false; new_object.matrixWorldNeedsUpdate = false; if (this.mesh !== null) { new_object.castShadow = this.mesh.castShadow; new_object.receiveShadow = this.mesh.receiveShadow; new_object.customDepthMaterial = this.mesh.customDepthMaterial; } this.mesh = new_object; } /** * * @param {DrawMode} v */ set draw_mode(v) { if (v === this.__draw_mode) { // no change return; } this.__draw_mode = v; this.__build_mesh(); } /** * * @param {Usage} v */ set instance_usage(v) { if (this.__instanceUsage === v) { return; } this.__instanceUsage = v; if (this.__attributeTransform !== null) { this.__attributeTransform.setUsage(v); } if (this.__attributeColor !== null) { this.__attributeColor.setUsage(v); } } /** * * @return {Usage} */ get instance_usage() { return this.__instanceUsage; } dispose() { if (this.__threeGeometry !== null) { this.__threeGeometry.dispose(); } } /** * * @param {THREE.BufferGeometry} geometry */ setGeometry(geometry) { if (geometry.isBufferGeometry !== true) { throw new Error(`Expected THREE.BufferedGeometry, got something else instead.`); } this.__threeInstanceGeometry = geometry; this.build(); } /** * * @param {THREE.Material|THREE.ShaderMaterial} sourceMaterial * @returns {CacheKey} */ #buildMaterial(sourceMaterial) { //console.warn(`building material : {id:${sourceMaterial.id}, name: ${sourceMaterial.name}, type: ${sourceMaterial.type}`) const material = sourceMaterial.clone(); if (material.isShaderMaterial) { // transfer source material uniforms for (const uniformsKey in material.uniforms) { material.uniforms[uniformsKey] = sourceMaterial.uniforms[uniformsKey]; } } material.onBeforeCompile = composeCompile(sourceMaterial.onBeforeCompile, rewriteMaterial); //we need a custom depth material to ensure shadows will be drawn correctly const depthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking, }); //if the source material uses alpha testing - enable it on the depth material if (material.alphaTest !== 0) { depthMaterial.alphaTest = material.alphaTest; depthMaterial.map = material.map; } depthMaterial.onBeforeCompile = rewriteMaterial; const cachedMaterial = StaticMaterialCache.Global.acquire(material); return { color: cachedMaterial, depth: StaticMaterialCache.Global.acquire(depthMaterial) }; } /** * * @param {THREE.Material|THREE.ShaderMaterial} sourceMaterial */ setMaterial(sourceMaterial) { const m = material_cache.getOrCompute(sourceMaterial, this.#buildMaterial, this); this.__material = m.color; this.mesh.material = m.color; this.mesh.customDepthMaterial = m.depth; } /** * * @param {number} size */ setCapacity(size) { if (this.capacity === size) { // no change return; } this.capacity = size; this.build(); } /** * * @param {number} size */ ensureCapacity(size) { const currentCapacity = this.capacity; if (currentCapacity < size) { // grow const newCapacityRaw = max3( size, currentCapacity * this.growFactor, currentCapacity + this.growConstant ); const newCapacityInteger = Math.ceil(newCapacityRaw); this.setCapacity(newCapacityInteger); } else if ( min2( currentCapacity * this.shrinkFactor, currentCapacity - this.shrinkConstant ) > size ) { // shrink this.setCapacity(size); } } /** * * @param {number} index * @param {number[]|ArrayLike<number>|Float32Array} transform 4x4 transform matrix of the instance */ setTransformAt(index, transform) { array_copy(transform, 0, this.__attributeTransformArray, index * 16, 16); } /** * * @param {number} index * @param {number[]|ArrayLike<number>|Float32Array} color RGBA color in uint8 format (0...255), LDR */ setColorAt(index, color) { array_copy(color, 0, this.__attributeColorArray, index * 4, 4); } /** * * @param {number} index * @param {number} r * @param {number} g * @param {number} b * @param {number} a */ setColorByComponentAt(index, r, g, b, a) { const color_array = this.__attributeColorArray; const i4 = index * 4; color_array[i4] = r; color_array[i4 + 1] = g; color_array[i4 + 2] = b; color_array[i4 + 3] = a; } requestAttributeUpdate() { this.__attributeTransform.needsUpdate = true; if (this.__attributeColor !== null) { this.__attributeColor.needsUpdate = true; } } /** * Swap position in attribute arrays of two elements * @param {int} indexA * @param {int} indexB */ swap(indexA, indexB) { throw new Error('Not Implemented Yet'); } /** * * @param {int} size */ setCount(size) { this.ensureCapacity(size); this.count = size; this.__threeGeometry.instanceCount = size; } /** * * @return {number|int} */ getCount() { return this.count; } /** * * @param {int} reference * @returns {int} */ add(reference) { //get index const index = this.count; //grow this.setCount(index + 1); this.indices[reference] = index; this.references[index] = reference; return index; } /** * * @param {function(index:int,reference:int)} visitor * @param {*} [thisArg] */ traverseReferences(visitor, thisArg) { const indices = this.indices; const index_count = indices.length; for (let i = 0; i < index_count; i++) { const index = indices[i]; if (index === undefined) { // unoccupied continue; } visitor.call(thisArg, index, i); } } /** * * @param {int} reference */ remove(reference) { //dereference const index = this.indices[reference]; if (index === undefined) { //reference is not known throw new Error(`Reference '${reference}' was not found`); } delete this.indices[reference]; const lastIndex = this.count - 1; const references = this.references; if (index === lastIndex) { //easy case, reference is placed at the end, no swap needed, we can just forget about it delete references[index]; } else { //not at the end, move the very last reference to this place, effectively shrinking the set const a_transform = this.__attributeTransform; a_transform.array.copyWithin(index * 16, lastIndex * 16, lastIndex * 16 + 16); a_transform.needsUpdate = true; const a_color = this.__attributeColor; if (a_color !== null) { a_color.array.copyWithin(index * 4, lastIndex * 4, lastIndex * 4 + 4); a_color.needsUpdate = true; } //update moved reference index const movedReference = references[lastIndex]; if (movedReference === undefined) { //moved reference not found throw new Error(`Moved reference was not found`); } delete references[lastIndex]; //assume the place of removed reference references[index] = movedReference; this.indices[movedReference] = index; } //update size this.setCount(this.count - 1); } build() { if (this.__threeGeometry !== null) { // dispose old geometry's GPU memory this.__threeGeometry.dispose(); } this.__threeGeometry = new InstancedBufferGeometry(); const instance = this.__threeInstanceGeometry; const geometry = this.__threeGeometry; geometry.instanceCount = this.count; geometry.dynamic = true; //copy instance attributes geometry_copy(geometry, instance); //build instanced attributes const newTransformArray = new Float32Array(this.capacity * 16); if (this.__attributeTransform !== null) { const oldTransformArray = this.__attributeTransform.array; typed_array_copy(oldTransformArray, newTransformArray); if (newTransformArray.length > oldTransformArray.length) { //fill the rest of the array with 0s // TODO consider writing identity matrix instead newTransformArray.fill(0, oldTransformArray.length); } } //rewrite old attributes this.__attributeTransformArray = newTransformArray; this.__attributeTransform = new InstancedBufferAttribute(newTransformArray, 16); this.__attributeTransform.setUsage(this.__instanceUsage); //add attributes to newly created geometry geometry.setAttribute("instanceMatrix", this.__attributeTransform); if (this.use_color) { // color attribute const newColorArray = new Uint8Array(this.capacity * 4); if (this.__attributeColor !== null) { const oldColorArray = this.__attributeColor.array; typed_array_copy(oldColorArray, newColorArray); if (newColorArray.length > oldColorArray.length) { newColorArray.fill(255, oldColorArray.length); } } this.__attributeColorArray = newColorArray; this.__attributeColor = new InstancedBufferAttribute(newColorArray, 4); this.__attributeColor.normalized = true; this.__attributeColor.setUsage(this.__instanceUsage); geometry.setAttribute("instanceColor", this.__attributeColor); } else { this.__attributeColorArray = null; this.__attributeColor = null; } this.mesh.geometry = geometry; } /** * * @param {THREE.BufferGeometry} geometry * @param {Material} material * @returns {InstancedMeshGroup} */ static from(geometry, material) { const r = new InstancedMeshGroup(); r.setGeometry(geometry); r.setMaterial(material); return r; } }