Basics
Create Objects
// Build-in geometry
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial(0xff0000)
)
// AxesHelper
const axesHelper = new THREE.AxesHelper()
// Custom geometry
const positionsArray = new Float32Array([
0, 0, 0,
1, 0, 0,
0, 1, 0,
])
const positionAttribute = new THREE.BufferAttribute(positionsArray, 3)
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', positionAttribute)
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
const fog = new THREE.Fog('#262837', 1, 15)
scene.fog = fog
Load from file:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
const gltfLoader = new GLTFLoader()
gltfLoader.load('/models/GlifhtHelmet/glTF/FlightHelmet.gltf',
(gltf) => {
gltf.scene.scale.set(10, 10, 10)
gltf.scene.position.set(0, -4, 0)
gltf.scene.rotation.y = Math.PI * 0.5
scene.add(gltf.scene)
updateAllMaterial()
}
)
const updateAllMaterial = () => {
scene.traverse((child) => {
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
child.material.envMap = environmentMap
child.material.envMapIntensity = 10
// for tonemapping
child.material.needsUpdate = true;
}
})
}
- Sample Models from KhronosGroup
Draco Compression:
- Website: http://google.github.io/draco
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const dracoLoader = new DRACOLoader()
// Copy dracto from three to your static folder
dracoLoader.setDecoderPaths('/draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
- Load animated gltf
const mixer = new THREE.AnimationMixer(gltf.scene)
const action = mixer.clipAction(gltf.animations[0])
action.play()
// Inside the rendering loop:
mixer.update(deltaTime)
Load from urdf:
yarn add urdf-loader
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import URDFLoader from 'urdf-loader';
const manager = new THREE.LoadingManager();
const loader = new URDFLoader(manager);
loader.loadMeshCb = (path, manager, onComplete) => {
const gltfLoader = new GLTFLoader(manager);
gltfLoader.load(
path,
result => {
onComplete(result.scene);
},
undefined,
err => {
console.error(err);
}
)
}
let robot
loader.load("models/model.urdf", robot_ => {
robot = robot_
robot.rotateX(-Math.PI/2.0);
robot.rotateZ(-Math.PI/2.0);
robot.rotateZ(Math.PI/2.0);
robot.setJointValue("joint0", 3.14)
scene.add(robot)
})
Group objects
const group = new THREE.Group()
group.add(cube)
group.add(axesHelper)
scene.add(group)
Transforms
mesh.position.z = 1
mesh.position.set(1, 2, 3)
mesh.rotation.y = Math.PI
mesh.rotation.set(1, 2, 3)
mesh.computeBoundingBox()
mesh.translate(
- mesh.boundingBox.max.x * 0.5
- mesh.boundingBox.max.y * 0.5
- mesh.boundingBox.max.z * 0.5
)
mesh.center()
Camera
camera.lookAt(mesh.position)
Render
const canvas = document.querySelector(".webgl")
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
})
let clock = new THREE.Clock()
const tick = () => {
const dt = clock.getElapsedTime()
mesh.rotation.y = 0.001 * dt
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick()
To make export from blender look the same in Three.JS:
renderer.physicallyCorrectLights = true
renderer.outputEncoding = THREE.sRGBEncoding
renderer.setClearColor('#262837')
Tone Mapping
gui.add(renderer, 'toneMapping', {
No: THREE.NoToneMapping,
Linear: THREE.LinearToneMapping,
Reinhard: THREE.ReinhardToneMapping,
Cineon: THREE.CineonToneMapping,
ACESFilmic: THREE.ACESFilmicToneMapping
})
.onFinishChange(()=>{
renderer.toneMapping = Number(renderer.toneMapping)
})
Animation
Install GSAP
npm install --save gsap@3.5.1
import gsap from 'gsap'
gsap.to(mesh.position, {duration: 1, delay: 1, x: 2})
Camera
// vertical FOV, aspect ratio, near, far
// Setting near plane to very low and far plane very high might cause depth fighting
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100)
// left, right, top, bottom, near, far
const aspectRatio = width / height
const camera = new THREE.OrthographicCamera(-aspectRatio, aspectRatio, 1, -1, 0.1, 100)
Controller
Manuel
const cursor = {
x: 0,
y: 0
}
window.addEventListener('mousemove', (event) => {
cursor.x = event.clientX / width - 0.5
cursor.y = - (event.clientY / height - 0.5)
})
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const controls = new OrbitControls(camera, canvas)
// controls.enable = false
controls.enableDamping = true
controls.target.y = 2
// Inside rendering loop
controls.update()
Resize
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))
})
window.addEventListener('dblclick', () => {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement
if (!fullscreenElement) {
if (canvas.requestFullscreen) {
canvas.requestFullscreen()
} else if (canvas.webkitRequestFullscreen) {
canvas.webkitRequestFullscreen()
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (canvas.webkitRequestFullscreen) {
document.webkitRequestFullscreen()
}
}
})
Debug GUI
npm install --save dat.gui
import * as dat from 'dat.gui'
const gui = new dat.GUI({closed: true, width:400})
gui.hide() // open with "h" key
// Range min, max
gui.add(camera.position, "y", -3, 3, 0.1)
gui
.add(mesh.position, "y")
.min(-3)
.max(3)
.step(0.01)
.name("elevation")
// Color
const parameters = {
color: 0xff0000
}
gui
.addColor(parameters, 'color')
.onChange(() => {
material.color.set(parameters.color)
})
// Text Simple Text
// Checkbox bool true/false
gui
.add(mesh, 'visible')
gui
.add(material, 'wireframe')
// Select Choice from list
// Button Trigger function
const parameters = {
spin: () => {
gsap.to(mesh.rotation, {duration: 1, y: mesh.rotation.y+10})
}
}
gui
.add(parameters, 'spin')
// Folder to organize panel
Textures
const image = new Image()
image.src = "/textures/door/color.jpg"
const texture = new THREE.Texture(image)
image.onload = () => {
texture.needsUpdate = true;
}
const material = new THREE.MeshBasicMaterial({map: texture})
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load("/textures/door/color.jpg")
const material = new THREE.MeshBasicMaterial({map: texture})
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () => {
console.log("onStart")
}
loadingManager.onLoad = () => {
console.log("onLoad")
}
loadingManager.onProgress = () => {
console.log("onProgress")
}
loadingManager.onError = () => {
console.log("onError")
}
const textureLoader = new THREE.TextureLoader(loadingManager)
const texture = textureLoader.load("/textures/door/color.jpg")
Texture Settings
const texture = ...
texture.repeat.x = 2
texture.repeat.y = 2
texture.wrapS = THREE.MirroredRepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.rotation = Math.PI / 4
texture.center.x = 0.5
texture.center.y = 0.5
texture.generateMipmaps = true
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
Matcap Textures
const material = new THREE.MeshMatcapMaterial({matcap: matcapTexture})
PBR Textures
const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)
const doorColorTexture = textureLoader.load("/textures/door/color.jpg")
const doorAlphaTexture = textureLoader.load("/textures/door/alpha.jpg")
const doorAmbientOcclusionTexture = textureLoader.load("/textures/door/ambientOcclusion.jpg")
const doorHeightTexture = textureLoader.load("/textures/door/height.jpg")
const doorNormalTexture = textureLoader.load("/textures/door/normal.jpg")
const doorMetalnessTexture = textureLoader.load("/textures/door/metalness.jpg")
const doorRoughnessTexture = textureLoader.load("/textures/door/roughness.jpg")
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)
const environmentMapTexture = cubeTextureLoader.load([
"/textures/environmentMaps/0/px.jpg",
"/textures/environmentMaps/0/nx.jpg",
"/textures/environmentMaps/0/py.jpg",
"/textures/environmentMaps/0/ny.jpg",
"/textures/environmentMaps/0/pz.jpg",
"/textures/environmentMaps/0/nz.jpg",
])
environmentMapTexture.encoding = THREE.sRGBEncoding
// Light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(ambientLight, pointLight)
const geometry = new THREE.PlaneGeometry(1, 1, 64, 64)
const material = new THREE.MeshStandardMaterial({
map: doorColorTexture,
aoMap: doorAmbientOcclusionTexture,
normalMap: doorNormalTexture,
displacementMap: doorHeightTexture,
alphaMap: doorAlphaTexture,
metalnessMap: doorMetalnessTexture,
roughnessMap: doorRoughnessTexture,
envMap: environmentMapTexture
})
material.displacementScale = 0.05
material.transparent = true
material.side = THREE.DoubleSide
const mesh = new THREE.Mesh(geometry, material)
mesh.geometry.setAttribute("uv2",
new THREE.BufferAttribute(mesh.geometry.attributes.uv.array, 2))
scene.add(mesh)
scene.background = environmentMapTexture;
Sources for textures
Sources for Environment Maps
Transform HDRI to cubemap
Sources for matcaps
Lights
Ambient Light
// color, intensity
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
Directional Light
// color, intensity
const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.position.set(0.25, 3, -2.25)
directionalLight.intensity = 1
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLightHelper, 0.2)
HemisphereLight
// topic color, bottom color, intensity
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3)
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
Point Light
- Illuminates light into every direction
// color, intensity, distance, decay
const pointLight = new THREE.PointLight(0xff9000, 0.5, 10, 2)
pointLight.position.set(1, -0.5, 1)
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
Rect Area Light
- Plane that illuminates light
// color, intensity, width, height
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
rectAreaLight.position.set(-1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js'
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
SpotLight
// color, intensity, distance, angle, penumbra (sharp (0) vs blurry edges), decay
const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
spotLight.target.position.x = -0.75
scene.add(spotLight.target)
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
window.requestAnimationFrame(() => spotLightHelper.update())
Shadows
mesh.castShadow = true
mesh.receiveShadow = true
directionalLight.castShadow = true
directionalLight.shadow.camera.far = 15
directionalLight.shadow.mapSize.set(1024, 1024)
// In case of shadow artefacts do
directionalLight.shadow.normalBias = 0.05
// And for flat surfaces:
directionalLight.shadow.bias = 0.05
// Adjust shadow camera
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = -2
directionalLight.shadow.camera.near = 3
directionalLight.shadow.camera.far = 5.5
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
directionalLight.shadow.radius = 10
const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3)
spotLight.castShadow = true
spotLight.position.set(0, 2, 2)
spotLight.shadow.mapSize.set(1024, 1024)
spotLight.shadow.camera.fov = 30
spotLight.shadow.camera.near = 2
spotLight.shadow.camera.far = 5
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLight, spotLight.target, spotLightCameraHelper)
const pointLight = new THREE.PointLight(0xffffff, 0.3)
pointLight.castShadow = true
pointLight.position.set(-1, 1, 1)
pointLight.shadow.mapSize.set(1024, 1024)
pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
renderer.shadowMap.enable = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
Backed shadow
const textureLoader = new THREE.TextureLoader()
const backedShadow = textureLoader.load('/textures/bakedShadow.jpg')
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10, 10),
new THREE.MeshBasicMaterial({map: backedShadow})
)
Raycaster
const raycaster = new THREE.Raycaster()
const rayOrigin = new THREE.Vector3(-3, 0, 0)
const rayDirection = new THREE.Vector3(10, 0, 0)
rayDirection.normalize()
raycaster.set(rayOrigin, rayDirection)
const intersect = raycaster.intersectObject(object2)
// or
const intersects = raycaster.intersectObjects([object1, object2, object3])
for (const intersect of intersects) {
intersect.object.material.color.set("#0000ff")
}
Use with the mouse
const mouse = new THREE.Vector2()
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX / sizes.width * 2 - 1
mouse.y = - (event.clientX / sizes.height) * 2 + 1
})
const tick = () => {
// ...
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects([object1, object2, object3])
for (const intersect of intersects) {
intersect.object.material.color.set("#0000ff")
}
}
On Click
window.addEventListener('click', (event) => {
})
Performance
Monitor FPS Count
npm install stats.js --save
import Stats from 'stats.js'
const stats = new State()
stats.showPanel(0)
document.body.appendChild(stats.dom)
const tick = () => {
stats.begin()
stats.end()
}
Disable FPS Limit
# Unix (Terminal)
open -a "Google Chrome" --args --disable-gpu-vsync --disable-frame-rate-limit
# Windows (Command prompt)
start chrome --args --disable-gpu-vsync --disable-frame-rate-limit
Monitor Draw Calls
- Monitor draw calls with Chrome Extension
Renderer Information
console.log(renderer.info)
Dispose Objects
scene.remove(cube)
cube.geometry.dispose()
cube.material.dispose()
Light Optimization
- Use as little lights as possible
- Use AmbientLight, DirectionalLight, HemisphereLight
- Don't add and remove lights too often
- Avoid shadows
- Use backed shadows
- Fit shadowmaps right into the sceen, use CameraHelper visualize the shadow map
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
- Deactivate shadow auto update
renderer.shadowMap.autoUpdate = false
- Update it on demand with:
renderer.shadowMap.neesUpdate = true
- For textures use power of 2 resolution
- Use tinypng.com to reduce size
Merge Geometry
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
const geometries = []
for(let i = 0; i < 50; i++) {
const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5)
geometry.translate(Math.random(), Math.random(), Math.random())
geometries.push(geometry)
}
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries()
Material optimization
- MeshStandardMaterial or MeshPhysicalMaterial need more resources than MeshBasicMaterial, MeshLamberMaterial or MeshPhongMaterial
Use Instancing
const geometry = new THREE:BoxBufferGeometry(0.5, 0.5, 0.5)
const material = new THREE.MeshNormalMaterial()
const mesh = new THREE.InstancedMesh(geometry, material, 50)
// If you update the matrices inside the tick function do:
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
scene.add(mesh);
for(let i = 0; i < 50; i++) {
const quaternion = new THREE.Quaternion()
const euler = new THREE.Euler((Math.random(), Math.random(), 0))
quaternion.setFromEuler(euler)
const position = new THREE.Vector3((Math.random(), Math.random(), Math.random()))
const matrix = new THREE.Matrix4()
matrix.makeRotationFromQuaternion(quaternion)
matrix.setPosition(position)
mesh.setMatrixAt(i, matrix)
}
Activate Gzip on your server
See here: https://docs.nginx.com/nginx/admin-guide/web-server/compression/
Provide PowerPreference to Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
powerPreference: 'high-performance'
})
Disable antialiasing
- If device pixel ratio is 2
Specify precision
const shaderMaterial = new THREE.ShaderMaterial({
precision: 'lowp'
})
Loading Screen
- Create a plane
import { gsap } from 'gsap'
const loadingBarElement = document.querySelector('.loading-bar')
const loadingManager = new THREE.LoadingManager(
// Loaded
() => {
gsap.delayedCall(0.5, ()=>{
gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0 })
loadingBarElement.classList.add("ended")
loadingBarElement.style.transform = ''
})
}
// Progress
(itemUrl, itemsLoaded, itemsTotal) => {
const progressRatio = itemLoaded / itemTotal
loadingBarElement.style.transform = `scaleX(${progressRatio})`
}
)
const gltfLoader = new GLTFLoader(loadingManager)
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)
const overlayGeometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1)
const overlayMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const overlayMaterial = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
uAlpha: { value: 0.5 }
}
vertexShader: `
void main() {
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float float uAlpha;
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha);
}
`
})
const overlay = new THREE.Mesh(overlayGeometry)
scene.add(overlay)
- Simulate bad bandwith
Chrome -> Developer Console -> Network -> Disable cache
Loading bar
.loading-bar
{
position: absolute;
top: 50%;
width: 100%;
height 2px;
background: #ffffff;
transform: scaleX(0.3);
transform-origin: top left;
transition: transform 0.5s;
will-change: transform;
}
.loading-bar.ended
{
transform-origin: top right;
transition: transform 1.5s ease-in-out;
}
Postprocessing
- Depth of field, Bloom, God ray, Motion blur, Glitch effect, Outlines, Color variation, Antialiasing, Reflections and refractions