Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model load refactor #332

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 source/client/components/CVAssetReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ export default class CVAssetReader extends Component
return this.fileLoader.getText(url);
}

async getModel(assetPath: string): Promise<Object3D>
async getModel(assetPath: string, {signal}:{signal?:AbortSignal}={}): Promise<Object3D>
{
const url = this.assetManager.getAssetUrl(assetPath);
return this.modelLoader.get(url);
return this.modelLoader.get(url, {signal});
}

async getGeometry(assetPath: string): Promise<BufferGeometry>
Expand Down
49 changes: 40 additions & 9 deletions source/client/components/CVModel2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ export default class CVModel2 extends CObject3D
private _derivatives = new DerivativeList();
private _activeDerivative: Derivative = null;

/**
* Separate from activeDerivative because when switching quality levels,
* we want to keep the active model until the new one is ready
*/
private _loadingDerivative :Derivative = null;

private _visible: boolean = true;
private _boxFrame: Mesh = null;
private _localBoundingBox = new Box3();
Expand Down Expand Up @@ -306,7 +312,7 @@ export default class CVModel2 extends CObject3D
}
else if (ins.quality.changed) {
const derivative = this.derivatives.select(EDerivativeUsage.Web3D, ins.quality.value);
if (derivative && derivative !== this.activeDerivative) {
if (derivative) {
this.loadDerivative(derivative)
.catch(error => {
console.warn("Model.update - failed to load derivative");
Expand Down Expand Up @@ -749,40 +755,62 @@ export default class CVModel2 extends CObject3D

// load sequence of derivatives one by one
return sequence.reduce((promise, derivative) => {
return promise.then(() => { this.loadDerivative(derivative)});
return promise.then(() => this.loadDerivative(derivative));
}, Promise.resolve());
}

/**
* Loads and displays the given derivative.
* @param derivative
*/
protected loadDerivative(derivative: Derivative): Promise<void>
protected async loadDerivative(derivative: Derivative): Promise<void>
{
if(!this.node || !this.assetReader) { // TODO: Better way to handle active loads when node has been disposed?
console.warn("Model load interrupted.");
return;
}
if(this._loadingDerivative && this._loadingDerivative != derivative) {
this._loadingDerivative.unload();
this._loadingDerivative = null;
}
if (this._activeDerivative == derivative){
return;
}
if(this._loadingDerivative == derivative) {
return new Promise(resolve=> this._loadingDerivative.on("load", resolve));
}

this._loadingDerivative = derivative;

return derivative.load(this.assetReader)
.then(() => {
if (!derivative.model || !this.node ||
(this._activeDerivative && derivative.data.quality != this.ins.quality.value)) {
if ( !derivative.model
|| !this.node
|| (this._activeDerivative && derivative.data.quality != this.ins.quality.value)
) {
//Either derivative is not valid, or we have been disconnected,
// or this derivative is no longer needed as it's not the requested quality
// AND we already have _something_ to display
derivative.unload();
return;
}

if(this._activeDerivative && this._activeDerivative == derivative){
//a race condition can happen where a derivative fires it's callback but it's already the active one.
return;
}

// set asset manager flag for initial model load
if(!this.assetManager.initialLoad && !this._activeDerivative) {
this.assetManager.initialLoad = true;
}

if (this._activeDerivative) {
this.removeObject3D(this._activeDerivative.model);
if(this._activeDerivative.model) this.removeObject3D(this._activeDerivative.model);
this._activeDerivative.unload();
}

this._activeDerivative = derivative;
this._loadingDerivative = null;
this.addObject3D(derivative.model);
this.renderer.activeSceneComponent.scene.updateMatrixWorld(true);

Expand Down Expand Up @@ -855,8 +883,11 @@ export default class CVModel2 extends CObject3D

this.emit<IModelLoadEvent>({ type: "model-load", quality: derivative.data.quality });
//this.getGraphComponent(CVSetup).navigation.ins.zoomExtents.set();
})
.catch(error => Notification.show(`Failed to load model derivative: ${error.message}`));
}).catch(error =>{
if(error.name == "AbortError" || error.name == "ABORT_ERR") return;
console.error(error);
Notification.show(`Failed to load model derivative: ${error.message}`)
});
}

protected addObject3D(object: Object3D)
Expand Down
101 changes: 81 additions & 20 deletions source/client/io/ModelReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@

//import resolvePathname from "resolve-pathname";
import UberPBRAdvMaterial from "client/shaders/UberPBRAdvMaterial";
import { LoadingManager, Object3D, Scene, Mesh, MeshStandardMaterial, SRGBColorSpace } from "three";
import { LoadingManager, Object3D, Scene, Mesh, MeshStandardMaterial, SRGBColorSpace, LoaderUtils } from "three";

import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader.js';
import {MeshoptDecoder} from "three/examples/jsm/libs/meshopt_decoder.module.js";
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
import {GLTF, GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';

import UberPBRMaterial from "../shaders/UberPBRMaterial";
Expand All @@ -39,6 +39,8 @@ export default class ModelReader
protected renderer: CRenderer;
protected gltfLoader :GLTFLoader;

protected loading :Record<string, {listeners : {onload: (data:ArrayBuffer)=>any, onerror: (e:Error)=>any, signal:AbortSignal}[], abortController :AbortController}> = {}

protected customDracoPath = null;

set dracoPath(path: string)
Expand Down Expand Up @@ -68,10 +70,10 @@ export default class ModelReader
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(DEFAULT_SYSTEM_ASSET_PATH + "/js/draco/");
this.renderer = renderer;
this.gltfLoader = new GLTFLoader(loadingManager);
this.gltfLoader = new GLTFLoader();
this.gltfLoader.setDRACOLoader(dracoLoader);
this.gltfLoader.setMeshoptDecoder(MeshoptDecoder);
const ktx2Loader = new KTX2Loader(this.loadingManager);
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath(DEFAULT_SYSTEM_ASSET_PATH + "/js/basis/");
this.gltfLoader.setKTX2Loader(ktx2Loader);
setTimeout(()=>{
Expand Down Expand Up @@ -100,28 +102,86 @@ export default class ModelReader
return ModelReader.mimeTypes.indexOf(mimeType) >= 0;
}

get(url: string): Promise<Object3D>
get(url: string, {signal}:{signal?:AbortSignal}={}): Promise<Object3D>
{
return new Promise((resolve, reject) => {
this.gltfLoader.load(url, gltf => {
resolve(this.createModelGroup(gltf));
}, null, error => {
if(this.gltfLoader === null || this.gltfLoader.dracoLoader === null) {
// HACK to avoid errors when component is removed while loading still in progress.
// Remove once Three.js supports aborting requests again.
resolve(null);
this.loadingManager.itemStart(url);
let resourcePath = LoaderUtils.extractUrlBase( url );
return this.loadModel(url, {signal})
.then(data=>this.gltfLoader.parseAsync(data, resourcePath))
.then(gltf=>this.createModelGroup(gltf))
.then((result)=>{
this.loadingManager.itemEnd(url);
return result;
}, (e)=> {
this.loadingManager.itemError(url);
throw e;
});
}

/**
*
* extracted from GLTFLoader https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/GLTFLoader.js#L186
* Adds an abort capability while waiting for [THREE.js #23070](https://github.com/mrdoob/three.js/pull/23070)
* This implementation does not quite match what is proposed there because it allows _some_ duplicate requests to be aborted without aborting the `fetch`
*/
async loadModel( url :string, {signal} :{signal?:AbortSignal}={}) :Promise<ArrayBuffer>{

// Tells the LoadingManager to track an extra item, which resolves after
// the model is fully loaded. This means the count of items loaded will
// be incorrect, but ensures manager.onLoad() does not fire early.
if(signal){
const onAbort = ()=>{
const idx = this.loading[url]?.listeners.findIndex(l=>l.signal === signal) ?? -1;
if(idx == -1) return;
const {onerror} = this.loading[url].listeners.splice(idx, 1)[0];
onerror(new DOMException(signal.reason, "AbortError"));

if(this.loading[url].listeners.length == 0){
ENV_DEVELOPMENT && console.debug("Abort request for URL : ", url);
this.loading[url].abortController.abort();
}else{
ENV_DEVELOPMENT && console.debug("Abort listener for URL : %s (%d)", url, this.loading[url].listeners.length );
}
else {
console.error(`failed to load '${url}': ${error}`);
reject(new Error(error as any));
}
signal.addEventListener("abort", onAbort);
}

if(!this.loading[url]?.listeners.length){
this.loadingManager.itemStart( url );

this.loading[url] = {listeners:[], abortController: new AbortController()};

fetch(url, {
signal: this.loading[url].abortController.signal,
}).then(r=>{
if(!r.ok){
throw new Error( `fetch for "${r.url}" responded with ${r.status}: ${r.statusText}`);
}
//Skip all the progress tracking from FileLoader since we don't use it.
return r.arrayBuffer();
}).then(data=> {
this.loadingManager.itemEnd( url );
this.loading[url].listeners.forEach(({onload})=>onload(data));
}, (e)=>{
this.loading[url].listeners.forEach(({onerror})=>onerror(e));
if(e.name != "AbortError" && e.name != "ABORT_ERR"){
console.error(e);
this.loadingManager.itemError( url );
}
})
this.loadingManager.itemEnd( url );
}).finally(()=>{
delete this.loading[url];
});
}

return new Promise((onload, onerror)=>{
this.loading[url].listeners.push({onload, onerror, signal});
});
}
}

protected createModelGroup(gltf): Object3D
protected async createModelGroup(gltf :GLTF): Promise<Object3D>
{
const scene: Scene = gltf.scene;
const scene = gltf.scene;

scene.traverse((object: any) => {
if (object.type === "Mesh") {
Expand Down Expand Up @@ -161,6 +221,7 @@ export default class ModelReader
}
});

await this.renderer.views[0].renderer.compileAsync(scene, this.renderer.activeCamera, this.renderer.activeScene);
return scene;
}
}
15 changes: 14 additions & 1 deletion source/client/models/Derivative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export default class Derivative extends Document<IDerivative, IDerivativeJSON>

model: Object3D = null;

abortControl :AbortController = null;

constructor(json?: IDerivativeJSON){
super(json);
this.addEvent("load");
}

dispose()
{
this.unload();
Expand All @@ -70,10 +77,15 @@ export default class Derivative extends Document<IDerivative, IDerivativeJSON>
throw new Error("can't load, not a Web3D derivative");
}

if(this.abortControl){
console.warn("Aborting inflight derivative load");
this.abortControl.abort("Derivative load cancelled"); //This should not happen, but if in doubt, cancel duplicates
}
this.abortControl = new AbortController();
const modelAsset = this.findAsset(EAssetType.Model);

if (modelAsset) {
return assetReader.getModel(modelAsset.data.uri)
return assetReader.getModel(modelAsset.data.uri, {signal: this.abortControl.signal})
.then(object => {
if (this.model) {
disposeObject(this.model);
Expand Down Expand Up @@ -114,6 +126,7 @@ export default class Derivative extends Document<IDerivative, IDerivativeJSON>

unload()
{
this.abortControl?.abort();
if (this.model) {
disposeObject(this.model);
this.model = null;
Expand Down