Created
March 1, 2026 21:03
-
-
Save EncodeTheCode/c112494ebb845f60e86f200f971462b3 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:#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