@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
447 lines (343 loc) • 12.1 kB
JavaScript
import { BoxBufferGeometry, MeshLambertMaterial } from "three";
import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { SignalBinding } from "../../../../core/events/signal/SignalBinding.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { GLTFAssetLoader } from "../../../asset/loaders/GLTFAssetLoader.js";
import { System } from "../../../ecs/System.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { make_bvh_visibility_builder } from "../../render/make_bvh_visibility_builder.js";
import checkerTexture from "../../texture/CheckersTexture.js";
import ThreeFactory from "../../three/ThreeFactory.js";
import { assetTypeByPath } from "./assetTypeByPath.js";
import Mesh, { MeshFlags } from "./Mesh.js";
import { MeshEvents } from "./MeshEvents.js";
import { setMesh } from "./setMesh.js";
const PLACEHOLDER_GEOMETRY = new BoxBufferGeometry(1, 1, 1);
PLACEHOLDER_GEOMETRY.computeBoundingSphere();
const PLACEHOLDER_TEXTURE = checkerTexture.create();
const PLACEHOLDER_MATERIAL = new MeshLambertMaterial({ map: PLACEHOLDER_TEXTURE });
export class MeshSystem extends System {
/**
*
* @param {Engine} engine
* @constructor
* @extends {System}
*/
constructor(engine) {
super();
assert.equal(engine.isEngine, true, 'engine.isEngine !== true');
const graphics = engine.graphics;
const assetManager = engine.assetManager;
/**
*
* @type {Engine}
* @private
*/
this.__engine = engine;
this.dependencies = [Mesh, Transform];
this.components_used = [
ResourceAccessSpecification.from(Mesh, ResourceAccessKind.Write)
];
/**
*
* @type {AssetManager}
*/
this.assetManager = assetManager;
this.waitingForMesh = [];
/**
*
* @type {Array.<Array.<SignalBinding>>}
*/
this.entityData = [];
this.graphics = graphics;
/**
*
* @type {RenderLayer|null}
*/
this.renderLayer = null;
/**
*
* @type {boolean}
* @private
*/
this.__use_placeholder_mesh = false;
/**
*
* @type {BVH}
* @private
*/
this.__bvh_binary = new BVH();
}
/**
*
* @param {number} entity
* @param {EntityComponentDataset} dataset
* @param {Asset} asset
* @param {Mesh} component
*/
__set_mesh_from_asset(entity, dataset, asset, component) {
const mesh = asset.create(); // remember the asset
component.asset = asset;
if (asset.boundingBox !== undefined) {
component.boundingBox.copy(asset.boundingBox);
} else if (mesh.isMesh) {
const geometry = mesh.geometry;
const bb = geometry.boundingBox;
component.boundingBox.setBounds(
bb.min.x,
bb.min.y,
bb.min.z,
bb.max.x,
bb.max.y,
bb.max.z
);
}
component.setFlag(MeshFlags.Loaded);
setMesh(dataset, entity, component, mesh);
// send event about asset being attached
dataset.sendEvent(entity, MeshEvents.AssetLoaded);
}
async startup(entityManager) {
const am = this.assetManager;
this.entityManager = entityManager;
if (!am.hasLoaderForType('model/gltf+json')) {
const gltf_asset_loader = new GLTFAssetLoader();
await am.registerLoader('model/gltf+json', gltf_asset_loader);
}
const graphics = this.graphics;
this.renderLayer = graphics.layers.create("mesh-system");
this.renderLayer.buildVisibleSet = make_bvh_visibility_builder(
this.entityManager, this.__bvh_binary,
(destination, offset, entity, ecd) => {
const component = ecd.getComponent(entity, Mesh);
if (component.mesh === null) {
return 0;
}
destination[offset] = component.mesh;
return 1;
}
)
const visibleSet = this.renderLayer.visibleSet;
visibleSet.onAdded.add(m => {
/**
*
* @type {Mesh}
*/
const component = m.__meep_ecs_component;
component.setFlag(MeshFlags.InView);
});
visibleSet.onRemoved.add(m => {
/**
*
* @type {Mesh}
*/
const component = m.__meep_ecs_component;
if (m !== component.mesh) {
//object does not match current component mesh, skip this event
return;
}
component.clearFlag(MeshFlags.InView);
});
}
/**
*
* @param {function(Mesh, number)} visitor
* @param {*} [thisArg]
*/
traverseVisible(visitor, thisArg) {
const em = this.entityManager;
const ecd = em.dataset;
if (ecd === null) {
return;
}
const visibleSet = this.renderLayer.visibleSet;
const visibleElements = visibleSet.elements;
const n = visibleSet.size;
for (let i = 0; i < n; i++) {
/**
*
* @type {Object3D}
*/
const m = visibleElements[i];
/**
*
* @type {Mesh}
*/
const component = m.__meep_ecs_component;
/**
* @type {number}
*/
const entity = m.__meep_ecs_entity;
if (!ecd.entityExists(entity)) {
//entity was destroyed at some point, visibility set updates asynchronously from ECS dataset updates
//skip
return;
}
if (component !== undefined) {
visitor.call(thisArg, component, entity);
}
}
}
async shutdown(entityManager) {
this.graphics.layers.remove(this.renderLayer);
}
/**
*
* @param {Transform} transform
* @param {Mesh} model
* @param {number} entity
*/
link(model, transform, entity) {
//remember entity for fast lookup
this.process(entity, model);
function apply_transform() {
model.internalApplyTransform(transform);
}
const bPosition = new SignalBinding(
transform.position.onChanged,
apply_transform
);
const bRotation = new SignalBinding(
transform.rotation.onChanged,
apply_transform
);
const bScale = new SignalBinding(
transform.scale.onChanged,
apply_transform
);
bPosition.link();
bRotation.link();
bScale.link();
this.entityData[entity] = [
bPosition,
bRotation,
bScale
];
model.__bvh_leaf.link(this.__bvh_binary, entity);
}
/**
*
* @param {Transform} transform
* @param {Mesh} model
* @param {string} entityId
*/
unlink(model, transform, entityId) {
const list = this.entityData[entityId];
if (list !== undefined) {
for (let i = 0; i < list.length; i++) {
const binding = list[i];
binding.unlink();
}
delete this.entityData[entityId];
}
//remove from 'waiting for mesh' set
const waitingForMesh = this.waitingForMesh;
let n = waitingForMesh.length;
for (let i = 0; i < n; i++) {
const element = waitingForMesh[i];
if (element.entity === entityId) {
waitingForMesh.splice(i, 1);
i--;
n--;
}
}
model.__bvh_leaf.unlink();
// cleanup resources
model.dispose();
}
/**
*
* @param {int} entity
* @param {Mesh} component
*/
process(entity, component) {
const em = this.entityManager;
const am = this.assetManager;
const dataset = em.dataset;
if (component.hasMesh()) {
//do not re-run if mesh is set
setMesh(dataset, entity, component, component.mesh);
return;
}
if (component.url === null) {
this.waitingForMesh.push({ component, entity });
return;
}
const assetType = assetTypeByPath(component.url);
const asset = am.tryGet(component.url, assetType);
if (asset !== null) {
// asset is already loaded, use it
this.__set_mesh_from_asset(entity, dataset, asset, component);
return;
}
// mesh was not found, use temp mesh and submit a request
if (this.__use_placeholder_mesh) {
// set placeholder mesh
const mesh = ThreeFactory.createMesh(PLACEHOLDER_GEOMETRY, PLACEHOLDER_MATERIAL);
setMesh(dataset, entity, component, mesh);
}
function assetFailure(error) {
console.error("failed to load model " + component.url, error);
dataset.sendEvent(entity, MeshEvents.AssetLoadFailed, {
reason: error
});
}
if (assetType === null) {
// couldn't figure out what the asset type is
assetFailure("no asset type deduced");
return;
}
/**
*
* @param {Asset<Object3D>} asset
*/
const assetLoaded = (asset) => {
if (!dataset.entityExists(entity)) {
//entity no longer exists, probably dataset has been switched, abort
return;
}
//check that component is still actual
const actualComponent = dataset.getComponent(entity, Mesh);
if (actualComponent === component) {
// scene.remove(component.mesh);
this.__set_mesh_from_asset(entity, dataset, asset, component);
} else {
//component is no longer in the manager. do nothing.
//console.warn("component is no longer in the manager");
}
}
// load the asset
am.get({ path: component.url, type: assetType, callback: assetLoaded, failure: assetFailure });
}
update(timeDelta) {
const waiting = this.waitingForMesh;
let i = 0;
let l = waiting.length;
for (; i < l; i++) {
const element = waiting[i];
const component = element.component;
if (component.url !== null) {
const entity = element.entity;
this.process(entity, component);
waiting.splice(i, 1);
i--;
l--;
}
}
}
/**
*
* @param {Mesh} component
* @param {HumanoidBoneType} boneType
* @param {Vector3} [result=new Vector3()]
* @returns {Vector3}
* @throws {Error} if no such bone exists
*/
static getBonePosition(component, boneType, result = new Vector3()) {
component.getBonePositionByType(result, boneType);
return result;
}
}