https://sites.google.com/view/jeep-3d-off-road-simulator/full-page-embed
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Jeep 3D Off-Road Simulator</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Three.js & OrbitControls -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
user-select: none;
-webkit-user-select: none;
background-color: #0f172a;
}
.font-orbitron {
font-family: 'Orbitron', sans-serif;
}
/* Custom glassmorphism styling */
.glass-panel {
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Custom slider styles */
input[type=range] {
-webkit-appearance: none;
background: transparent;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 10px;
cursor: pointer;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
input[type=range]::-webkit-slider-thumb {
height: 22px;
width: 22px;
border-radius: 50%;
background: #f59e0b;
cursor: pointer;
-webkit-appearance: none;
margin-top: -6px;
box-shadow: 0 0 10px rgba(245, 158, 11, 0.8);
transition: transform 0.1s ease;
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.15);
}
/* Controls key visualization */
.key-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: inset 0 -2px 0 rgba(0,0,0,0.5);
transition: all 0.1s ease;
}
.key-btn.active {
background: #f59e0b;
color: #000;
transform: translateY(2px);
box-shadow: none;
}
</style>
</head>
<body class="w-screen h-screen overflow-hidden text-white">
<!-- Canvas Container -->
<div id="canvas-container" class="w-full h-full absolute inset-0 z-0"></div>
<!-- UI Overlay HUD -->
<div id="hud-layer" class="relative z-10 w-full h-full pointer-events-none flex flex-col justify-between p-4 sm:p-6">
<!-- Top Bar Header & Controls info -->
<div class="flex justify-between items-start w-full">
<!-- Brand / Title -->
<div class="glass-panel p-4 rounded-2xl pointer-events-auto shadow-2xl flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-amber-500 text-slate-950 flex items-center justify-center font-extrabold text-xl shadow-lg">
<i class="fa-solid fa-truck-monster"></i>
</div>
<div>
<h1 class="font-orbitron font-extrabold text-base sm:text-lg tracking-wider text-amber-400">JEEP OFF-ROAD 3D</h1>
<p class="text-xs text-slate-300">Simulador de terreno con baches</p>
</div>
</div>
<!-- Action buttons (Respawn & Camera Mode) -->
<div class="flex gap-3 pointer-events-auto">
<button id="btn-respawn" title="Reiniciar Posición (R)" class="glass-panel hover:bg-amber-500/20 active:scale-95 text-amber-400 hover:text-amber-300 font-semibold px-4 py-3 rounded-xl flex items-center gap-2 transition-all shadow-lg border border-amber-500/30">
<i class="fa-solid fa-rotate-left text-lg"></i>
<span class="hidden sm:inline text-xs font-orbitron uppercase tracking-wider">Reiniciar (R)</span>
</button>
<button id="btn-camera" title="Cambiar vista de cámara (C)" class="glass-panel hover:bg-slate-700/50 active:scale-95 text-slate-200 font-semibold p-3 sm:px-4 sm:py-3 rounded-xl flex items-center gap-2 transition-all shadow-lg">
<i class="fa-solid fa-video text-lg"></i>
<span class="hidden sm:inline text-xs font-orbitron uppercase tracking-wider">Cámara (C)</span>
</button>
</div>
</div>
<!-- Center Notification Toast / Flip Warning -->
<div id="warning-toast" class="self-center glass-panel border-amber-500/50 bg-amber-500/10 text-amber-300 px-6 py-3 rounded-full flex items-center gap-3 opacity-0 transition-opacity duration-300 pointer-events-none">
<i class="fa-solid fa-triangle-exclamation text-xl animate-bounce text-amber-400"></i>
<span class="font-orbitron text-xs sm:text-sm tracking-wide">¡Jeep Inclinado/Volcado! Presiona R para colocarlo derecho</span>
</div>
<!-- Bottom Controls & Speedometer Section -->
<div class="flex flex-col sm:flex-row justify-between items-end gap-4 w-full">
<!-- Calibration Panel (Slider Potencia/Velocidad) -->
<div class="glass-panel p-4 rounded-2xl pointer-events-auto w-full sm:w-80 shadow-2xl space-y-3">
<div class="flex justify-between items-center border-b border-white/10 pb-2">
<span class="text-xs font-orbitron text-amber-400 font-bold uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-gauge-high"></i> Calibrar Motor
</span>
<span id="speed-limit-val" class="font-orbitron text-sm font-extrabold text-white">80 km/h</span>
</div>
<div>
<label class="text-[11px] text-slate-300 flex justify-between mb-1">
<span>Potencia Máxima (V-Max)</span>
<span id="power-percentage" class="text-amber-400 font-semibold">100%</span>
</label>
<input type="range" id="slider-speed" min="30" max="150" value="80" class="w-full">
</div>
<div class="grid grid-cols-2 gap-2 pt-1 text-[10px] text-slate-400">
<div class="flex items-center gap-1.5 bg-slate-800/50 p-1.5 rounded-lg border border-white/5">
<i class="fa-solid fa-mountain text-amber-400"></i> Terreno: <span class="text-white font-medium">Baches 3D</span>
</div>
<div class="flex items-center gap-1.5 bg-slate-800/50 p-1.5 rounded-lg border border-white/5">
<i class="fa-solid fa-car-side text-amber-400"></i> Tracción: <span class="text-white font-medium">4x4 OffRoad</span>
</div>
</div>
</div>
<!-- Digital Speedometer HUD -->
<div class="glass-panel p-5 rounded-3xl pointer-events-auto flex items-center gap-5 shadow-2xl self-center sm:self-end">
<div class="relative flex items-center justify-center">
<!-- Circular speed meter SVG -->
<svg class="w-24 h-24 transform -rotate-90">
<circle cx="48" cy="48" r="40" stroke="rgba(255,255,255,0.1)" stroke-width="8" fill="transparent"/>
<circle id="speed-circle" cx="48" cy="48" r="40" stroke="#f59e0b" stroke-width="8" fill="transparent"
stroke-dasharray="251.2" stroke-dashoffset="251.2" stroke-linecap="round" class="transition-all duration-100"/>
</svg>
<div class="absolute flex flex-col items-center justify-center text-center">
<span id="speed-display" class="font-orbitron font-black text-2xl sm:text-3xl text-white tracking-tighter">0</span>
<span class="text-[9px] font-orbitron text-amber-400 font-bold uppercase tracking-widest">KM/H</span>
</div>
</div>
<div class="flex flex-col gap-2 border-l border-white/10 pl-4">
<div class="flex items-center gap-2">
<span class="text-[10px] text-slate-400 font-orbitron">MARCHA</span>
<span id="gear-display" class="font-orbitron text-sm font-bold bg-amber-500 text-slate-950 px-2 py-0.5 rounded">D</span>
</div>
<div class="flex items-center gap-2">
<span class="text-[10px] text-slate-400 font-orbitron">SUSPENSIÓN</span>
<span id="suspension-status" class="text-xs font-semibold text-emerald-400 flex items-center gap-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span> ACTIVA
</span>
</div>
</div>
</div>
<!-- Controls Keyboard Legend / Touch Controls -->
<div class="glass-panel p-4 rounded-2xl pointer-events-auto hidden md:flex flex-col items-center gap-2 shadow-2xl">
<span class="text-[10px] font-orbitron text-slate-400 uppercase tracking-wider font-bold mb-1">Controles Teclado</span>
<div class="flex gap-1.5">
<div id="key-w" class="key-btn w-9 h-9 rounded-lg flex items-center justify-center font-orbitron font-bold text-sm">W</div>
</div>
<div class="flex gap-1.5">
<div id="key-a" class="key-btn w-9 h-9 rounded-lg flex items-center justify-center font-orbitron font-bold text-sm">A</div>
<div id="key-s" class="key-btn w-9 h-9 rounded-lg flex items-center justify-center font-orbitron font-bold text-sm">S</div>
<div id="key-d" class="key-btn w-9 h-9 rounded-lg flex items-center justify-center font-orbitron font-bold text-sm">D</div>
</div>
</div>
</div>
</div>
<!-- Mobile Touch Controls Overlay (visible standard touch devices) -->
<div class="md:hidden fixed bottom-4 right-4 z-20 pointer-events-auto flex flex-col gap-2">
<div class="flex justify-center">
<button id="touch-up" class="w-14 h-14 bg-amber-500/80 active:bg-amber-400 text-slate-950 rounded-2xl font-bold text-xl flex items-center justify-center shadow-lg active:scale-95 touch-none">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="flex gap-2">
<button id="touch-left" class="w-14 h-14 bg-slate-800/80 active:bg-slate-700 text-white rounded-2xl font-bold text-xl flex items-center justify-center shadow-lg active:scale-95 touch-none">
<i class="fa-solid fa-chevron-left"></i>
</button>
<button id="touch-down" class="w-14 h-14 bg-slate-800/80 active:bg-slate-700 text-white rounded-2xl font-bold text-xl flex items-center justify-center shadow-lg active:scale-95 touch-none">
<i class="fa-solid fa-chevron-down"></i>
</button>
<button id="touch-right" class="w-14 h-14 bg-slate-800/80 active:bg-slate-700 text-white rounded-2xl font-bold text-xl flex items-center justify-center shadow-lg active:scale-95 touch-none">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
</div>
<!-- MAIN GAME LOGIC SCRIPT -->
<script>
// --- PERLIN NOISE IMPLEMENTATION FOR TERRAIN HEIGHT MAP ---
const SimplexNoise = function() {
var F3 = 1.0/3.0, G3 = 1.0/6.0;
var p = new Uint8Array(256);
for (var i=0; i<256; i++) p[i] = Math.floor(Math.random()*256);
var perm = new Uint8Array(512), permMod12 = new Uint8Array(512);
for (var i=0; i<512; i++) {
perm[i] = p[i & 255];
permMod12[i] = (perm[i] % 12);
}
return {
noise2D: function(xin, yin) {
var n0, n1, n2;
var s = (xin+yin)*0.5*(Math.sqrt(3.0)-1.0);
var i = Math.floor(xin+s), j = Math.floor(yin+s);
var t = (i+j)*(3.0-Math.sqrt(3.0))/6.0;
var X0 = i-t, Y0 = j-t;
var x0 = xin-X0, y0 = yin-Y0;
var i1, j1;
if(x0>y0) {i1=1; j1=0;} else {i1=0; j1=1;}
var x1 = x0 - i1 + (3.0-Math.sqrt(3.0))/6.0, y1 = y0 - j1 + (3.0-Math.sqrt(3.0))/6.0;
var x2 = x0 - 1.0 + 2.0*(3.0-Math.sqrt(3.0))/6.0, y2 = y0 - 1.0 + 2.0*(3.0-Math.sqrt(3.0))/6.0;
var ii = i & 255, jj = j & 255;
var gi0 = permMod12[ii+perm[jj]], gi1 = permMod12[ii+i1+perm[jj+j1]], gi2 = permMod12[ii+1+perm[jj+1]];
var t0 = 0.5 - x0*x0-y0*y0; if(t0<0) n0 = 0.0; else { t0 *= t0; n0 = t0 * t0 * (gi0%2 === 0 ? 1 : -1)*(x0 + y0); }
var t1 = 0.5 - x1*x1-y1*y1; if(t1<0) n1 = 0.0; else { t1 *= t1; n1 = t1 * t1 * (gi1%2 === 0 ? 1 : -1)*(x1 + y1); }
var t2 = 0.5 - x2*x2-y2*y2; if(t2<0) n2 = 0.0; else { t2 *= t2; n2 = t2 * t2 * (gi2%2 === 0 ? 1 : -1)*(x2 + y2); }
return 70.0 * (n0 + n1 + n2);
}
};
};
const noise = SimplexNoise();
// Dynamic Heightmap function for bumps and hills
function getTerrainHeight(x, z) {
// Multi-octave bumps & hills
const detailBumps = noise.noise2D(x * 0.08, z * 0.08) * 0.7; // Fast small bumps
const mediumBumps = noise.noise2D(x * 0.03, z * 0.03) * 2.5; // Rough uneven ground
const hills = noise.noise2D(x * 0.008, z * 0.008) * 12.0; // Mountain rolling hills
// Safe flat spawn area around origin (0,0)
const distFromOrigin = Math.sqrt(x*x + z*z);
const flatFactor = Math.min(1.0, Math.max(0.0, (distFromOrigin - 12) / 30));
return (detailBumps + mediumBumps + hills) * flatFactor;
}
// --- GLOBAL SCENE SETUP ---
let scene, camera, renderer, orbitControls;
let jeepGroup, wheels = [], headlights = [];
let terrainMesh, terrainGeometry;
const TERRAIN_SIZE = 500;
const TERRAIN_SEGMENTS = 250;
// Vehicle Physics & Dynamics State
const vehicle = {
x: 0,
z: 0,
y: 0,
speed: 0,
maxSpeed: 80, // Calibrated by slider (converted to physics scale)
acceleration: 35.0,
friction: 0.965,
reverseSpeed: -25,
steeringAngle: 0,
maxSteer: 0.5,
steerSpeed: 3.5,
heading: 0, // Yaw orientation in radians
pitch: 0, // Front/back tilt based on terrain slope
roll: 0, // Left/right tilt based on terrain slope
wheelRotation: 0,
isFlipped: false
};
// Inputs State
const keys = { w: false, a: false, s: false, d: false };
// Camera mode: 0 = Chase 3rd Person, 1 = Orbit Free Camera
let cameraMode = 0;
// --- INIT FUNCTION ---
function init() {
const container = document.getElementById('canvas-container');
// 1. Scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // Sky blue
scene.fog = new THREE.FogExp2(0x87ceeb, 0.007);
// 2. Camera setup
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, -14);
// 3. Renderer setup
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
container.appendChild(renderer.domElement);
// 4. Controls (Orbit mode toggleable)
orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.05;
orbitControls.enabled = false; // Off by default for 3rd person chase camera
// 5. Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xfffaed, 1.2);
sunLight.position.set(100, 150, 80);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 400;
const d = 100;
sunLight.shadow.camera.left = -d;
sunLight.shadow.camera.right = d;
sunLight.shadow.camera.top = d;
sunLight.shadow.camera.bottom = -d;
scene.add(sunLight);
// Hemisphere light for natural sky ground shading
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x3d5a2b, 0.4);
scene.add(hemiLight);
// 6. Build Environment
createTerrain();
createTreesAndRocks();
// 7. Build Jeep Vehicle Model
createJeep();
// 8. Event Listeners & UI hooks
setupEventListeners();
// 9. Start Render Loop
window.addEventListener('resize', onWindowResize);
animate();
}
// --- CREATING TERRAIN WITH BUMPS ---
function createTerrain() {
terrainGeometry = new THREE.PlaneGeometry(TERRAIN_SIZE, TERRAIN_SIZE, TERRAIN_SEGMENTS, TERRAIN_SEGMENTS);
terrainGeometry.rotateX(-Math.PI / 2); // Make horizontal
const posAttribute = terrainGeometry.attributes.position;
const colors = [];
const colorSand = new THREE.Color(0xd2b48c);
const colorGrass = new THREE.Color(0x4a7c59);
const colorRock = new THREE.Color(0x6b7280);
const colorHighGrass = new THREE.Color(0x2d5a27);
for (let i = 0; i < posAttribute.count; i++) {
const x = posAttribute.getX(i);
const z = posAttribute.getZ(i);
const h = getTerrainHeight(x, z);
posAttribute.setY(i, h);
// Vertex coloring based on height and slope
const mixColor = new THREE.Color();
if (h < 0.5) {
mixColor.copy(colorSand);
} else if (h < 5.0) {
mixColor.copy(colorGrass).lerp(colorHighGrass, (h - 0.5) / 4.5);
} else {
mixColor.copy(colorHighGrass).lerp(colorRock, Math.min(1.0, (h - 5.0) / 7.0));
}
// Add tiny color noise variations
mixColor.r += (Math.random() - 0.5) * 0.03;
mixColor.g += (Math.random() - 0.5) * 0.03;
colors.push(mixColor.r, mixColor.g, mixColor.b);
}
terrainGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
terrainGeometry.computeVertexNormals();
const terrainMaterial = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.9,
metalness: 0.1,
flatShading: true
});
terrainMesh = new THREE.Mesh(terrainGeometry, terrainMaterial);
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);
}
// --- DECORATIONS: TREES & ROCKS ---
function createTreesAndRocks() {
const treeGeo = new THREE.ConeGeometry(1.6, 5, 5);
const trunkGeo = new THREE.CylinderGeometry(0.3, 0.4, 1.5, 5);
const treeMat = new THREE.MeshStandardMaterial({ color: 0x1e4620, roughness: 0.8 });
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a2e18, roughness: 0.9 });
const rockMat = new THREE.MeshStandardMaterial({ color: 0x78716c, roughness: 0.9, flatShading: true });
const treeInstanced = new THREE.Group();
for (let i = 0; i < 200; i++) {
const rx = (Math.random() - 0.5) * (TERRAIN_SIZE - 40);
const rz = (Math.random() - 0.5) * (TERRAIN_SIZE - 40);
// Keep clear from spawn center
if (Math.abs(rx) < 15 && Math.abs(rz) < 15) continue;
const ry = getTerrainHeight(rx, rz);
if (Math.random() > 0.35) {
// Create Tree
const tree = new THREE.Group();
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
trunk.position.y = 0.75;
trunk.castShadow = true;
const leaves = new THREE.Mesh(treeGeo, treeMat);
leaves.position.y = 3.25;
leaves.castShadow = true;
tree.add(trunk, leaves);
tree.position.set(rx, ry, rz);
const scale = 0.7 + Math.random() * 0.7;
tree.scale.set(scale, scale, scale);
treeInstanced.add(tree);
} else {
// Create Rock/Boulder bump obstacle
const rockGeo = new THREE.DodecahedronGeometry(0.8 + Math.random() * 0.8, 1);
const rock = new THREE.Mesh(rockGeo, rockMat);
rock.position.set(rx, ry + 0.3, rz);
rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
rock.castShadow = true;
rock.receiveShadow = true;
treeInstanced.add(rock);
}
}
scene.add(treeInstanced);
}
// --- DETAILED 3D JEEP VEHICLE GENERATOR ---
function createJeep() {
jeepGroup = new THREE.Group();
// Materials
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd97706, roughness: 0.3, metalness: 0.4 }); // Amber / Orange Paint
const darkMetalMat = new THREE.MeshStandardMaterial({ color: 0x1f2937, roughness: 0.6, metalness: 0.8 }); // Bumpers, Rollbars
const glassMat = new THREE.MeshPhysicalMaterial({ color: 0x111827, transparent: true, opacity: 0.7, roughness: 0.1, transmission: 0.6 });
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.9 });
const rimMat = new THREE.MeshStandardMaterial({ color: 0x9ca3af, metalness: 0.9, roughness: 0.2 });
const lightFrontMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xfffbeb, emissiveIntensity: 0.8 });
const lightBackMat = new THREE.MeshStandardMaterial({ color: 0xef4444, emissive: 0xd97706, emissiveIntensity: 0.6 });
// 1. Chassis Base Body
const bodyMesh = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.8, 3.8), bodyMat);
bodyMesh.position.y = 0.8;
bodyMesh.castShadow = true;
jeepGroup.add(bodyMesh);
// Engine Hood Front
const hoodMesh = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.5, 1.4), bodyMat);
hoodMesh.position.set(0, 1.15, 1.1);
hoodMesh.castShadow = true;
jeepGroup.add(hoodMesh);
// Front Off-Road Grille
const grille = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.5, 0.1), darkMetalMat);
grille.position.set(0, 1.1, 1.81);
jeepGroup.add(grille);
// Headlights
const headLightGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.1, 16);
headLightGeo.rotateX(Math.PI / 2);
const headL = new THREE.Mesh(headLightGeo, lightFrontMat);
headL.position.set(-0.65, 1.15, 1.82);
const headR = new THREE.Mesh(headLightGeo, lightFrontMat);
headR.position.set(0.65, 1.15, 1.82);
jeepGroup.add(headL, headR);
// Spotlights (Front beams)
const spotL = new THREE.SpotLight(0xfff5ea, 2, 40, Math.PI / 6, 0.4);
spotL.position.set(-0.65, 1.15, 1.85);
spotL.target.position.set(-0.65, 0.5, 10);
jeepGroup.add(spotL, spotL.target);
const spotR = new THREE.SpotLight(0xfff5ea, 2, 40, Math.PI / 6, 0.4);
spotR.position.set(0.65, 1.15, 1.85);
spotR.target.position.set(0.65, 0.5, 10);
jeepGroup.add(spotR, spotR.target);
// Tail Lights
const tailLightGeo = new THREE.BoxGeometry(0.3, 0.2, 0.05);
const tailL = new THREE.Mesh(tailLightGeo, lightBackMat);
tailL.position.set(-0.75, 1.1, -1.91);
const tailR = new THREE.Mesh(tailLightGeo, lightBackMat);
tailR.position.set(0.75, 1.1, -1.91);
jeepGroup.add(tailL, tailR);
// Windshield Frame & Glass
const frameMat = new THREE.MeshStandardMaterial({ color: 0x1f2937 });
const windshieldFrame = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.8, 0.1), frameMat);
windshieldFrame.position.set(0, 1.6, 0.35);
windshieldFrame.rotation.x = -0.25;
jeepGroup.add(windshieldFrame);
const glass = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 0.65), glassMat);
glass.position.set(0, 1.6, 0.41);
glass.rotation.x = -0.25;
jeepGroup.add(glass);
// Roll Cage / Roof Bars
const barGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.4);
const bar1 = new THREE.Mesh(barGeo, darkMetalMat);
bar1.position.set(-0.85, 1.7, -0.4);
const bar2 = new THREE.Mesh(barGeo, darkMetalMat);
bar2.position.set(0.85, 1.7, -0.4);
const bar3 = new THREE.Mesh(barGeo, darkMetalMat);
bar3.position.set(-0.85, 1.7, -1.5);
const bar4 = new THREE.Mesh(barGeo, darkMetalMat);
bar4.position.set(0.85, 1.7, -1.5);
const topBarGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.2);
topBarGeo.rotateZ(Math.PI / 2);
const barTopF = new THREE.Mesh(topBarGeo, darkMetalMat);
barTopF.position.set(0, 2.3, -0.4);
const barTopB = new THREE.Mesh(topBarGeo, darkMetalMat);
barTopB.position.set(0, 2.3, -1.5);
jeepGroup.add(bar1, bar2, bar3, bar4, barTopF, barTopB);
// Front Bumpers & Tow Hooks
const bumper = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.35, 0.4), darkMetalMat);
bumper.position.set(0, 0.6, 2.0);
jeepGroup.add(bumper);
// Spare Tire on Back
const spareTire = createSingleWheelMesh(0.5, 0.4, wheelMat, rimMat);
spareTire.rotation.z = Math.PI / 2;
spareTire.position.set(0, 1.2, -2.15);
jeepGroup.add(spareTire);
// 2. Wheels Assembly (4 Wheels)
const wheelPositions = [
{ x: -1.1, y: 0.5, z: 1.1, name: 'FL' }, // Front Left
{ x: 1.1, y: 0.5, z: 1.1, name: 'FR' }, // Front Right
{ x: -1.1, y: 0.5, z: -1.1, name: 'RL' }, // Rear Left
{ x: 1.1, y: 0.5, z: -1.1, name: 'RR' } // Rear Right
];
wheels = [];
wheelPositions.forEach(pos => {
const wheelPivot = new THREE.Group();
wheelPivot.position.set(pos.x, pos.y, pos.z);
const wheelMesh = createSingleWheelMesh(0.55, 0.45, wheelMat, rimMat);
wheelPivot.add(wheelMesh);
// Shock Absorber / Spring visual
const springGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.6, 8);
const springMat = new THREE.MeshStandardMaterial({ color: 0xef4444, metalness: 0.8 });
const spring = new THREE.Mesh(springGeo, springMat);
spring.position.set(pos.x > 0 ? -0.2 : 0.2, 0.3, 0);
wheelPivot.add(spring);
jeepGroup.add(wheelPivot);
wheels.push({ group: wheelPivot, mesh: wheelMesh, config: pos });
});
scene.add(jeepGroup);
}
// Helper for creating detailed off-road wheels
function createSingleWheelMesh(radius, width, tireMat, rimMat) {
const wheelGroup = new THREE.Group();
// Tire rubber
const tireGeo = new THREE.CylinderGeometry(radius, radius, width, 24);
tireGeo.rotateZ(Math.PI / 2);
const tire = new THREE.Mesh(tireGeo, tireMat);
tire.castShadow = true;
wheelGroup.add(tire);
// Metallic Rim
const rimGeo = new THREE.CylinderGeometry(radius * 0.55, radius * 0.55, width + 0.02, 16);
rimGeo.rotateZ(Math.PI / 2);
const rim = new THREE.Mesh(rimGeo, rimMat);
wheelGroup.add(rim);
// Rim Spokes / Off-road bolts
const spokeGeo = new THREE.BoxGeometry(width + 0.04, radius * 0.9, 0.1);
for(let a=0; a<3; a++) {
const spoke = new THREE.Mesh(spokeGeo, rimMat);
spoke.rotation.x = (a * Math.PI) / 3;
wheelGroup.add(spoke);
}
return wheelGroup;
}
// --- VEHICLE PHYSICS & TERRAIN SUSPENSION ADAPTATION ---
function updateVehiclePhysics(delta) {
// 1. Inputs handling
const throttle = keys.w ? 1 : (keys.s ? -0.6 : 0);
// Steering Angle
let targetSteer = 0;
if (keys.a) targetSteer += vehicle.maxSteer;
if (keys.d) targetSteer -= vehicle.maxSteer;
// Smooth Steering transition
vehicle.steeringAngle += (targetSteer - vehicle.steeringAngle) * vehicle.steerSpeed * delta;
// 2. Speed / Acceleration Dynamics
if (throttle !== 0) {
// Scaled max speed from UI Slider
const effectiveMaxSpeed = throttle > 0 ? (vehicle.maxSpeed * 0.35) : (vehicle.reverseSpeed * 0.35);
vehicle.speed += throttle * vehicle.acceleration * delta;
// Clamp speed
if (throttle > 0 && vehicle.speed > effectiveMaxSpeed) vehicle.speed = effectiveMaxSpeed;
if (throttle < 0 && vehicle.speed < effectiveMaxSpeed) vehicle.speed = effectiveMaxSpeed;
} else {
// Natural Friction / Engine Braking
vehicle.speed *= Math.pow(vehicle.friction, delta * 60);
if (Math.abs(vehicle.speed) < 0.01) vehicle.speed = 0;
}
// 3. Turn Heading Yaw angle
if (Math.abs(vehicle.speed) > 0.05) {
const directionFactor = vehicle.speed > 0 ? 1 : -1;
vehicle.heading += vehicle.steeringAngle * directionFactor * (vehicle.speed / 15.0) * delta;
}
// 4. Update Horizontal Position (X, Z)
vehicle.x += Math.sin(vehicle.heading) * vehicle.speed * delta;
vehicle.z += Math.cos(vehicle.heading) * vehicle.speed * delta;
// 5. Calculate Terrain Heights under 4 Wheels for Pitch/Roll Physics
const wheelBase = 2.2; // Distance front-to-back
const trackWidth = 2.2; // Distance left-to-right
// Compute world positions for 4 corners of the jeep
const sinH = Math.sin(vehicle.heading);
const cosH = Math.cos(vehicle.heading);
// Front & Back offsets
const frontX = vehicle.x + sinH * (wheelBase * 0.5);
const frontZ = vehicle.z + cosH * (wheelBase * 0.5);
const rearX = vehicle.x - sinH * (wheelBase * 0.5);
const rearZ = vehicle.z - cosH * (wheelBase * 0.5);
// Left & Right corner heights
const hFL = getTerrainHeight(frontX - cosH * (trackWidth * 0.5), frontZ + sinH * (trackWidth * 0.5));
const hFR = getTerrainHeight(frontX + cosH * (trackWidth * 0.5), frontZ - sinH * (trackWidth * 0.5));
const hRL = getTerrainHeight(rearX - cosH * (trackWidth * 0.5), rearZ + sinH * (trackWidth * 0.5));
const hRR = getTerrainHeight(rearX + cosH * (trackWidth * 0.5), rearZ - sinH * (trackWidth * 0.5));
// Average Height under center of vehicle
const targetY = (hFL + hFR + hRL + hRR) / 4.0;
// Smooth vertical suspension dampening
vehicle.y += (targetY - vehicle.y) * 12.0 * delta;
// Compute Terrain Slopes (Pitch & Roll)
const frontAvgHeight = (hFL + hFR) / 2.0;
const rearAvgHeight = (hRL + hRR) / 2.0;
const leftAvgHeight = (hFL + hRL) / 2.0;
const rightAvgHeight = (hFR + hRR) / 2.0;
const targetPitch = Math.atan2(frontAvgHeight - rearAvgHeight, wheelBase);
const targetRoll = Math.atan2(leftAvgHeight - rightAvgHeight, trackWidth);
// Dampened tilt response
vehicle.pitch += (targetPitch - vehicle.pitch) * 10.0 * delta;
vehicle.roll += (targetRoll - vehicle.roll) * 10.0 * delta;
// 6. Apply transformations to Jeep 3D Group
jeepGroup.position.set(vehicle.x, vehicle.y + 0.1, vehicle.z);
// Quaternion rotation order: Heading (Yaw) -> Pitch -> Roll
const euler = new THREE.Euler(vehicle.pitch, vehicle.heading, vehicle.roll, 'YXZ');
jeepGroup.quaternion.setFromEuler(euler);
// 7. Animate Front Wheel Steering & All Wheel Rotations
const wheelRotationDelta = (vehicle.speed / 0.55) * delta;
vehicle.wheelRotation += wheelRotationDelta;
wheels.forEach(w => {
// Spin wheels forward/backward
w.mesh.rotation.x = vehicle.wheelRotation;
// Turn front wheels left/right
if (w.config.name === 'FL' || w.config.name === 'FR') {
w.group.rotation.y = vehicle.steeringAngle;
}
});
// 8. Check Flip / Warning Condition
vehicle.isFlipped = Math.abs(vehicle.roll) > 1.1 || Math.abs(vehicle.pitch) > 1.2;
const warningToast = document.getElementById('warning-toast');
if (vehicle.isFlipped) {
warningToast.classList.remove('opacity-0');
} else {
warningToast.classList.add('opacity-0');
}
// 9. Update UI HUD (Speedometer & Dash)
updateHUD();
}
// --- CAMERA CONTROLLER ---
function updateCamera() {
if (cameraMode === 0) {
// 3rd Person Smooth Follow Camera
const cameraDistance = 11.0;
const cameraHeight = 4.2;
// Position behind jeep based on current vehicle heading
const targetCamX = vehicle.x - Math.sin(vehicle.heading) * cameraDistance;
const targetCamZ = vehicle.z - Math.cos(vehicle.heading) * cameraDistance;
const targetCamY = vehicle.y + cameraHeight;
// Lerp smooth movement
camera.position.x += (targetCamX - camera.position.x) * 0.1;
camera.position.y += (targetCamY - camera.position.y) * 0.1;
camera.position.z += (targetCamZ - camera.position.z) * 0.1;
// Look at point slightly above vehicle center
const lookAtTarget = new THREE.Vector3(vehicle.x, vehicle.y + 1.2, vehicle.z);
camera.lookAt(lookAtTarget);
} else {
// Orbit mode controls update target
orbitControls.target.set(vehicle.x, vehicle.y + 1.0, vehicle.z);
orbitControls.update();
}
}
// --- HUD / UI UPDATER ---
function updateHUD() {
// Speed in km/h scale conversion
const speedKmh = Math.round(Math.abs(vehicle.speed) * 3.6 * 0.85);
document.getElementById('speed-display').innerText = speedKmh;
// Circular Speed Meter SVG Offset calculation
const maxDisplaySpeed = vehicle.maxSpeed;
const progress = Math.min(1.0, speedKmh / maxDisplaySpeed);
const dashoffset = 251.2 * (1 - progress);
document.getElementById('speed-circle').style.strokeDashoffset = dashoffset;
// Gear indicator
const gearDisplay = document.getElementById('gear-display');
if (keys.s && vehicle.speed < 0) {
gearDisplay.innerText = 'R';
gearDisplay.className = "font-orbitron text-sm font-bold bg-rose-500 text-white px-2 py-0.5 rounded";
} else if (vehicle.speed === 0) {
gearDisplay.innerText = 'N';
gearDisplay.className = "font-orbitron text-sm font-bold bg-slate-600 text-white px-2 py-0.5 rounded";
} else {
gearDisplay.innerText = 'D';
gearDisplay.className = "font-orbitron text-sm font-bold bg-amber-500 text-slate-950 px-2 py-0.5 rounded";
}
// Key Buttons Highlight Feedback in HUD
toggleKeyHighlight('key-w', keys.w);
toggleKeyHighlight('key-a', keys.a);
toggleKeyHighlight('key-s', keys.s);
toggleKeyHighlight('key-d', keys.d);
}
function toggleKeyHighlight(elementId, isActive) {
const el = document.getElementById(elementId);
if (!el) return;
if (isActive) el.classList.add('active');
else el.classList.remove('active');
}
// --- RESPAWN VEHICLE ---
function respawnVehicle() {
vehicle.x = 0;
vehicle.z = 0;
vehicle.speed = 0;
vehicle.heading = 0;
vehicle.pitch = 0;
vehicle.roll = 0;
vehicle.steeringAngle = 0;
vehicle.y = getTerrainHeight(0, 0);
// Reset camera back smoothly
camera.position.set(0, vehicle.y + 5, -12);
}
// --- EVENT LISTENERS & UI HOOKS ---
function setupEventListeners() {
// Keyboard controls
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
if (k === 'w' || e.key === 'ArrowUp') keys.w = true;
if (k === 's' || e.key === 'ArrowDown') keys.s = true;
if (k === 'a' || e.key === 'ArrowLeft') keys.a = true;
if (k === 'd' || e.key === 'ArrowRight') keys.d = true;
if (k === 'r') respawnVehicle();
if (k === 'c') toggleCameraMode();
});
window.addEventListener('keyup', (e) => {
const k = e.key.toLowerCase();
if (k === 'w' || e.key === 'ArrowUp') keys.w = false;
if (k === 's' || e.key === 'ArrowDown') keys.s = false;
if (k === 'a' || e.key === 'ArrowLeft') keys.a = false;
if (k === 'd' || e.key === 'ArrowRight') keys.d = false;
});
// Speed Limit Slider Event
const slider = document.getElementById('slider-speed');
const speedLimitValStr = document.getElementById('speed-limit-val');
const powerPercentage = document.getElementById('power-percentage');
slider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
vehicle.maxSpeed = val;
speedLimitValStr.innerText = val + ' km/h';
// Percentage relative to slider max (150)
const pct = Math.round((val / 150) * 100);
powerPercentage.innerText = pct + '%';
});
// UI Button Hooks
document.getElementById('btn-respawn').addEventListener('click', respawnVehicle);
document.getElementById('btn-camera').addEventListener('click', toggleCameraMode);
// Mobile Touch Controls Binding
bindTouchButton('touch-up', 'w');
bindTouchButton('touch-down', 's');
bindTouchButton('touch-left', 'a');
bindTouchButton('touch-right', 'd');
}
function bindTouchButton(elementId, keyProp) {
const btn = document.getElementById(elementId);
if (!btn) return;
const startPress = (e) => {
e.preventDefault();
keys[keyProp] = true;
};
const endPress = (e) => {
e.preventDefault();
keys[keyProp] = false;
};
btn.addEventListener('touchstart', startPress);
btn.addEventListener('touchend', endPress);
btn.addEventListener('mousedown', startPress);
btn.addEventListener('mouseup', endPress);
btn.addEventListener('mouseleave', endPress);
}
function toggleCameraMode() {
cameraMode = cameraMode === 0 ? 1 : 0;
orbitControls.enabled = (cameraMode === 1);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- MAIN ANIMATION LOOP ---
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const delta = Math.min((now - lastTime) / 1000, 0.1); // Cap delta to prevent huge jumps
lastTime = now;
// Physics Update
updateVehiclePhysics(delta);
// Camera Update
updateCamera();
// Render
renderer.render(scene, camera);
}
// Start initialization on window load
window.onload = init;
</script>
</body>
</html>
