Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^4.0.1",
"semver": "^7.3.2",
"three": "github:mozillareality/three.js#hubs/master",
"three": "github:mozillareality/three.js#hubs/r128GLTFLoader",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Hubs-Foundation/three.js#57 is merged, I would update this line back to master in this PR or follow up PR. package-lock.json is same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually do it just before merging, and I leave a [ ] todo in the PR description to remind me

"three-ammo": "^1.0.12",
"three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master",
"three-mesh-bvh": "^0.1.2",
Expand Down
279 changes: 186 additions & 93 deletions src/components/gltf-model-plus.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { getCustomGLTFParserURLResolver } from "../utils/media-url-utils";
import { promisifyWorker } from "../utils/promisify-worker.js";
import { MeshBVH, acceleratedRaycast } from "three-mesh-bvh";
import { disposeNode, cloneObject3D } from "../utils/three-utils";
import HubsTextureLoader from "../loaders/HubsTextureLoader";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader";

THREE.Mesh.prototype.raycast = acceleratedRaycast;
Expand Down Expand Up @@ -341,127 +340,221 @@ function runMigration(version, json) {
}
}

const loadLightmap = async (parser, materialIndex) => {
const lightmapDef = parser.json.materials[materialIndex].extensions.MOZ_lightmap;
const [material, lightMap] = await Promise.all([
parser.getDependency("material", materialIndex),
parser.getDependency("texture", lightmapDef.index)
]);
material.lightMap = lightMap;
material.lightMapIntensity = lightmapDef.intensity !== undefined ? lightmapDef.intensity : 1;
return lightMap;
};

let ktxLoader;

