Transform to true spherical Earth: realistic topology, smooth mountains, proper globe geometry

This commit is contained in:
Alexa Louise
2025-12-22 14:23:38 -06:00
parent 3c9cbb4007
commit 251074ef6f

View File

@@ -712,7 +712,7 @@
// Camera // Camera
let isDragging = false; let isDragging = false;
let prevMouse = { x: 0, y: 0 }; let prevMouse = { x: 0, y: 0 };
let camAngle = 0, camHeight = 80, camDist = 150; let camAngle = 0, camHeight = 0, camDist = 400; // Start far enough to see whole sphere
// Terrain settings // Terrain settings
const TERRAIN = { const TERRAIN = {
@@ -807,83 +807,86 @@
} }
function generateTerrain() { function generateTerrain() {
const { size, segments, maxHeight, waterLevel } = TERRAIN; const { segments } = TERRAIN;
const geo = new THREE.PlaneGeometry(size, size, segments, segments); const EARTH_RADIUS = 150; // Base radius for Earth sphere
const MAX_ELEVATION = 2; // Max elevation change (realistic Earth proportions)
const geo = new THREE.SphereGeometry(EARTH_RADIUS, segments, segments);
const pos = geo.attributes.position.array; const pos = geo.attributes.position.array;
const colors = new Float32Array(pos.length); const colors = new Float32Array(pos.length);
// Generate heightmap with multiple noise layers // Generate heightmap with realistic Earth topology
for (let i = 0; i <= segments; i++) {
terrainData.heights[i] = [];
terrainData.biomes[i] = [];
for (let j = 0; j <= segments; j++) {
const x = (j / segments - 0.5) * size;
const z = (i / segments - 0.5) * size;
// Continental shape - large scale
const continental = noise.fbm(x * 0.003, z * 0.003, 3, 2, 0.5);
// Mountain ranges - medium scale with ridges
const mountains = Math.pow(Math.abs(noise.fbm(x * 0.008, z * 0.008, 4, 2.5, 0.6)), 1.5) * 2;
// Hills - small scale variation
const hills = noise.fbm(x * 0.02, z * 0.02, 3, 2, 0.5) * 0.3;
// Detail - fine texture
const detail = noise.fbm(x * 0.05, z * 0.05, 2, 2, 0.5) * 0.1;
// Combine
let height = continental * 0.6 + mountains * 0.8 + hills + detail;
// Create some flat areas for building
const flatness = noise.fbm(x * 0.01, z * 0.01, 2, 2, 0.5);
if (flatness > 0.2 && height > 0 && height < 0.4) {
height = height * 0.3 + 0.15;
}
// Edge falloff for island feel
const dist = Math.sqrt(x * x + z * z) / (size * 0.5);
if (dist > 0.7) {
const falloff = 1 - Math.pow((dist - 0.7) / 0.3, 2);
height *= Math.max(0, falloff);
height -= (1 - falloff) * 0.5;
}
// Scale to world height
height *= maxHeight;
terrainData.heights[i][j] = height;
// Determine biome
let biome;
if (height < waterLevel - 5) biome = 'ocean';
else if (height < waterLevel + 1) biome = 'beach';
else if (height < 8) {
const moisture = noise.fbm(x * 0.01 + 100, z * 0.01, 2, 2, 0.5);
if (moisture < -0.3) biome = 'desert';
else if (moisture > 0.4) biome = 'swamp';
else biome = 'plains';
}
else if (height < 20) biome = 'forest';
else if (height < 35) biome = 'hills';
else if (height < 50) biome = 'mountain';
else biome = 'snow';
terrainData.biomes[i][j] = biome;
}
}
// Apply heights and colors to geometry
for (let i = 0; i < pos.length; i += 3) { for (let i = 0; i < pos.length; i += 3) {
const x = pos[i];
const y = pos[i + 1];
const z = pos[i + 2];
// Convert to spherical coordinates
const radius = Math.sqrt(x * x + y * y + z * z);
const lat = Math.asin(y / radius);
const lon = Math.atan2(z, x);
// REALISTIC EARTH TOPOLOGY
// Continental plates - large scale features
const continentalBase = noise.fbm(lat * 3, lon * 3, 3, 2, 0.5);
// Mountain ranges - realistic smooth ridges (not pointy!)
const mountainBase = noise.fbm(lat * 6, lon * 6, 4, 2, 0.6);
const mountains = Math.pow(Math.abs(mountainBase), 0.7) * 0.5; // Smooth, not sharp!
// Rolling hills
const hills = noise.fbm(lat * 12, lon * 12, 3, 2, 0.5) * 0.15;
// Fine detail
const detail = noise.fbm(lat * 24, lon * 24, 2, 2, 0.5) * 0.05;
// Combine with realistic proportions
let elevation = continentalBase * 0.4 + mountains * 0.3 + hills + detail;
// Smooth transition (no sharp spikes)
elevation = Math.tanh(elevation * 1.5); // Smooth sigmoid curve
// Store height for this vertex
const idx = i / 3; const idx = i / 3;
const row = Math.floor(idx / (segments + 1)); const row = Math.floor(idx / (segments + 1));
const col = idx % (segments + 1); const col = idx % (segments + 1);
if (!terrainData.heights[row]) terrainData.heights[row] = [];
if (!terrainData.biomes[row]) terrainData.biomes[row] = [];
const height = terrainData.heights[row]?.[col] || 0; // Scale to realistic Earth proportions (Everest is ~0.13% of Earth radius)
const biome = terrainData.biomes[row]?.[col] || 'plains'; const heightMeters = elevation * 9000; // Max ~9km (like Everest)
terrainData.heights[row][col] = heightMeters;
pos[i + 2] = height; // Determine biome based on latitude and elevation
let biome;
const latDegrees = (lat * 180 / Math.PI);
const absLat = Math.abs(latDegrees);
const temperature = 30 - absLat * 0.6; // Temperature based on latitude
if (heightMeters < -100) biome = 'ocean';
else if (heightMeters < 10) biome = 'beach';
else if (heightMeters > 5000) biome = 'snow';
else if (heightMeters > 3000) biome = 'mountain';
else if (heightMeters > 1500) biome = 'hills';
else {
const moisture = noise.fbm(lat * 8 + 100, lon * 8, 2, 2, 0.5);
if (temperature < 0) biome = 'tundra';
else if (temperature < 10) biome = 'forest';
else if (moisture < -0.3) biome = 'desert';
else if (moisture > 0.4) biome = 'swamp';
else if (temperature > 25) biome = 'plains';
else biome = 'forest';
}
terrainData.biomes[row][col] = biome;
// DEFORM SPHERE VERTICES (realistic Earth topology)
const elevationScale = heightMeters / 1000; // Convert to sphere units
const deformFactor = 1 + (elevationScale / EARTH_RADIUS);
pos[i] *= deformFactor;
pos[i + 1] *= deformFactor;
pos[i + 2] *= deformFactor;
// Color based on biome // Color based on biome
const biomeData = BIOMES[biome]; const biomeData = BIOMES[biome];
@@ -912,30 +915,29 @@
}); });
terrain = new THREE.Mesh(geo, mat); terrain = new THREE.Mesh(geo, mat);
terrain.rotation.x = -Math.PI / 2; // No rotation needed - it's a sphere!
terrain.receiveShadow = true; terrain.receiveShadow = true;
terrain.castShadow = true; terrain.castShadow = true;
scene.add(terrain); scene.add(terrain);
} }
function createWater() { function createWater() {
// Main ocean // Spherical ocean layer
const waterGeo = new THREE.PlaneGeometry(600, 600, 100, 100); const EARTH_RADIUS = 150;
const waterGeo = new THREE.SphereGeometry(EARTH_RADIUS * 0.998, 64, 64); // Slightly smaller than terrain
const waterMat = new THREE.MeshStandardMaterial({ const waterMat = new THREE.MeshStandardMaterial({
color: 0x2196F3, color: 0x1a5fb4,
transparent: true, transparent: true,
opacity: 0.75, opacity: 0.8,
roughness: 0.1, roughness: 0.2,
metalness: 0.3 metalness: 0.4
}); });
water = new THREE.Mesh(waterGeo, waterMat); water = new THREE.Mesh(waterGeo, waterMat);
water.rotation.x = -Math.PI / 2;
water.position.y = TERRAIN.waterLevel;
water.receiveShadow = true; water.receiveShadow = true;
scene.add(water); scene.add(water);
// Add some wave animation data // Add wave animation data
water.userData = { water.userData = {
originalPositions: waterGeo.attributes.position.array.slice() originalPositions: waterGeo.attributes.position.array.slice()
}; };
@@ -1763,7 +1765,7 @@
const dx = e.clientX - prevMouse.x; const dx = e.clientX - prevMouse.x;
const dy = e.clientY - prevMouse.y; const dy = e.clientY - prevMouse.y;
camAngle += dx * 0.005; camAngle += dx * 0.005;
camHeight = Math.max(20, Math.min(150, camHeight - dy * 0.3)); camHeight = Math.max(-Math.PI/2, Math.min(Math.PI/2, camHeight - dy * 0.003));
updateCamera(); updateCamera();
prevMouse = { x: e.clientX, y: e.clientY }; prevMouse = { x: e.clientX, y: e.clientY };
}); });
@@ -1772,7 +1774,7 @@
renderer.domElement.addEventListener('mouseleave', () => isDragging = false); renderer.domElement.addEventListener('mouseleave', () => isDragging = false);
renderer.domElement.addEventListener('wheel', e => { renderer.domElement.addEventListener('wheel', e => {
camDist = Math.max(50, Math.min(350, camDist + e.deltaY * 0.15)); camDist = Math.max(200, Math.min(800, camDist + e.deltaY * 0.3));
updateCamera(); updateCamera();
}); });
@@ -1786,7 +1788,7 @@
const dx = e.touches[0].clientX - prevMouse.x; const dx = e.touches[0].clientX - prevMouse.x;
const dy = e.touches[0].clientY - prevMouse.y; const dy = e.touches[0].clientY - prevMouse.y;
camAngle += dx * 0.005; camAngle += dx * 0.005;
camHeight = Math.max(20, Math.min(150, camHeight - dy * 0.3)); camHeight = Math.max(-Math.PI/2, Math.min(Math.PI/2, camHeight - dy * 0.003));
updateCamera(); updateCamera();
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}, { passive: true }); }, { passive: true });
@@ -1826,10 +1828,11 @@
} }
function updateCamera() { function updateCamera() {
camera.position.x = Math.sin(camAngle) * camDist; // Orbit camera around sphere center
camera.position.z = Math.cos(camAngle) * camDist; camera.position.x = Math.sin(camAngle) * Math.cos(camHeight) * camDist;
camera.position.y = camHeight; camera.position.y = Math.sin(camHeight) * camDist;
camera.lookAt(0, 10, 0); camera.position.z = Math.cos(camAngle) * Math.cos(camHeight) * camDist;
camera.lookAt(0, 0, 0); // Look at sphere center
// Compass // Compass
const needle = document.getElementById('compassNeedle'); const needle = document.getElementById('compassNeedle');