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

Viewer: add a way to match POV of other cameras (from gltf file loaded for example) #16076

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
89 changes: 79 additions & 10 deletions packages/tools/viewer/src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@
getHotSpotToRef(query: Readonly<ViewerHotSpotQuery>, result: ViewerHotSpotResult): boolean;
}>;

/**
* Provides the information of alpha, beta, radius and target of the camera.
*/
export type ViewerArcRotateCameraInfos = { alpha: number; beta: number; radius: number; target: Vector3 };

/**
* @experimental
* Provides an experience for viewing a single 3D model.
Expand Down Expand Up @@ -349,6 +354,7 @@
private _toneMappingType: number;
private _contrast: number;
private _exposure: number;
private _modelWorldCenter: Nullable<Vector3> = null;

private _suspendRenderCount = 0;
private _isDisposed = false;
Expand Down Expand Up @@ -1046,7 +1052,7 @@
}

/**
* retrun world and canvas coordinates of an hot spot
* Return world and canvas coordinates of an hot spot
* @param query mesh index and surface information to query the hot spot positions
* @param result Query a Hot Spot and does the conversion for Babylon Hot spot to a more generic HotSpotPositions, without Vector types
* @returns true if hotspot found
Expand Down Expand Up @@ -1102,6 +1108,76 @@
return true;
}

/**
* Refresh bounding info to ensure morph target and skeletal animations are taken into account.
* @param model The AssetContainer to refresh.
*/
private _refreshBoundingInfo(model: AssetContainer): void {
model.meshes.forEach((mesh) => {
let cache = this._meshDataCache.get(mesh);
if (!cache) {
cache = {};
this._meshDataCache.set(mesh, cache);
}
mesh.refreshBoundingInfo({ applyMorph: true, applySkeleton: true, cache });
});
}

/**
* Calculates the `alpha`, `beta`, and `radius` angles, along with the target point.
* The target point is determined based on the camera's forward ray:
* - If an intersection with the main model is found, the first hit point is used as the target.
* - If no intersection is detected, a fallback target is calculated by projecting
* the distance between the camera and the main model's center along the forward ray.
*
* @param camera The reference camera used to computes infos.
* @returns An object containing the `alpha`, `beta`, `radius`, and `target` properties, or `null` if no model found.
*/
public async getArcRotateCameraInfos(camera: Camera): Promise<Nullable<ViewerArcRotateCameraInfos>> {
await import("core/Culling/ray");
const ray = camera.getForwardRay(100, camera.getWorldMatrix(), camera.globalPosition);
ryantrem marked this conversation as resolved.
Show resolved Hide resolved
const comGlobalPos = camera.globalPosition.clone();

if (this._modelInfo && this._modelWorldCenter) {
const assetContainer = this._modelInfo.assetContainer;
this._refreshBoundingInfo(assetContainer);

// Target
let targetPoint: Vector3 = Vector3.Zero();

Check failure on line 1146 in packages/tools/viewer/src/viewer.ts

View check run for this annotation

Azure Pipelines / CI-Monorepo (Format, Lint, and more)

packages/tools/viewer/src/viewer.ts#L1146

packages/tools/viewer/src/viewer.ts(1146,17): error prefer-const: 'targetPoint' is never reassigned. Use 'const' instead.

Check failure on line 1146 in packages/tools/viewer/src/viewer.ts

View check run for this annotation

Azure Pipelines / CI-Monorepo

packages/tools/viewer/src/viewer.ts#L1146

packages/tools/viewer/src/viewer.ts(1146,17): error prefer-const: 'targetPoint' is never reassigned. Use 'const' instead.
const pickingInfo = this._scene.pickWithRay(ray, (mesh) => assetContainer.meshes.includes(mesh));
if (pickingInfo && pickingInfo.hit) {
targetPoint.copyFrom(pickingInfo.pickedPoint!);
} else {
const direction = ray.direction.clone();
targetPoint.copyFrom(comGlobalPos);
const distance = Vector3.Distance(comGlobalPos, this._modelWorldCenter);
direction.scaleAndAddToRef(distance, targetPoint);
}

const computationVector = Vector3.Zero();
cournoll marked this conversation as resolved.
Show resolved Hide resolved
comGlobalPos.subtractToRef(targetPoint, computationVector);
cournoll marked this conversation as resolved.
Show resolved Hide resolved

// Radius
const radius = computationVector.length();

// Alpha
let alpha = Math.PI / 2;
if (!(computationVector.x === 0 && computationVector.z === 0)) {
alpha = Math.acos(computationVector.x / Math.sqrt(Math.pow(computationVector.x, 2) + Math.pow(computationVector.z, 2)));
}
if (computationVector.z < 0) {
alpha = 2 * Math.PI - alpha;
}

// Beta
const beta = Math.acos(computationVector.y / radius);

return { alpha, beta, radius, target: targetPoint };
}

return null;
}

