Preview and Video at https://github.com/yusufmalik2008/PBR-three.js
๐ก Introduction
This is a minimalistic three.js setup demonstrating PBR shading, Image-Based Lighting (IBL), shadows, and FPS-style camera control. Itโs built with zero dependencies beyond Three.js itself and runs directly in the browser.
It's aimed for beginners and creative developers who want to learn how to:
Set up PBR lighting and reflections
Load an HDRI environment map
Create simple dynamic shadows
Use standard materials instead of legacy Phong
Add FPS controls using pointer lock and keyboard
๐ท Live Preview
Paste the code into an .html file and open in a browser. No local server or build tool required.
โ๏ธ Step 1 โ Project Setup & Structure
What you'll learn: Creating a minimalist Three.js project:
<!DOCTYPE html><html lang="en"><head><meta charset="UTFโ8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>PBR Demo</title><style>body{margin:0;background:#111}canvas{display:block}</style></head><body>
<script type="module">
// ๐ Import Three.js
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
// ๐ Create scene + camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
// ๐ฅ Create renderer
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
</script>
</body></html>
Goal: Run this in a browser and confirm a blank canvas loads with no build tools required.
๐ Step 2 โ Add Environment Map + IBL
Goal: Learn how PMREMGenerator converts HDR imagery for real-time reflections.
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
new THREE.TextureLoader().load(
'https://threejs.org/examples/textures/2294472375_24a3b8ef46_o.jpg',
(tex) => {
const env = pmrem.fromEquirectangular(tex).texture;
scene.environment = env;
scene.background = env;
tex.dispose(); pmrem.dispose();
}
);
โ๏ธ Test: Replace textures or change background โ reflections update.
๐ฆ Step 3 โ Set Up Lighting & Shadows
Goal: Use AmbientLight and DirectionalLight with shadows enabled.
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const ambient = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 1);
dir.position.set(4, 10, 4);
dir.castShadow = true;
dir.shadow.mapSize.set(512, 512);
[dir.shadow.camera.left, dir.shadow.camera.right, dir.shadow.camera.top, dir.shadow.camera.bottom] = [-20,20,20,-20];
scene.add(dir);
๐ Test: Add a cube or sphere to the scene and see the shadow effect.
๐งฉ Step 4 โ Create PBR Ground + Cubes
Goal: Build a reflective ground and rotating PBR cubes.
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(100, 100),
new THREE.MeshStandardMaterial({ color:0x334477, roughness:0.2, metalness:0.6, side:THREE.DoubleSide })
);
ground.rotation.x = -Math.PI/2;
ground.receiveShadow = true;
scene.add(ground);
const cubes = [];
const mat = new THREE.MeshStandardMaterial({ color:0x44ccff, metalness:0.5, roughness:0.1 });
const boxGeo = new THREE.BoxGeometry();
for(let i=-2;i<=2;i++){
const cube = new THREE.Mesh(boxGeo, mat);
cube.position.set(i*1.5,0.5,0);
cube.castShadow = true;
scene.add(cube);
cubes.push(cube);
// optional visual "shadow blob":
const blob = new THREE.Mesh(
new THREE.CircleGeometry(0.7,32),
new THREE.MeshBasicMaterial({ color:0x000000, transparent:true, opacity:0.4 })
);
blob.rotation.x=-Math.PI/2;
blob.position.set(i*1.5,0.01,0);
scene.add(blob);
}
โก๏ธ Visual check: Cubes reflect on the ground and cast shadows.
๐ฎ Step 5 โ FPS Camera Controls
Goal: Implement pointer-lock for free-look controls + WASD movement.
let yaw=0, pitch=0;
const velocity=new THREE.Vector3(), keys={};
document.body.addEventListener('click',()=>document.body.requestPointerLock());
document.addEventListener('pointerlockchange',()=>{
const fn = document.pointerLockElement ? onMouseMove : () => {};
document[document.pointerLockElement ? 'addEventListener' : 'removeEventListener']('mousemove', fn);
});
function onMouseMove(e){
yaw -= e.movementX * 0.002;
pitch = THREE.MathUtils.clamp(pitch - e.movementY * 0.002, -Math.PI/2, Math.PI/2);
}
document.addEventListener('keydown', e => (keys[e.key.toLowerCase()]=true));
document.addEventListener('keyup', e => (keys[e.key.toLowerCase()]=false));
โ Confirm mouse-lock and mouse look work after clicking the canvas.
๐ Step 6 โ Animation Loop & Movement Logic
Goal: Implement animate loop & rotate cubes.
function animate(){
requestAnimationFrame(animate);
// handle movement
const dir = new THREE.Vector3(
(keys['d']?1:0) - (keys['a']?1:0),
0,
(keys['s']?1:0) - (keys['w']?1:0)
).normalize().applyAxisAngle(new THREE.Vector3(0,1,0), yaw);
camera.position.addScaledVector(dir, 0.05);
camera.rotation.set(pitch, yaw, 0, 'YXZ');
// rotate cubes
cubes.forEach(c => {
c.rotation.x += 0.01;
c.rotation.y += 0.01;
});
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
โก๏ธ Visual check: camera movement and cube rotation are smooth.
๐ How You Can Extend This
Add emissive cubes (light sources)
Add bloom post-process for glow effects
Swap materials dynamically (metalness / roughness)
Add moving point lights
Add collision or physics interactions
๐พ Link to Full Source & Repo
You can find the full, clean, no-tooling demo here:
๐ https://github.com/yusufmalik2008/PBR-three.js
Clone it with:
bash
Copy
Edit
git clone https://github.com/yusufmalik2008/PBR-three.js.git
cd PBR-three.js
open index.html
Top comments (0)