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

Add multiple models support #16143

Merged
Merged
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
85 changes: 52 additions & 33 deletions packages/tools/viewer/src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ export class Viewer implements IDisposable {
private readonly _autoRotationBehavior: AutoRotationBehavior;
private readonly _imageProcessingConfigurationObserver: Observer<ImageProcessingConfiguration>;
private _renderLoopController: Nullable<IDisposable> = null;
private _modelInfo: Nullable<Model> = null;
private _loadedModelsBacking: Model[] = [];
private _activeModelBacking: Nullable<Model> = null;
private _skybox: Nullable<Mesh> = null;
private _skyboxBlur: number = 0.3;
private _skyboxVisible: boolean = true;
Expand Down Expand Up @@ -546,7 +547,7 @@ export class Viewer implements IDisposable {
this._scene.skipPointerMovePicking = true;
this._snapshotHelper = new SnapshotRenderingHelper(this._scene, { morphTargetsNumMaxInfluences: 30 });
this._camera.attachControl();
this._updateCamera(); // set default camera values
this._reframeCamera(); // set default camera values
this._autoRotationBehavior = this._camera.getBehaviorByName("AutoRotation") as AutoRotationBehavior;

// Default to KHR PBR Neutral tone mapping.
Expand All @@ -565,7 +566,7 @@ export class Viewer implements IDisposable {
scene: viewer._scene,
camera: viewer._camera,
get model() {
return viewer._modelInfo ?? null;
return viewer._activeModel ?? null;
},
suspendRendering: () => this._suspendRendering(),
markSceneMutated: () => this._markSceneMutated(),
Expand Down Expand Up @@ -787,24 +788,28 @@ export class Viewer implements IDisposable {
return false;
}

protected get _model(): Nullable<Model> {
return this._modelInfo;
protected get _loadedModels(): readonly Model[] {
return this._loadedModelsBacking;
}

protected _setModel(
protected get _activeModel(): Nullable<Model> {
return this._activeModelBacking;
}

protected _setActiveModel(
...args: [model: null] | [model: Model, options?: UpdateModelOptions & Partial<{ source: string | File | ArrayBufferView; interpolateCamera: boolean }>]
): void {
const [model, options] = args;
if (model !== this._modelInfo) {
this._modelInfo = model;
if (model !== this._activeModelBacking) {
this._activeModelBacking = model;
this._updateLight();
this._applyAnimationSpeed();
this._selectAnimation(options?.defaultAnimation ?? 0, false);
if (options?.animationAutoPlay) {
this.playAnimation();
}
this.onSelectedMaterialVariantChanged.notifyObservers();
this._updateCamera(options?.interpolateCamera);
this._reframeCamera(options?.interpolateCamera);
this.onModelChanged.notifyObservers(options?.source ?? null);
}
}
Expand All @@ -813,7 +818,7 @@ export class Viewer implements IDisposable {
* The list of animation names for the currently loaded model.
*/
public get animations(): readonly string[] {
return this._modelInfo?.assetContainer.animationGroups.map((group) => group.name) ?? [];
return this._activeModelBacking?.assetContainer.animationGroups.map((group) => group.name) ?? [];
}

/**
Expand Down Expand Up @@ -861,7 +866,7 @@ export class Viewer implements IDisposable {
}),
];

this._updateCamera(interpolateCamera);
this._reframeCamera(interpolateCamera);
}

this.onSelectedAnimationChanged.notifyObservers();
Expand Down Expand Up @@ -909,27 +914,27 @@ export class Viewer implements IDisposable {
}

private get _activeAnimation(): Nullable<AnimationGroup> {
return this._modelInfo?.assetContainer.animationGroups[this._selectedAnimation] ?? null;
return this._activeModel?.assetContainer.animationGroups[this._selectedAnimation] ?? null;
}

/**
* The list of material variant names for the currently loaded model.
*/
public get materialVariants(): readonly string[] {
return this._modelInfo?.materialVariantsController?.variants ?? [];
return this._activeModel?.materialVariantsController?.variants ?? [];
}

/**
* The currently selected material variant.
*/
public get selectedMaterialVariant(): Nullable<string> {
return this._modelInfo?.materialVariantsController?.selectedVariant ?? null;
return this._activeModel?.materialVariantsController?.selectedVariant ?? null;
}

public set selectedMaterialVariant(value: string) {
if (value !== this.selectedMaterialVariant && this._modelInfo?.materialVariantsController?.variants.includes(value)) {
if (value !== this.selectedMaterialVariant && this._activeModel?.materialVariantsController?.variants.includes(value)) {
this._snapshotHelper.disableSnapshotRendering();
this._modelInfo.materialVariantsController.selectedVariant = value;
this._activeModel.materialVariantsController.selectedVariant = value;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
this.onSelectedMaterialVariantChanged.notifyObservers();
Expand Down Expand Up @@ -1034,16 +1039,25 @@ export class Viewer implements IDisposable {

const cachedWorldBounds: ViewerBoundingInfo[] = [];

return {
const model = {
assetContainer,
materialVariantsController,
getHotSpotToRef: (query, result) => {
getHotSpotToRef: (query: Readonly<ViewerHotSpotQuery>, result: ViewerHotSpotResult) => {
return this._getHotSpotToRef(assetContainer, query, result);
},
dispose: () => {
this._snapshotHelper.disableSnapshotRendering();
assetContainer.meshes.forEach((mesh) => this._meshDataCache.delete(mesh));
assetContainer.dispose();

const index = this._loadedModelsBacking.indexOf(model);
if (index !== -1) {
this._loadedModelsBacking.splice(index, 1);
if (model === this._activeModel) {
this._setActiveModel(null);
}
}

this._snapshotHelper.enableSnapshotRendering();
},
getWorldBounds: (animationIndex: number): Nullable<ViewerBoundingInfo> => {
Expand All @@ -1060,6 +1074,10 @@ export class Viewer implements IDisposable {
cachedWorldBounds.length = 0;
},
};

this._loadedModelsBacking.push(model);

return model;
} catch (e) {
this.onModelError.notifyObservers(e);
throw e;
Expand All @@ -1078,12 +1096,12 @@ export class Viewer implements IDisposable {

await this._loadModelLock.lockAsync(async () => {
throwIfAborted(abortSignal, abortController.signal);
this._model?.dispose();
this._setModel(null);
this._activeModel?.dispose();
this._setActiveModel(null);
this.selectedAnimation = -1;

if (source) {
this._setModel(await this._loadModel(source, options, abortController.signal), Object.assign({ source, interpolateCamera: false }, options));
this._setActiveModel(await this._loadModel(source, options, abortController.signal), Object.assign({ source, interpolateCamera: false }, options));
}
});
}
Expand Down Expand Up @@ -1243,7 +1261,8 @@ export class Viewer implements IDisposable {
this._loadModelAbortController?.abort(new AbortError("Thew viewer is being disposed."));

this._renderLoopController?.dispose();
this._modelInfo?.dispose();
this._activeModel?.dispose();
this._loadedModelsBacking.forEach((model) => model.dispose());
this._scene.dispose();

this.onEnvironmentChanged.clear();
Expand Down Expand Up @@ -1271,7 +1290,7 @@ export class Viewer implements IDisposable {
* @returns true if hotspot found
*/
public getHotSpotToRef(query: Readonly<ViewerHotSpotQuery>, result: ViewerHotSpotResult): boolean {
return this._modelInfo?.getHotSpotToRef(query, result) ?? false;
return this._activeModel?.getHotSpotToRef(query, result) ?? false;
}

protected _getHotSpotToRef(assetContainer: Nullable<AssetContainer>, query: Readonly<ViewerHotSpotQuery>, result: ViewerHotSpotResult): boolean {
Expand Down Expand Up @@ -1333,7 +1352,7 @@ export class Viewer implements IDisposable {
this._sceneMutated ||
!this._snapshotHelper.isReady ||
this.isAnimationPlaying ||
this._model?.assetContainer.animationGroups.some((group) => group.animatables.some((animatable) => animatable.animationStarted))
this._activeModel?.assetContainer.animationGroups.some((group) => group.animatables.some((animatable) => animatable.animationStarted))
);
}

Expand Down Expand Up @@ -1421,7 +1440,7 @@ export class Viewer implements IDisposable {
}
}

private _updateCamera(interpolate = false): void {
private _reframeCamera(interpolate = false): void {
this._camera.useFramingBehavior = true;
const framingBehavior = this._camera.getBehaviorByName("Framing") as FramingBehavior;
framingBehavior.framingTime = 0;
Expand All @@ -1440,7 +1459,7 @@ export class Viewer implements IDisposable {
let goalTarget = currentTarget;

const selectedAnimation = this._selectedAnimation === -1 ? 0 : this._selectedAnimation;
const worldBounds = this._modelInfo?.getWorldBounds(selectedAnimation);
const worldBounds = this._activeModel?.getWorldBounds(selectedAnimation);
if (worldBounds) {
// get bounds and prepare framing/camera radius from its values
this._camera.lowerRadiusLimit = null;
Expand Down Expand Up @@ -1482,13 +1501,13 @@ export class Viewer implements IDisposable {

private _updateLight() {
let shouldHaveDefaultLight: boolean;
if (!this._modelInfo) {
if (!this._activeModel) {
shouldHaveDefaultLight = false;
} else {
const hasModelProvidedLights = this._modelInfo.assetContainer.lights.length > 0;
const hasModelProvidedLights = this._activeModel.assetContainer.lights.length > 0;
const hasImageBasedLighting = !!this._reflectionTexture;
const hasMaterials = this._modelInfo.assetContainer.materials.length > 0;
const hasNonPBRMaterials = this._modelInfo.assetContainer.materials.some((material) => !(material instanceof PBRMaterial));
const hasMaterials = this._activeModel.assetContainer.materials.length > 0;
const hasNonPBRMaterials = this._activeModel.assetContainer.materials.some((material) => !(material instanceof PBRMaterial));

if (hasModelProvidedLights) {
shouldHaveDefaultLight = false;
Expand All @@ -1508,13 +1527,13 @@ export class Viewer implements IDisposable {
}

private _applyAnimationSpeed() {
this._modelInfo?.assetContainer.animationGroups.forEach((group) => (group.speedRatio = this._animationSpeed));
this._activeModel?.assetContainer.animationGroups.forEach((group) => (group.speedRatio = this._animationSpeed));
}

protected async _pick(screenX: number, screenY: number): Promise<Nullable<PickingInfo>> {
await import("core/Culling/ray");
if (this._modelInfo) {
const model = this._modelInfo?.assetContainer;
if (this._activeModel) {
const model = this._activeModel?.assetContainer;
// Refresh bounding info to ensure morph target and skeletal animations are taken into account.
model.meshes.forEach((mesh) => {
let cache = this._meshDataCache.get(mesh);
Expand Down