Skip to content

Commit

Permalink
feat: add orthoperspective camera tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
agviegas committed May 16, 2024
1 parent e0db231 commit 4c16880
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 33 deletions.
1 change: 1 addition & 0 deletions packages/components/src/core/Grids/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class Grids extends Component implements Disposable {
world.onDisposed.add(() => {
this.delete(world);
});
return grid;
}

delete(world: World) {
Expand Down
34 changes: 34 additions & 0 deletions packages/components/src/core/OrthoPerspectiveCamera/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="../../../resources/styles.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="icon" type="image/x-icon" href="../../../resources/favicon.ico">
<title>Simple 2D Scene</title>
<style>
body {
margin: 0;
padding: 0;
font-family: "Plus Jakarta Sans", sans-serif;
}

.full-screen {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
</style>
</head>

<body>
<div class="full-screen" id="container"></div>
<script type="module" src="./example.ts"></script>
</body>

</html>
210 changes: 210 additions & 0 deletions packages/components/src/core/OrthoPerspectiveCamera/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* eslint import/no-extraneous-dependencies: 0 */

import Stats from "stats.js";

import * as THREE from "three";
import * as BUI from "@thatopen/ui";
import * as OBC from "../..";

/* MD
### 📽️ Managing Multiple Views
---
Perspective view adds depth and realism, which helps in creating visually compelling representations in 3D scenes.🛤️
While, Orthographic view is important for precise measurements and proportions.📐
:::tip First, let's set up a simple scene!
👀 If you haven't started there, check out [that tutorial first](SimpleScene.mdx)!
:::
We'll be using an advanced camera component for this tutorial.
OrthoPerspectiveCamera makes it simple to use Orthographic and Perspective projections.
### 🎲 Creating a Cube Mesh
---
First, let's create a simple Cube, which will render differently depending on the projection you choose.🧊
We will create a [Cube](https://threejs.org/docs/index.html?q=box#api/en/geometries/BoxGeometry)
with `3x3x3` dimensions and use red color for the material.🖍️
*/

const cubeGeometry = new THREE.BoxGeometry();
const cubeMaterial = new THREE.MeshStandardMaterial({ color: "#6528D7" });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 0.5, 0);

/* MD
### 🎞️ Developing an OrthoPerspective Camera
---
We will create OrthoPerspectiveCamera by passing `components` as an argument to it.🗃️
The OrthoPerspective Camera extends the SimpleCamera by providing you with extra controls.
We will then configure the camera location and update the look at target using `setLookAt()` API.👀
*/

const container = document.getElementById("container")!;

const components = new OBC.Components();

const worlds = components.get(OBC.Worlds);

const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();

world.scene = new OBC.SimpleScene(components);
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);

components.init();

world.scene.setup();

world.camera.controls.setLookAt(3, 3, 3, 0, 0, 0);

world.scene.three.add(cube);
world.meshes.add(cube);

const grids = components.get(OBC.Grids);
const grid = grids.create(world);

/* MD
:::info Igniting Components!
🔥 Whenever the components like scene, camera are created, you need to initialize the component library.
Check out components.init() for more info!🔖
:::
### 🕹️ Changing Views and Navigation
---
Now, that our camera setup is done, we need to manage the camera projection on demand.
#### Toggling Orthographic View and Perspective View
Let's create a simple method **`toggleProjection()`** which toggles the Camera View using `camera.toggleProjection`.
Alternatively, you can also use `camera.setProjection()` and pass `'Orthographic'` or `'Perspective'` to manage the views.💡
*/

// @ts-ignore
function toggleProjection() {
world.camera.projection.toggle();
}

/* MD
You can also subscribe to an event for when the projection changes. For instance, let's change the grid fading mode
when the projection changes. This will make the grid look good in orthographic mode:
*/

world.camera.projection.onChanged.add(() => {
const projection = world.camera.projection.current;
grid.fade = projection === "Perspective";
});

