Skip to content
Draft
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
110 changes: 89 additions & 21 deletions packages/tools/sandbox/src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { type GlobalState } from "../globalState";
import { type GlobalState, type SandboxSceneLoadedInfo, type SandboxSceneLoadKind } from "../globalState";
import { FooterButton } from "./footerButton";
import { DropUpButton } from "./dropUpButton";
import { EnvironmentTools } from "../tools/environmentTools";
Expand All @@ -8,13 +8,17 @@ import { AnimationBar } from "./animationBar";
import { type Nullable } from "core/types";
import { type KHR_materials_variants } from "loaders/glTF/2.0/Extensions/KHR_materials_variants";
import { type Mesh } from "core/Meshes/mesh";
import { type Camera } from "core/Cameras/camera";
import { type Observer } from "core/Misc/observable";
import { DefaultCameraPresetOption } from "../tools/cameraPresetManager";

import "../scss/footer.scss";
import babylonIdentity from "../img/babylon-identity.svg";
import iconEdit from "../img/icon-edit.svg";
import iconOpen from "../img/icon-open.svg";
import iconIBL from "../img/icon-ibl.svg";
import iconCameras from "../img/icon-cameras.svg";
import iconCameraPreset from "../img/icon-camera-preset.svg";
import iconVariants from "../img/icon-variants.svg";

