DEV Community

Cover image for 🔷 Tutorial: Building a Simple PBR Scene with Shadows and FPS Controls in Three.js
Joseph
Joseph

Posted on

🔷 Tutorial: Building a Simple PBR Scene with Shadows and FPS Controls in Three.js

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> 
Enter fullscreen mode Exit fullscreen mode

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();
  }
);

Enter fullscreen mode Exit fullscreen mode

✔️ 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);

Enter fullscreen mode Exit fullscreen mode

🔍 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);
}

Enter fullscreen mode Exit fullscreen mode

➡️ 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));

Enter fullscreen mode Exit fullscreen mode

✅ 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);
});

Enter fullscreen mode Exit fullscreen mode

➡️ 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)