terraingen-explorer / script.js
latterworks's picture
/**
eb8c123 verified
// 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');
});