Created
March 1, 2026 21:06
-
-
Save EncodeTheCode/91cab75953b9af2fab4db0effe1f05fa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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