/* MD
#### Managing Navigation Modes
Along with projection, we can also manage Navigation modes using **OrthoPerspective** camera.
To update navigation modes, we will use `camera.setNavigationMode('Orbit' | 'FirstPerson' | 'Plan')`
- **Orbit** - Orbit Mode helps us to easily navigate around the 3D Elements.
- **FirstPerson** - It helps you to visualize scene from your own perspective.
First Person mode is only available for Perspective Projection.
- **Plan** - This mode helps you to easily navigate in 2D Projections.
*/

// @ts-ignore
function setNavigationMode(navMode: OBC.NavModeID) {
world.camera.set(navMode);
}

/* MD
:::info MORE CONTROLS, MORE POWER
🧮 OrthoPerspective Camera also provides you an option to adjust your camera to fit the 3D elements.
You can simply use fitModelToFrame(mesh)
and provide the mesh which you want to fit to your window frame
:::
**Congratulations** 🎉 on completing this tutorial!
Now you can add Advance Camera System to your web-app in minutes using
**OrthoPerspectiveCamera** ⌚📽️
Let's keep it up and check out another tutorial! 🎓
*/

BUI.Manager.registerComponents();

const panel = BUI.Component.create<BUI.PanelSection>(() => {
return BUI.html`
<bim-panel active label="Orthoperspective Camera Tutorial"
style="position: fixed; top: 5px; right: 5px">
<bim-panel-section style="padding-top: 10px;">
<bim-dropdown required label="Navigation mode"
@change="${({ target }: { target: BUI.Dropdown }) => {
const selected = target.value[0] as OBC.NavModeID;
const { current } = world.camera.projection;
const isOrtho = current === "Orthographic";
const isFirstPerson = selected === "FirstPerson";
if (isOrtho && isFirstPerson) {
alert("First person is not compatible with ortho!");
target.value[0] = world.camera.mode.id;
return;
}
world.camera.set(selected);
}}">
<bim-option checked label="Orbit"></bim-option>
<bim-option label="FirstPerson"></bim-option>
<bim-option label="Plan"></bim-option>
</bim-dropdown>
<bim-dropdown required label="Camera projection"
@change="${({ target }: { target: BUI.Dropdown }) => {
const selected = target.value[0] as OBC.CameraProjection;
const isOrtho = selected === "Orthographic";
const isFirstPerson = world.camera.mode.id === "FirstPerson";
if (isOrtho && isFirstPerson) {
alert("First person is not compatible with ortho!");
target.value[0] = world.camera.projection.current;
return;
}
world.camera.projection.set(selected);
}}">
<bim-option checked label="Perspective"></bim-option>
<bim-option label="Orthographic"></bim-option>
</bim-dropdown>
<bim-button
label="Fit cube"
@click="${() => {
world.camera.fit([cube]);
}}">
</bim-button>
</bim-panel-section>
</bim-panel>
`;
});

document.body.append(panel);

// Set up stats
const stats = new Stats();
stats.showPanel(2);
document.body.append(stats.dom);
stats.dom.style.left = "0px";
world.renderer.onBeforeUpdate.add(() => stats.begin());
world.renderer.onAfterUpdate.add(() => stats.end());
45 changes: 30 additions & 15 deletions packages/components/src/core/OrthoPerspectiveCamera/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as THREE from "three";
import { Components } from "../../core/Components";
import { SimpleCamera } from "../../core";
import { Components } from "../Components";
import { SimpleCamera } from "..";

