Initial Setup
This is the boilerplate code required for starting a new ThreeJs Project.
Basic Setup
- React
- Webpack
- Javascript
npx create-react-app my-app --template typescript
cd my-app
yarn add three
yarn add @types/three --dev
src/index.css
*
{
margin: 0;
padding: 0;
}
html,
body
{
overflow: hidden;
}
src/App.css
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}
- Run Dev Server
yarn start
- Build Project
yarn build
- Option A) Use this script:
bash <(wget -qO- https://dimitri-henkel.web.app/scripts/create-threejs-app.sh)
- Option B) Create all files manually:
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Demo</title>
</head>
<body>
<canvas class="webgl"></canvas>
</body>
</html>
src/style.css
*
{
margin: 0;
padding: 0;
}
html,
body
{
overflow: hidden;
}
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}
static/empty.png
package.json
{
"repository": "#",
"license": "UNLICENSED",
"scripts": {
"build": "webpack --config ./webpack.prod.js",
"start": "webpack serve --config ./webpack.dev.js"
},
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^7.0.0",
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^5.0.0-alpha.7",
"mini-css-extract-plugin": "^1.3.5",
"portfinder-sync": "0.0.2",
"raw-loader": "^4.0.2",
"style-loader": "^2.0.0",
"three": "^0.125.1",
"webpack": "^5.18.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
}
}
webpack.common.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCSSExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
module.exports = {
entry: path.resolve(__dirname, './src/script.js'),
output: {
filename: 'bundle.[contenthash].js',
path: path.resolve(__dirname, './dist')
},
devtool: 'source-map',
plugins:
[
new CopyWebpackPlugin({
patterns: [
{ from: path.resolve(__dirname, './static') }
]
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
minify: true
}),
new MiniCSSExtractPlugin()
],
module:
{
rules:
[
{
test: /\.(html)$/,
use: ['html-loader']
},
{
test: /\.js$/,
exclude: /node_modules/,
use:
[
'babel-loader'
]
},
{
test: /\.css$/,
use:
[
MiniCSSExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(jpg|png|gif|svg)$/,
use:
[
{
loader: 'file-loader',
options:
{
outputPath: 'assets/images/'
}
}
]
},
{
test: /\.(ttf|eot|woff|woff2)$/,
use:
[
{
loader: 'file-loader',
options:
{
outputPath: 'assets/fonts/'
}
}
]
},
{
test: /\.(glsl|vs|fs|vert|frag)$/,
exclude: /node_modules/,
use: [
'raw-loader'
]
}
]
}
}
webpack.dev.js
const { merge } = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const ip = require('internal-ip')
const portFinderSync = require('portfinder-sync')
const infoColor = (_message) => {
return `\u001b[1m\u001b[34m${_message}\u001b[39m\u001b[22m`
}
module.exports = merge(
commonConfiguration,
{
mode: 'development',
devServer:
{
host: '0.0.0.0',
port: portFinderSync.getPort(8080),
contentBase: './dist',
watchContentBase: true,
open: true,
https: false,
useLocalIp: true,
disableHostCheck: true,
overlay: true,
noInfo: true,
after: function(app, server, compiler)
{
const port = server.options.port
const https = server.options.https ? 's' : ''
const localIp = ip.v4.sync()
const domain1 = `http${https}://${localIp}:${port}`
const domain2 = `http${https}://localhost:${port}`
console.log(`Project running at:\n - ${infoColor(domain1)}\n - ${infoColor(domain2)}`)
}
}
}
)
webpack.prod.js
const { merge } = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(
commonConfiguration,
{
mode: 'production',
plugins:
[
new CleanWebpackPlugin()
]
}
)
- Install dependencies
yarn install
- Run Dev Server
yarn start
- Build Project
yarn build
Download Three.JS
Copy three.js-master/build/three.min.js into your project folder.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Demo</title>
</head>
<body>
<canvas class="webgl"></canvas>
<script src="./three.min.js"></script>
<script src="./script.js"></script>
</body>
</html>
Basic Scene
- React
- Webpack
- Javascript
App.tsx
import React, { useRef, useEffect } from 'react';
import * as THREE from 'three'
import './App.css';
function App() {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const scene = new THREE.Scene()
const geometry = new THREE.CircleGeometry(1, 50)
const material = new THREE.MeshBasicMaterial({color: 0xff0000})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(75, sizes.width/ sizes.height)
camera.position.z = 2.0
scene.add(camera)
const renderer = new THREE.WebGLRenderer({
antialias: true
});
const canvas = mountRef?.current?.appendChild(renderer.domElement);
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
window.addEventListener('resize', () => {
sizes.width = window.innerWidth
sizes.height = window.innerHeight
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
const tick = () => {
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick()
}, [])
return <div ref={mountRef}></div>;
}
export default App;
- Result

src/script.js
import * as THREE from 'three'
const scene = new THREE.Scene()
const geometry = new THREE.CircleGeometry(1, 50)
const material = new THREE.MeshBasicMaterial({color: 0xff0000})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(75, sizes.width/ sizes.height)
camera.position.z = 2.0
scene.add(camera)
const canvas = document.querySelector(".webgl")
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
});
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
window.addEventListener('resize', () => {
sizes.width = window.innerWidth
sizes.height = window.innerHeight
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
const tick = () => {
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick()
- Result

script.js
import './style.css'
import * as THREE from 'three'
const scene = new THREE.Scene()
const geometry = new THREE.CircleGeometry(1, 50)
const material = new THREE.MeshBasicMaterial({color: 0xff0000})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(75, sizes.width/ sizes.height)
camera.position.z = 2.0
scene.add(camera)
const canvas = document.querySelector(".webgl")
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
});
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
window.addEventListener('resize', () => {
sizes.width = window.innerWidth
sizes.height = window.innerHeight
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
const tick = () => {
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick()