// Global Three.js variables let scene, camera, renderer, terrainMesh, controls; let terrainSize = 64; let isAnimating = false; let animationId = null; // Initialize the 3D scene function initScene() { const canvas = document.getElementById('terrain-canvas'); // Scene setup scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a202c); // Camera setup camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000); camera.position.set(terrainSize * 0.7, terrainSize * 0.7, terrainSize * 0.7); camera.lookAt(0, 0, 0); // Renderer setup renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(canvas.clientWidth, canvas.clientHeight); // Lighting const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(1, 1, 1); scene.add(directionalLight); // Orbit controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; // Handle window resize window.addEventListener('resize', onWindowResize); } // Generate terrain using specified algorithm function generateTerrain(algorithm) { // Remove existing terrain if any if (terrainMesh) { scene.remove(terrainMesh); terrainMesh.geometry.dispose(); terrainMesh.material.dispose(); } // Get parameters from sliders terrainSize = parseInt(document.getElementById('size-slider').value); const roughness = parseFloat(document.getElementById('roughness-slider').value); const heightScale = parseInt(document.getElementById('height-slider').value); const waterLevel = parseInt(document.getElementById('water-slider').value) / 100; // Generate heightmap let heightmap = generateHeightmap(algorithm, terrainSize, roughness, heightScale); // Create geometry from heightmap const geometry = createGeometryFromHeightmap(heightmap, terrainSize, heightScale); // Create material with water effect const material = createTerrainMaterial(heightmap, terrainSize, heightScale, waterLevel); // Create mesh terrainMesh = new THREE.Mesh(geometry, material); scene.add(terrainMesh); // Update camera position based on terrain size camera.position.set(terrainSize * 0.7, terrainSize * 0.7, terrainSize * 0.7); camera.lookAt(0, 0, 0); controls.update(); // Start animation if not already running if (!isAnimating) { animate(); } // Update 2D previews updatePreviews(heightmap, algorithm); } // Generate heightmap using specified algorithm function generateHeightmap(algorithm, size, roughness, heightScale) { const heightmap = new Array(size * size).fill(0); // Simplified implementations for demo purposes switch(algorithm) { case 'diamond-square': // Implement diamond-square algorithm diamondSquare(heightmap, size, roughness); break; case 'perlin': // Implement Perlin noise for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const value = noise.perlin2(x * roughness / 10, y * roughness / 10); heightmap[y * size + x] = (value + 1) * 0.5 * heightScale; } } break; case 'simplex': // Implement Simplex noise for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const value = noise.simplex2(x * roughness / 10, y * roughness / 10); heightmap[y * size + x] = (value + 1) * 0.5 * heightScale; } } break; default: console.error('Unknown algorithm:', algorithm); } return heightmap; } // Simplified Diamond-Square implementation function diamondSquare(heightmap, size, roughness) { // Initialize corners heightmap[0] = Math.random() * roughness; heightmap[size - 1] = Math.random() * roughness; heightmap[size * (size - 1)] = Math.random() * roughness; heightmap[size * size - 1] = Math.random() * roughness; let step = size - 1; let scale = roughness; while (step > 1) { // Diamond step for (let y = 0; y < size - 1; y += step) { for (let x = 0; x < size - 1; x += step) { const avg = ( heightmap[y * size + x] + heightmap[y * size + (x + step)] + heightmap[(y + step) * size + x] + heightmap[(y + step) * size + (x + step)] ) / 4; const centerX = x + step / 2; const centerY = y + step / 2; const centerIndex = centerY * size + centerX; heightmap[centerIndex] = avg + (Math.random() * 2 - 1) * scale; } } // Square step for (let y = 0; y < size; y += step / 2) { for (let x = (y + step / 2) % step; x < size; x += step) { let sum = 0; let count = 0; // Top neighbor if (y - step / 2 >= 0) { sum += heightmap[(y - step / 2) * size + x]; count++; } // Bottom neighbor if (y + step / 2 < size) { sum += heightmap[(y + step / 2) * size + x]; count++; } // Left neighbor if (x - step / 2 >= 0) { sum += heightmap[y * size + (x - step / 2)]; count++; } // Right neighbor if (x + step / 2 < size) { sum += heightmap[y * size + (x + step / 2)]; count++; } if (count > 0) { heightmap[y * size + x] = sum / count + (Math.random() * 2 - 1) * scale; } } } step = Math.floor(step / 2); scale *= roughness; } } // Create Three.js geometry from heightmap function createGeometryFromHeightmap(heightmap, size, heightScale) { const geometry = new THREE.PlaneGeometry(size, size, size - 1, size - 1); // Apply heightmap to vertices for (let i = 0; i < geometry.attributes.position.count; i++) { const x = i % size; const y = Math.floor(i / size); geometry.attributes.position.setZ(i, heightmap[y * size + x]); } geometry.rotateX(-Math.PI / 2); geometry.computeVertexNormals(); return geometry; } // Create terrain material with water effect function createTerrainMaterial(heightmap, size, heightScale, waterLevel) { // Find min and max heights let minHeight = Infinity; let maxHeight = -Infinity; for (const height of heightmap) { if (height < minHeight) minHeight = height; if (height > maxHeight) maxHeight = height; } const waterHeight = minHeight + (maxHeight - minHeight) * waterLevel; // Create color gradient const texture = new THREE.CanvasTexture(createTerrainTexture(size, minHeight, maxHeight, waterHeight)); return new THREE.MeshStandardMaterial({ map: texture, metalness: 0.1, roughness: 0.7, flatShading: false }); } // Create terrain texture with elevation colors function createTerrainTexture(size, minHeight, maxHeight, waterHeight) { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // Draw gradient based on height const imageData = ctx.createImageData(size, size); for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const height = heightmap[y * size + x]; // Water if (height <= waterHeight) { const depth = (waterHeight - height) / (waterHeight - minHeight); imageData.data[idx] = 30 + 50 * depth; // R imageData.data[idx + 1] = 80 + 100 * depth; // G imageData.data[idx + 2] = 150 + 100 * depth; // B } // Beach/Sand else if (height <= waterHeight + (maxHeight - waterHeight) * 0.05) { imageData.data[idx] = 210; // R imageData.data[idx + 1] = 190; // G imageData.data[idx + 2] = 140; // B } // Grass else if (height <= waterHeight + (maxHeight - waterHeight) * 0.3) { const t = (height - waterHeight) / ((maxHeight - waterHeight) * 0.3); imageData.data[idx] = 50 + 80 * t; // R imageData.data[idx + 1] = 100 + 80 * t; // G imageData.data[idx + 2] = 50 + 30 * t; // B } // Rock else if (height <= waterHeight + (maxHeight - waterHeight) * 0.7) { const t = (height - waterHeight - (maxHeight - waterHeight) * 0.3) / ((maxHeight - waterHeight) * 0.4); imageData.data[idx] = 100 + 50 * t; // R imageData.data[idx + 1] = 80 + 20 * t; // G imageData.data[idx + 2] = 60 + 20 * t; // B } // Snow else { const t = (height - waterHeight - (maxHeight - waterHeight) * 0.7) / ((maxHeight - waterHeight) * 0.3); imageData.data[idx] = 180 + 75 * t; // R imageData.data[idx + 1] = 190 + 65 * t; // G imageData.data[idx + 2] = 200 + 55 * t; // B } imageData.data[idx + 3] = 255; // Alpha } } ctx.putImageData(imageData, 0, 0); return canvas; } // Update 2D preview canvases function updatePreviews(heightmap, algorithm) { const size = Math.min(128, terrainSize); const previewCanvas = document.getElementById(`${algorithm}-canvas`); const ctx = previewCanvas.getContext('2d'); // Scale heightmap to fit preview const step = Math.floor(terrainSize / size); const imageData = ctx.createImageData(size, size); for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const height = heightmap[y * step * terrainSize + x * step]; const normalizedHeight = Math.floor((height / Math.max(...heightmap)) * 255); imageData.data[idx] = normalizedHeight; // R imageData.data[idx + 1] = normalizedHeight; // G imageData.data[idx + 2] = normalizedHeight; // B imageData.data[idx + 3] = 255; // Alpha } } ctx.putImageData(imageData, 0, 0); } // Animation loop function animate() { isAnimating = true; animationId = requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } // Handle window resize function onWindowResize() { const canvas = document.getElementById('terrain-canvas'); camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(canvas.clientWidth, canvas.clientHeight); } // Camera controls function rotateCamera(direction) { const angle = direction === 'left' ? -Math.PI / 8 : Math.PI / 8; camera.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle); camera.lookAt(0, 0, 0); } function resetCamera() { camera.position.set(terrainSize * 0.7, terrainSize * 0.7, terrainSize * 0.7); camera.lookAt(0, 0, 0); controls.update(); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { initScene(); generateTerrain('diamond-square'); });