Vue and ThreeJs - Part Two

Three.js Vue Vuex

Now that we have our three-dimensional screen rendering, let's see if we can add some controls that will allow the user to manipulate what they see.

Let's start by displaying the coordinates of the camera position within the scene. Create a new ControlPanel.vue component in your src/components/ directory.

We will use a Vuex getter to retrieve the camera position and display it on the control panel:

34getters: {
35 CAMERA_POSITION: state => {
36 return state.camera ? state.camera.position : null;
37 }
38},

Let's import this getter into our Control Panel component:

80import { mapGetters, mapMutations } from "vuex";
81export default {
82 data () {
83 return {
84 axisLinesVisible: true,
85 pyramidsVisible: true
86 };
87 },
88 computed: {
89 ...mapGetters(["CAMERA_POSITION"])
90 },
91 // ...
92}

You can see that we have also added two boolean flags to the component's data object. Later on we will use these to toggle the visibility of the pyramids and axis lines in our scene.

By mapping our vuex CAMERA_POSITION into the Control Panel's computed data object, we can display those coordinates and they will update in real time:

36<div
37 v-if="CAMERA_POSITION"
38 class="border-b border-grey-darkest mb-2 pb-2"
39>
40 <p class="mb-1 text-grey-light font-bold">
41 Camera Position
42 </p>
43 <p class="flex justify-between w-full mb-2 text-grey-light">
44 X:<span class="text-white">{{ CAMERA_POSITION.x }}</span>
45 </p>
46 <p class="flex justify-between w-full mb-2 text-grey-light">
47 Y:<span class="text-white">{{ CAMERA_POSITION.y }}</span>
48 </p>
49 <p class="flex justify-between w-full mb-2 text-grey-light">
50 Z:<span class="text-white">{{ CAMERA_POSITION.z }}</span>
51 </p>
52 <!-- more... -->
53</div>

(Check out the project repo to see how this component has been styled with Tailwind utility classes.)

I have found that the trackball control implementation in Three.js can be counter-intuitive at times. It is very easy for the user to end up somewhere they did not intend to go. Let's add a button to our control panel that will reset the camera position to origin. We will do that with (you guessed it) a Vuex mutation:

145mutations: {
146 // ...
147 SET_CAMERA_POSITION(state, { x, y, z }) {
148 if (state.camera) {
149 state.camera.position.set(x, y, z);
150 }
151 },
152 RESET_CAMERA_ROTATION(state) {
153 if (state.camera) {
154 state.camera.rotation.set(0, 0, 0);
155 state.camera.quaternion.set(0, 0, 0, 1);
156 state.camera.up.set(0, 1, 0);
157 state.controls.target.set(0, 0, 0);
158 }
159 },
160 // ...
161}

Note that resetting the camera position requires more than just changing the camera position. We also have to account for the rotation of the camera around three additional axis. (Check out the Three.js documentation for more details.)

Now that the mutation is in place, let's add it to our Control Panel:

52<p class="flex items-center">
53 <button
54 class="bg-grey-light cursor-pointer shadow p-2 mx-auto"
55 @click="resetCameraPosition"
56 >
57 Reset Camera
58 </button>
59</p>
91methods: {
92 ...mapMutations([
93 "SET_CAMERA_POSITION",
94 "RESET_CAMERA_ROTATION",
95 ]),
96 resetCameraPosition() {
97 this.SET_CAMERA_POSITION({ x: 0, y: 0, z: 500 });
98 this.RESET_CAMERA_ROTATION();
99 },
100 // ...
101}

Excellent! Everything is turning out very well so far. To wrap things up we will allow the user to manipulate what they see by selectively hiding the content of the scene. We will have two toggles in the control panel: one to control the pyramids and one to control the axis lines. Each will get it's own vuex mutation.

First the axis lines:

158mutations: {
159 // ..
160 HIDE_AXIS_LINES(state) {
161 state.scene.remove(...state.axisLines);
162 state.renderer.render(state.scene, state.camera);
163 },
164 SHOW_AXIS_LINES(state) {
165 state.scene.add(...state.axisLines);
166 state.renderer.render(state.scene, state.camera);
167 },
168 // ..
169}

Next, the pyramids:

166mutations: {
167 // ...
168 HIDE_PYRAMIDS(state) {
169 state.scene.remove(...state.pyramids);
170 state.renderer.render(state.scene, state.camera);
171 },
172 SHOW_PYRAMIDS(state) {
173 state.scene.add(...state.pyramids);
174 state.renderer.render(state.scene, state.camera);
175 }
176}

It is important to note here that we are only able to do this because we are keeping track of the scenery in our application state, separate from the camera and the rendered scene. If we had generated the scenery and used it to render the scene without saving it anywhere this would not be possible.

We can now import these methods into our Control Panel:

15<p class="flex items-center justify-between mb-1">
16 Pyramids
17 <input
18 type="checkbox"
19 name="pyramids"
20 id="pyramids"
21 v-model="pyramidsVisible"
22 @click="togglePyramids"
23 />
24</p>
25<p class="flex items-center justify-between">
26 Axis Lines
27 <input
28 type="checkbox"
29 name="axis-lines"
30 id="axis-lines"
31 v-model="axisLinesVisible"
32 @click="toggleAxisLines"
33 />
34</p>
91methods: {
92 ...mapMutations([
93 "SET_CAMERA_POSITION",
94 "RESET_CAMERA_ROTATION",
95 "HIDE_AXIS_LINES",
96 "SHOW_AXIS_LINES",
97 "HIDE_PYRAMIDS",
98 "SHOW_PYRAMIDS"
99 ]),
100 resetCameraPosition() {
101 this.SET_CAMERA_POSITION({ x: 0, y: 0, z: 500 });
102 this.RESET_CAMERA_ROTATION();
103 },
104 toggleAxisLines() {
105 if (this.axisLinesVisible) {
106 this.HIDE_AXIS_LINES();
107 this.axisLinesVisible = false;
108 } else {
109 this.SHOW_AXIS_LINES();
110 this.axisLinesVisible = true;
111 }
112 },
113 togglePyramids() {
114 if (this.pyramidsVisible) {
115 this.HIDE_PYRAMIDS();
116 this.pyramidsVisible = false;
117 } else {
118 this.SHOW_PYRAMIDS();
119 this.pyramidsVisible = true;
120 }
121 }
122}

You have now successfully used Vue, Vuex and Three.js to create and manage a three-dimensional scene in your browser. You can see this code in action here. This demonstration is somewhat simplistic, but the ideas here should provide a solid foundation for building a more realistic application.

About the Author

Ryan C. Durham is a software developer who lives in Portland, Oregon, with his wife and daughter. His numerous areas of interest include PHP, Laravel, Elixir and PostgreSQL, as well as organizational efficiency and communications strategies.

You can find him on GitHub and LinkedIn.