UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

327 lines (236 loc) • 8.11 kB
import { Group, Matrix4, Mesh } from "three"; import { mergeBufferGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; import { array_copy } from "../../../../../core/collection/array/array_copy.js"; import { HashMap } from "../../../../../core/collection/map/HashMap.js"; import { m4_multiply } from "../../../../../core/geom/3d/mat4/m4_multiply.js"; import { MATRIX_4_IDENTITY } from "../../../../../core/geom/3d/mat4/MATRIX_4_IDENTITY.js"; import { StaticMaterialCache } from "../../../../asset/loaders/material/StaticMaterialCache.js"; import { computeGeometryEquality } from "../../buffered/computeGeometryEquality.js"; import { computeGeometryHash } from "../../buffered/computeGeometryHash.js"; class GeometryContext { constructor() { this.matrix = new Float32Array(MATRIX_4_IDENTITY); this.material = null; this.geometry = null; } } const DEFAULT_INSTANCING_MERGE_POLYCOUNT = 1024; /** * * @param {THREE.Object3D} object * @param {THREE.Object3D} ancestor_root * @param {Float32Array} result */ function compute_transform_matrix(object, ancestor_root, result) { /** * * @type {THREE.Object3D[]} */ const path = []; let node = object; while (node !== ancestor_root) { if (path.indexOf(node) !== -1) { throw new Error('Circular dependency'); } path.push(node); node = node.parent; if (node === null) { throw new Error('Ancestor not found'); } } // path.reverse(); result.set(MATRIX_4_IDENTITY); for (let i = 0; i < path.length; i++) { const el = path[i]; m4_multiply(result, result, el.matrix.elements); } } /** * * @param {THREE.BufferGeometry} a * @param {THREE.BufferGeometry} b * @returns {boolean} */ function equal_attribute_structure(a, b) { if (a === b) { return true; } const a_attribute_names = Object.keys(a.attributes); const b_attribute_names = Object.keys(b.attributes); const n = a_attribute_names.length; if (n !== b_attribute_names.length) { return false; } for (let i = 0; i < n; i++) { const a_key = a_attribute_names[i]; const b_attribute_index = b_attribute_names.indexOf(a_key); if (b_attribute_index === -1) { return -1; } const a_attribute = a.attributes[a_key]; const b_attribute = b.attributes[a_key]; if (a_attribute.itemSize !== b_attribute.itemSize) { return false; } if (a_attribute.normalized !== b_attribute.normalized) { return false; } if (a_attribute.array.constructor !== b_attribute.array.constructor) { return false; } } return true; } /** * * @param {GeometryContext[]} contexts * @returns {GeometryContext} */ function merge_contexts(contexts) { const geometries = contexts.map(ctx => { const g = ctx.geometry.clone(); const matrix4 = new Matrix4(); matrix4.elements = ctx.matrix; g.applyMatrix4(matrix4); return g; }); const merged = mergeBufferGeometries(geometries); const merged_context = new GeometryContext(); merged_context.geometry = merged; merged_context.material = contexts[0].material; return merged_context; } /** * * @param {GeometryContext[]} input * @param {GeometryContext[]} output * @param {number} output_offset * @returns {number} */ function merge_by_material(input, output, output_offset) { let result_offset = output_offset; const undecided = input.slice(); while (undecided.length > 0) { for (let i = 0; i < undecided.length; i++) { const ref_0 = undecided[i]; const set_indices = [i]; const set_contexts = [ref_0]; for (let j = i + 1; j < undecided.length; j++) { const ref_1 = undecided[j]; const geo_0 = ref_0.geometry; const geo_1 = ref_1.geometry; if (geo_0 === geo_1) { const tri_count = (geo_0.getIndex().count) / 3; if (tri_count > DEFAULT_INSTANCING_MERGE_POLYCOUNT) { // instancing, and instances are large enough for instancing to be efficient continue; } } if (!equal_attribute_structure(geo_0, geo_1)) { continue; } if (ref_0.material !== ref_1.material) { continue; } set_indices.push(j); set_contexts.push(ref_1); } let out = ref_0; for (let j = set_indices.length - 1; j >= 0; j--) { undecided.splice(set_indices[j], 1); } if (set_indices.length > 1) { out = merge_contexts(set_contexts); i--; } output[result_offset++] = out; } } return result_offset - output_offset; } /** * * @param {GeometryContext} ctx * @returns {Mesh} */ function context_to_mesh(ctx) { const mesh = new Mesh(ctx.geometry, ctx.material); matrix_array_to_three_object(ctx.matrix, mesh); return mesh; } /** * * @param {number[]} matrix * @param {THREE.Object3D} destination */ function matrix_array_to_three_object(matrix, destination) { array_copy(matrix, 0, destination.matrix.elements, 0, 16); destination.matrix.decompose(destination.position, destination.quaternion, destination.scale); destination.rotation.setFromQuaternion(destination.quaternion); } /** * * @param {THREE.Object3D} source * @param {THREE.Object3D} destination */ function apply_transform_to_another_three_object(source, destination) { const m4 = new Matrix4(); m4.multiplyMatrices(source.matrix, destination.matrix); matrix_array_to_three_object(m4.elements, destination); } /** * * @param {THREE.Object3D} input * @returns {THREE.Object3D} */ export function merge_geometry_hierarchy(input) { // material cache const material_cache = new StaticMaterialCache(); const geometry_cache = new HashMap({ keyEqualityFunction: computeGeometryEquality, keyHashFunction: computeGeometryHash }); /** * * @param {THREE.BufferGeometry} geo * @return {THREE.BufferGeometry} */ function dedupe_geometry(geo) { const existing = geometry_cache.get(geo); if (existing !== undefined) { return existing; } geometry_cache.set(geo, geo); return geo; } let contexts = []; // collect geometries input.traverse(o => { if (o.isMesh && !!(o.geometry)) { const ctx = new GeometryContext(); ctx.material = material_cache.acquire(o.material); ctx.geometry = dedupe_geometry(o.geometry); compute_transform_matrix(o, input, ctx.matrix); contexts.push(ctx); } }); const contexts_merge_stage_0 = []; merge_by_material(contexts, contexts_merge_stage_0, 0); // convert contexts to Meshes const meshes = contexts_merge_stage_0.map(context_to_mesh); let result_object; if (meshes.length === 1) { // only one mesh is present, return that result_object = meshes[0]; } else { result_object = new Group(); for (let i = 0; i < meshes.length; i++) { const m = meshes[i]; result_object.add(m); } } // copy transform from original apply_transform_to_another_three_object(input, result_object); return result_object; }