UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

440 lines (342 loc) • 13.7 kB
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); } }