Setting up the Three.js scene and camera
Introduction
In the previous articles in this series we talked about:
- Part 1: Reading VTK format particles with Javascript in a browser
- Part 2: Saving the read-in particle data in a Vuex store
- Part 3: Initialization of a store and the user interface.
- Part 4: Setting up the Three.js renderer
Let us now discuss how we can set up the scene and the camera.
Flash-back: The graphics panel
Recall that the three-graphics-panel
template introduced the components three-scene
and three-camera
in the file ThreeGraphicsPanel.vue
.
<template>
<div id='three-graphics-container'>
<div class="uk-card uk-card-default uk-card-large">
<div class="uk-card-body">
<three-renderer v-bind:size="{w:500, h:500}">
<three-scene v-bind:size="size">
<three-camera v-bind:size="size" v-bind:position="{x: 100, z: 15 }">
</three-camera>
<three-ellipsoid-particles> </three-ellipsoid-particles>
</three-scene>
</three-renderer>
</div>
</div>
</div>
</template>
<script src="./ThreeGraphicsPanel.ts"> </script>
The three-scene
component is a child of three-renderer
and has two children
of its own: three-camera
and three-ellipsoid-particles
.
The size
parameter is passed down from three-renderer
to its children.
The three-camera
component takes an additional position
property as input.
The three-scene
component
The three-scene
component has a slot for the three-camera
and
three-ellipsoid-particles
and the corresponding ‘ThreeScene.vue` file contains:
<template>
<div id="three-scene-div">
<slot></slot>
</div>
</template>
<script src="./ThreeScene.ts"> </script>
Note that we could have included the scene in three-renderer
instead of creating a
new component for it. That choice depends on the use-case, e.g., if there are
multiple scenes assigned to a single renderer and we would like to switch between them
then it makes sense to make the scene into a component.
The scene component is implemented in ThreeScene.ts
:
import * as Vue from "vue";
import THREE = require("three");
import { Component, Lifecycle, Prop, p } from 'av-ts';
import Store from './Store';
@Component({
name: 'ThreeScene'
})
export default class ThreeRenderer extends Vue {
@Prop
size = p({
type: Object, // { w, h }
required: true
});
@Lifecycle
created() {
// Get the scene from the store
let scene = Store.getters.scene;
// If a scene doesn't exist, create one
if (!scene) {
// Create the scene
scene = new THREE.Scene();
// Add the lights
let dirLight = new THREE.DirectionalLight( 0xffffff );
dirLight.position.set( 1, 1, 1 );
scene.add( dirLight );
dirLight = new THREE.DirectionalLight( 0x002288 );
dirLight.position.set( -1, -1, -1 );
scene.add( dirLight );
let ambLight = new THREE.AmbientLight( 0x222222 );
scene.add( ambLight );
// Update the scene
Store.commit('SET_SCENE', scene);
}
}
@Lifecycle
mounted() {
// Get the scene and the camera
let scene = Store.getters.scene;
let camera = Store.getters.camera;
if (camera) {
// Add the camera to the scene
scene.add(camera);
// Update the scene
Store.commit('SET_SCENE', scene);
} else {
console.log("Camera has not been created yet");
}
}
}
During the creation phase, we add lights to the scene but not the camera(s). This is because the camera component has not been instantiated yet. The camera is added to the scene during the mount stage.
We don’t use the size
property here, but keep it around in case it’s needed by
child components.
The three-camera
component
Now we are ready to create a camera that can be used to view the scene. The template
file ThreeCamera.vue
is essentially empty:
<template>
</template>
<script src="./ThreeCamera.ts"> </script>
The empty template
tags look redundant, but if we don’t include them in ThreeCamera.vue
, the ts-loader
fails with a compilation error.
The implementation of the camera component in ThreeCamera.ts
contains:
import * as Vue from "vue";
import THREE = require("three");
import { Component, Lifecycle, Prop, p } from 'av-ts';
import Store from "./Store";
import assign from './util';
@Component({
name: 'Camera',
})
export default class Camera extends Vue {
// "props"
@Prop
size = p({
type: Object, // { w, h }
});
@Prop
position = p({
type: Object // { x, y, z }
})
// "data"
d_size: any;
d_position: any;
@Lifecycle
created() {
// Keep local copies of the properties in case we need to modify them
this.d_size = this.size;
this.d_position = this.position;
let camera = Store.getters.camera;
if (!camera) {
let w = (<any>this.size).w;
let h = (<any>this.size).h;
camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000);
// Update the position if the template specifies that
if (this.position) {
this.updateCameraPosition(camera, this.position);
camera.updateProjectionMatrix(); // Don't forget this update
}
Store.commit('SET_CAMERA', camera);
}
}
private updateCameraPosition(camera: THREE.Camera, pos: any) : void {
console.log(pos);
assign(camera.position, pos);
camera.lookAt(new THREE.Vector3(0.0, 0.0, 0.0));
}
}
We use a THREE.PerspectiveCamera
in this example. The four parameters are the field of view angle in degrees, the
aspect ratio, the near plane distance, and the far plane distance of the camera frustum.
The camera position is determined by the input parameters and not by the positions of
the objects in the scene.
The aspect ratio is determined at this stage by the input parameters and not by the actual
window dimensions. Also, I recommended that you use the lookAt
method to make sure
that the camera is oriented as you would expect.
Don’t forget to update the projection matrix of the PerspectiveCamera
after you change
its position.
Remarks
Now that the scene and the camera have been set up, we can see how ellipsoid are created and displayed in the next part of this series.