// Global variables
let scene, mainCamera, renderer, car;
let keys = {};
let speed = 0;
const maxSpeed = 0.3,
acceleration = 0.001,
deceleration = 0.001,
turnSpeed = 0.03;
// Terrain generation parameters
const chunkSize = 5000;
const chunkSegments = 200;
const terrainRange = 2;
const terrainChunks = {};
const noise = new SimplexNoise();
// Infinite Road parameters
let roadPoints = [];
let roadMesh;
const roadSegmentLength = 500;
const roadTubeRadius = 100;
const roadUpdateDistance = 10000;
// Global texture variable.
let grassTexture;
// Start initialization (animation will start after texture loads)
init();
// Returns terrain height (y) for world coordinates (x,z)
function getTerrainHeight(x, z) {
return noise.noise2D(x / 500, z / 500) * 50;
}
// Create a terrain chunk at grid (cx, cz)
function createTerrainChunk(cx, cz) {
const geometry = new THREE.PlaneGeometry(chunkSize, chunkSize, chunkSegments, chunkSegments);
geometry.rotateX(-Math.PI / 2);
const positions = geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
let x = positions.getX(i) + cx * chunkSize;
let z = positions.getZ(i) + cz * chunkSize;
let h = getTerrainHeight(x, z);
positions.setY(i, h);
}
positions.needsUpdate = true;
geometry.computeVertexNormals();
// Create the material using the loaded texture.
const material = new THREE.MeshLambertMaterial({
map: grassTexture,
color: 0xffffff
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(cx * chunkSize, 0, cz * chunkSize);
return mesh;
}
// Update terrain: generate chunks around the car and remove distant ones.
function updateTerrain() {
const carPos = car.position;
const carChunkX = Math.floor(carPos.x / chunkSize);
const carChunkZ = Math.floor(carPos.z / chunkSize);
const needed = {};
for (let dx = -terrainRange; dx <= terrainRange; dx++) {
for (let dz = -terrainRange; dz <= terrainRange; dz++) {
let cx = carChunkX + dx;
let cz = carChunkZ + dz;
needed[`${cx},${cz}`] = true;
if (!terrainChunks[`${cx},${cz}`]) {
let chunk = createTerrainChunk(cx, cz);
terrainChunks[`${cx},${cz}`] = chunk;
scene.add(chunk);
}
}
}
for (let key in terrainChunks) {
if (!needed[key]) {
scene.remove(terrainChunks[key]);
delete terrainChunks[key];
}
}
}
// Extend the road by adding one more segment.
function extendRoad() {
let lastPoint = roadPoints[roadPoints.length - 1];
let newAngle;
if (roadPoints.length < 2) {
newAngle = 0;
} else {
let prev = roadPoints[roadPoints.length - 2];
newAngle = Math.atan2(lastPoint.z - prev.z, lastPoint.x - prev.x);
}
newAngle += (Math.random() - 0.5) * 0.4;
const newX = lastPoint.x + Math.cos(newAngle) * roadSegmentLength;
const newZ = lastPoint.z + Math.sin(newAngle) * roadSegmentLength;
const newY = getTerrainHeight(newX, newZ) + 2;
roadPoints.push(new THREE.Vector3(newX, newY, newZ));
rebuildRoadMesh();
}
// Rebuild the road mesh from current roadPoints.
function rebuildRoadMesh() {
if (roadMesh) {
scene.remove(roadMesh);
}
const curve = new THREE.CatmullRomCurve3(roadPoints);
const geometry = new THREE.TubeGeometry(curve, Math.max(100, roadPoints.length * 10), roadTubeRadius, 8, false);
const material = new THREE.MeshLambertMaterial({
color: 0x000000
});
roadMesh = new THREE.Mesh(geometry, material);
scene.add(roadMesh);
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
mainCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
mainCamera.position.set(0, 150, -300);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Increase overall brightness with multiple lights.
const ambientLight = new THREE.AmbientLight(0xffffff, 5.0); // Ambient light with increased intensity.
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 5.0); // Bright directional light.
directionalLight.position.set(100, 500, 100);
scene.add(directionalLight);
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.0); // Additional fill light.
hemisphereLight.position.set(0, 500, 0);
scene.add(hemisphereLight);
// Load the local grass texture (image.png).
grassTexture = new THREE.TextureLoader().load(
'https://i.sstatic.net/gwCGUO2I.png', // image.png
function(texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(8, 8);
texture.needsUpdate = true;
// Once the texture loads, update the terrain and start the animation loop.
updateTerrain();
animate();
},
undefined,
function(error) {
console.error('Error loading texture:', error);
animate();
}
);
const carGeometry = new THREE.BoxGeometry(40, 20, 80);
const carMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000
});
car = new THREE.Mesh(carGeometry, carMaterial);
car.position.set(0, getTerrainHeight(0, 0) + 10, 0);
scene.add(car);
roadPoints.push(new THREE.Vector3(0, getTerrainHeight(0, 0) + 2, 0));
roadPoints.push(new THREE.Vector3(roadSegmentLength, getTerrainHeight(roadSegmentLength, 0) + 2, 0));
rebuildRoadMesh();
window.addEventListener("keydown", (event) => {
keys[event.code] = true;
});
window.addEventListener("keyup", (event) => {
keys[event.code] = false;
});
window.addEventListener("resize", onWindowResize, false);
}
function onWindowResize() {
mainCamera.aspect = window.innerWidth / window.innerHeight;
mainCamera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
updateCar();
updateTerrain();
updateRoadIfNeeded();
updateCamera();
renderer.render(scene, mainCamera);
}
function updateRoadIfNeeded() {
const lastPoint = roadPoints[roadPoints.length - 1];
const dist = car.position.distanceTo(lastPoint);
if (dist < roadUpdateDistance) {
for (let i = 0; i < 5; i++) {
extendRoad();
}
}
}
function updateCar() {
if (keys["KeyS"]) {
speed = Math.min(speed + acceleration, maxSpeed);
} else if (keys["KeyW"]) {
speed = Math.max(speed - acceleration, -maxSpeed / 2);
} else {
if (speed > 0) speed = Math.max(speed - deceleration, 0);
else if (speed < 0) speed = Math.min(speed + deceleration, 0);
}
if (keys["KeyA"]) {
car.rotation.y += turnSpeed * (Math.abs(speed) / maxSpeed);
}
if (keys["KeyD"]) {
car.rotation.y -= turnSpeed * (Math.abs(speed) / maxSpeed);
}
car.position.x -= Math.sin(car.rotation.y) * speed * 30;
car.position.z -= Math.cos(car.rotation.y) * speed * 30;
car.position.y = getTerrainHeight(car.position.x, car.position.z) + 10;
}
function updateCamera() {
const followDistance = 300;
const heightOffset = 150;
mainCamera.position.x = car.position.x - Math.sin(car.rotation.y) * followDistance;
mainCamera.position.z = car.position.z - Math.cos(car.rotation.y) * followDistance;
mainCamera.position.y = car.position.y + heightOffset;
mainCamera.lookAt(car.position);
}
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
<!-- Include Three.js and SimplexNoise -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>