Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save EncodeTheCode/91cab75953b9af2fab4db0effe1f05fa 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:#0b0b0d; }
#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);
});
// 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));
// ---------- Original SVG textures ----------
function svgDataURI(svg){return 'data:image/svg+xml;utf8,'+encodeURIComponent(svg);}
function createTexture(svg){
const tex = new THREE.TextureLoader().load(svgDataURI(svg));
tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.anisotropy=0; tex.needsUpdate=true;
return tex;
}
const gravelSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<rect width="256" height="256" fill="#777974"/>
<g fill="#9b9b98">
<ellipse cx="32" cy="50" rx="6" ry="4"/>
<ellipse cx="72" cy="30" rx="4" ry="3"/>
<ellipse cx="110" cy="60" rx="5" ry="3"/>
<ellipse cx="160" cy="20" rx="7" ry="5"/>
<ellipse cx="204" cy="56" rx="5" ry="4"/>
<ellipse cx="230" cy="210" rx="6" ry="4"/>
</g>
</svg>`;
const brickSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<rect width="256" height="256" fill="#7a3024"/>
<g fill="#8b3d2e">
<rect x="0" y="0" width="64" height="32"/>
<rect x="0" y="32" width="64" height="32"/>
</g>
</svg>`;
function crateFaceSVG(i){
const colors=['#8b5a2b','#7a4f24','#b07a3f','#996032'];
return `<svg xmlns='http://www.w3.org/2000/svg' width='256' height='256'>
<rect width='256' height='256' fill='${colors[i%4]}'/>
</svg>`;
}
const textures = {
gravel: createTexture(gravelSVG),
brick: createTexture(brickSVG),
crate0: createTexture(crateFaceSVG(0)),
crate1: createTexture(crateFaceSVG(1)),
crate2: createTexture(crateFaceSVG(2)),
crate3: createTexture(crateFaceSVG(3))
};
// ---------- Map boundaries ----------
const levelHalf=10, padding=320;
const bounds={min:new THREE.Vector3(-levelHalf-padding,0,-levelHalf-padding), max:new THREE.Vector3(levelHalf+padding,24,levelHalf+padding)};
const collisionBoxes=[];
// Floor
const floorGeo = new THREE.PlaneGeometry(bounds.max.x-bounds.min.x, bounds.max.z-bounds.min.z);
const floorMat = new THREE.MeshStandardMaterial({map:textures.gravel, roughness:1, metalness:0});
const floor = new THREE.Mesh(floorGeo,floorMat); floor.rotation.x=-Math.PI/2; floor.position.set((bounds.min.x+bounds.max.x)/2,0,(bounds.min.z+bounds.max.z)/2);
floor.receiveShadow = true; scene.add(floor);
// Walls
function addWall(min,max){
const geo = new THREE.BoxGeometry(max.x-min.x, max.y-min.y, max.z-min.z);
const mat = new THREE.MeshStandardMaterial({map:textures.brick, roughness:0.9, metalness:0});
const mesh = new THREE.Mesh(geo,mat);
mesh.position.set((min.x+max.x)/2,(min.y+max.y)/2,(min.z+max.z)/2);
mesh.castShadow=true; mesh.receiveShadow=true; scene.add(mesh);
collisionBoxes.push(new THREE.Box3().setFromObject(mesh));
return mesh;
}
const wallThickness=2;
addWall(new THREE.Vector3(bounds.min.x-wallThickness,0,bounds.min.z-wallThickness), new THREE.Vector3(bounds.max.x+wallThickness,18,bounds.min.z));
addWall(new THREE.Vector3(bounds.min.x-wallThickness,0,bounds.max.z), new THREE.Vector3(bounds.max.x+wallThickness,18,bounds.max.z+wallThickness));
addWall(new THREE.Vector3(bounds.min.x-wallThickness,0,bounds.min.z), new THREE.Vector3(bounds.min.x,18,bounds.max.z));
addWall(new THREE.Vector3(bounds.max.x,0,bounds.min.z), new THREE.Vector3(bounds.max.x+wallThickness,18,bounds.max.z));
// Crate
const crateMats = [];
for(let i=0;i<6;i++){
crateMats.push(new THREE.MeshStandardMaterial({map:textures['crate'+(i%4)], 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); collisionBoxes.push(new THREE.Box3().setFromObject(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;
[...collisionBoxes].forEach(box=>{
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
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;
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);
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.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