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)