export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) {
let gltfUrl = src;
let fileMap;

if (contentType && (contentType.includes("model/gltf+zip") || contentType.includes("application/x-zip-compressed"))) {
fileMap = await extractZipFile(gltfUrl);
gltfUrl = fileMap["scene.gtlf"];
class GLTFHubsPlugin {
constructor(parser, jsonPreprocessor, fileMap) {
this.parser = parser;
this.jsonPreprocessor = jsonPreprocessor;
this.fileMap = fileMap;

// The latest GLTFLoader doesn't use ImageBitmapLoader for Firefox
// because the latest ImageBitmapLoader passes an option parameter
// to createImageBitmap() but Firefox createImageBitmap() fails
// if an option parameter is passed (known bug and already reported to bugzilla).
// But our r111 based ImageBitmapLoader doesn't pass the option parameter yet
// so we can use ImageBitmapLoader even for Firefox.
// When we replace our Three.js fork with the latest official one
// we need to revisit this workaround.
if (!parser.textureLoader.isImageBitmapLoader && typeof createImageBitmap !== undefined) {
parser.textureLoader = new THREE.ImageBitmapLoader(parser.options.manager);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImageBitmapLoader returns an ImageBitmap not a Texture instance I thought. Doesn't this need to be a some kind of texture loader?

Copy link
Contributor Author

@takahirox takahirox May 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GLTFLoader makes CanvasTexture from ImageBitmap if textureLoader is ImageBitmapLoader so this line should be fine.

But this line may be confusing for people who don't know the inside of GLTFLoader. I will add comment.

}
}

const loadingManager = new THREE.LoadingManager();
loadingManager.setURLModifier(getCustomGLTFParserURLResolver(gltfUrl));
const gltfLoader = new THREE.GLTFLoader(loadingManager);
beforeRoot() {
const parser = this.parser;
const jsonPreprocessor = this.jsonPreprocessor;

// TODO some models are loaded before the renderer exists. This is likely things like the camera tool and loading cube.
// They don't currently use KTX textures but if they did this would be an issue. Fixing this is hard but is part of
// "taking control of the render loop" which is something we want to tackle for many reasons.
if (!ktxLoader && AFRAME && AFRAME.scenes && AFRAME.scenes[0]) {
ktxLoader = new KTX2Loader(loadingManager).detectSupport(AFRAME.scenes[0].renderer);
}
//
if (jsonPreprocessor) {
parser.json = jsonPreprocessor(parser.json);
}

if (ktxLoader) {
gltfLoader.setKTX2Loader(ktxLoader);
}
//
let version = 0;
if (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly important but eventually we might want this to be its own MozHubsComponetnsExtension that just handles the components extension and not the other weird hubs specific workarounds. Doesn't really matter too much now since the components stuff is all still very tightly integrated into aframe and the rest of Hubs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment so far and would like to think of the cleaning up later.

parser.json.extensions &&
parser.json.extensions.MOZ_hubs_components &&
parser.json.extensions.MOZ_hubs_components.hasOwnProperty("version")
) {
version = parser.json.extensions.MOZ_hubs_components.version;
}
runMigration(version, parser.json);

const parser = await new Promise((resolve, reject) => gltfLoader.createParser(gltfUrl, resolve, onProgress, reject));
// Note: Here may be rewritten with the one with parser.associations
const nodes = parser.json.nodes;
if (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];

parser.textureLoader = new HubsTextureLoader(loadingManager);
if (!node.extras) {
node.extras = {};
}

if (jsonPreprocessor) {
parser.json = jsonPreprocessor(parser.json);
node.extras.gltfIndex = i;
}
}
}

let version = 0;
if (
parser.json.extensions &&
parser.json.extensions.MOZ_hubs_components &&
parser.json.extensions.MOZ_hubs_components.hasOwnProperty("version")
) {
version = parser.json.extensions.MOZ_hubs_components.version;
}
runMigration(version, parser.json);
afterRoot(gltf) {
gltf.scene.traverse(object => {
// GLTFLoader sets matrixAutoUpdate on animated objects, we want to keep the defaults
// @TODO: Should this be fixed in the gltf loader?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was just copied over from our existing code, but it still does seem like the GLTFLoader should be respecting this default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think GLTFLoader should respect the default configuration, too. I'm going to suggest it to Three.js at some point. If it will be merged, we can drop this code.

object.matrixAutoUpdate = THREE.Object3D.DefaultMatrixAutoUpdate;
const materialQuality = window.APP.store.materialQualitySetting;
object.material = mapMaterials(object, material => convertStandardMaterial(material, materialQuality));
});

const nodes = parser.json.nodes;
if (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
//
gltf.scene.traverse(object => {
if (!object.material) {
return;
}
const materials = Array.isArray(object.material) ? object.material : [object.material];
// @TODO: Handle more efficiently
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already overriding parser.textureLoader in the constructor. Seems like we can still use the HubsTextureLoader to take care of this no?

Copy link
Contributor Author

@takahirox takahirox May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't really want to override parser.textureLoader because GLTFLoader doesn't have a proper API to override the texture loader. Overriding parser.textureLoader = new CustomLoader() is hacky and the GLTFLoader doesn't expect the texture loader to be overridden. There might be a chance that it causes future errors especially when updating to the future glTF loader upstream.

I did override the texture loader because there doesn't seem to be other options to let GLTFLoader use ImageBitmapLoader for Firefox. Yes, overriding the texture loader is hacky as I wrote but GLTFLoader expects that the texture loader can be ImageBitmapLoader so I think it would be less problematic than using our custom texture loader. And if Firefox and Three.js will be updated to support ImageBitmapLoader we will be able to simply drop this hacky code.

But if we realize that this traverse is slow I want to revisit.

for (const material of materials) {
for (const key in material) {
const prop = material[key];
if (prop && prop.isTexture && !prop.onUpdate) {
prop.onUpdate = () => {
// Delete texture data once it has been uploaded to the GPU
prop.image.close && prop.image.close();
delete prop.image;
};
}
}
}
});

if (!node.extras) {
node.extras = {};
// Replace animation target node name with the node uuid.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we have discussed before, it still seems a bit weird that this isn't just the default behavior since GLTF does not guarantee unique names... This is definitely preferable to maintaining a fork, but would like to get to the bottom of why this isn't always desirable.

// I assume track name is 'nodename.property'.
if (gltf.animations) {
for (const animation of gltf.animations) {
for (const track of animation.tracks) {
const parsedPath = THREE.PropertyBinding.parseTrackName(track.name);
for (const scene of gltf.scenes) {
const node = THREE.PropertyBinding.findNode(scene, parsedPath.nodeName);
if (node) {
track.name = track.name.replace(/^[^.]*\./, node.uuid + ".");
break;
}
}
}
}
}

node.extras.gltfIndex = i;
//
if (this.fileMap) {
// The GLTF is now cached as a THREE object, we can get rid of the original blobs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a mental note I re-remember while reading through this PR. This is to clean up the root level resources we create (from Sketchfab zip files) to load the GLTF.

Does the official GLTFLoader have any method of cleaning up the resources it creates?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a plugin manage resources afterRoot hook would be a good place to release the resources.

Object.keys(this.fileMap).forEach(URL.revokeObjectURL);
}

//
gltf.scene.animations = gltf.animations;
}
}

// Mark the special nodes/meshes in json for efficient parse, all json manipulation should happen before this point
parser.markDefs();
class GLTFHubsLightMapExtension {
constructor(parser) {
this.parser = parser;
this.name = "MOZ_lightmap";
}

// @TODO: Ideally we should use extendMaterialParams hook.
// But the current official glTF loader doesn't fire extendMaterialParams
// hook for unlit and specular-glossiness materials.
// So using loadMaterial hook as workaround so far.
// Cons is loadMaterial hook is fired as _invokeOne so
// if other plugins defining loadMaterial is registered
// there is a chance that this light map extension handler isn't called.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have control over the loader this shouldnt really be an issue in practice, but we will need to be careful when doing upgrades that we don't accidentally break it by some plugin being introduced upstream. Hopefully this workaround doesn't last too long anyway.

// The glTF loader should be updated to remove the limitation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah we should discuss. Figuring out ow multiple extensions handling the same thing should interact is tricky, but I could definitely see cases where there are multiple layers of extensions for loading a single thing.

loadMaterial(materialIndex) {
const parser = this.parser;
const json = parser.json;
const materialDef = json.materials[materialIndex];

if (!materialDef.extensions || !materialDef.extensions[this.name]) {
return null;
}

const materials = parser.json.materials;
const extensionDeps = [];
if (materials) {
for (let i = 0; i < materials.length; i++) {
const materialNode = materials[i];
const extensionDef = materialDef.extensions[this.name];

if (!materialNode.extensions) continue;
const pending = [];

if (materialNode.extensions.MOZ_lightmap) {
extensionDeps.push(loadLightmap(parser, i));
}
pending.push(parser.loadMaterial(materialIndex));
pending.push(parser.getDependency("texture", extensionDef.index));

return Promise.all(pending).then(results => {
const material = results[0];
const lightMap = results[1];
material.lightMap = lightMap;
material.lightMapIntensity = extensionDef.intensity !== undefined ? extensionDef.intensity : 1;
return material;
});
}
}

class GLTFHubsTextureBasisExtension {
constructor(parser) {
this.parser = parser;
this.name = "MOZ_HUBS_texture_basis";
}

loadTexture(textureIndex) {
const parser = this.parser;
const json = parser.json;
const textureDef = json.textures[textureIndex];

if (!textureDef.extensions || !textureDef.extensions[this.name]) {
return null;
}

if (!parser.options.ktx2Loader) {
// @TODO: Display warning (only if the extension is in extensionsRequired)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice we will always be providing a ktx2loader so if this happens its certainly a bug... We probably don't want to fail silently.

Copy link
Contributor Author

@takahirox takahirox May 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will add warning here

return null;
}

console.warn(`The ${this.name} extension is deprecated, you should use KHR_texture_basisu instead.`);

const extensionDef = textureDef.extensions[this.name];
const source = json.images[extensionDef.source];
const loader = parser.options.ktx2Loader.basisLoader;

return parser.loadTextureImage(textureIndex, source, loader);
}
}

// Note this is being done in place of parser.parse() which we now no longer call. This gives us more control over the order of execution.
// TODO all of the weird stuff we are doing here and above would be much better implemented as "plugins" for the latst GLTFLoader
const [scenes, animations, cameras] = await Promise.all([
parser.getDependencies("scene"),
parser.getDependencies("animation"),
parser.getDependencies("camera"),
Promise.all(extensionDeps)
]);
const gltf = {
scene: scenes[parser.json.scene || 0],
scenes,
animations,
cameras,
asset: parser.json.asset,
parser,
userData: {}
};
export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) {
let gltfUrl = src;
let fileMap;

// this is likely a noop since the whole parser will get GCed
parser.cache.removeAll();
if (contentType && (contentType.includes("model/gltf+zip") || contentType.includes("application/x-zip-compressed"))) {
fileMap = await extractZipFile(gltfUrl);
gltfUrl = fileMap["scene.gtlf"];
}

gltf.scene.traverse(object => {
// GLTFLoader sets matrixAutoUpdate on animated objects, we want to keep the defaults
object.matrixAutoUpdate = THREE.Object3D.DefaultMatrixAutoUpdate;
const materialQuality = window.APP.store.materialQualitySetting;
object.material = mapMaterials(object, material => convertStandardMaterial(material, materialQuality));
});
const loadingManager = new THREE.LoadingManager();
loadingManager.setURLModifier(getCustomGLTFParserURLResolver(gltfUrl));
const gltfLoader = new THREE.GLTFLoader(loadingManager);
gltfLoader
.register(parser => new GLTFHubsPlugin(parser, jsonPreprocessor, fileMap))
.register(parser => new GLTFHubsLightMapExtension(parser))
.register(parser => new GLTFHubsTextureBasisExtension(parser));

if (fileMap) {
// The GLTF is now cached as a THREE object, we can get rid of the original blobs
Object.keys(fileMap).forEach(URL.revokeObjectURL);
// TODO some models are loaded before the renderer exists. This is likely things like the camera tool and loading cube.
// They don't currently use KTX textures but if they did this would be an issue. Fixing this is hard but is part of
// "taking control of the render loop" which is something we want to tackle for many reasons.
if (!ktxLoader && AFRAME && AFRAME.scenes && AFRAME.scenes[0]) {
ktxLoader = new KTX2Loader(loadingManager).detectSupport(AFRAME.scenes[0].renderer);
}

gltf.scene.animations = gltf.animations;
if (ktxLoader) {
gltfLoader.setKTX2Loader(ktxLoader);
}

return gltf;
return new Promise((resolve, reject) => {
gltfLoader.load(gltfUrl, resolve, onProgress, reject);
});
}

export async function loadModel(src, contentType = null, useCache = false, jsonPreprocessor = null) {
Expand Down Expand Up @@ -569,7 +662,7 @@ AFRAME.registerComponent("gltf-model-plus", {
// If we had inflated something already before, clean that up
this.disposeLastInflatedEl();

this.model = gltf.scene || gltf.scenes[0];
this.model = gltf.scene;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this guaranteed to be set now? I think the reason we had it originally is that not GLTFs are not required to have a default scene (though almost all I have encountered in practice do).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's guaranteed in the latest GLTFLoader. IIRC the official loader didn't do that before so I think you encountered the problem with the old loader.


if (this.data.batch) {
this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.addObject(this.model);
Expand Down
Loading