A three.js starter based on object-oriented programming (demo).
- Introduction
- Getting Started
- Event Emitter
- Mouse Control
- Add Material
- Create Mesh
- Load Model
- Customized Shader
- GSAP Animation
- Debug Mode
- Notes
This is a three.js starter written in OOP based on Bruno Simon's project of this cool website. I think this would really help for web developers, who are trying to use three.js library in a more modular and manageable way in their future projects. It's also a quick guide to understand some basic structure of Bruno Simon's project.
To use this starter, I would go through some main steps. However, you don't need to complete the entire document before getting started, instead, you can come back if needed while developing your own project.
The main
branch is written in Javascript. Feel free to check out the vite/typescript
branch if you prefer the Typescript version.
Features: Three.js, WebGL, GSAP, dat.gui, webpack, Babel, ESLint, OOP
Type the following steps into the terminal.
git clone https://github.com/TomHsiao1260/three-starter.git
cd three-starter
npm install
Then start the dev server and navigate to http://localhost:8080
npm run dev
Event Emitter
allows us to handle some custom events. For example, we can create an instance of Time
class which is inherited from Event Emitter
.
// src/javascript/Application.js
this.time = new Time();
Let's put some callbacks via Event Emitter
built-in on
method.
this.time.on('tick', myCallback);
Now, we can use trigger
method to execute those callbacks. Note that it would only trigger events with the same name (tick
in this example).
this.time.trigger('tick');
You can also remove callbacks via remove
method.
const event = this.time.on('tick', myCallback);
this.time.remove(event);
In src/javascript/Utils/Time.js
, the trigger('tick')
would be executed for each frame, so callbacks such as this.time.on('tick', ... )
would be automatically triggered once for each frame. In addition, the Resources
class in this starter is also inherited from Event Emitter
and callbacks such as this.resources.on('ready', ... )
would be triggered after all resources are loaded.
You can also append multiple names to the callback, such as .on('name1/name2/...', ... )
, so that both .trigger('name1')
and .trigger('name2')
will trigger the same callback. If you want to send parameters to the callback, you can use .trigger('name', [para1, para2, ...])
which would send para1
and para2
as the first and second parameters to the callback, respectively.
The mouse controls are written in src/javascript/World/Controls.js
. The instance of this class can be created as follows.
// src/javascript/World/index.js
this.controls = new Controls({
time: this.time,
sizes: this.sizes,
});
You can put some callbacks after the mouse is pressed or released via the Event Emitter
, or receive the current mouse position in Normalized Device Coordinate (NDC) as follows.
// execute when pressing down the mouse button
this.time.on('mouseDown', myCallback);
// execute when releasing the mouse button
this.time.on('mouseUp', myCallback);
// current mouse x position: [-1, 1] from left to right
this.controls.mouse.x;
// current mouse y position: [-1, 1] from bottom to top
this.controls.mouse.y;
You can write your own custom events in src/javascrpt/World/Controls.js
such as double click, keyboard controls, Raycaster, etc.
To create a material with external resources (ex: matcap), you can do the following steps. First, import the resources and put them into an array of objects.
// src/javascript/Resources.js
import matcapGoldSource from '../models/matcaps/gold.png';
this.loader.load([
{ name: 'matcapGold', source: matcapGoldSource },
...
]);
Then create the material.
// src/javascript/World/Materials.js
const { matcapGold } = this.resources.items;
this.items.matcap.gold = new THREE.MeshMatcapMaterial({ matcap: matcapGold });
Now, we can create the mesh using this material.
// src/javascript/World/Torus.js
const material = this.material.items.matcap.gold;
const mesh = new THREE.Mesh(geometry, material);
Let's create a THREE.Mesh
(ex: Torus) and add it to the scene. First, create a Torus
class which can generate the THREE.mesh
.
// src/javascript/World/Torus.js
const geometry = new THREE.TorusGeometry(0.25, 0.08, 32, 100);
const material = this.material.items.matcap.gold;
const mesh = new THREE.Mesh(geometry, material);
this.container.add(mesh);
Then create an instance of this class. Note that the mesh would be added to the scene graph via the connection of container
property, which is a THREE.Object3D
.
// src/javascript/World/index.js
import Torus from './Torus';
start() {
...
this.setTorus();
}
setTorus() {
this.torus = new Torus({
material: this.material,
});
this.container.add(this.torus.container);
}
The following steps can load some external models (ex: .glb
, .fbx
, etc.). First, we need to import our model file.
// src/javascript/Resources.js
import foxSource from '../models/fox/glTF-Binary/Fox.glb';
this.loader.load([
{ name: 'fox', source: foxSource },
...
]);
Then create a class and add the loaded model to the container
property.
// src/javascript/World/Fox.js
const gltf = this.resources.items.fox;
this.container.add(gltf.scene);
Now we can create an instance of this class (just like Create Mesh).
// src/javascript/World/index.js
import Fox from './Fox';
start() {
...
this.setFox();
}
setFox() {
this.fox = new Fox({
resources: this.resources,
});
this.container.add(this.fox.container);
}
For simplicity, this starter only support .glb
, .gltf
and .fbx
extensions. Draco compression would automatically support for models with .glb
extension. If you want to load models with other extensions, you can check out src/javascript/Utils/Loader.js
and write some custom model loaders.
You can create some customized shader materials as follows.
// src/javascript/Materials/Plane.js
import vertexShader from '../../shaders/plane/vertex.glsl';
import fragmentShader from '../../shaders/plane/fragment.glsl';
export default function PlaneMaterial() {
const uniforms = {
uTime: { value: 0 },
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
});
return material;
}
Then create an instance object of this material.
// src/javascript/World/Materials.js
import PlaneMaterial from '../Materials/Plane';
...
this.items.shader.plane = new PlaneMaterial();
Now, we can use this shader material to create a plane mesh (just like Create Mesh).
// src/javascript/World/Plane.js
const geometry = new THREE.PlaneGeometry(1.2, 0.7, 50, 50);
const material = this.material.items.shader.plane;
const mesh = new THREE.Mesh(geometry, material);
To handle animations in Three.js, one of the well-known options is GSAP which is a blazing fast library for JavaScript animations. For convenience and predictable behavior, we use only single GSAP Timeline and apply GSAP Labels to handle all animations.
// src/javascript/World/Transition.js
// all animations should be applied via this timeline
this.timeline = gsap.timeline();
...
const targetA = this.camera.instance.position;
const targetB = this.light.directionalLight;
const targetC = this.light.ambientLight;
this.timeline.to(targetA, { z: 1, duration: 2 }, 'firstTransition');
this.timeline.to(targetB, { intensity: 0.8, duration: 3 }, '<');
this.timeline.to(targetC, { intensity: 0.8, duration: 3 }, '<');
return this.timeline;
In GSAP 3
, the timeline
can be a promise which would be resolved when all animations are complete. As a result, we can return this.timeline
at the end of the function, and then use await
to manage the animation workflow as follows.
// src/javscript/World/index.js
async start() {
...
await this.transition.firstTransition();
await this.transition.secondTransition();
...
}
This can ensure that .secondTransition()
would not be executed until .firstTransition()
is resolved. You can also add some procedures between each transition function if needed.
To enter the debug mode, you can append #debug
at the end of the route http://localhost:8080/#debug. It would have stats.js performance monitor, and lil-gui controller on the upper left and upper right, respectively.
In order to tune some parameters via lil-gui
(ex: visibility of a Torus), you can do the following steps.
// src/javascript/World/Torus.js
// create lil-gui folder
if (this.debug) {
this.debugFolder = this.debug.addFolder('torus');
this.debugFolder.open();
}
// add some tuning parameters to the 'torus' folder
if (this.debug) {
this.debugFolder.add(mesh, 'visible').name('visible');
...
}
Sometimes, printing out the whole scene graph is helpful. In this starter, the entire application has already been added to the global window
object, so that you can easily print it out by typing window.application
on your browser console panel.
// src/index.js
window.application = new Application({
$canvas: document.querySelector('.js-canvas'),
});
Thanks Daniele Tabanella for contributing the Typescript version in vite/typescript
branch to make this repository better.
Would be really appreciated if you are willing to give me a star 🎉
If you have any questions or somewhere I can do better, welcome to send an issue or e-mail to me.
e-mail: tomtogo1260@gmail.com