protected _suspendRendering(): IDisposable {
this._renderLoopController?.dispose();
this._suspendRenderCount++;
Expand Down Expand Up @@ -1180,6 +1256,7 @@

const worldSize = worldExtents.max.subtract(worldExtents.min);
const worldCenter = worldExtents.min.add(worldSize.scale(0.5));
this._modelWorldCenter = worldCenter.clone();
cournoll marked this conversation as resolved.
Show resolved Hide resolved

goalRadius = worldSize.length() * 1.1;

Expand Down Expand Up @@ -1249,15 +1326,7 @@
await import("core/Culling/ray");
if (this._modelInfo) {
const model = this._modelInfo?.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);
if (!cache) {
cache = {};
this._meshDataCache.set(mesh, cache);
}
mesh.refreshBoundingInfo({ applyMorph: true, applySkeleton: true, cache });
});
this._refreshBoundingInfo(model);

const pickingInfo = this._scene.pick(screenX, screenY, (mesh) => model.meshes.includes(mesh));
if (pickingInfo.hit) {
Expand Down
87 changes: 81 additions & 6 deletions packages/tools/viewer/src/viewerElement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-disable-next-line import/no-internal-modules
import type { ArcRotateCamera, Nullable, Observable } from "core/index";
import type { Nullable, Observable } from "core/index";
import { ArcRotateCamera, Camera } from "core/index";

Check failure on line 3 in packages/tools/viewer/src/viewerElement.ts

View check run for this annotation

Azure Pipelines / CI-Monorepo (Format, Lint, and more)

packages/tools/viewer/src/viewerElement.ts#L3

packages/tools/viewer/src/viewerElement.ts(3,41): error import/no-internal-modules: Reaching to "core/index" is not allowed.

Check failure on line 3 in packages/tools/viewer/src/viewerElement.ts

View check run for this annotation

Azure Pipelines / CI-Monorepo

packages/tools/viewer/src/viewerElement.ts#L3

packages/tools/viewer/src/viewerElement.ts(3,41): error import/no-internal-modules: Reaching to "core/index" is not allowed.

import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { EnvironmentOptions, ToneMapping, ViewerDetails, ViewerHotSpotQuery } from "./viewer";
Expand All @@ -25,6 +26,8 @@
"M7.20711 2.54289C7.59763 2.93342 7.59763 3.56658 7.20711 3.95711L5.41421 5.75H13.25C17.6683 5.75 21.25 9.33172 21.25 13.75C21.25 18.1683 17.6683 21.75 13.25 21.75C8.83172 21.75 5.25 18.1683 5.25 13.75C5.25 13.1977 5.69772 12.75 6.25 12.75C6.80228 12.75 7.25 13.1977 7.25 13.75C7.25 17.0637 9.93629 19.75 13.25 19.75C16.5637 19.75 19.25 17.0637 19.25 13.75C19.25 10.4363 16.5637 7.75 13.25 7.75H5.41421L7.20711 9.54289C7.59763 9.93342 7.59763 10.5666 7.20711 10.9571C6.81658 11.3476 6.18342 11.3476 5.79289 10.9571L2.29289 7.45711C1.90237 7.06658 1.90237 6.43342 2.29289 6.04289L5.79289 2.54289C6.18342 2.15237 6.81658 2.15237 7.20711 2.54289Z";
const targetFilledIcon =
"M12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14ZM6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4Z";
const cameraFilledIcon =
"M8.75 3.5C8.75 3.08579 9.08579 2.75 9.5 2.75H14.5C14.9142 2.75 15.25 3.08579 15.25 3.5C15.25 3.91421 14.9142 4.25 14.5 4.25H9.5C9.08579 4.25 8.75 3.91421 8.75 3.5ZM9.95827 5.25022H14.0414C15.4106 5.25021 16.4955 5.25021 17.3619 5.33834C18.2496 5.42864 18.9906 5.61761 19.6388 6.05074C20.1575 6.39729 20.6028 6.84261 20.9493 7.36126C21.3824 8.00945 21.5715 8.7504 21.6618 9.63802C21.75 10.5047 21.75 11.5901 21.75 12.9601V13.0416C21.75 14.4108 21.75 15.4957 21.6619 16.3621C21.5716 17.2497 21.3826 17.9907 20.9495 18.639C20.6029 19.1576 20.1576 19.6029 19.639 19.9495C18.9907 20.3826 18.2497 20.5716 17.3621 20.6619C16.4957 20.75 15.4108 20.75 14.0416 20.75H9.95844C8.58922 20.75 7.50431 20.75 6.63794 20.6619C5.7503 20.5716 5.00926 20.3826 4.36104 19.9495C3.84239 19.6029 3.39707 19.1576 3.05052 18.639C2.61739 17.9907 2.42841 17.2497 2.33812 16.3621C2.24998 15.4957 2.24999 14.4108 2.25 13.0416V12.9566C2.24999 11.588 2.24998 10.5035 2.3381 9.63749C2.42838 8.75016 2.61733 8.00935 3.05037 7.36126C3.39692 6.84261 3.84224 6.39729 4.36089 6.05074C5.00911 5.61761 5.75015 5.42864 6.6378 5.33834C7.50415 5.25021 8.58906 5.25021 9.95827 5.25022ZM6.7896 6.83064C6.02056 6.90887 5.55492 7.05695 5.19425 7.29795C4.83938 7.53506 4.53469 7.83975 4.29758 8.19462C4.05664 8.5552 3.90861 9.02065 3.83039 9.78933C3.75091 10.5706 3.75 11.5787 3.75 12.9981V13C3.75 14.4201 3.75091 15.4287 3.83041 16.2102C3.90865 16.9793 4.05673 17.4449 4.29772 17.8056C4.53484 18.1605 4.83953 18.4652 5.1944 18.7023C5.55507 18.9433 6.02071 19.0914 6.78975 19.1696C7.57133 19.2491 8.57993 19.25 10 19.25H14C15.4201 19.25 16.4287 19.2491 17.2102 19.1696C17.9793 19.0914 18.4449 18.9433 18.8056 18.7023C19.1605 18.4652 19.4652 18.1605 19.7023 17.8056C19.9433 17.4449 20.0914 16.9793 20.1696 16.2102C20.2491 15.4287 20.25 14.4201 20.25 13C20.25 11.5799 20.2491 10.5714 20.1695 9.7899C20.0913 9.02094 19.9431 8.55533 19.7021 8.19462C19.465 7.83975 19.1603 7.53506 18.8055 7.29795C18.4448 7.05695 17.9791 6.90887 17.2101 6.83064C16.4285 6.75113 15.4199 6.75022 13.9999 6.75022H9.99985C8.57978 6.75022 7.57118 6.75113 6.7896 6.83064ZM12 10.75C10.7574 10.75 9.75 11.7574 9.75 13C9.75 14.2426 10.7574 15.25 12 15.25C13.2426 15.25 14.25 14.2426 14.25 13C14.25 11.7574 13.2426 10.75 12 10.75ZM8.25 13C8.25 10.9289 9.92893 9.25 12 9.25C14.0711 9.25 15.75 10.9289 15.75 13C15.75 15.0711 14.0711 16.75 12 16.75C9.92893 16.75 8.25 15.0711 8.25 13ZM16.75 10C16.75 9.58579 17.0858 9.25 17.5 9.25H18C18.4142 9.25 18.75 9.58579 18.75 10C18.75 10.4142 18.4142 10.75 18 10.75H17.5C17.0858 10.75 16.75 10.4142 16.75 10Z";

const allowedAnimationSpeeds = [0.5, 1, 1.5, 2] as const;

Expand Down Expand Up @@ -484,6 +487,31 @@
return false;
}

