Transform to true spherical Earth: realistic topology, smooth mountains, proper globe geometry
This commit is contained in:
175
index.html
175
index.html
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user