@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
327 lines (236 loc) • 8.11 kB
JavaScript
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;
}