/**
* Interpolates the default camera to match the point of view of another camera in the scene.
* @param id The id of the target camera to interpolate to.
* @returns A promise that resolves to `true` if the target camera is found and the interpolation is successful, `false` otherwise.
*/
public async matchCameraPOV(id: string): Promise<boolean> {
if (this._viewerDetails) {
let cameraInfos;
this._viewerDetails.viewer.pauseAnimation();
const camera = this._viewerDetails.scene.getCameraById(id);

if (camera instanceof ArcRotateCamera) {
cameraInfos = { alpha: camera.alpha, beta: camera.beta, radius: camera.radius, target: camera.target };
} else if (camera instanceof Camera) {
cameraInfos = await this._viewerDetails.viewer.getArcRotateCameraInfos(camera);
}

if (cameraInfos) {
this._viewerDetails.camera.interpolateTo(cameraInfos.alpha, cameraInfos.beta, cameraInfos.radius, cameraInfos.target);
return true;
}
}
return false;
}

/**
* The engine to use for rendering.
*/
Expand Down Expand Up @@ -702,6 +730,20 @@
return Object.keys(this.hotSpots).length > 0;
}

/**
* Get every scene cameras except of the default one
*/
private get _cameras(): Camera[] {
const cameras = this.viewerDetails?.scene.cameras.filter((camera) => {
return this.viewerDetails?.camera.id !== camera.id;
});
return cameras ?? [];
}

