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 10 commits into
base: master
Choose a base branch
from

Conversation

cournoll
Copy link

This change introduces a way to interpolate the viewer's default camera to other existing cameras in the scene. The primary goal of this feature is to enable matching the POV of cameras defined in a GLTF file, without switching the active camera to try to keep a "simple" experience for non-expert users.

  • getArcRotateCameraInfos function compute the alpha, beta, and radius angles, as well as the target point for a camera that is not an ArcRotateCamera.
  • For target selection, I used a straightforward approach, the target point is determined based on the camera's forward ray. If an intersection with the meshes from AssetContainer 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 AssetContainer world center along the forward ray.

@cournoll cournoll force-pushed the viewer-select-cameras branch from 1a18638 to 471b841 Compare January 16, 2025 16:45
@bjsplat
Copy link
Collaborator

bjsplat commented Jan 16, 2025

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat
Copy link
Collaborator

bjsplat commented Jan 16, 2025

@bjsplat
Copy link
Collaborator

bjsplat commented Jan 16, 2025

@bjsplat
Copy link
Collaborator

bjsplat commented Jan 16, 2025

@ryantrem
Copy link
Member

I was thinking from our initial discussions that we would simply translate these cameras directly to hotspot entries on the element (rather than adding a new cameras concept). Any reason not to go that route? If we do, then I think a few changes to this PR would be good:

  1. getArcRotateCameraInfos can be a private function in ViewerElement (just an internal implementation detail for now).
  2. ViewerDetails could be updated such that the model property returns an object of type Model rather than AssetContainer (breaking change on Viewer, but I still think this is ok since it is @experimental and lower level with minimal current use outside of ViewerElement). Alternatively we could just add more properties to ViewerDetails that are model specific, like the model's bounding center point.
  3. If we only want hotspots for the active model, then in ViewerElements onModelChanged handler, add to the hotspot collection camera-based hotspots for that model (and remove any previous camera-based hotspots).
  4. If we want hotspots for all cameras from all loaded models, then I think it is a little trickier because we need to add camera-based hotspots any time a model is loaded and remove them any time a previously loaded model is removed from the scene.

Which behavior between #3 and #4 are you guys aiming for?

@cournoll
Copy link
Author

cournoll commented Jan 17, 2025

I was thinking from our initial discussions that we would simply translate these cameras directly to hotspot entries on the element (rather than adding a new cameras concept). Any reason not to go that route?

Not really any specific reason. Initially, during development, I wanted a distinct visual indication so the user could understand that this behavior in the viewer was tied to the cameras in the scene. But once I finished my code and realized that focusing on a hotspot or matching a camera’s POV ultimately relies on the same principle (an interpolation) I understood that we could merge the two. :)

That said, I’m still wondering how we could clarify for users where the names and values of these "undeclared hotspots" originate.

@cournoll
Copy link
Author

cournoll commented Jan 17, 2025

1. `getArcRotateCameraInfos` can be a private function in `ViewerElement` (just an internal implementation detail for now).

I had a similar feeling about that, so it works perfectly for me.

2. `ViewerDetails` could be updated such that the `model` property returns an object of type `Model` rather than `AssetContainer` (breaking change on `Viewer`, but I still think this is ok since it is @experimental and lower level with minimal current use outside of `ViewerElement`). Alternatively we could just add more properties to `ViewerDetails` that are model specific, like the model's bounding center point.

Yes, yes, yes! Having the bounding info in ViewerDetails would be very nice for the user. Got it, I’ll add that to this PR.

3. If we only want hotspots for the active model, then in `ViewerElement`s `onModelChanged` handler, add to the hotspot collection camera-based hotspots for that model (and remove any previous camera-based hotspots).

4. If we want hotspots for all cameras from all loaded models, then I think it is a little trickier because we need to add camera-based hotspots any time a model is loaded and remove them any time a previously loaded model is removed from the scene.

We will aiming for behavior 4, so i'll try to do something that stay consistent with the Babylon viewer approach and let us fit with our needed 👍

