Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion packages/model-viewer-effects/src/test/utilities-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ suite('Screenshot Baseline Test', () => {
expect(renderer).to.not.be.undefined;
element.jumpCameraToGoal();
await rafPasses();
await new Promise(
resolve =>
setTimeout(resolve, 100)); // Allow shader recompilation frames
// to catch up on Chromium
composerScreenshot = screenshot(element);
await rafPasses();
const screenshot2 = screenshot(element);
Expand All @@ -89,7 +93,10 @@ suite('Screenshot Baseline Test', () => {
}
});

test('Empty EffectComposer and base Renderer are identical', () => {
test('Empty EffectComposer and base Renderer are identical', async () => {
await new Promise(
resolve => setTimeout(
resolve, 100)); // Wait for potential rendering stabilization
const similarity = CompareArrays(baseScreenshot, composerScreenshot);
if (!Number.isNaN(similarity)) {
expect(similarity).to.be.greaterThan(0.999);
Expand Down
44 changes: 27 additions & 17 deletions packages/model-viewer/src/features/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const $paused = Symbol('paused');
interface PlayAnimationOptions {
repetitions: number;
pingpong: boolean;
modelIndex?: number;
}

interface AppendAnimationOptions {
Expand All @@ -40,10 +41,12 @@ interface AppendAnimationOptions {
warp?: boolean|number;
relativeWarp?: boolean;
time?: number|null;
modelIndex?: number;
}

interface DetachAnimationOptions {
fade?: boolean|number;
modelIndex?: number;
}

const DEFAULT_PLAY_OPTIONS: PlayAnimationOptions = {
Expand Down Expand Up @@ -71,6 +74,7 @@ export declare interface AnimationInterface {
animationName: string|void;
animationCrossfadeDuration: number;
readonly availableAnimations: Array<string>;
readonly appendedAnimations: Array<string>;
readonly paused: boolean;
readonly duration: number;
currentTime: number;
Expand Down Expand Up @@ -145,7 +149,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
}

get paused(): boolean {
return this[$paused];
return this[$scene].isAllAnimationsPaused();
}

get currentTime(): number {
Expand All @@ -169,18 +173,21 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
this[$scene].animationTimeScale = value;
}

pause() {
if (this[$paused]) {
pause(options?: {modelIndex?: number}) {
if (options?.modelIndex == null && this.paused) {
return;
}

this[$paused] = true;
const modelIndex = options?.modelIndex ?? null;
this[$scene].pauseAnimation(modelIndex);

this.dispatchEvent(new CustomEvent('pause'));
}

play(options?: PlayAnimationOptions) {
if (this.availableAnimations.length > 0) {
this[$paused] = false;
const modelIndex = options?.modelIndex ?? null;
this[$scene].unpauseAnimation(modelIndex);

this[$changeAnimation](options);

Expand All @@ -191,6 +198,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
appendAnimation(animationName: string, options?: AppendAnimationOptions) {
if (this.availableAnimations.length > 0) {
this[$paused] = false;
this[$scene].unpauseAnimation(options?.modelIndex ?? null);

this[$appendAnimation](animationName, options);

Expand All @@ -201,6 +209,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
detachAnimation(animationName: string, options?: DetachAnimationOptions) {
if (this.availableAnimations.length > 0) {
this[$paused] = false;
this[$scene].unpauseAnimation(options?.modelIndex ?? null);

this[$detachAnimation](animationName, options);

Expand All @@ -211,7 +220,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
[$onModelLoad]() {
super[$onModelLoad]();

this[$paused] = true;
this[$scene].pauseAnimation();

if (this.animationName != null) {
this[$changeAnimation]();
Expand All @@ -225,7 +234,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
[$tick](_time: number, delta: number) {
super[$tick](_time, delta);

if (this[$paused] ||
if (this.paused ||
(!this[$getModelIsVisible]() && !this[$renderer].isPresenting)) {
return;
}
Expand Down Expand Up @@ -256,7 +265,8 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
this.animationName,
this.animationCrossfadeDuration / MILLISECONDS_PER_SECOND,
mode,
repetitions);
repetitions,
options.modelIndex);

// If we are currently paused, we need to force a render so that
// the scene updates to the first frame of the new animation
Expand All @@ -267,13 +277,11 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
}

[$appendAnimation](
animationName: string = '',
options: AppendAnimationOptions = {}) {
animationName: string = '', options: AppendAnimationOptions = {}) {
const opts = {...DEFAULT_APPEND_OPTIONS, ...options};
const repetitions = opts.repetitions ?? Infinity;
const mode = opts.pingpong ?
LoopPingPong :
(repetitions === 1 ? LoopOnce : LoopRepeat);
const mode = opts.pingpong ? LoopPingPong :
(repetitions === 1 ? LoopOnce : LoopRepeat);

const needsToStop = !!options.repetitions || 'pingpong' in options;

Expand All @@ -287,7 +295,8 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
opts.warp,
opts.relativeWarp,
opts.time,
needsToStop);
needsToStop,
opts.modelIndex);

// If we are currently paused, we need to force a render so that
// the scene updates to the first frame of the new animation
Expand All @@ -298,11 +307,12 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
}

[$detachAnimation](
animationName: string = '',
options: DetachAnimationOptions = {}) {
animationName: string = '', options: DetachAnimationOptions = {}) {
const opts = {...DEFAULT_DETACH_OPTIONS, ...options};
this[$scene].detachAnimation(
animationName ? animationName : this.animationName, opts.fade);
animationName ? animationName : this.animationName,
opts.fade,
opts.modelIndex);

// If we are currently paused, we need to force a render so that
// the scene updates to the first frame of the new animation
Expand Down
36 changes: 28 additions & 8 deletions packages/model-viewer/src/features/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import {Matrix4, Vector3} from 'three';
import {Vector3} from 'three';

import ModelViewerElementBase, {$needsRender, $onModelLoad, $scene, $tick, toVector2D, toVector3D, Vector2D, Vector3D} from '../model-viewer-base.js';
import {Hotspot, HotspotConfiguration} from '../three-components/Hotspot.js';
Expand All @@ -26,7 +26,7 @@ const $observer = Symbol('observer');
const $addHotspot = Symbol('addHotspot');
const $removeHotspot = Symbol('removeHotspot');

const worldToModel = new Matrix4();


export declare type HotspotData = {
position: Vector3D,
Expand All @@ -38,8 +38,12 @@ export declare type HotspotData = {
export declare interface AnnotationInterface {
updateHotspot(config: HotspotConfiguration): void;
queryHotspot(name: string): HotspotData|null;
positionAndNormalFromPoint(pixelX: number, pixelY: number):
{position: Vector3D, normal: Vector3D, uv: Vector2D|null}|null;
positionAndNormalFromPoint(pixelX: number, pixelY: number): {
position: Vector3D,
normal: Vector3D,
uv: Vector2D|null,
modelIndex?: number
}|null;
surfaceFromPoint(pixelX: number, pixelY: number): string|null;
}

Expand Down Expand Up @@ -106,6 +110,7 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(

const scene = this[$scene];
scene.forHotspots((hotspot) => {
scene.updateHotspotAttachment(hotspot);
scene.updateSurfaceHotspot(hotspot);
});
}
Expand Down Expand Up @@ -140,6 +145,9 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
hotspot.updatePosition(config.position);
hotspot.updateNormal(config.normal);
hotspot.surface = config.surface;
if (config.modelIndex !== undefined && config.modelIndex !== null) {
hotspot.modelIndex = config.modelIndex;
}
this[$scene].updateSurfaceHotspot(hotspot);
this[$needsRender]();
}
Expand Down Expand Up @@ -190,8 +198,12 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
* hotspot's data-position and data-normal attributes. If the mesh is
* not hit, the result is null.
*/
positionAndNormalFromPoint(pixelX: number, pixelY: number):
{position: Vector3D, normal: Vector3D, uv: Vector2D|null}|null {
positionAndNormalFromPoint(pixelX: number, pixelY: number): {
position: Vector3D,
normal: Vector3D,
uv: Vector2D|null,
modelIndex?: number
}|null {
const scene = this[$scene];
const ndcPosition = scene.getNDC(pixelX, pixelY);

Expand All @@ -200,7 +212,7 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
return null;
}

worldToModel.copy(scene.target.matrixWorld).invert();
const worldToModel = hit.worldToModel;
const position = toVector3D(hit.position.applyMatrix4(worldToModel));
const normal = toVector3D(hit.normal.transformDirection(worldToModel));

Expand All @@ -209,7 +221,12 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
uv = toVector2D(hit.uv);
}

return {position: position, normal: normal, uv: uv};
return {
position: position,
normal: normal,
uv: uv,
modelIndex: hit.modelIndex
};
}

/**
Expand Down Expand Up @@ -242,6 +259,9 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
position: node.dataset.position,
normal: node.dataset.normal,
surface: node.dataset.surface,
modelIndex: node.dataset.modelIndex != null ?
parseInt(node.dataset.modelIndex) :
null,
});
this[$hotspotMap].set(node.slot, hotspot);
this[$scene].addHotspot(hotspot);
Expand Down
64 changes: 50 additions & 14 deletions packages/model-viewer/src/features/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import {property} from 'lit/decorators.js';
import {Object3D} from 'three';
import {USDZExporter} from 'three/examples/jsm/exporters/USDZExporter.js';

import {IS_AR_QUICKLOOK_CANDIDATE, IS_SCENEVIEWER_CANDIDATE, IS_WEBXR_AR_CANDIDATE} from '../constants.js';
Expand Down Expand Up @@ -212,7 +213,7 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
await this[$enterARWithWebXR]();
break;
case ARMode.SCENE_VIEWER:
this[$openSceneViewer]();
await this[$openSceneViewer]();
break;
default:
console.warn(
Expand Down Expand Up @@ -316,10 +317,30 @@ configuration or device capabilities');
* Takes a URL and a title string, and attempts to launch Scene Viewer on
* the current device.
*/
[$openSceneViewer]() {
async[$openSceneViewer]() {
const location = self.location.toString();
const locationUrl = new URL(location);
const modelUrl = new URL(this.src!, location);
const extraModels = Array.from(this.querySelectorAll('extra-model')) as
Array<import('./extra-model.js').ExtraModelElement>;
const extraUrlsList =
extraModels.map(m => m.src).filter(src => src != null) as
Array<string>;
const firstSrc = this.src || extraUrlsList[0] || null;
if (!firstSrc) {
console.warn(
'No src or extra-model provided for Scene Viewer fallback.');
return;
}
let modelUrl = new URL(firstSrc, location);

// Note: While it would be ideal to export and pass a composited GLB for
// multi-model scenes, Android's Scene Viewer app cannot securely read
// browser-generated `blob:` URIs due to cross-process security
// restrictions. Attempting to pass one will cause Scene Viewer to crash
// or fail silently. To prevent this, we intentionally skip exporting the
// scene and gracefully degrade to serving only the base model's remote
// URI.

if (modelUrl.hash)
modelUrl.hash = '';
const params = new URLSearchParams(modelUrl.search);
Expand Down Expand Up @@ -392,9 +413,17 @@ configuration or device capabilities');
if (generateUsdz) {
const location = self.location.toString();
const locationUrl = new URL(location);
const srcUrl = new URL(this.src!, locationUrl);
if (srcUrl.hash) {
modelUrl.hash = srcUrl.hash;
const extraModels = Array.from(this.querySelectorAll('extra-model')) as
Array<import('./extra-model.js').ExtraModelElement>;
const extraUrlsList =
extraModels.map(m => m.src).filter(src => src != null) as
Array<string>;
const firstSrc = this.src || extraUrlsList[0] || null;
if (firstSrc) {
const srcUrl = new URL(firstSrc, locationUrl);
if (srcUrl.hash) {
modelUrl.hash = srcUrl.hash;
}
}
}

Expand Down Expand Up @@ -435,8 +464,8 @@ configuration or device capabilities');

await this[$triggerLoad]();

const {model, shadow, target} = this[$scene];
if (model == null) {
const {models, shadow, target} = this[$scene];
if (models.length === 0 || models[0] == null) {
return '';
}

Expand All @@ -452,18 +481,25 @@ configuration or device capabilities');

const exporter = new USDZExporter();

target.remove(model);
model.position.copy(target.position);
model.updateWorldMatrix(false, true);
const exportGroup = new Object3D();
exportGroup.position.copy(target.position);

const arraybuffer = await exporter.parseAsync(model, {
for (const m of models) {
target.remove(m);
exportGroup.add(m);
}
exportGroup.updateWorldMatrix(false, true);

const arraybuffer = await exporter.parseAsync(exportGroup, {
maxTextureSize: isNaN(this.arUsdzMaxTextureSize as any) ?
Infinity :
Math.max(parseInt(this.arUsdzMaxTextureSize), 16),
});

model.position.set(0, 0, 0);
target.add(model);
for (const m of models) {
exportGroup.remove(m);
target.add(m);
}

const blob = new Blob([arraybuffer], {
type: 'model/vnd.usdz+zip',
Expand Down
1 change: 1 addition & 0 deletions packages/model-viewer/src/features/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const maxCameraOrbitIntrinsics = (element: ModelViewerElementBase) => {
};

export const cameraTargetIntrinsics = (element: ModelViewerElementBase) => {
element[$scene].updateBoundingBoxAndShadowIfDirty();
const center = element[$scene].boundingBox.getCenter(new Vector3());

return {
Expand Down
Loading
Loading