@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
440 lines (342 loc) • 13.7 kB
JavaScript
import { MeshDepthMaterial, RGBADepthPacking } from "three";
import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { HashSet } from "../../../core/collection/set/HashSet.js";
import { AABB3 } from "../../../core/geom/3d/aabb/AABB3.js";
import { AnimationOptimizer } from "../../ecs/animation/AnimationOptimizer.js";
import { Transform } from "../../ecs/transform/Transform.js";
import { traverseThreeObject } from "../../graphics/ecs/highlight/renderer/traverseThreeObject.js";
import { computeGeometryEquality } from "../../graphics/geometry/buffered/computeGeometryEquality.js";
import { computeGeometryHash } from "../../graphics/geometry/buffered/computeGeometryHash.js";
import { ensureGeometryBoundingSphere } from "../../graphics/geometry/buffered/ensureGeometryBoundingSphere.js";
import {
computeSkinnedMeshBoundingVolumes
} from "../../graphics/geometry/skining/computeSkinnedMeshBoundingVolumes.js";
import { cloneObject3D } from "../../graphics/three/cloneObject3D.js";
import { three_computeObjectBoundingBox } from "../../graphics/three/three_computeObjectBoundingBox.js";
import { prepareMaterial, prepareObject } from "../../graphics/three/ThreeFactory.js";
import { ensureGeometryBoundingBox } from "../../graphics/util/ensureGeometryBoundingBox.js";
import { Asset } from "../Asset.js";
import { CrossOriginKind } from "../CORS/CrossOriginKind.js";
import { AssetLoader } from "./AssetLoader.js";
import { async_traverse_three_object } from "./async_traverse_three_object.js";
import { computeObjectBoundingSphere } from "./gltf/computeObjectBoundingSphere.js";
import GLTFTextureDDSExtension from "./gltf/extensions/MSFT_texture_dds.js";
import { isMesh } from "./gltf/isMesh.js";
import { computeTextureEquality } from "./material/computeTextureEquality.js";
import { computeTextureHash } from "./material/computeTextureHash.js";
import { StaticMaterialCache } from "./material/StaticMaterialCache.js";
import { TextureAttachmentsByMaterialType } from "./material/TextureAttachmensByMaterialType.js";
const animationOptimizer = new AnimationOptimizer();
const materialCache = StaticMaterialCache.Global;
/**
*
* @param {THREE.Material} material
* @param {HashSet<Texture>} texture_cache
*/
function replace_material_textures_from_cache(material, texture_cache) {
const attachments = TextureAttachmentsByMaterialType[material.type];
if (attachments === undefined) {
// unsupported material
return;
}
const attachment_count = attachments.length;
for (let i = 0; i < attachment_count; i++) {
const attachment = attachments[i];
const texture = attachment.read(material);
if (texture !== undefined && texture !== null) {
const cached = texture_cache.ensure(texture);
if (cached !== texture) {
// got an existing cached texture, write that instead
attachment.write(material, cached);
}
}
}
}
/**
* Transfer any transform on the root down to the children instead
* @param {Object3D} root
* @param {Transform} result
* @returns {boolean} whether or not any transform was carried out
*/
function transferRootTransform(root, result) {
if (
root.position.x === 0 && root.position.y === 0 && root.position.z === 0
&& root.rotation.x === 0 && root.rotation.y === 0 && root.rotation.z === 0
&& root.scale.x === 1 && root.scale.y === 1 && root.scale.z === 1
) {
// no transform to apply
return false;
}
// record transformation matrix on the root
root.updateMatrix();
result.fromMatrix(root.matrix.elements);
return true;
}
/**
*
* @param {Material} material
*/
function configureTextures(material) {
//patch textures
const materialType = material.type;
/**
*
* @type {TextureAttachment[]}
*/
const attachments = TextureAttachmentsByMaterialType[materialType];
if (attachments !== undefined) {
const n = attachments.length;
for (let i = 0; i < n; i++) {
const attachment = attachments[i];
const texture = attachment.read(material);
if (texture === null || texture === undefined) {
continue;
}
//enable anisotropic filtering
texture.anisotropy = 8;
}
}
}
/**
* Path to be supplied to draco decoder
* @returns {string}
*/
function buildDracoDecoderPath() {
let pathname = window.location.pathname;
if (pathname.length > 0 && pathname.endsWith('/')) {
pathname = pathname.slice(0, -1);
}
return `${pathname}/libs/draco/`;
}
export class GLTFAssetLoader extends AssetLoader {
constructor() {
super();
this.__dracoLoader = new DRACOLoader();
this.__dracoLoader.setWorkerLimit(1);
// set default path
this.__dracoLoader.setDecoderPath(buildDracoDecoderPath());
const loader = new GLTFLoader();
const ddsLoader = new DDSLoader();
loader.register(p => new GLTFTextureDDSExtension(p, ddsLoader));
loader.setDRACOLoader(this.__dracoLoader);
this.loader = loader;
/**
* Asset optimization flag
* Whether or not geometry de-duping should be done
* @type {boolean}
* @private
*/
this.__enable_geometry_cache = false;
/**
* Asset optimization flag
* Whether or not texture de-duping should be done
* @type {boolean}
* @private
*/
this.__enable_texture_cache = false;
/**
*
* @type {HashSet<THREE.BufferGeometry>}
* @private
*/
this.__geometry_cache = new HashSet({
keyHashFunction: computeGeometryHash,
keyEqualityFunction: computeGeometryEquality,
capacity: 1024
});
/**
*
* @type {HashSet<Texture>}
* @private
*/
this.__texture_cache = new HashSet({
keyHashFunction: computeTextureHash,
keyEqualityFunction: computeTextureEquality,
capacity: 2048
});
}
/**
*
* @param {string} p
*/
setDracoDecoderPath(p) {
this.__dracoLoader.setDecoderPath(p);
}
/**
*
* @param {boolean} v
*/
set is_micron_enabled(v) {
console.warn('Deprecated. Handled by the MicronRenderPlugin. Remove usage, no further action required');
}
/**
*
* @returns {boolean}
*/
get is_micron_enabled() {
throw new Error('Deprecated. Handled by the MicronRenderPlugin. Remove usage, no further action required. To check for presence of micron, check plugins instead.');
}
async link(assetManager, engine) {
await super.link(assetManager, engine);
this.loader.setCrossOrigin(assetManager.crossOriginConfig.kind);
if (assetManager.crossOriginConfig.kind === CrossOriginKind.UseCredentials) {
this.loader.setWithCredentials(true);
}
}
/**
*
* @param {THREE.Mesh|THREE.Object3D} object
* @private
*/
__prepareObjectMaterial(object) {
if (!isMesh(object)) {
return;
}
/**
*
* @type {GraphicsEngine}
*/
const graphics = this.context.graphics;
/**
*
* @type {MaterialManager}
*/
const materialManager = graphics.getMaterialManager();
prepareMaterial(object.material);
configureTextures(object.material);
//re-write material with a cached one if possible to reduce draw calls and texture unit usage
object.material = materialManager.obtain(object.material).getValue();
//if material uses alpha testing, we need a custom depth material for shadows to look right
if (object.material.alphaTest !== 0) {
const depthMaterial = new MeshDepthMaterial({
depthPacking: RGBADepthPacking,
map: object.material.map,
alphaTest: object.material.alphaTest
});
if (this.__enable_texture_cache) {
object.customDepthMaterial = materialCache.acquire(depthMaterial);
}
}
}
load(scope, path, success, failure, progress) {
const loader = this.loader;
const texture_cache = this.__texture_cache;
/**
*
* @param {THREE.Mesh|THREE.SkinnedMesh} mesh
*/
const processMesh = async (mesh) => {
let geometry = mesh.geometry;
if (geometry === undefined) {
throw new Error(`No geometry found`);
}
if (this.__enable_geometry_cache) {
// replace geometry
geometry = this.__geometry_cache.ensure(geometry);
mesh.geometry = geometry;
}
if (this.__enable_texture_cache) {
// replace textures from cache
replace_material_textures_from_cache(mesh.material, texture_cache);
}
const isSkinned = mesh.isSkinnedMesh === true;
if (isSkinned) {
// this used to be done inside SkinnedMesh constructor in thee.js prior to r99
mesh.normalizeSkinWeights();
//compute bounding box and sphere for the mesh, do it using the skeleton data
computeSkinnedMeshBoundingVolumes(mesh);
} else {
ensureGeometryBoundingBox(geometry);
ensureGeometryBoundingSphere(geometry);
}
}
loader.load(path, async (gltf) => {
const scene = gltf.scene;
scene.updateMatrixWorld();
/**
* {Array.<THREE.Object3D>}
*/
const children = scene.children;
if (children.length === 0) {
failure("Scene is empty");
return;
}
let root = scene;
if (children.length === 1 && isMesh(children[0])) {
// scene only contains a single mesh, use that directly
root = children[0];
}
traverseThreeObject(root, this.__prepareObjectMaterial, this);
const root_transform = new Transform();
// extract root transform
const has_root_transform = transferRootTransform(root, root_transform);
// clear transform on the root element
root.position.set(0, 0, 0);
root.rotation.set(0, 0, 0);
root.scale.set(1, 1, 1);
/**
*
* @param {THREE.Object3D} o
* @returns {Promise<void>}
*/
async function process_object(o) {
o.updateMatrix();
prepareObject(o);
if (isMesh(o)) {
processMesh(o);
}
}
await async_traverse_three_object(root, process_object);
// compute object bounding sphere
root.boundingSphere = computeObjectBoundingSphere(root);
// compute bounding box
const boundingBox = new AABB3(0, 0, 0, 0, 0, 0);
three_computeObjectBoundingBox(root, boundingBox);
/**
*
* @returns {THREE.Object3D}
*/
function assetFactory() {
const result = cloneObject3D(root);
result.castShadow = true;
result.receiveShadow = false;
if (asset.animations !== undefined) {
//animations are present
result.animations = asset.animations;
}
// Copy bounding sphere
result.boundingSphere = root.boundingSphere;
return result;
}
const byteSize = 1;
const asset = new Asset(assetFactory, byteSize);
asset.boundingBox = boundingBox;
asset.has_root_transform = has_root_transform;
asset.root_transform = root_transform;
// remember GLTF metadata
asset.gltf = gltf;
if (gltf.animations !== undefined) {
/**
*
* @type {AnimationClip[]}
*/
const animations = gltf.animations;
//validate and optimize animations
const animation_count = animations.length;
for (let i = 0; i < animation_count; i++) {
const animation = animations[i];
if (animation.validate()) {
animationOptimizer.optimize(animation);
}
}
asset.animations = animations;
}
success(asset);
}, function (xhr) {
//dispatch progress callback
progress(xhr.loaded, xhr.total);
}, failure);
}
}