Daniel Stokes

How to bundle threejs + typescript with webpack

October 23rd 2019 6:00pm
Updated: November 5th 2019 2:08pm

Web Dev Game Dev

ThreeJs makes it easy to get started. Just include the library script in your html document and you can get started right in the browser with no build process.

That being said I am going to run through a build process I have put together that utilizes webpack and typescript.

So why integrate any build process? It is true that three js doesn't require this extra work but using typescript (typed javascript) does introduce a build step so why not take things slightly further for smaller bundles, intuitive imports and much more.


If you have heard of webpack it is probably because of its relation to the latest front end javascript solutions like react and vue. These frameworks utilize webpack with plugins for when features are used that are not supported by browsers natively.

Webpack looks at the entry code file and follows import statements to grab all the code and bundles it all together. 

We will use that feature to leverage typescripts easy and familiar class structure. We can create classes in separate files and not have to include all the scripts in the html file and worry about the order of those files.


Here's another bonus: threejs has a typescript type definition included already so if you're using an intelligent code editor like vscode you can get code hinting, error highlighting and you can even peek the threejs code base to understand it better.


Prerequisites:

You are going to need node/npm installed. If you don't have it installed yet and you are doing web development then give it a download and explore all the things that can be done with it.

Node is a javascript engine and npm is a package manager. In tandem they allow you to download packages and run javascript code. We will be utilizing them to download webpack, typescript and threejs and to execute our webpack build process

Download them here they come as a package together.


Steps:

1. Initialize

First step is to initialize a node project. In your project folder run this command

npm init

If you have already started a project but have not included any node stuff you can still run this command in your project folder.

This command gets us started with a package.json file that keeps track of all the packages our project uses/requires.


2. Download / Install

Next we want to install the packages we need with npm

For the packages we run the following commands: 

a) typescript

npm i typescript --save-dev

b) webpack 

npm i webpack webpack-cli ts-loader --save-dev

c) threejs

npm i three


We should see we have a new folder called node_modules. This folder contains our packages. Our package.json file has also been updated.


3. Configure Webpack and typescript

Create these files in your project folder

tsconfig.json

	{
	"compilerOptions": {
		"outDir": "dist",
		"noImplicitAny": true,
		"target": "es5",
		"allowJs": true,
		"experimentalDecorators":true
	}
}
	

webpack.config.js

	const path = require('path');

module.exports = {
	entry: './src/index.ts',
	mode: 'development',
	watch: true,
	module: {
		rules: [
			{
				test: /\.tsx?$/,
				use: 'ts-loader',
				exclude: /node_modules/,
			},
		],
	},
	resolve: {
		extensions: [ '.tsx', '.ts', '.js' ],
	},
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist'),
	},
};
	

In this file I recommend you change things to match your project. Where you see "entry" is where you can control which file is your code starting point.

Under output you can control where webpack will place the files after it finished the build.


4. Coding Time

With all of the packages setup we can start coding for three js. Here is some started code:

index.ts

	import * as THREE from 'three';


let scene = new THREE.Scene();

var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();

var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;


var viewport: HTMLElement = document.querySelector('#viewport');
renderer.setSize(  viewport.clientWidth, viewport.clientHeight );
viewport.appendChild( renderer.domElement );

window.addEventListener('resize', function(){
	var viewport: HTMLElement = document.querySelector('#viewport');
	camera.aspect = viewport.clientWidth / viewport.clientHeight;
	camera.updateProjectionMatrix();
	renderer.setSize( viewport.clientWidth, viewport.clientHeight );
	console.log('resize');
})

var animate = function () {
	requestAnimationFrame( animate );

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render( scene, camera );
};

animate();
	

That gives us a good start but we can take this so much further.

We could use inheritance to extend the 3D object class to include more functionality.

Lets create a GameObject and Scene class and swap out our cube and scene for an instance of these classes

GameObject.ts

	import * as THREE from 'three';

export class GameObject extends THREE.Object3D {

	cube: THREE.Mesh;
    constructor(objectColor: string){
		super();
		var geometry = new THREE.BoxGeometry( 1, 1, 1 );
		var material = new THREE.MeshBasicMaterial( { color: objectColor } );
		this.cube = new THREE.Mesh( geometry, material );
		console.log(this.cube);
		this.add(this.cube);
	}


	Start(){
		this.cube.rotation.z = 1;
		console.log('start game object');
	}

	Update(){
		console.log("cube update");
		this.cube.rotation.x += 0.01;
		this.cube.rotation.y += 0.01;
	}

}
	

Scene.ts

	import * as THREE from 'three';
import { GameObject } from './GameObject';

export class Scene extends THREE.Scene {

	children: any = [];

    constructor(color: string){
		super();
		this.background = new THREE.Color(color);
	}

	add(...object: THREE.Object3D[]):any{

		for ( var i = 0; i < object.length; i ++ ) {

			super.add(object[i]);

			if(object[i] instanceof GameObject){
				let gameObject = object[i] as GameObject;
				gameObject.Start();
			}
		}
	}

	Update(){
		
		for(var i = 0; i < this.children.length; i++){

			if( this.children[i].Update !== undefined){
				this.children[i].Update();
			}

		}
	}

}
	

index.ts

	import * as THREE from 'three';

import { GameObject } from "./GameObject";
import { Scene } from './Scene';

let scene = new Scene('#3be5ff');

var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();

var object = new GameObject('#00ff00');
scene.add( object );
camera.position.z = 5;


var viewport: HTMLElement = document.querySelector('#viewport');
renderer.setSize(  viewport.clientWidth, viewport.clientHeight );
viewport.appendChild( renderer.domElement );

window.addEventListener('resize', function(){
	var viewport: HTMLElement = document.querySelector('#viewport');
	camera.aspect = viewport.clientWidth / viewport.clientHeight;
    camera.updateProjectionMatrix();
	renderer.setSize( viewport.clientWidth, viewport.clientHeight );
	console.log('resize');
})

var animate = function () {
	requestAnimationFrame( animate );

	scene.Update();

	renderer.render( scene, camera );
};

animate();
	

A quick summary of what we have done is we have added update methods to our game object and scene that we then call every frame.


5. Building

To start the build run this console command

npx webpack

If all goes well you should see a successful build in the console.

Now just load up an html file in the browser and include your script.

index.html

	<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>Getting Started</title>

	</head>
	<body style="margin: 0px; padding: 0px;">

		<div id="viewport" style="width: 100vw; height: 100vh;"></div>
		<script src="./bundle.js"></script>
			
		
		<script>

		
		</script>
	</body>
</html>
	

Comments