Skip to content

Commit

Permalink
Change func generateArcRotateCamera in getArcRotateCameraInfos
Browse files Browse the repository at this point in the history
  • Loading branch information
cournoll committed Jan 16, 2025
1 parent 89202c3 commit 1a18638
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 57 deletions.
95 changes: 53 additions & 42 deletions packages/tools/viewer/src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ export class ViewerHotSpotResult {
public visibility: number = NaN;
}

/**
* 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 @@ -334,6 +339,7 @@ export class Viewer implements IDisposable {
private _toneMappingType: number;
private _contrast: number;
private _exposure: number;
private _modelWorldCenter: Nullable<Vector3> = null;

private _suspendRenderCount = 0;
private _isDisposed = false;
Expand Down Expand Up @@ -1021,58 +1027,70 @@ export class Viewer implements IDisposable {
}

/**
* Creates a new `ArcRotateCamera` positioned at the same location as the given camera and targeted at a computed point.
* - The target point is determined based on the camera's forward ray:
* 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 set the position and compute the target.
* @returns A new `ArcRotateCamera` instance targeted at the determined point, or `null` if no target is available.
* @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 generateArcRotateCamera(camera: Camera): Promise<Nullable<ArcRotateCamera>> {
public async getArcRotateCameraInfos(camera: Camera): Promise<Nullable<ViewerArcRotateCameraInfos>> {
await import("core/Culling/ray");
const ray = camera.getForwardRay(100, camera.getWorldMatrix(), camera.globalPosition);
const comGlobalPos = camera.globalPosition.clone();

if (this._details.model) {
if (this._details.model && this._modelWorldCenter) {
const model = this._details.model;
let rootMesh: Nullable<AbstractMesh> = null; // the mesh with name "__root__" or the first of model.meshes

// Refresh bounding info to ensure morph target and skeletal animations are taken into account.
model.meshes.forEach((mesh) => {
if (!rootMesh || mesh.name === "__root__") rootMesh = mesh;
this._refreshBoundingInfo(model);

let cache = this._meshDataCache.get(mesh);
if (!cache) {
cache = {};
this._meshDataCache.set(mesh, cache);
}
mesh.refreshBoundingInfo({ applyMorph: true, applySkeleton: true, cache });
});

let targetPoint: Nullable<Vector3>;
// Target
let targetPoint: Vector3 = Vector3.Zero();
const pickingInfo = this._details.scene.pickWithRay(ray, (mesh) => model.meshes.includes(mesh));
if (pickingInfo && pickingInfo.hit) {
targetPoint = pickingInfo.pickedPoint;
targetPoint.copyFrom(pickingInfo.pickedPoint!);
} else {
const direction = ray.direction.clone();
const cameraPosition = camera.globalPosition;
targetPoint = cameraPosition.clone();

const rootMeshCenter = rootMesh ? (rootMesh as AbstractMesh).getBoundingInfo().boundingBox.centerWorld : null;
const distance = rootMeshCenter ? Vector3.Distance(cameraPosition, rootMeshCenter) : 0.1;

targetPoint.copyFrom(comGlobalPos);
const distance = Vector3.Distance(comGlobalPos, this._modelWorldCenter);
direction.scaleAndAddToRef(distance, targetPoint);
}

const newCamera = new ArcRotateCamera("ArcRotateCamera " + camera.name, 0, 0, 1, Vector3.Zero(), this._details.scene);
if (targetPoint) {
newCamera.setPosition(camera.globalPosition.clone());
newCamera.setTarget(targetPoint);
return newCamera;
const computationVector = Vector3.Zero();
comGlobalPos.subtractToRef(targetPoint, computationVector);

// 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;
}

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

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

return null;
Expand Down Expand Up @@ -1156,6 +1174,7 @@ export class Viewer implements IDisposable {

const worldSize = worldExtents.max.subtract(worldExtents.min);
const worldCenter = worldExtents.min.add(worldSize.scale(0.5));
this._modelWorldCenter = worldCenter.clone();

goalRadius = worldSize.length() * 1.1;

Expand Down Expand Up @@ -1225,15 +1244,7 @@ export class Viewer implements IDisposable {
await import("core/Culling/ray");
if (this._details.model) {
const model = this._details.model;
// 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._details.scene.pick(screenX, screenY, (mesh) => model.meshes.includes(mesh));
if (pickingInfo.hit) {
Expand Down
30 changes: 15 additions & 15 deletions packages/tools/viewer/src/viewerElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,24 +488,24 @@ export class HTML3DElement extends LitElement {
* @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 interpolateCameraTo(id: string): Promise<boolean> {
let result = false;
let generatedCamera = false;

public async matchCameraPOV(id: string): Promise<boolean> {
if (this._viewerDetails) {
let camera = this._viewerDetails.scene.getCameraById(id);
if (camera instanceof Camera && !(camera instanceof ArcRotateCamera)) {
camera = await this._viewerDetails.viewer.generateArcRotateCamera(camera);
generatedCamera = true;
}
let cameraInfos;
this._viewerDetails.viewer.pauseAnimation();
const camera = this._viewerDetails.scene.getCameraById(id);

if (camera instanceof ArcRotateCamera) {
this._viewerDetails.camera.interpolateTo(camera.alpha, camera.beta, camera.radius, camera.target);
if (generatedCamera) camera.dispose();
result = true;
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);
}
}

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

/**
Expand Down Expand Up @@ -1071,7 +1071,7 @@ export class HTML3DElement extends LitElement {
const cameraId = selectElement.value;
// We don't actually want a selected value, this is just a one time trigger.
selectElement.value = "";
this.interpolateCameraTo(cameraId);
this.matchCameraPOV(cameraId);
}

// Helper function to simplify keeping Viewer properties in sync with HTML3DElement properties.
Expand Down

0 comments on commit 1a18638

Please sign in to comment.