Skip to content

Commit

Permalink
Add @mml-io/model-loader (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcusLongmuir authored Apr 24, 2024
1 parent 1f6a932 commit 1194038
Show file tree
Hide file tree
Showing 22 changed files with 568 additions and 159 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.11.0
v20.11.1
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/mml-web-runner/test/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { TextDecoder, TextEncoder } from "util";

import { jest } from "@jest/globals";
import jestFetchMock from "jest-fetch-mock";
import ResizeObserverPolyfill from "resize-observer-polyfill";

jestFetchMock.enableMocks();

(window as any).TextEncoder = TextEncoder;
(window as any).TextDecoder = TextDecoder;

(window as any).URL.createObjectURL = jest.fn();

// Mock the pause method for HTMLMediaElement
Expand Down
43 changes: 1 addition & 42 deletions packages/mml-web/build.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,3 @@
import fs from "fs";

import { handleLibraryBuild } from "../../utils/build-library";

const dracoDecoderWasm = fs.readFileSync(
"../../node_modules/three/examples/jsm/libs/draco/gltf/draco_decoder.wasm",
);
if (!dracoDecoderWasm) {
throw new Error("Failed to read draco_decoder.wasm");
}
const dracoWasmWrapperJs = fs.readFileSync(
"../../node_modules/three/examples/jsm/libs/draco/gltf/draco_wasm_wrapper.js",
);
if (!dracoWasmWrapperJs) {
throw new Error("Failed to read draco_wasm_wrapper.js");
}

handleLibraryBuild([
{
name: "embed-draco-decoder",
setup({ onResolve, onLoad }) {
onResolve(
{ filter: /(esbuild-embed-draco-decoder-wasm|esbuild-embed-draco-wasm-wrapper-js)/ },
(args) => {
return { path: args.path, namespace: "embed-draco-decoder" };
},
);
onLoad({ filter: /.*/, namespace: "embed-draco-decoder" }, (args) => {
if (args.path === "esbuild-embed-draco-decoder-wasm") {
return {
contents: dracoDecoderWasm,
loader: "base64",
};
} else if (args.path === "esbuild-embed-draco-wasm-wrapper-js") {
return {
contents: dracoWasmWrapperJs,
loader: "text",
};
}
throw new Error("Unknown path for embed-draco-decoder plugin: " + args.path);
});
},
},
]);
handleLibraryBuild();
2 changes: 1 addition & 1 deletion packages/mml-web/src/elements/Character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class Character extends Model {
}

public getCharacter(): THREE.Object3D | null {
return this.loadedState?.gltfScene || null;
return this.loadedState?.group || null;
}

public parentTransformed(): void {
Expand Down
63 changes: 31 additions & 32 deletions packages/mml-web/src/elements/Model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ModelLoader, ModelLoadResult } from "@mml-io/model-loader";
import * as THREE from "three";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";

import { MElement } from "./MElement";
import { TransformableElement } from "./TransformableElement";
Expand All @@ -10,7 +10,6 @@ import {
parseFloatAttribute,
} from "../utils/attribute-handling";
import { CollideableHelper } from "../utils/CollideableHelper";
import { ModelLoader } from "../utils/ModelLoader";
import { OrientedBoundingBox } from "../utils/OrientedBoundingBox";

const defaultModelSrc = "";
Expand Down Expand Up @@ -48,8 +47,8 @@ export class Model extends TransformableElement {
private static modelLoader = new ModelLoader();

protected loadedState: {
gltfScene: THREE.Object3D;
gtlfSceneBones: Map<string, THREE.Bone>;
group: THREE.Object3D;
bones: Map<string, THREE.Bone>;
boundingBox: OrientedBoundingBox;
} | null = null;
private animationGroup: THREE.AnimationObjectGroup = new THREE.AnimationObjectGroup();
Expand All @@ -64,8 +63,8 @@ export class Model extends TransformableElement {
private currentAnimationClip: THREE.AnimationClip | null = null;
private currentAnimationAction: THREE.AnimationAction | null = null;
private collideableHelper = new CollideableHelper(this);
private latestAnimPromise: Promise<GLTF> | null = null;
private latestSrcModelPromise: Promise<GLTF> | null = null;
private latestAnimPromise: Promise<ModelLoadResult> | null = null;
private latestSrcModelPromise: Promise<ModelLoadResult> | null = null;
private registeredParentAttachment: Model | null = null;
private srcLoadingInstanceManager = new LoadingInstanceManager(
`${(this.constructor as typeof Model).tagName}.src`,
Expand All @@ -88,7 +87,7 @@ export class Model extends TransformableElement {
"cast-shadows": (instance, newValue) => {
instance.props.castShadows = parseBoolAttribute(newValue, defaultModelCastShadows);
if (instance.loadedState) {
instance.loadedState.gltfScene.traverse((node) => {
instance.loadedState.group.traverse((node) => {
if ((node as THREE.Mesh).isMesh) {
node.castShadow = instance.props.castShadows;
}
Expand Down Expand Up @@ -130,7 +129,7 @@ export class Model extends TransformableElement {
// Add the socketed children back to the parent
if (this.loadedState) {
this.socketChildrenByBone.forEach((children, boneName) => {
const bone = this.loadedState!.gtlfSceneBones.get(boneName);
const bone = this.loadedState!.bones.get(boneName);
children.forEach((child) => {
if (bone) {
bone.add(child.getContainer());
Expand All @@ -151,7 +150,7 @@ export class Model extends TransformableElement {
children.add(child);

if (this.loadedState) {
const bone = this.loadedState.gtlfSceneBones.get(socketName);
const bone = this.loadedState.bones.get(socketName);
if (bone) {
bone.add(child.getContainer());
} else {
Expand Down Expand Up @@ -183,15 +182,15 @@ export class Model extends TransformableElement {
this.attachments.add(attachment);
// Temporarily remove the sockets from the attachment so that they don't get animated
attachment.disableSockets();
this.animationGroup.add(attachment.loadedState!.gltfScene);
this.animationGroup.add(attachment.loadedState!.group);
// Restore the sockets after adding the attachment to the animation group
attachment.restoreSockets();
this.updateAnimation(this.getDocumentTime() || 0, true);
}

public unregisterAttachment(attachment: Model) {
this.attachments.delete(attachment);
this.animationGroup.remove(attachment.loadedState!.gltfScene);
this.animationGroup.remove(attachment.loadedState!.group);
}

static get observedAttributes(): Array<string> {
Expand Down Expand Up @@ -225,8 +224,8 @@ export class Model extends TransformableElement {
this.props.src = (newValue || "").trim();
if (this.loadedState !== null) {
this.collideableHelper.removeColliders();
this.loadedState.gltfScene.removeFromParent();
Model.disposeOfGroup(this.loadedState.gltfScene);
this.loadedState.group.removeFromParent();
Model.disposeOfGroup(this.loadedState.group);
this.loadedState = null;
if (this.registeredParentAttachment) {
this.registeredParentAttachment.unregisterAttachment(this);
Expand Down Expand Up @@ -262,40 +261,40 @@ export class Model extends TransformableElement {
.then((result) => {
if (this.latestSrcModelPromise !== srcModelPromise || !this.isConnected) {
// If we've loaded a different model since, or we're no longer connected, dispose of this one
Model.disposeOfGroup(result.scene);
Model.disposeOfGroup(result.group);
return;
}
result.scene.traverse((child) => {
result.group.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
child.castShadow = this.props.castShadows;
child.receiveShadow = true;
}
});
this.latestSrcModelPromise = null;
const gltfScene = result.scene;
const gtlfSceneBones = new Map<string, THREE.Bone>();
gltfScene.traverse((object) => {
const group = result.group;
const bones = new Map<string, THREE.Bone>();
group.traverse((object) => {
if (object instanceof THREE.Bone) {
gtlfSceneBones.set(object.name, object);
bones.set(object.name, object);
}
});
const boundingBox = new THREE.Box3();
gltfScene.updateWorldMatrix(true, true);
boundingBox.expandByObject(gltfScene);
group.updateWorldMatrix(true, true);
boundingBox.expandByObject(group);

const orientedBoundingBox = OrientedBoundingBox.fromSizeMatrixWorldProviderAndCenter(
boundingBox.getSize(new THREE.Vector3(0, 0, 0)),
this.container,
boundingBox.getCenter(new THREE.Vector3(0, 0, 0)),
);
this.loadedState = {
gltfScene,
gtlfSceneBones,
group,
bones,
boundingBox: orientedBoundingBox,
};
this.container.add(gltfScene);
this.container.add(group);
this.applyBounds();
this.collideableHelper.updateCollider(gltfScene);
this.collideableHelper.updateCollider(group);

const parent = this.parentElement;
if (parent instanceof Model) {
Expand Down Expand Up @@ -331,11 +330,11 @@ export class Model extends TransformableElement {
this.resetAnimationMixer();
if (this.loadedState) {
this.disableSockets();
this.animationGroup.add(this.loadedState.gltfScene);
this.animationGroup.add(this.loadedState.group);
}
for (const animationAttachment of this.attachments) {
animationAttachment.disableSockets();
this.animationGroup.add(animationAttachment.loadedState!.gltfScene);
this.animationGroup.add(animationAttachment.loadedState!.group);
}
const action = this.animationMixer.clipAction(this.currentAnimationClip);
action.play();
Expand Down Expand Up @@ -432,8 +431,8 @@ export class Model extends TransformableElement {
this.registeredParentAttachment = null;
}
if (this.loadedState) {
this.loadedState.gltfScene.removeFromParent();
Model.disposeOfGroup(this.loadedState.gltfScene);
this.loadedState.group.removeFromParent();
Model.disposeOfGroup(this.loadedState.group);
this.loadedState = null;
}
this.srcLoadingInstanceManager.dispose();
Expand Down Expand Up @@ -526,7 +525,7 @@ export class Model extends TransformableElement {
}

public getModel(): THREE.Object3D | null {
return this.loadedState?.gltfScene || null;
return this.loadedState?.group || null;
}

public getCurrentAnimation(): THREE.AnimationClip | null {
Expand All @@ -536,8 +535,8 @@ export class Model extends TransformableElement {
async asyncLoadSourceAsset(
url: string,
onProgress: (loaded: number, total: number) => void,
): Promise<GLTF> {
return await Model.modelLoader.loadGltf(url, onProgress);
): Promise<ModelLoadResult> {
return await Model.modelLoader.load(url, onProgress);
}

private static disposeOfGroup(group: THREE.Object3D) {
Expand Down
59 changes: 0 additions & 59 deletions packages/mml-web/src/utils/ModelLoader.ts

This file was deleted.

7 changes: 1 addition & 6 deletions packages/mml-web/test/character.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,8 @@ describe("m-character", () => {
.spyOn(Character.prototype, "asyncLoadSourceAsset")
.mockImplementation(() => {
return Promise.resolve({
group: testNode,
animations: [],
scene: testNode,
scenes: [],
cameras: [],
asset: {},
userData: {},
parser: {} as any,
});
});

Expand Down
6 changes: 6 additions & 0 deletions packages/mml-web/test/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { TextDecoder, TextEncoder } from "util";

import { jest } from "@jest/globals";
import jestFetchMock from "jest-fetch-mock";
import ResizeObserverPolyfill from "resize-observer-polyfill";

import { MockAudioContext } from "./mocks/MockAudioContext";

jestFetchMock.enableMocks();

(window as any).TextEncoder = TextEncoder;
(window as any).TextDecoder = TextDecoder;

(window as any).URL.createObjectURL = jest.fn();

// Mock the pause method for HTMLMediaElement
Expand Down
Loading

0 comments on commit 1194038

Please sign in to comment.