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)