import {
NavigationMode,
Expand All @@ -23,37 +23,51 @@ export class OrthoPerspectiveCamera extends SimpleCamera {
/**
* The current {@link NavigationMode}.
*/
mode: NavigationMode;
_mode: NavigationMode | null = null;

readonly projection: ProjectionManager;

readonly threeOrtho: THREE.OrthographicCamera;

readonly threePersp: THREE.PerspectiveCamera;

protected readonly _userInputButtons: any = {};

protected readonly _frustumSize = 50;

protected readonly _navigationModes = new Map<NavModeID, NavigationMode>();

get current() {
return this.projection.camera;
get mode() {
if (!this._mode) {
throw new Error("Mode not found, camera not initialized");
}
return this._mode;
}

constructor(components: Components) {
super(components);

this.threePersp = this.three as THREE.PerspectiveCamera;
this.threeOrtho = this.newOrthoCamera();
this.projection = new ProjectionManager(this);

this._navigationModes.set("Orbit", new OrbitMode(this));
this._navigationModes.set("FirstPerson", new FirstPersonMode(this));
this._navigationModes.set("Plan", new PlanMode(this));

this.mode = this._navigationModes.get("Orbit")!;
this.mode.set(true, { preventTargetAdjustment: true });
this.onAspectUpdated.add(() => {
this.setOrthoCameraAspect();
});

this.projection = new ProjectionManager(this);
this.projection.onChanged.add(
(camera: THREE.PerspectiveCamera | THREE.OrthographicCamera) => {
this.three = camera;
},
);

this.onAspectUpdated.add(() => this.setOrthoCameraAspect());
this.onWorldChanged.add(() => {
this._navigationModes.clear();
this._navigationModes.set("Orbit", new OrbitMode(this));
this._navigationModes.set("FirstPerson", new FirstPersonMode(this));
this._navigationModes.set("Plan", new PlanMode(this));
this._mode = this._navigationModes.get("Orbit")!;
this.mode.set(true, { preventTargetAdjustment: true });
});
}

/** {@link Disposable.dispose} */
Expand All @@ -68,12 +82,13 @@ export class OrthoPerspectiveCamera extends SimpleCamera {
* @param mode - The {@link NavigationMode} to set.
*/
set(mode: NavModeID) {
if (this.mode === null) return;
if (this.mode.id === mode) return;
this.mode.set(false);
if (!this._navigationModes.has(mode)) {
throw new Error("The specified mode does not exist!");
}
this.mode = this._navigationModes.get(mode)!;
this._mode = this._navigationModes.get(mode)!;
this.mode.set(true);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as THREE from "three";
import CameraControls from "camera-controls";
import { NavigationMode } from "./types";
import { OrthoPerspectiveCamera } from "../index";

/**
* A {@link NavigationMode} that allows first person navigation,
Expand All @@ -13,15 +14,15 @@ export class FirstPersonMode implements NavigationMode {
/** {@link NavigationMode.id} */
readonly id = "FirstPerson";

constructor(private camera: any) {}
constructor(private camera: OrthoPerspectiveCamera) {}

/** {@link NavigationMode.set} */
set(active: boolean) {
this.enabled = active;
if (active) {
const projection = this.camera.getProjection();
const projection = this.camera.projection.current;
if (projection !== "Perspective") {
this.camera.setNavigationMode("Orbit");
this.camera.set("Orbit");
return;
}
this.setupFirstPersonCamera();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as THREE from "three";
import { NavigationMode } from "./types";
import { OrthoPerspectiveCamera } from "../index";

/**
* A {@link NavigationMode} that allows 3D navigation and panning
Expand All @@ -12,7 +13,7 @@ export class OrbitMode implements NavigationMode {
/** {@link NavigationMode.id} */
readonly id = "Orbit";

constructor(public camera: any) {
constructor(public camera: OrthoPerspectiveCamera) {
this.activateOrbitControls();
}

Expand All @@ -33,7 +34,7 @@ export class OrbitMode implements NavigationMode {
const distance = position.length();
controls.distance = distance;
controls.truckSpeed = 2;
const { rotation } = this.camera.get();
const { rotation } = this.camera.three;
const direction = new THREE.Vector3(0, 0, -1).applyEuler(rotation);
const target = position.addScaledVector(direction, distance);
controls.moveTo(target.x, target.y, target.z);
Expand Down
Loading

0 comments on commit 4c16880

Please sign in to comment.