Spaces:
Running
Running
| // 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'); | |
| }); |