Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created March 1, 2026 21:03
Show Gist options
  • Select an option

  • Save EncodeTheCode/c112494ebb845f60e86f200f971462b3 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/c112494ebb845f60e86f200f971462b3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js FPS Flashlight Demo</title>
<style>
body { margin:0; overflow:hidden; background:#000; }
#ui { position:absolute; top:10px; left:10px; color:#fff; font-family:monospace; background:rgba(0,0,0,0.3); padding:8px; border-radius:8px; }
</style>
</head>
<body>
<div id="ui">
<label><input type="checkbox" id="flashToggle"> Flashlight</label>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.150.1/build/three.min.js"></script>
<script>
(() => {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 5000);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Simple sky
scene.background = new THREE.Color(0x87ceeb);
// Lights
const dirLight = new THREE.DirectionalLight(0xffffff,0.8);
dirLight.position.set(50,80,50);
dirLight.castShadow = true;
scene.add(dirLight);
scene.add(new THREE.AmbientLight(0xffffff,0.3));
// ---------- Textures ----------
function createTexture(color="#777") {
const size = 256;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = color; ctx.fillRect(0,0,size,size);
return new THREE.CanvasTexture(canvas);
}
const gravelTex = createTexture("#777");
gravelTex.wrapS = gravelTex.wrapT = THREE.RepeatWrapping; gravelTex.repeat.set(16,16);
const brickTex = createTexture("#7a3024");
brickTex.wrapS = brickTex.wrapT = THREE.RepeatWrapping; brickTex.repeat.set(8,8);
const crateTexs = [
createTexture("#8b5a2b"),
createTexture("#7a4f24"),
createTexture("#b07a3f"),
createTexture("#996032")
];
crateTexs.forEach(t=>{t.wrapS=t.wrapT=THREE.RepeatWrapping;});
// ---------- Floor ----------
const floorMat = new THREE.MeshStandardMaterial({map:gravelTex, roughness:1, metalness:0});
const floorGeo = new THREE.PlaneGeometry(200,200);
const floor = new THREE.Mesh(floorGeo,floorMat);
floor.rotation.x = -Math.PI/2; floor.position.y=0;
floor.receiveShadow = true;
scene.add(floor);
// ---------- Walls ----------
const wallMat = new THREE.MeshStandardMaterial({map:brickTex, roughness:0.9, metalness:0});
const walls = [];
const wallGeo1 = new THREE.BoxGeometry(200,20,2);
const wallGeo2 = new THREE.BoxGeometry(2,20,200);
const wall1 = new THREE.Mesh(wallGeo1, wallMat); wall1.position.set(0,10,-100); wall1.castShadow=true; scene.add(wall1); walls.push(wall1);
const wall2 = new THREE.Mesh(wallGeo1, wallMat); wall2.position.set(0,10,100); wall2.castShadow=true; scene.add(wall2); walls.push(wall2);
const wall3 = new THREE.Mesh(wallGeo2, wallMat); wall3.position.set(-100,10,0); wall3.castShadow=true; scene.add(wall3); walls.push(wall3);
const wall4 = new THREE.Mesh(wallGeo2, wallMat); wall4.position.set(100,10,0); wall4.castShadow=true; scene.add(wall4); walls.push(wall4);
// ---------- Crate ----------
const crateMats = crateTexs.map(tex=>new THREE.MeshStandardMaterial({map:tex, roughness:0.75, metalness:0.1}));
const crateGeo = new THREE.BoxGeometry(3,3,3);
const crate = new THREE.Mesh(crateGeo, crateMats);
crate.position.set(0,1.5,0); crate.castShadow=true; crate.receiveShadow=true;
scene.add(crate);
// ---------- Flashlight ----------
const flashlight = new THREE.SpotLight(0xfff8e0,0,100,Math.PI/8,0.5,2);
flashlight.castShadow = true;
flashlight.shadow.mapSize.set(1024,1024);
const flashTarget = new THREE.Object3D();
scene.add(flashlight);
scene.add(flashTarget);
flashlight.target = flashTarget;
const flashToggle = document.getElementById("flashToggle");
// ---------- Camera & Controls ----------
const player = {pos:new THREE.Vector3(0,2,6), velocity:new THREE.Vector3(), speed:6, radius:1};
let yaw=0, pitch=0, lmb=false;
const keys = {};
document.addEventListener('keydown',e=>keys[e.key.toLowerCase()]=true);
document.addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);
document.addEventListener('mousedown', e=>{if(e.button===0)lmb=true;});
document.addEventListener('mouseup', e=>{if(e.button===0)lmb=false;});
document.addEventListener('mousemove', e=>{
if(lmb){
yaw -= e.movementX*0.0025;
pitch -= e.movementY*0.0025;
pitch = Math.max(-Math.PI/2+0.01, Math.min(Math.PI/2-0.01, pitch));
}
});
function clamp(v,min,max){return Math.max(min,Math.min(max,v));}
function moveCollide(pos, delta){
let next = pos.clone().add(delta);
const radius = player.radius;
[...walls, crate].forEach(obj=>{
const box = new THREE.Box3().setFromObject(obj);
const closest = new THREE.Vector3(clamp(next.x,box.min.x,box.max.x), clamp(next.y,box.min.y,box.max.y), clamp(next.z,box.min.z,box.max.z));
const diff = next.clone().sub(closest);
if(diff.length()<radius){diff.setLength(radius-diff.length()); next.add(diff);}
});
return next;
}
// ---------- Animation Loop ----------
let last=performance.now(), acc=0, fpsTarget=30;
function animate(now){
const dtMs = now-last; last=now; acc+=dtMs;
const frameMs = 1000/fpsTarget;
if(acc>=frameMs){
const dt=acc/1000; acc=0;
// Movement
let dir = new THREE.Vector3();
if(keys['w']) dir.z=-1; if(keys['s']) dir.z=1;
if(keys['a']) dir.x=-1; if(keys['d']) dir.x=1;
if(dir.length()>0){dir.normalize(); dir.applyEuler(new THREE.Euler(pitch,yaw,0,'YXZ')); dir.multiplyScalar(player.speed*dt); player.pos.copy(moveCollide(player.pos,dir));}
camera.position.copy(player.pos);
camera.rotation.set(pitch,yaw,0);
// Flashlight
if(flashToggle.checked){
flashlight.position.copy(camera.position);
const forward = new THREE.Vector3(0,0,-1).applyEuler(camera.rotation).multiplyScalar(100);
flashTarget.position.copy(camera.position.clone().add(forward));
flashlight.target.updateMatrixWorld();
flashlight.intensity = 2.5;
} else flashlight.intensity = 0;
renderer.render(scene,camera);
}
requestAnimationFrame(animate);
}
animate(performance.now());
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment