Table of Contents
# Install dependencies (only the first time)
npm install
# Run the local server at localhost:8080
npm run dev
# Build for production in the dist/ directory
npm run build
You can try out by clicking on this demo links:
three.js important topics - (just an example from each topic):
- Scene:
const scene = new THREE.Scene()
- Object (cube):
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
scene.add(mesh)
- Camera:
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)
- Renderer:
const renderer = new THREE.WebGLRenderer({ canvas })
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)
- Axes Helper:
new THREE.AxesHelper() // 3D helper (x: red, y: green, z: blue) like RGB
// Example
const axesHelper = new THREE.AxesHelper( 5 )
scene.add( axesHelper )
- Position:
cube.position.set(2, 0.7, 1) //(x, y, z)
//cube.position.x
//cube.position.y
//cube.position.z
cube.position.length() // The distance from the center of the object to the center of scene
cube.position.distanceTo(camera.position) // distance from other object
cube.position.normalize() // object comes to distance(1) from center of scene
- Scale:
cube.scale.set(2, 0.7, 1) //(x, y, z)
//cube.scale.x
//cube.scale.y
//cube.scale.z
- Rotation:
cube.rotation.set(Math.PI, Math.PI, Math.PI) //(x, y, z)
//cube.rotation.x = Math.PI * 2 // 360deg
//cube.rotation.y = Math.PI // 180deg
//cube.rotation.z = Math.PI / 4 // 45deg
- Reorder:
cube.rotation.reorder('YXZ')
- lookAt:
camera.lookAt(cube.position) // change camera position to current object
window.requestAnimationFrame(callback) // pass a function before print a next frame of window
Don't forget you need to render in animation function
- Time:
const clock = new THREE.Clock() // for the same animation in all computer
const elapsedTime = clock.getElapsedTime() // time of reprint
mesh.position = elapsedTime
const clock = new THREE.Clock()
let previousTime = 0
const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - previousTime
previousTime = elapsedTime
mesh.rotation.x += deltaTime
}
- Sin & Cos:
Math.sin(elapsedTime) / Math.cos(elapsedTime)
- GSAP - GreenSock (Animation Library):
import gsap from 'gsap'
gsap.to(mesh.position, { duration: 1, delay: 1, x: 2, y: '+=5', ease: 'power2.inOut' })
- ArrayCamera => multiple camera and switch between
- StereoCamera => using for AI camera, have two camera for eyes(left and right) - TOOL ;)
- CubeCamera => 6 camera (left, right, top, bottom, front, back)
- OrthographicCamera => render the scene without perspective (like games with character fixed size)
- PerspectiveCamera => render the scene with perspective
perspectiveCamera(fov, aspect, near = 0.1 , far = 100)
perspectiveCamera Parameters:
- fov: filed of view, vertical vision angle, in degrees
- aspect: aspect ratio: the width of the render divided by the height of the render
- near & far: how close and how far the camera can use
OrthograpicCamera(left, right, top, bottom, near = 0.1, far = 100)
const aspect = sizes.width / sizes.height
const camera = new THREE.OrthographicCamera( -1 * aspect, aspect, -1, 1, 0.1, 100 )
- DeviceOrientationControls: mobile gyroscope (deprecated!)
- FlyControls: enables a navigation similar to fly modes in DCC tools like Blender.
- FirstPersonControls: is like FlyControl, but with a fixed-up axis. (birds & game)
- PointerLockControls: change camera by mouse (Counter-Strike game), is a perfect choice for first person 3D games.
- OrbitControls: similar to the controls we made with more features (simon website)
- TrackballControls : like orbitControl without the vertical angle limit
- TransformControls: changes objects position
- DragControls: can be used to provide a drag and drop interaction.
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true // smooth end of moving
controls.update() // when change the controls you need update it before render (in this case "dampin" update each time in tick)
- FullScreen & Resizing:
//resize window
window.addEventListener('resize', () => {
// Update size
sizes.width = window.innerWidth
sizes.height = window.innerHeight
// Update Camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update Renderer
renderer.setSize(sizes.width, sizes.height)
})
- Pixel Ratio:
renderer.setPixelRation(Math.min(window.devicePixelRation, 2))
- BoxGeometry
- CapsuleGeometry
- CircleGeometry
- ConeGeometry
- CylinderGeometry
- DodecahedronGeometry
- EdgesGeometry
- ExtrudeGeometry
- IcosahedronGeometry
- LatheGeometry
- OctahedronGeometry
- PlaneGeometry
- PolyhedronGeometry
- RingGeometry
- ShapeGeometry
- SphereGeometry
- TetrahedronGeometry
- TorusGeometry
- TorusKnotGeometry
- TubeGeometry
- WireframeGeometry
// BoxGeometry
const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)
BoxGeometry Parameters:
- width — Width; that is, the length of the edges parallel to the X axis. Optional; defaults to 1.
- height — Height; that is, the length of the edges parallel to the Y axis. Optional; defaults to 1.
- depth — Depth; that is, the length of the edges parallel to the Z axis. Optional; defaults to 1.
- widthSegments — Number of segmented rectangular faces along the width of the sides. Optional; defaults to 1.
- heightSegments — Number of segmented rectangular faces along the height of the sides. Optional; defaults to 1.
- depthSegments — Number of segmented rectangular faces along the depth of the sides. Optional; defaults to 1.
// Wireframe
const material = new THREE.MeshBasicMaterial( {
color: 0x00ff00,
wireframe: true
} )
While wandering the three.js documentation you probably came across "BufferGeometry" Buffer Geometries are more efficient and optimized but less developer friendly!
// BoxBufferGeometry
const geometry = new THREE.BoxBufferGeometry(1, 1, 1, 2, 2, 2)
Create Custom Geometry:
// create a triangle
const positionsArray = new Float32Array([
0, 0, 0, // first vertical
0, 1, 0, // second vertical
1, 0, 0 // third vertical
])
const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3)
const geometry = new THREE.BoxBufferGeometry()
geometry.setAttribute('position', positionsAttribute)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const geometry = new THREE.BufferGeometry()
const count = 2000
const positionsArray = new Float32Array(count * 3 * 3) // count * vertex * (x+y+z)
for(let i = 0; i < count * 3 * 3; i++) {
positionsArray[i] = (Math.random() - 0.5) * 4 // optional amounts
}
const positionsAttr = new THREE.BufferAttribute(positionsArray, 3)
geometry.setAttribute('position', positionsAttr)
debug libraries:
- dat.GUI
- control-panel
- ControlKit
- Guify
- Oui
dat.GUI:
import * as dat from 'dat.gui'
const gui = new dat.GUI()
dat.GUI parameters:
- Range
- Color
- Text
- Checkbox
- Select
- Button
- Folder
/**
* GUI
*/
const gui = new dat.GUI()
// Parameters
const parameters = {
color: 0x303030,
spin: () => {
gsap.to(group.rotation, {
duration: 1, y: group.rotation.y + (Math.PI * 2)
})
}
}
// Cube Positions
gui.add(group.position, 'x').min(-3).max(3).step(0.01).name('position')
// Cube Visibility
gui.add(group, 'visible').name('box')
gui.add(mesh, 'visible').name('box inside')
gui.add(mesh2, 'visible').name('box wireframe')
// Material
gui.add(mesh2.material, 'wireframe')
// Change Color
gui.addColor(parameters, 'color')
.onChange(() => {
mesh.material.color.set(parameters.color)
})
// Functions
gui.add(parameters, 'spin').name('CLICK TO SPIN')
// Select
gui.add(renderer, 'toneMapping', {
No: THREE.NoToneMapping,
Linear: THREE.LinearToneMapping,
Reinhard: THREE.ReinhardToneMapping,
Cineon: THREE.CineonToneMapping,
ACESFilmic: THREE.ACESFilmicToneMapping,
})
Press 'H' to hide the panel.
if you want the panel to be hidden at start:
gui.hide()
if you want the panel to be closed at start:
const gui = new dat.GUI({ closed: true })
You can drag and drop the panel to change its width You can change the default width:
const gui = new dat.GUI({ width: 400 })
Github repository: dataarts/dat.gui
-
textures resources:
https://ambientcg.com/
https://3dtextures.me
https://www.poliigon.com
https://www.arroway-textures.ch/ -
you can create your own texture:
http://substance3d.com/ -
read the following documents to understand the textures:
https://marmoset.co/posts/basic-theory-of-physically-based-rendering/
https://marmoset.co/posts/physically-based-rendering-and-you-can-too/
load and create a texture:
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
const material = new THREE.MeshBasicMaterial({ map: texture })
LoadingManager
const LoadingManager = new THREE.LoadingManager()
LoadingManager.onStart = () => {
console.log('onStart')
}
LoadingManager.onProgress = () => {
console.log('onProgress')
}
LoadingManager.onLoad = () => {
console.log('onLoad')
}
LoadingManager.onError = () => {
console.log('onError')
}
const textureLoader = new THREE.TextureLoader(LoadingManager)
textures some options:
colorTexture.repeat.x = 2
colorTexture.repeat.y = 3
// or
colorTexture.repeat.set( 2, 3 )
colorTexture.wrapS = THREE.MirroredRepeatWrapping
colorTexture.wrapT = THREE.RepeatWrapping
colorTexture.offset.x = 0.5
colorTexture.offset.y = 0.5
colorTexture.rotation = Math.PI / 4
colorTexture.center.x = 0.5
colorTexture.center.y = 0.5
Filtering & Mipmapping:
- THREE.NearestFilter
- THREE.LinearFilter
- THREE.NearestMipmapNearestFilter
- THREE.NearestMipmapLinearFilter
- THREE.LinearMipmapNearestFilter
- THREE.LinearMipmapLinearFilter (Default)
// better for performance and sharp result
colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter
colorTexture.magFilter = THREE.NearestFilter
Format and Optimisation: Size , Weight, Data
- MeshBasicMaterial
- MeshNormalMaterial
- MeshMatcapMaterial
- MeshDepthMaterial
- MeshLambertMaterial (need lights)
- MeshPhongMaterial (need lights)
- MeshToonMaterial (need lights)
materials common properties:
const material = new THREE.MeshNormalMaterial()
material.color = new THREE.Color(0x00ff00)
material.wireframe = true
material.side = THREE.DoubleSide // FrontSide(Default), BackSide, DoubleSide
material.transparent = true
material.opacity = 0.7
material.map = colorDoorTexture
material.alphaMap = alphaDoorTexture
material.flatShading = true
const material = new THREE.MeshPhongMaterial()
material.shininess = 100
material.specular = new THREE.Color(0xff00ff)
Simple Light (just for testing materials):
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.x = 2
pointLight.position.x = 3
pointLight.position.x = 4
scene.add(pointLight)
MeshStandardMaterial:
const textureLoader = new THREE.TextureLoader()
const ambientOcclusionDoorTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const material = new THREE.MeshStandardMaterial()
material.metalness = 0
material.roughness = 1
material.aoMapIntensity = 1
material.side = THREE.DoubleSide
material.map = colorDoorTexture
material.aoMap = ambientOcclusionDoorTexture
material.displacementMap = heightDoorTexture
material.displacementScale = 0.05
material.metalnessMap = metalnessDoorTexture
material.roughnessMap = roughnessDoorTexture
material.normalMap = normalDoorTexture
material.normalScale.set(0.5, 0.5)
material.transparent = true
material.alphaMap = alphaDoorTexture
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
material
)
plane.geometry.setAttribute(
'uv2',
new THREE.BufferAttribute(plane.geometry.attributes.uv.array, 2)
)
MeshToonMaterial:
const textureLoader = new THREE.TextureLoader()
const gradientTexture = textureLoader.load('/textures/gradients/3.jpg')
gradientTexture.magFilter = THREE.NearestFilter
const material = new THREE.MeshToonMaterial({
color: parameters.materialColor,
gradientMap: gradientTexture
})
- environmentMaps
const cubeTextureLoader = new THREE.CubeTextureLoader()
const environmentMapsTexture = cubeTextureLoader.load([
'/textures/environmentMaps/1/px.jpg',
'/textures/environmentMaps/1/nx.jpg',
'/textures/environmentMaps/1/py.jpg',
'/textures/environmentMaps/1/ny.jpg',
'/textures/environmentMaps/1/pz.jpg',
'/textures/environmentMaps/1/nz.jpg'
])
const material = new THREE.MeshStandardMaterial()
material.metalness = 1
material.roughness = 0
material.side = THREE.DoubleSide
material.envMap = environmentMapsTexture
https://github.com/nidorx/matcaps
HDRIs: https://polyhaven.com/hdris/
HTRI to CubeMap: https://matheowis.github.io/HDRI-to-CubeMap/
we can convert a font with tools like: facetype.js
in this case we use three js example font
check this out: 'node_modules/three/examples/fonts/helvetiker_regular.typeface.json'
and copy this font to static directory. now you can use it:
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) => {
const textGeometry = new TextGeometry(
'Poorya & Three.js',
{
font,
size: 0.5,
height: 0.2,
curveSegments: 5,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegment: 4,
}
)
const textMaterial = new THREE.MeshBasicMaterial({ wireframe: true })
const text = new THREE.Mesh(textGeometry, textMaterial)
scene.add(text)
}
)
move text to center:
textGeometry.computeBoundingBox()
textGeometry.translate(
- (textGeometry.boundingBox.max.x - 0.02) * 0.5,
- (textGeometry.boundingBox.max.y - 0.02) * 0.5,
- (textGeometry.boundingBox.max.z - 0.03) * 0.5
)
// or
textGeometry.center()
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0x00ffff, 0.3)
directionalLight.position.set(2.5, 0.25, 0)
scene.add(directionalLight)
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3)
scene.add(hemisphereLight)
const pointLight = new THREE.PointLight(0xff9000, 0.5, 10, 2)
pointLight.position.set(1, -0.5, 1)
scene.add(pointLight)
// * the RectAreaLight only works with MeshStandardMaterial and MeshPhysicalMaterial
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
rectAreaLight.position.set(-1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())
scene.add(rectAreaLight)
const spotLight = new THREE.SpotLight(0x00ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)
spotLight.target.position.x = -0.75
scene.add(spotLight.target)
min cost: AmbientLight & HemisphereLight mid cost: DirectionalLight & PointLight max cost: SpotLight & RectAreaLight
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)
spotLightHelper.color = new THREE.Color(0x00ff00)
window.requestAnimationFrame(() => spotLightHelper.update())
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js'
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)
just working on: directionalLight, spotLight and pointLight
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024
directionalLight.shadow.camera.top = 1
directionalLight.shadow.camera.right = 1
directionalLight.shadow.camera.bottom = -1
directionalLight.shadow.camera.left = -1
directionalLight.shadow.camera.near = 0.1
directionalLight.shadow.camera.far = 5
directionalLight.shadow.radius = 10
plane.receiveShadow = true
cube.castShadow = true
renderer.shadowMap.enabled = true
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
scene.add(pointLightCameraHelper)
- THREE.BasicShadowMap: best performance but lousy quality
- THREE.PCFShadowMap (default): less performance but smoother edges
- THREE.PCFSoftShadowMap: less performance but even softer edges
- THREE.VSMShadowMap: less performance, more constraints, can have unexpected results
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.BasicShadowMap
const ParticlesGeometry = new THREE.SphereBufferGeometry(1, 32, 32)
const ParticlesMaterial = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: true
})
const particles = new THREE.Points(ParticlesGeometry, ParticlesMaterial)
scene.add(particles)
const ParticlesGeometry = new THREE.BufferGeometry()
const count = 5000
const positions = new Float32Array(count * 3) // 3 => (x, y, z)
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10
}
ParticlesGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
)
const ParticlesMaterial = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: true
})
const particles = new THREE.Points(ParticlesGeometry, ParticlesMaterial)
scene.add(particles)
ParticlesMaterial.alphaTest = 0.001
// OR
ParticlesMaterial.depthTest = false
// OR
ParticlesMaterial.depthWrite = false // (best solution)
ParticlesMaterial.depthWrite = false
ParticlesMaterial.blending = THREE.AdditiveBlending
const colors = new Float32Array(count * 3) // 3 => (R, G, B)
for (let i = 0; i < count * 3; i++) {
colors[i] = Math.random()
}
ParticlesGeometry.setAttribute(
'color',
new THREE.BufferAttribute(colors, 3)
)
const ParticlesMaterial = new THREE.PointsMaterial({
...others,
vertexColors: true
})
- all of particles together:
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update particles
particles.rotation.y = elapsedTime * 0.2 // position / scale / rotation
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
- particlesGeometry:
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update particles (bad performance)
for(let i = 0; i < count; i++) {
const i3 = i * 3 // (x, y, z)
// i3 + 0 => x
// i3 + 1 => y
// i3 + 2 => z
const x = particlesGeometry.attributes.position.array[i3] // i3 === i3 + 0 => x
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
}
particlesGeometry.attributes.position.needsUpdate = true
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
https://kenney.nl/assets/particle-pack
Usage example:
- Detect if there is a wall in front of player
- Test if the laser gun hit something
- Test if something is currently under the mouse to simulate mouse events
- Show and alert message if the spaceship is heading toward a planet
const rayCaster = new THREE.Raycaster()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// Update Objects
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5
// Cast a Ray
const reyOrigin = new THREE.Vector3(-3, 0, 0)
const rayDirection = new THREE.Vector3(1, 0, 0)
rayDirection.normalize()
rayCaster.set(reyOrigin, rayDirection)
// OR
rayCaster.setFromCamera(mouse, camera)
const objects = [object1, object2, object3]
const intersects = rayCaster.intersectObjects(objects)
for (const object of objects) {
object.material.color.set('#ffffff')
}
for (let item of intersects) {
item.object.material.color.set('#ff0000')
}
}
3D libraries:
Ammo.js
Cannon.js
Oimo.js
2D libraries:
matter.js
P2.js
Planck.js
Box2D.js
npm i --save cannon
We need to create an environment just like the scene of Three.js as known as the 'world'
import CANNON from 'cannon'
// World
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
// Sphere
const sphereShape = new CANNON.Sphere(0.5)
const sphereBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape
})
world.addBody(sphereBody)
// Floor
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({
mess: 0, // static object
shape: floorShape
// the plane don't need a position
})
// Floor Rotation
floorBody.quaternion.setFromAxisAngle(
new CANNON.Vec3(-1, 0, 0),
Math.PI * 0.5
)
world.addBody(floorBody)
const clock = new THREE.Clock()
let oldElapsedTime = 0
const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - oldElapsedTime
oldElapsedTime = elapsedTime
// Update Physic world
world.step(1 / 16, deltaTime, 3)
sphere.position.copy(sphereBody.position)
}
two different material:
const concreteMaterial = new CANNON.Material('concrete')
const plasticMaterial = new CANNON.Material('plastic')
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1,
restitution: 0.7
}
)
world.addContactMaterial(concretePlasticContactMaterial)
const sphereBody = new CANNON.Body({
...,
material: plasticMaterial
})
const floorBody = new CANNON.Body({
...,
material: concreteMaterial
})
the same material:
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.7
}
)
world.addContactMaterial(defaultContactMaterial)
world.defaultContactMaterial = defaultContactMaterial
// You Don't need to add this material to each object(objectBody)
// applyLocalForce
sphereBody.applyLocalForce(new CANNON.Vec3(50, 0, 0), new CANNON.Vec3(0, 0, 0))
const tick = () => {
// applyForce
sphereBody.applyForce(new CANNON.Vec3(-0.5, 0, 0), sphereBody.position)
}
// Box sizes in Cannon.js is equal to half of the box sizes in three.js
const shape = new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2))
const body = new CANNON.Body({
shape,
mass: 1,
position: new CANNON.Vec3(0, 3, 0)
})
body.position.copy(position)
world.addBody(body)
const tick = () => {
// Position
box.mesh.position.copy(box.body.position)
// Rotation (we need to update the rotation for boxes)
box.mesh.quaternion.copy(box.body.quaternion)
}
- Broadphase
// Broadphase => GridBroadphase / NaiveBroadphase / SAPBroadphase
world.broadphase = new CANNON.SAPBroadphase(world) // (best performance)
- Sleep
world.allowSleep = true
- Collide
const hitSound = new Audio('/sounds/hit.mp3')
const playHitSound = (collicion) => {
const impactStrength = collicion.contact.getImpactVelocityAlongNormal()
if(impactStrength > 0.7) {
hitSound.volume = impactStrength / 10
hitSound.currentTime = 0
hitSound.play()
}
}
body.addEventListener('collide', playHitSound)
const reset = () => {
// Remove Body
body.removeEventListener('collide', playHitSound)
world.removeBody(body)
// Remove Mesh
scene.remove(mesh)
}
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const gltfLoader = new GLTFLoader()
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) => {
scene.add(gltf.scene)
// OR -> better solution (smaller object)
const children = [...gltf.scene.children]
for(const child of children) {
scene.add(child)
}
}
)
Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics. Website | GitHub
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const dracoLoader = new DRACOLoader();
// copy draco directory from 'node_modules/three/examples/js/libs/draco' to your static directory
dracoLoader.setDecoderPath('/draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load(
'/models/Duck/glTF-Draco/Duck.gltf',
(gltf) =>
{
scene.add(gltf.scene)
}
)
let mixer = null
gltfLoader.load(
'/models/Fox/glTF/Fox.gltf',
(gltf) =>
{
gltf.scene.scale.set(0.025, 0.025, 0.025)
scene.add(gltf.scene)
// Animation
mixer = new THREE.AnimationMixer(gltf.scene)
const action = mixer.clipAction(gltf.animations[2])
action.play()
}
)
const tick = () => {
if(mixer)
{
mixer.update(deltaTime)
}
//...
}
Renderer Physically Correct Lights
renderer.physicallyCorrectLights = true // default = false
Renderer Encoding
- LinearEncoding (default)
- sRGBEncoding
- GammaEncoding
// all textures
renderer.outputEncoding = THREE.sRGBEncoding
// Specific Texture
environmentMapTexture.encoding = THREE.sRGBEncoding
Renderer Tone Mapping
- NoToneMapping (default)
- LinearToneMapping
- ReinhardToneMapping
- CineonToneMapping
- ACESFilmicToneMapping
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 3
Antialias
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true // bad performance :((
})
Shadow Acne problem (Normal Bias Light)
directionalLight.shadow.normalBias = 0.05
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer"
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass"
const effectProcessor = new EffectComposer(renderer)
effectProcessor.setSize(sizes.width, sizes.height)
effectProcessor.setPixelRatio(Math.min(window.devicePixelRatio, 2))
const renderPass = new RenderPass(scene, camera)
effectProcessor.addPass(renderPass)
const tick = () => {
//...
// renderer.render(scene, camera)
effectProcessor.render()
}
DotScreenPass
import { DotScreenPass } from "three/examples/jsm/postprocessing/DotScreenPass"
const dotScreenPass = new DotScreenPass()
effectProcessor.addPass(dotScreenPass)
// enable or disable effect
dotScreenPass.enabled = false
GlitchPass
import { GlitchPass } from "three/examples/jsm/postprocessing/GlitchPass"
const glitchPass = new GlitchPass()
glitchPass.goWild = true // show effect without break
effectProcessor.addPass(glitchPass)
ShaderPass
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass"
import { RGBShiftShader } from "three/examples/jsm/shaders/RGBShiftShader"
const rgbShiftPass = new ShaderPass(RGBShiftShader)
effectProcessor.addPass(rgbShiftPass)
UnrealBloomPass
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass"
const unrealBloomPass = new UnrealBloomPass
unrealBloomPass.strength = 0.5
unrealBloomPass.radius = 1
unrealBloomPass.threshold = 0.9
effectProcessor.addPass(unrealBloomPass)
Fixing the color by Render Target
const renderTarget = new THREE.WebGLRenderTarget(
sizes.width,
sizes.height,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
encoding: THREE.sRGBEncoding
}
)
const effectProcessor = new EffectComposer(renderer, renderTarget)
Fixing the Antialias
// using the WebGLMultisampleRenderTarget instead of WebGLRenderTarget
const renderTarget = new THREE.WebGLMultisampleRenderTarget(
// ...
)
// SMAAPass
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass"
const smaaPass = new SMAAPass()
effectProcessor.addPass(smaaPass)
Resizing
window.addEventListener('resize', () =>
{
//...
effectProcessor.setSize(sizes.width, sizes.height)
effectProcessor.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
Documentation:
- The Vertex Shader position the vertices on the render
- The Fragment Shader color each visible fragment (or pixel) of that geometry
- The Fragment Shader is executed after the Vertex Shader
- Information that changes between each vertex (like their positions) are called Attributes and can only be used in Vertex Shader
- Information that doesn't change between vertices (or fragment) are called Uniforms and can be used in both the Vertex Shader and the Fragment Shader
- We can send data from the Vertex Shader to the Fragment Shader by using Varying
- Varying values are interpolated between vertices
- Data (Attributes & Uniforms)
- Vertex Shader
- Varying (Uniform)
- Fragment Shader
- Render
const material = new THREE.RawShaderMaterial({
vertexShader: `
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 0.1);
}
`,
fragmentShader: `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`,
// You can use 'side' and 'transparent' and 'wireframe' properties
})
index.js
import testVertexShader from './shaders/test/vertex.glsl'
import testFragmentShader from './shaders/test/fragment.glsl'
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testFragmentShader
})
create a file with .glsl extension for vertex shader
vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 0.1);
}
create a file with .glsl extension for fragment shader
fragment.glsl
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
config the bundler for glsl files
webpack.common.js
module.exports = {
module: {
rules: [
{
test: /\.(glsl|vs|fs|vert|frag)$/,
exclude: /node_modules/,
use: ['raw-loader']
}]}}
The Shader language called (OpenGL Shading Language)
close to C Language
There is no console
The indentation is not essential ...
GLSL_Shaders
void main() {
// Integer & Float
float foo = -1.0;
int bar = 1;
float a = 1.5;
int b = 2;
float c = a * float(b);
// Boolian
bool x = true;
bool y = false;
// Verctor 2
vec2 test = vec2(1.0, 2.0);
vec2 test = vec2(0.0);
test.x = 1.0;
test.y = 2.0;
test *= 2.0; // result: (2.0, 4.0)
// Vector3 (just like vec2)
vec3 vertex = vec3(1.0, 0.0, 3.0);
vec3 color = vec3(0.0);
color.r = 3.0; // or color.x
color.g = 2.2; // or color.y
color.b = 2.2; // or color.z
// and you can mix them
vec2 foo = vec2(1.0, 2.0);
vec3 bar = vec3(foo, 3.0);
vec2 fooBar = bar.xy; // or bar.xz | bar.yz
// Vector4 (just like vec2 & vec3)
vec4 foo = vec4(1.0, 2.0, 3.0, 4.0); // (x, y, z, w)
float bar = foo.w;
}
void justDoSomething() {
int x = 1;
int y = 3;
}
float sum(float a, float b) {
return a + b;
}
void main() {
float result = sum(1.0, 2.0);
}
index.js
const geometry = new THREE.PlaneBufferGeometry(1, 1, 32, 32)
const count = geometry.attributes.position.count
const random = new Float32Array(count)
for(let i = 0; i <= random.length; i++) {
random[i] = Math.random()
}
geometry.setAttribute('aRandom', new THREE.BufferAttribute(random, 1))
vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
attribute float aRandom;
varying float vRandom;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += aRandom * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vRandom = aRandom;
}
fragment.glsl
precision mediump float;
varying float vRandom;
void main() {
gl_FragColor = vec4(1.0, vRandom, 0.0, 1.0);
}
const textureLoader = new THREE.TextureLoader()
const flagTexture = textureLoader.load('/textures/iran-flag.jpg')
const material = new THREE.RawShaderMaterial({
// ...
uniforms: {
uFrequency: { value: new THREE.Vector4(10, 5) },
uTime: { value: 0 },
uColor: { value: new THREE.Color('orange') },
uTexture: { value: flagTexture }
}
})
vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;
uniform float uTime;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * uFrequency.x - uTime) * 0.1;
modelPosition.z += sin(modelPosition.y * uFrequency.y - uTime) * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vUv = uv;
}
fragment.glsl
precision mediump float;
uniform vec3 uColor;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 textureColor = texture2D(uTexture, vUv);
gl_FragColor = textureColor;
}
remove the following uniform and attribute and precision in both shaders:
- uniform mat4 projectionMatrix;
- uniform mat4 viewMatrix;
- uniform mat4 modelMatrix;
- attribute vec3 position;
- attribute vec2 uv;
- precision mediump float;
HTML
<div class="loading-bar" />
CSS
.loading-bar
{
position: absolute;
top: 50%;
width: 100%;
height: 2px;
background: #ffffff;
transform: scaleX(0);
transform-origin: top left;
transition: transform 0.5s;
will-change: transform;
}
.loading-bar.ended
{
transform: scaleX(0);
transform-origin: top right;
transition: transform 1.5s ease-in-out;
}
Overlay
const overlayGeometry = new THREE.PlaneGeometry(2, 2, 1, 1)
const overlayMaterial = new THREE.ShaderMaterial({
// wireframe: true,
transparent: true,
uniforms:
{
uAlpha: { value: 1 }
},
vertexShader: `
void main()
{
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uAlpha;
void main()
{
gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha);
}
`
})
const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial)
scene.add(overlay)
Progress
const loadingBarElement = document.querySelector('.loading-bar')
const loadingManager = new THREE.LoadingManager(
// Loaded
() => {
// Wait a little (gsap.delayedCall or window.setTimeout)
gsap.delayedCall(0.5, () => {
// Animate overlay
gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0, delay: 1 })
// Update loadingBarElement
loadingBarElement.classList.add('ended')
loadingBarElement.style.transform = ''
})
},
// Progress
(itemUrl, itemsLoaded, itemsTotal) => {
// Calculate the progress and update the loadingBarElement
const progressRatio = itemsLoaded / itemsTotal
loadingBarElement.style.transform = `scaleX(${progressRatio})`
}
)
const gltfLoader = new GLTFLoader(loadingManager)
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)
const group = new THREE.Group()
scene.add(group)
group.add(obj1, obj2, ...[])
const mouse = new THREE.Vector2()
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX / sizes.width * 2 - 1
mouse.y = - (event.clientY / sizes.height * 2 - 1)
})
const fog = new THREE.Fog(0x262837, 1, 16)
scene.fog = fog
renderer.setClearColor(0x262837) // fog color
const environmentMap = 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'
])
scene.background = environmentMap
scene.environment = environmentMap
const updateAllMaterials = () =>
{
scene.traverse((child) =>
{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
{
child.material.envMap = environmentMap
child.material.envMapIntensity = debugObject.envMapIntensity
child.material.needsUpdate = true
}
})
}
const insideColor = new THREE.Color('#ff6030')
const outsideColor = new THREE.Color('#1b3948')
const mixedColor = insideColor.clone()
mixedColor.lerp(outsideColor, alpha) // alpha: 0 to 1
const cursor = new THREE.Vector2()
window.addEventListener('mousemove', (event) => {
cursor.x = (event.clientX / sizes.width) - 0.5
cursor.y = (event.clientY / sizes.height) - 0.5
})
let scrollY = window.scrollY // need update in EventListener
const objectsDistance = 4
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - previousTime
previousTime = elapsedTime
// Scroll
camera.position.y = - scrollY / sizes.height * objectsDistance
// Parallax
const parallaxX = cursor.x * 0.5
const parallaxY = - cursor.y * 0.5
cameraGroup.position.x += (parallaxX - cameraGroup.position.x) * 5 * deltaTime
cameraGroup.position.y += (parallaxY - cameraGroup.position.y) * 5 * deltaTime
}