packages/tools/viewer/src/viewer.ts Outdated Show resolved Hide resolved
*/
public async getArcRotateCameraInfos(camera: Camera): Promise<Nullable<ViewerArcRotateCameraInfos>> {
await import("core/Culling/ray");
const ray = camera.getForwardRay(100, camera.getWorldMatrix(), camera.globalPosition);
Copy link
Member

Choose a reason for hiding this comment

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

This is equivalent to just getForwardRay() (which we do in another part of this file).

Copy link
Author

Choose a reason for hiding this comment

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

It seems that the default origin of the forward ray is actually the camera position rather than the camera’s global position.

packages/tools/viewer/src/viewer.ts Outdated Show resolved Hide resolved
packages/tools/viewer/src/viewer.ts Outdated Show resolved Hide resolved
@ryantrem
Copy link
Member

We will aiming for behavior 4, so i'll try to do something that stay consistent with the Babylon viewer approach and let us fit with our needed 👍

Ok, I guess in this case we can just check Scene.cameras and watch Scene.onNewCameraAddedObservable and Scene.onCameraRemovedObservable. I think we should probably also have a new property/attribute like cameras-as-hotspots that is disabled by default (e.g. you have to explicitly opt into this behavior).

Remove useless ray parameters
Factor code on radius calculation
Use _tempVectors for computation
Handle mesh predicate as parameter of function getArcRotateCameraInfos
Add property camerasAsHotSpots to active cameras hot spots
Fix camera forward ray
* @param offset The directional offset between the source position and the target position
* @returns The alpha angle in radians
*/
public static computeAlpha(offset: Vector3): number {
Copy link
Member

Choose a reason for hiding this comment

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

Since these functions don't access this, can we just make them module level functions?

Comment on lines +240 to +247
export type ModelBoundingInfo = {
worldExtents: {
min: Vector3;
max: Vector3;
};
worldSize: Vector3;
worldCenter: Vector3;
};
Copy link
Member

Choose a reason for hiding this comment

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

A few thoughts on this new type:

  1. We don't expose Vector3 directly for other parts of the interface (like hotspots). Could we switch these to [x: number, y: number, z: number] like we have elsewhere?
  2. Make it readonly either in the definition or in the usage (since it is a stored/persistent value, not a transient value that is just returned from a function with a single usage)? I think definition would be ok, but either works.
  3. To make this more re-usable (in case in the future we want to use it for something not in world space, or for something other than models), could we rename to not include "world" or "model"?
Suggested change
export type ModelBoundingInfo = {
worldExtents: {
min: Vector3;
max: Vector3;
};
worldSize: Vector3;
worldCenter: Vector3;
};
export type BoundingInfo = {
extents: Readonly<{
readonly min: readonly [x: number, y: number, z: number];
readonly max: readonly [x: number, y: number, z: number];
}>;
readonly size: readonly [x: number, y: number, z: number];
readonly center: readonly [x: number, y: number, z: number];
};

And then in the usage below, have worldBounds?: BoundingInfo

Comment on lines +698 to +704
set hotSpots(value: Record<string, HotSpot>) {
this._hotSpots = value;
}

get hotSpots() {
return this._hotSpots;
}
Copy link
Member

Choose a reason for hiding this comment

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

Any reason for changing this to have a getter/setter?

* A string value that encodes one or more hotspots.
*/
private _hotSpots: Record<string, HotSpot> = {};

@property({
Copy link
Member

Choose a reason for hiding this comment

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

Did we lose this doc comment?

@@ -1125,6 +1136,27 @@ export abstract class ViewerElement<ViewerClass extends Viewer = Viewer> extends
this._dispatchCustomEvent("animationprogresschange", (type) => new Event(type));
});

details.scene.onNewCameraAddedObservable.add((camera) => {
if (camera.uniqueId !== details.camera.uniqueId && this.camerasAsHotSpots) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we just compare the camera instance rather than the uniqueId?

Suggested change
if (camera.uniqueId !== details.camera.uniqueId && this.camerasAsHotSpots) {
if (camera !== details.camera && this.camerasAsHotSpots) {

@@ -1125,6 +1136,27 @@ export abstract class ViewerElement<ViewerClass extends Viewer = Viewer> extends
this._dispatchCustomEvent("animationprogresschange", (type) => new Event(type));
});

details.scene.onNewCameraAddedObservable.add((camera) => {
if (camera.uniqueId !== details.camera.uniqueId && this.camerasAsHotSpots) {
Copy link
Member

Choose a reason for hiding this comment

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

With the current impl, if you load some models and then toggle camerasAsHotSpots, it will not actually affect the hotSpots. I think it would be a good improvement to factor our the handlers to functions like addCameraHotSpot and removeCameraHotSpot and then call them for these observables, but also when camerasAsHotSpots is enabled/disabled (probably in update if the property has changed).

/**
* Updates the bounding info for the model by computing its maximum extents, size, and center considering animation, skeleton, and morph targets.
*/
private _updateModelBoundingInfo(): void {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of having this function mutate this._modelInfo, can we:

  1. Move it out to a utility function (within this file, but not inside the class) that takes an AssetContainer and returns a BoundingInfo
  2. Call it in loadModel and set the bounding info for the model when it is loaded
  3. Make the worldBounds property of the ModelInfo part of the readonly section (since it will always be available as it would be computed when the model loads)

* @param predicate Used to define predicate for selecting meshes and instances (if exist)
* @returns An object containing the alpha, beta, radius and target properties, or null if no model found
*/
public async getArcRotateCameraInfos(camera: Camera, predicate?: MeshPredicate): Promise<Nullable<ViewerArcRotateCameraInfos>> {
Copy link
Member

Choose a reason for hiding this comment

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

As we discussed a bit on the side, can we move this now to be a private function inside viewerElement.ts since ViewerElement will now have access to the bounding info through the model info?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants