<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js FPS Demo Engine</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
pointer-events: none;
border-radius: 5px;
}
#editor-pane {
position: absolute;
right: 10px;
top: 10px;
width: 200px;
background: rgba(20, 20, 20, 0.8);
color: #ccc;
padding: 15px;
display: none; /* Toggle this when an object is clicked */
}
input { width: 50px; background: #333; color: white; border: 1px solid #555; }
</style>
</head>
<body>
<div id="ui">
<!--<b>Controls:</b> WASD (Move) | Arrows (Turn) | Q/E (Strafe) <br>-->
<b>Controls:</b> WASD / Arrows (Move) | Q/E (Strafe)<br>
Edit mode A/D (Turn) | Play mode A/D (Strafe)<br>
Shift (Sprint) | Space (Jump) | C (Crouch) <br>
Click Plane to Test Raycast or press G for Play mode
</div>
<div id="editor-pane" id="editor">
<h3>Transform</h3>
X: <input type="number" id="posX"> <br>
Y: <input type="number" id="posY"> <br>
Z: <input type="number" id="posZ">
</div>
<script type="importmap">
{
"imports": {
"three": "./build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// --- Core Variables ---
let scene, camera, renderer, clock;
let floor, raycaster, mouse;
// --- Player State ---
const player = {
height: 1.7,
velocity: new THREE.Vector3(),
speed: 5.0,
rotation: 0, // Yaw (Left/Right)
pitch: 0, // Pitch (Up/Down)
isGrounded: true,
isCrouching: false,
sensitivity: 0.002
};
const keys = {};
let isLocked = false;
let initialized = false;
function init() {
const textureLoader = new THREE.TextureLoader();
// Load Grass Textures
const grassDiffuse = textureLoader.load('./textures/grass2.webp');
const grassNormal = textureLoader.load('./textures/grass2_normal.webp');
const grassRough = textureLoader.load('./textures/grass2_rough.webp');
// Configure Tiling
[grassDiffuse, grassNormal, grassRough].forEach(tex => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(100, 100); // Adjust based on your scene scale
});
// 1. Scene & Camera
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // Sky blue
scene.fog = new THREE.Fog(0x87ceeb, 10, 50);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, player.height, 5);
// 2. Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
clock = new THREE.Clock();
// 3. Lighting
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(10, 20, 10);
sun.castShadow = true;
sun.shadow.camera.left = -20;
sun.shadow.camera.right = 20;
sun.shadow.camera.top = 20;
sun.shadow.camera.bottom = -20;
scene.add(sun);
// 4. Floor (The Collider)
const floorGeo = new THREE.PlaneGeometry(100, 100);
//const floorMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
const floorMat = new THREE.MeshStandardMaterial({
map: grassDiffuse,
normalMap: grassNormal,
roughnessMap: grassRough,
roughness: 0.8
});
floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
floor.name = "Floor";
scene.add(floor);
// 5. Input Listeners
window.addEventListener('keydown', (e) => keys[e.code] = true);
window.addEventListener('keyup', (e) => keys[e.code] = false);
// 6. Interaction Setup
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
window.addEventListener('click', onMouseClick);
window.addEventListener('resize', onWindowResize);
//--// --- Mouse Look Logic ---
//--document.addEventListener('click', () => {
//-- // Only lock if we aren't clicking an UI element or already locked
//-- if(!isLocked) renderer.domElement.requestPointerLock();
//--});
//--
//--document.addEventListener('pointerlockchange', () => {
//-- isLocked = document.pointerLockElement === renderer.domElement;
//--});
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
// Toggle Play/Edit Mode
if (e.code === 'KeyG') {
if (!isLocked) {
renderer.domElement.requestPointerLock();
} else {
document.exitPointerLock();
}
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === renderer.domElement;
// Visual feedback (optional)
const ui = document.getElementById('ui');
ui.style.border = isLocked ? "2px solid lime" : "2px solid red";
ui.innerHTML = isLocked ? "<b>MODE: PLAY</b> (G to Edit)" : "<b>MODE: EDIT</b> (G to Play)";
});
window.addEventListener('mousemove', (e) => {
if (!isLocked) return;
// 1. Update Yaw (Left/Right)
player.rotation -= e.movementX * player.sensitivity;
// 2. Update Pitch (Up/Down)
player.pitch -= e.movementY * player.sensitivity;
// Clamp pitch to 90 degrees up/down
player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));
});
initialized = true;
}
function onMouseClick(event) {
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const hit = intersects[0];
console.log("Hit:", hit.object.name, "at", hit.point);
// logic for placing or selecting object would go here
}
}
function handleMovement(delta) {
if(!initialized) return;
let currentSpeed = player.speed;
if (keys['ShiftLeft']) currentSpeed *= 1.8;
// 1. Determine Direction Scalars (Cancels out if both keys pressed)
let moveForward = 0;
if (keys['KeyW'] || keys['ArrowUp']) moveForward += 1;
if (keys['KeyS'] || keys['ArrowDown']) moveForward -= 1;
let moveStrafe = 0;
let turnDir = 0;
if(isLocked) { // play mode controls
if (keys['KeyQ']) document.exitPointerLock();;
if (keys['KeyA']) moveStrafe -= 1;
if (keys['KeyD']) moveStrafe += 1;
if (keys['ArrowLeft']) turnDir += 1;
if (keys['ArrowRight']) turnDir -= 1;
} else { // edit mode controls
if (keys['KeyE']) moveStrafe += 1;
if (keys['KeyQ']) moveStrafe -= 1;
if (keys['KeyA'] || keys['ArrowLeft']) turnDir += 1;
if (keys['KeyD'] || keys['ArrowRight']) turnDir -= 1;
}
// 2. Apply Turning
const turnSpeed = 2.5 * delta;
player.rotation += turnDir * turnSpeed;
// 3. Calculate Final Movement Vector
// We clone the vectors so we don't mutate the camera's base orientation
const forwardVec = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
forwardVec.y = 0;
forwardVec.normalize();
const rightVec = new THREE.Vector3().crossVectors(forwardVec, new THREE.Vector3(0, 1, 0)).normalize();
// Reset horizontal velocity but keep vertical (for gravity/jumping)
player.velocity.x = 0;
player.velocity.z = 0;
// Add Forward/Back contribution
if (moveForward !== 0) {
player.velocity.add(forwardVec.multiplyScalar(moveForward * currentSpeed));
}
// Add Strafe contribution
if (moveStrafe !== 0) {
player.velocity.add(rightVec.multiplyScalar(moveStrafe * currentSpeed));
}
// 4. Update Camera Orientation
camera.quaternion.setFromEuler(new THREE.Euler(player.pitch, player.rotation, 0, 'YXZ'));
// 5. Physics & Gravity
if (player.isGrounded && keys['Space']) {
player.velocity.y = 6.0;
player.isGrounded = false;
}
player.velocity.y -= 15.0 * delta;
camera.position.add(player.velocity.clone().multiplyScalar(delta));
// Floor Snap
const targetHeight = keys['KeyC'] ? 0.8 : 1.7;
player.height = THREE.MathUtils.lerp(player.height, targetHeight, 0.1);
if (camera.position.y < player.height) {
camera.position.y = player.height;
player.velocity.y = 0;
player.isGrounded = true;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
handleMovement(delta);
renderer.render(scene, camera);
}
init();
animate();
</script>
</body>
</html>
Top