private get _hasCameras(): boolean {
return Object.keys(this._cameras).length > 0;
}

/**
* True if the default animation should play automatically when a model is loaded.
*/
Expand Down Expand Up @@ -766,8 +808,11 @@
@query("#canvasContainer")
private _canvasContainer: HTMLDivElement | undefined;

@query("#materialSelect")
private _materialSelect: HTMLSelectElement | undefined;
@query("#hotSpotSelect")
private _hotSpotSelect: HTMLSelectElement | undefined;

@query("#cameraSelect")
private _cameraSelect: HTMLSelectElement | undefined;

/**
* Toggles the play/pause animation state if there is a selected animation.
Expand Down Expand Up @@ -800,8 +845,12 @@
protected override update(changedProperties: PropertyValues<this>): void {
super.update(changedProperties);

if (this._materialSelect) {
this._materialSelect.value = "";
if (this._hotSpotSelect) {
this._hotSpotSelect.value = "";
}

if (this._cameraSelect) {
this._cameraSelect.value = "";
}

if (changedProperties.get("engine")) {
Expand Down Expand Up @@ -905,7 +954,7 @@
if (this._hasHotSpots) {
toolbarControls.push(html`
<div class="select-container">
<select id="materialSelect" aria-label="Select HotSpot" @change="${this._onHotSpotsChanged}">
<select id="hotSpotSelect" aria-label="Select HotSpot" @change="${this._onHotSpotsChanged}">
<!-- When the select is forced to be less wide than the options, padding on the right is lost. Pad with white space. -->
${Object.keys(this.hotSpots).map((name) => html`<option value="${name}">${name}&nbsp;&nbsp;</option>`)}
</select>
Expand All @@ -919,6 +968,24 @@
`);
}

// If other cameras exist in the scene, add camera controls.
if (this._hasCameras) {
toolbarControls.push(html`
<div class="select-container">
<select id="cameraSelect" aria-label="Select Camera" @change="${this._onCamerasChanged}">
<!-- When the select is forced to be less wide than the options, padding on the right is lost. Pad with white space. -->
${this._cameras.map((camera) => html`<option value="${camera.id}">${camera.name}&nbsp;&nbsp;</option>`)}
</select>
<!-- This button is not actually interactive, we want input to pass through to the select below. -->
<button style="pointer-events: none">
<svg viewBox="0 0 24 24">
<path d="${cameraFilledIcon}" fill="currentColor"></path>
</svg>
</button>
</div>
`);
}

// Add a vertical divider between each toolbar control.
const controlCount = toolbarControls.length;
const separator = html`<div class="divider"></div>`;
Expand Down Expand Up @@ -1003,6 +1070,14 @@
this.focusHotSpot(hotSpotName);
}

private _onCamerasChanged(event: Event) {
const selectElement = event.target as HTMLSelectElement;
const cameraId = selectElement.value;
// We don't actually want a selected value, this is just a one time trigger.
selectElement.value = "";
this.matchCameraPOV(cameraId);
}

// Helper function to simplify keeping Viewer properties in sync with HTML3DElement properties.
private _createPropertyBinding(
property: keyof ViewerElement,
Expand Down
Loading