ThreeJs Vite Skeleton project
This hre is a small skeleton starter project from my tutorial over at dev.to.
It includes Three, Three-Controls and Vite.
You can clone this repo, run 'npm install' and the 'npm run dev'.
Posted on February 7, 2022
If you don't know what Threejs is i've created a example of a playable 3d Chess board that runs in the browser and could live on your webpage. And if that isn't enough to get you excited the ThreeJS homepage has a ton of amazing examples.
Now if you have never worked with 3D software, or ThreeJs, it can all seem very daunting at first because there are a lot of moving parts. We will need a scene were our objects will live, then we need a camera, 3D objects, Controls, Lights and a Renderer that turns our 3D into 2D that our monitor can display. I know it made my head spin for a bit.
So in this article i want to ease you through the moving parts of ThreeJs. And by the end of this article you will have a nice skeleton app that you can start messing with. The final result will look something like this.
This article is not meant as a tutorial, but as a low level getting started, so you can go and explore on your own.
But enough talk. Let s get started.
Like with any project we are gonna need some libraries to work with. In this case we need ThreeJs itself and Three-Controls. So lets install them with this npm command.
npm install three three-controls
And then we have to import these into our project like shown below.
import * as THREE from "three";
import * as ThreeControls from "three-controls";
Basic good stuff :).
To give our app some organization we are gonna split things up a bit into three functions. So lets paste the code below into our project.
let scene, camera, renderer, controls;
const init = () => {};
const animate = () => {
render();
};
const render = () => {};
init();
animate();
First we declare some variables for scene, camera, renderer and controls. We do this outside of our functions so that each function can have access to them.
The init function will be responsible for creating our 3D scene and objects. The animate function will run as a recursive function so that we can animate things and finaly the render function we be responsible for rendering things to the screen.
And of course we have to call these functions at the end.
Before we can display any 3D objects, we first need a Scene for our objects to live inside. If you ever used any 3D software you can think of this as your viewport. The scene is basically a container the lets us add and position 3D objects inside of.
const init = () => {
/*
* 01 - Create the scene.
* Docs: ttps://threejs.org/docs/#api/en/scenes/Scene
*/
scene = new THREE.Scene();
};
Here we simply create a new instance of THREE.Scene and assign it to the scene variable we declared at the top of our project.
Next we need a camera in our scene that the renderer will use to determine what it should display to the screen.
const init = () => {
/*
* 02 - Create Camera.
* Docs: https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
* Camera: https://threejs.org/docs/#api/en/cameras/Camera
*/
camera = new THREE.PerspectiveCamera(
43,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.lookAt(0, 0, 0);
camera.position.set(20, 20, 20);
};
So we create a new instance of THREE.PerspectiveCamera and assign it to the camera variable, and we pass it a config object that determines the frustum. Frustum culling is a entire topic of its own, but basicaly it determines what the camera is able to seen as far as height, width and depth.
We then call the lookAt method and pass it 0,0,0 for the x,y and z coordinates. This makes sure that the camera will always point to the center of our scene.
And lastly we call the set method on it's position. This moves our camera away from the center. So that it can actualy see the center instead of sitting on it.
Next we need a renderer. A renderer's job is to turn our 3D scene into a 2D image that our monitors can display. So lets add one.
const init = () => {
/*
* 03 - Create renderer.
* Docs: https://threejs.org/docs/#api/en/renderers/WebGLRenderer
*/
renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
};
With this snippet we create a new instance of THREE.WebGLRenderer, and pass it a config object. The alpha setting makes sure that our canvas will have a transparent background, so we can set the background color ourselfs using css. And we set antialias to true. This will make sure that we dont have any ugly sharp edges on our objects.
We then call setSize on the renderer so that it knows what size image we want to render. In this case we just pass it the width and height of our viewport. But if you are rendering to an element of your webpage you will have to change these to the desired dimensions.
Now that we have a renderer we have to add it to the document so we can see it..
const init = () => {
/*
* 04 - Append to document.
*/
document.body.appendChild(renderer.domElement);
};
The renderer provides a convenient domElement that we can use. And in this example we append that to the body of the document. Again if you are rendering to a specific element you will have to append the renderer.domElement to that element.
There is nothing we can do to prevent users from resizing their window, so we have to make sure we update our renderer and camera when this happens.
const init = () => {
/*
* 05 - Update renderer on window resize.
*/
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
};
To do that we add an event listener to the window, and when this fires we update our renderer's size, the camera's aspect ratio and we update the camera's ProjectionMatrix.
3D is no fun if we cant move around our scene, so lets add some controls to make that possible.
const init = () => {
/*
* 06 - Create OrbitControls.
* Docs: https://threejs.org/docs/#examples/en/controls/OrbitControls
*/
controls = new ThreeControls.OrbitControls(camera, renderer.domElement);
};
To enable these controls we have to create a new instance of ThreeControls.OrbitControls and pass it our camera and renderer.domElement so that it knows what to control.
These controls will allow us to zoom and pan around our scene by dragging our mouse or using our mouse wheel.
Now lets create something that we can look at. First we are gonna have to create a geometry. A geometry object contains the mathematical representation of an object. in this case we will create a simple cube.
const init = () => {
/*
* 07 - Create a geometry.
* Docs: https://threejs.org/docs/#api/en/geometries/BoxGeometry
* BufferGeometry: https://threejs.org/docs/#api/en/core/BufferGeometry
*/
const cubeGeometry = new THREE.BoxGeometry(5, 5, 5);
};
This line of code create a new instance of the THREE.BoxGeometry and passes it 5 for the x, y and z dimensions. This will result in a cube with a size of 5 units.
To see our new cube we have to give it a material. A material determines what the colors on the outside of the cube will look like.
const init = () => {
/*
* 08 - Create a material.
* Docs: https://threejs.org/docs/#api/en/materials/MeshLambertMaterial
* Materials: https://threejs.org/docs/#api/en/materials/Material
*/
const whiteMaterial = new THREE.MeshLambertMaterial(0x7f7f7f);
};
Here we create a pure white material by creating a new THREE.MeshLambertMaterial instance, and passing it a white color code.
Next we have to combine our cube geometry with our white material into a mesh object that we can place into our scene.
In general all visible objects in our scene will be a combination of a geometry and a material combined into a mesh. So this is a repeating process for most of our objects. Keep in mind that we can reuse our geometries and materials to make other combinations.
const init = () => {
/*
* 09 - Create a mesh.
* Docs: https://threejs.org/docs/#api/en/objects/Mesh
*/
const cubeMesh = new THREE.Mesh(cubeGeometry, whiteMaterial);
cubeMesh.position.set(0, 0, 0);
scene.add(cubeMesh);
};
Here we create a new THREE.Mesh instance and pass it our cubeGeometry and whiteMaterial to create a cubeMesh.
Then we set its position to 0,0,0 (center of scene) and add it to the scene with the scene.add() method.
Our scene now has a cube but in order to see it we are going to need some lights as well. So lets add some.
const init = () => {
/*
* 10 - Add ambient light to the scene
* Docs: https://threejs.org/docs/#api/en/lights/AmbientLight
* Light: https://threejs.org/docs/#api/en/lights/Light
*/
const ambient_light = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient_light);
};
Here we create a THREE.AmbientLight instance, and pass it the color and intensity of the light. And we add that to the scene.
AmbientLight creates an even lighting throughout our scene meaning every side of our cube will have the same amount of light. To get a sence of 3D we will also need a bit of directional light. The easiest way to do that is by adding an extra point-light.
const init = () => {
/*
* 11 - Add point-light to the scene
* Docs: https://threejs.org/docs/#api/en/lights/PointLight
* Light: https://threejs.org/docs/#api/en/lights/Light
*/
const light = new THREE.PointLight(0x00baff, 1, 100);
light.position.set(15, 15, 15);
scene.add(light);
};
So lets create a new THREE.PointLight instance passing it a color, intensity and distance. The distance is the maximum range the light can shine.
And we move it 15 units from the center of the scene.
In order to have things moving we have to make it so that ThreeJs can render subsequent frames. To make that happen we add an animate function to our project.
const animate = () => {
requestAnimationFrame(animate);
render();
};
In this function we call the requestAnimationFrame function and pass it our animate function, basicaly creating a recursive loop. And in the animate function we also call the our render function making ThreeJs render output on every frame.
Now all that's left is to do the actual rendering. For this we add a render function (The one thats called from our animate function).
const render = () => {
renderer.render(scene, camera);
};
Within this function we call the rendor method from our renderer and pass it the scene we created and our camera.
Finaly we can call our init and animate functions to get things going.
init();
animate();
And now we have a completed skeleton ThreeJs app.
To safe you the trouble of piecing together all the code i made it available below for you to grab. Or if you want to be super lazy, scroll down for a starter project! :p
import * as THREE from "three";
import * as ThreeControls from "three-controls";
let scene, camera, renderer, controls;
const init = () => {
/*
* 01 - Create the scene.
* Docs: ttps://threejs.org/docs/#api/en/scenes/Scene
*/
scene = new THREE.Scene();
/*
* 02 - Create Camera.
* Docs: https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
* Camera: https://threejs.org/docs/#api/en/cameras/Camera
*/
camera = new THREE.PerspectiveCamera(
43,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.lookAt(0, 0, 0);
camera.position.set(20, 20, 20);
/*
* 03 - Create renderer.
* Docs: https://threejs.org/docs/#api/en/renderers/WebGLRenderer
*/
renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
/*
* 04 - Append to document.
*/
document.body.appendChild(renderer.domElement);
/*
* 05 - Update renderer on window resize.
*/
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
/*
* 06 - Create OrbitControls.
* Docs: https://threejs.org/docs/#examples/en/controls/OrbitControls
*/
controls = new ThreeControls.OrbitControls(camera, renderer.domElement);
/*
* 07 - Create a geometry.
* Docs: https://threejs.org/docs/#api/en/geometries/BoxGeometry
* BufferGeometry: https://threejs.org/docs/#api/en/core/BufferGeometry
*/
const cubeGeometry = new THREE.BoxGeometry(5, 5, 5);
/*
* 08 - Create a material.
* Docs: https://threejs.org/docs/#api/en/materials/MeshLambertMaterial
* Materials: https://threejs.org/docs/#api/en/materials/Material
*/
const whiteMaterial = new THREE.MeshLambertMaterial(0x7f7f7f);
/*
* 09 - Create a mesh.
* Docs: https://threejs.org/docs/#api/en/objects/Mesh
*/
const cubeMesh = new THREE.Mesh(cubeGeometry, whiteMaterial);
cubeMesh.position.set(0, 0, 0);
scene.add(cubeMesh);
/*
* 10 - Add ambient light to the scene
* Docs: https://threejs.org/docs/#api/en/lights/AmbientLight
* Light: https://threejs.org/docs/#api/en/lights/Light
*/
const ambient_light = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient_light);
/*
* 11 - Add point-light to the scene
* Docs: https://threejs.org/docs/#api/en/lights/PointLight
* Light: https://threejs.org/docs/#api/en/lights/Light
*/
const light = new THREE.PointLight(0x00baff, 1, 100);
light.position.set(15, 15, 15);
scene.add(light);
};
const animate = () => {
requestAnimationFrame(animate);
render();
};
const render = () => {
renderer.render(scene, camera);
};
init();
animate();
To make your live even easier i have whipped up a ThreeJs starter project. It contains all the code from this article plus a bit of Vite magic to get you up and running fast.
Simply Clone. Install and run.
This hre is a small skeleton starter project from my tutorial over at dev.to.
It includes Three, Three-Controls and Vite.
You can clone this repo, run 'npm install' and the 'npm run dev'.
Your welcome! :p
Now that you came this far go play around with it, Check out the examples on the ThreeJs homepage, dive into the docs, and explore your own ideas. I would love to see what you come up with so leave a comment if you made something cool!
If you all like this article i might write up some more about ThreeJs goodies. I'm already thinking about a follow up on how you can bring Blender3D objects directly into the browser. So don't forget to follow me on Twitter or here on Dev.to @Vanaf1979 for more things to come.
Thanks for reading, stay safe and stay the right kind of positive!
Posted on February 7, 2022
Sign up to receive the latest update from our blog.
February 7, 2022