interface IFooterProps {
Expand All @@ -27,20 +31,41 @@ interface IFooterState {}
* Footer
*/
export class Footer extends React.Component<IFooterProps, IFooterState> {
private _cameraNames: string[] = [];
private _cameras: Camera[] = [];
private _sceneHadCameras = false;
private _sceneLoadKind: SandboxSceneLoadKind = "scene";
private readonly _onSceneLoadedObserver: Nullable<Observer<SandboxSceneLoadedInfo>>;
private readonly _onCameraChangedObserver: Nullable<Observer<Camera>>;
private readonly _onCameraPresetChangedObserver: Nullable<Observer<void>>;

public constructor(props: IFooterProps) {
super(props);
props.globalState.onSceneLoaded.add(() => {
this._updateCameraNames();
this._onSceneLoadedObserver = props.globalState.onSceneLoaded.add((info) => {
this._sceneHadCameras = info.scene.cameras.length > 0;
this._sceneLoadKind = info.loadKind;
this._updateCameras(info.scene);
this.forceUpdate();
});
if (props.globalState.currentScene) {
this._updateCameraNames();
this._onCameraChangedObserver = props.globalState.onCameraChanged.add(() => {
this._updateCameras();
this.forceUpdate();
});
this._onCameraPresetChangedObserver = props.globalState.cameraPresetManager.onChanged.add(() => {
this.forceUpdate();
});
if (props.globalState.currentScene) {
this._sceneHadCameras = props.globalState.currentSceneHadCameras;
this._sceneLoadKind = props.globalState.currentSceneLoadKind;
this._updateCameras(props.globalState.currentScene);
}
}

override componentWillUnmount() {
this._onSceneLoadedObserver?.remove();
this._onCameraChangedObserver?.remove();
this._onCameraPresetChangedObserver?.remove();
}

showInspector() {
if (this.props.globalState.currentScene) {
if (this.props.globalState.isDebugLayerEnabled) {
Expand All @@ -52,22 +77,42 @@ export class Footer extends React.Component<IFooterProps, IFooterState> {
}

switchCamera(index: number) {
const camera = this.props.globalState.currentScene.cameras[index];
const scene = this.props.globalState.currentScene;
const camera = this._cameras[index];

if (camera) {
if (this.props.globalState.currentScene.activeCamera) {
this.props.globalState.currentScene.activeCamera.detachControl();
if (scene && camera) {
const activeCamera = this.props.globalState.cameraPresetManager.deactivatePreset(scene, camera);
if (activeCamera) {
this.props.globalState.onCameraChanged.notifyObservers(activeCamera);
}
this.props.globalState.currentScene.activeCamera = camera;
camera.attachControl();
}
}

private _updateCameraNames(): void {
if (!!this.props.globalState.currentScene && this.props.globalState.currentScene.cameras.length > 0) {
this._cameraNames = this.props.globalState.currentScene.cameras.map((c) => c.name);
this._cameraNames.push("default camera");
switchCameraPreset(index: number) {
const scene = this.props.globalState.currentScene;
if (!scene) {
return;
}

if (index === 0) {
const camera = this.props.globalState.cameraPresetManager.deactivatePreset(scene);
if (camera) {
this.props.globalState.onCameraChanged.notifyObservers(camera);
}
return;
}

const preset = this.props.globalState.cameraPresetManager.presets[index - 1];
if (preset && this._sceneLoadKind === "scene") {
const camera = this.props.globalState.cameraPresetManager.activatePreset(preset.id, scene);
if (camera) {
this.props.globalState.onCameraChanged.notifyObservers(camera);
}
}
}

private _updateCameras(scene = this.props.globalState.currentScene): void {
this._cameras = scene ? scene.cameras.filter((camera) => !this.props.globalState.cameraPresetManager.isPresetCamera(camera)) : [];
}

private _getVariantsExtension(): Nullable<KHR_materials_variants> {
Expand Down Expand Up @@ -114,13 +159,21 @@ export class Footer extends React.Component<IFooterProps, IFooterState> {
}
}

const hasCameras = this._cameraNames.length > 1;
const cameraNames = this._cameras.map((camera) => camera.name);
const cameraPresets = this.props.globalState.cameraPresetManager.presets;
const cameraPresetNames = [DefaultCameraPresetOption, ...cameraPresets.map((preset) => preset.name)];
// A scene that arrived with one embedded camera historically showed this control; a camera-less scene with one generated camera did not.
const hasCameras = cameraNames.length > 1 || (cameraNames.length === 1 && this._sceneHadCameras);
const hasCameraPresets = !!this.props.globalState.currentScene && cameraPresets.length > 0 && this._sceneLoadKind === "scene";

// Determine footer class based on which controls are present
let footerClass = "footer";
if (hasCameras && hasVariants) {
const optionalControlCount = Number(hasCameras) + Number(hasCameraPresets) + Number(hasVariants);
if (optionalControlCount === 3) {
footerClass += " longest";
} else if (optionalControlCount === 2) {
footerClass += " longer";
} else if (hasCameras || hasVariants) {
} else if (optionalControlCount === 1) {
footerClass += " long";
}

Expand Down Expand Up @@ -162,12 +215,27 @@ export class Footer extends React.Component<IFooterProps, IFooterState> {
globalState={this.props.globalState}
icon={iconCameras}
label="Select camera"
options={this._cameraNames}
options={cameraNames}
activeEntry={() => this.props.globalState.currentScene?.activeCamera?.name || ""}
onOptionPicked={(option, index) => this.switchCamera(index)}
enabled={this._cameraNames.length > 1}
enabled={hasCameras}
searchPlaceholder="Search camera"
/>
<DropUpButton
globalState={this.props.globalState}
icon={iconCameraPreset}
label="Select camera preset"
options={cameraPresetNames}
activeEntry={() => {
const activeCamera = this.props.globalState.currentScene?.activeCamera;
return activeCamera && this.props.globalState.cameraPresetManager.isPresetCamera(activeCamera)
? (this.props.globalState.cameraPresetManager.activePreset?.name ?? DefaultCameraPresetOption)
: DefaultCameraPresetOption;
}}
onOptionPicked={(option, index) => this.switchCameraPreset(index)}
enabled={hasCameraPresets}
searchPlaceholder="Search camera preset"
/>
<DropUpButton
globalState={this.props.globalState}
icon={iconVariants}
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/sandbox/src/components/reflectorZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Reflector {
});
}

this._globalState.onSceneLoaded.notifyObservers({ scene: scene, filename: "Reflector scene" });
this._globalState.onSceneLoaded.notifyObservers({ scene: scene, filename: "Reflector scene", loadKind: "scene" });

// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._globalState.showDebugLayer();
Expand Down
80 changes: 50 additions & 30 deletions packages/tools/sandbox/src/components/renderingZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { WebGPUEngine } from "core/Engines/webgpuEngine";
import { SceneLoader } from "core/Loading/sceneLoader";
import { GLTFFileLoader } from "loaders/glTF/glTFFileLoader";
import { Scene } from "core/scene";
import { type ArcRotateCamera } from "core/Cameras/arcRotateCamera";
import { ArcRotateCamera } from "core/Cameras/arcRotateCamera";
import { type Camera } from "core/Cameras/camera";
import { type FramingBehavior } from "core/Behaviors/Cameras/framingBehavior";
import { EnvironmentTools } from "../tools/environmentTools";
import { Tools } from "core/Misc/tools";
Expand Down Expand Up @@ -53,6 +54,14 @@ function IsProjectAsset(extension: string): boolean {
return extension.toLowerCase() === "babylonproj";
}

function AddMissingKeys(keys: number[], additionalKeys: readonly number[]): void {
for (const key of additionalKeys) {
if (!keys.includes(key)) {
keys.push(key);
}
}
}

interface IRenderingZoneProps {
globalState: GlobalState;
expanded: boolean;
Expand All @@ -68,6 +77,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
private _scene: Scene;
private _canvas: HTMLCanvasElement;
private _restoreInspector = false;
private readonly _texturePreviewScenes = new WeakSet<Scene>();

public constructor(props: IRenderingZoneProps) {
super(props);
Expand Down Expand Up @@ -210,30 +220,30 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
});
}

prepareCamera() {
let camera = this._scene.activeCamera as ArcRotateCamera;
prepareCamera(): Camera {
let camera = this._scene.activeCamera;
// Attach camera to canvas inputs
if (!camera) {
this._scene.createDefaultCamera(true);

camera = this._scene.activeCamera! as ArcRotateCamera;
const defaultCamera = this._scene.activeCamera! as ArcRotateCamera;

if (this._currentPluginName === "gltf" || this._currentPluginName === "obj" || this._currentPluginName === "fbx") {
// glTF assets use a +Z forward convention while the default camera faces +Z. Rotate the camera to look at the front of the asset.
// We do this same for obj as it matches other viewers, but obj does not specify a forward convention.
// The FBX loader applies the same right-handed-to-left-handed flip as glTF, so its assets share the +Z forward convention.
camera.alpha += Math.PI;
defaultCamera.alpha += Math.PI;
}

// Enable camera's behaviors
camera.useFramingBehavior = true;
defaultCamera.useFramingBehavior = true;

const framingBehavior = camera.getBehaviorByName("Framing") as FramingBehavior;
const framingBehavior = defaultCamera.getBehaviorByName("Framing") as FramingBehavior;
framingBehavior.framingTime = 0;
framingBehavior.elevationReturnTime = -1;

if (this._scene.meshes.length) {
camera.lowerRadiusLimit = null;
defaultCamera.lowerRadiusLimit = null;

const worldExtends = this._scene.getWorldExtends(function (mesh) {
return mesh.isVisible && mesh.isEnabled();
Expand All @@ -242,20 +252,22 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
}

if (this.props.globalState.autoRotate) {
camera.useAutoRotationBehavior = true;
defaultCamera.useAutoRotationBehavior = true;
}

camera.pinchPrecision = 200 / camera.radius;
camera.upperRadiusLimit = 5 * camera.radius;
defaultCamera.pinchPrecision = 200 / defaultCamera.radius;
defaultCamera.upperRadiusLimit = 5 * defaultCamera.radius;

camera.wheelDeltaPercentage = 0.01;
camera.pinchDeltaPercentage = 0.01;
defaultCamera.wheelDeltaPercentage = 0.01;
defaultCamera.pinchDeltaPercentage = 0.01;

if (this.props.globalState.cameraPosition) {
camera.lowerRadiusLimit = null;
camera.setPosition(this.props.globalState.cameraPosition);
camera.lowerRadiusLimit = camera.radius;
defaultCamera.lowerRadiusLimit = null;
defaultCamera.setPosition(this.props.globalState.cameraPosition);
defaultCamera.lowerRadiusLimit = defaultCamera.radius;
}

camera = defaultCamera;
}

camera.attachControl();
Expand All @@ -274,13 +286,10 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
// this._canvas.style.opacity = "1";
const camera = this._scene.activeCamera! as ArcRotateCamera;
if (camera.keysUp) {
camera.keysUp.push(90); // Z
camera.keysUp.push(87); // W
camera.keysDown.push(83); // S
camera.keysLeft.push(65); // A
camera.keysLeft.push(81); // Q
camera.keysRight.push(69); // E
camera.keysRight.push(68); // D
AddMissingKeys(camera.keysUp, [90, 87]); // Z, W
AddMissingKeys(camera.keysDown, [83]); // S
AddMissingKeys(camera.keysLeft, [65, 81]); // A, Q
AddMissingKeys(camera.keysRight, [69, 68]); // E, D
}
}
}
Expand Down Expand Up @@ -314,6 +323,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
}

onSceneLoaded(filename: string) {
const loadKind = this._texturePreviewScenes.has(this._scene) ? "texture" : "scene";
this._scene.skipFrustumClipping = true;

if (this.props.globalState.toneMapping !== undefined) {
Expand All @@ -324,15 +334,21 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
this._scene.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_KHR_PBR_NEUTRAL;
}

this.props.globalState.onSceneLoaded.notifyObservers({ scene: this._scene, filename: filename });
this.props.globalState.onSceneLoaded.notifyObservers({ scene: this._scene, filename: filename, loadKind });

// The FBX loader creates animation groups but does not auto-play them the way the glTF loader does.
// Treat FBX assets like glTF in the Sandbox by playing the first animation group (looped) on load.
if (this._currentPluginName === "fbx" && this._scene.animationGroups.length > 0) {
this._scene.animationGroups[0].start(true);
}

const camera = this.prepareCamera();
this.prepareCamera();
if (loadKind === "scene" && !this.props.globalState.cameraPresetOverrideFromUrl) {
this.props.globalState.cameraPresetManager.applyActivePreset(this._scene);
}
if (this._scene.activeCamera) {
this.props.globalState.onCameraChanged.notifyObservers(this._scene.activeCamera);
}
Comment thread
VicenteCartas marked this conversation as resolved.
this.prepareLighting();
this.handleErrors();

Expand All @@ -345,11 +361,14 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
this._engine.runRenderLoop(() => {
// NOTE: this logic to adjust camera parameters based on radius is copied in viewer.ts.
// Please keep them in sync.
// Adapt the camera sensibility based on the distance to the object
camera.panningSensibility = 5000 / camera.radius;
// Update the camera speed based on the camera's distance from the target.
// TODO: This makes mouse wheel zooming behave well, but makes mouse based rotation a bit worse.
camera.speed = camera.radius * 0.2;
const activeCamera = this._scene.activeCamera;
if (activeCamera instanceof ArcRotateCamera && activeCamera.radius > 0) {
// Adapt the camera sensibility based on the distance to the object
activeCamera.panningSensibility = 5000 / activeCamera.radius;
// Update the camera speed based on the camera's distance from the target.
// TODO: This makes mouse wheel zooming behave well, but makes mouse based rotation a bit worse.
activeCamera.speed = activeCamera.radius * 0.2;
}
this._scene.render();
});
});
Expand All @@ -359,6 +378,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {

loadTextureAsset(url: string): Scene {
const scene = new Scene(this._engine);
this._texturePreviewScenes.add(scene);

const prevousUseOpenGLOrientationForUV = useOpenGLOrientationForUV;
setOpenGLOrientationForUV(true);
Expand Down
Loading
Loading