293 lines
8.4 KiB
JavaScript
293 lines
8.4 KiB
JavaScript
/**
|
|
* Earth Shader Example
|
|
*
|
|
* Complete procedural Earth with:
|
|
* - Day/night texture blending
|
|
* - Atmospheric glow (fresnel)
|
|
* - Cloud layer
|
|
* - City lights at night
|
|
* - Bump mapping
|
|
*
|
|
* Based on Three.js webgpu_tsl_earth example (MIT License)
|
|
* https://github.com/mrdoob/three.js
|
|
*/
|
|
|
|
import * as THREE from 'three/webgpu';
|
|
import {
|
|
Fn,
|
|
If,
|
|
float,
|
|
vec2,
|
|
vec3,
|
|
vec4,
|
|
color,
|
|
uniform,
|
|
texture,
|
|
uv,
|
|
time,
|
|
mix,
|
|
smoothstep,
|
|
pow,
|
|
clamp,
|
|
normalize,
|
|
dot,
|
|
max,
|
|
positionWorld,
|
|
normalWorld,
|
|
normalLocal,
|
|
cameraPosition,
|
|
bumpMap
|
|
} from 'three/tsl';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
let camera, scene, renderer, controls;
|
|
let earth, clouds, atmosphere;
|
|
|
|
// Uniforms
|
|
const sunDirection = uniform(new THREE.Vector3(1, 0.2, 0.5).normalize());
|
|
const atmosphereDayColor = uniform(new THREE.Color(0x4db2ff));
|
|
const atmosphereTwilightColor = uniform(new THREE.Color(0xbd5f1b));
|
|
const cloudSpeed = uniform(0.01);
|
|
const cityLightIntensity = uniform(1.5);
|
|
|
|
async function init() {
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
|
|
camera.position.set(0, 0, 4);
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x000011);
|
|
|
|
// Load textures
|
|
const loader = new THREE.TextureLoader();
|
|
|
|
// Note: Replace with actual texture paths
|
|
const earthDayTexture = loader.load('textures/earth_day.jpg');
|
|
const earthNightTexture = loader.load('textures/earth_night.jpg');
|
|
const earthCloudsTexture = loader.load('textures/earth_clouds.jpg');
|
|
const earthBumpTexture = loader.load('textures/earth_bump.jpg');
|
|
|
|
// Set texture properties
|
|
[earthDayTexture, earthNightTexture, earthCloudsTexture, earthBumpTexture].forEach((tex) => {
|
|
tex.colorSpace = THREE.SRGBColorSpace;
|
|
tex.wrapS = THREE.RepeatWrapping;
|
|
tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
});
|
|
|
|
// Create Earth
|
|
earth = createEarth(earthDayTexture, earthNightTexture, earthBumpTexture);
|
|
scene.add(earth);
|
|
|
|
// Create cloud layer
|
|
clouds = createClouds(earthCloudsTexture);
|
|
scene.add(clouds);
|
|
|
|
// Create atmosphere glow
|
|
atmosphere = createAtmosphere();
|
|
scene.add(atmosphere);
|
|
|
|
// Stars background
|
|
createStars();
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGPURenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
document.body.appendChild(renderer.domElement);
|
|
await renderer.init();
|
|
|
|
// Controls
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.minDistance = 2;
|
|
controls.maxDistance = 10;
|
|
|
|
// Events
|
|
window.addEventListener('resize', onWindowResize);
|
|
|
|
renderer.setAnimationLoop(animate);
|
|
}
|
|
|
|
function createEarth(dayTex, nightTex, bumpTex) {
|
|
const geometry = new THREE.SphereGeometry(1, 64, 64);
|
|
const material = new THREE.MeshStandardNodeMaterial();
|
|
|
|
// Sun illumination factor
|
|
const sunOrientation = Fn(() => {
|
|
return normalWorld.dot(sunDirection).mul(0.5).add(0.5);
|
|
});
|
|
|
|
// Day/night color mixing
|
|
material.colorNode = Fn(() => {
|
|
const dayColor = texture(dayTex, uv());
|
|
const nightColor = texture(nightTex, uv());
|
|
|
|
const orientation = sunOrientation();
|
|
const dayNight = smoothstep(0.4, 0.6, orientation);
|
|
|
|
// Add city lights on night side
|
|
const cityLights = nightColor.mul(cityLightIntensity).mul(
|
|
float(1.0).sub(dayNight)
|
|
);
|
|
|
|
const baseColor = mix(nightColor, dayColor, dayNight);
|
|
return baseColor.add(cityLights.mul(float(1.0).sub(orientation).pow(2.0)));
|
|
})();
|
|
|
|
// Bump mapping for terrain
|
|
material.normalNode = bumpMap(texture(bumpTex, uv()), 0.03);
|
|
|
|
// PBR properties vary with day/night
|
|
material.roughnessNode = Fn(() => {
|
|
const orientation = sunOrientation();
|
|
return mix(float(0.8), float(0.4), smoothstep(0.3, 0.7, orientation));
|
|
})();
|
|
|
|
material.metalnessNode = float(0.0);
|
|
|
|
// Subtle atmospheric rim on day side
|
|
material.emissiveNode = Fn(() => {
|
|
const viewDir = normalize(cameraPosition.sub(positionWorld));
|
|
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).saturate()), 4.0);
|
|
|
|
const orientation = sunOrientation();
|
|
const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, orientation);
|
|
|
|
return atmosphereColor.mul(fresnel).mul(orientation).mul(0.3);
|
|
})();
|
|
|
|
return new THREE.Mesh(geometry, material);
|
|
}
|
|
|
|
function createClouds(cloudsTex) {
|
|
const geometry = new THREE.SphereGeometry(1.01, 64, 64);
|
|
const material = new THREE.MeshStandardNodeMaterial();
|
|
|
|
// Animated UV for cloud movement
|
|
const cloudUV = Fn(() => {
|
|
const baseUV = uv();
|
|
const offset = time.mul(cloudSpeed);
|
|
return vec2(baseUV.x.add(offset), baseUV.y);
|
|
});
|
|
|
|
// Cloud color (white with transparency)
|
|
material.colorNode = color(0xffffff);
|
|
|
|
// Cloud opacity from texture
|
|
material.opacityNode = Fn(() => {
|
|
const cloudAlpha = texture(cloudsTex, cloudUV()).r;
|
|
|
|
// Fade clouds on night side
|
|
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
|
|
const dayFactor = smoothstep(0.2, 0.5, sunOrientation);
|
|
|
|
return cloudAlpha.mul(0.8).mul(dayFactor.mul(0.5).add(0.5));
|
|
})();
|
|
|
|
material.transparent = true;
|
|
material.depthWrite = false;
|
|
material.side = THREE.DoubleSide;
|
|
|
|
// Slight self-illumination
|
|
material.emissiveNode = Fn(() => {
|
|
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
|
|
return color(0xffffff).mul(sunOrientation.mul(0.1));
|
|
})();
|
|
|
|
return new THREE.Mesh(geometry, material);
|
|
}
|
|
|
|
function createAtmosphere() {
|
|
const geometry = new THREE.SphereGeometry(1.15, 64, 64);
|
|
const material = new THREE.MeshBasicNodeMaterial();
|
|
|
|
material.colorNode = Fn(() => {
|
|
const viewDir = normalize(cameraPosition.sub(positionWorld));
|
|
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 3.0);
|
|
|
|
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
|
|
const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, sunOrientation);
|
|
|
|
return atmosphereColor;
|
|
})();
|
|
|
|
material.opacityNode = Fn(() => {
|
|
const viewDir = normalize(cameraPosition.sub(positionWorld));
|
|
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 2.5);
|
|
|
|
// Stronger on day side
|
|
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
|
|
|
|
return fresnel.mul(sunOrientation.mul(0.5).add(0.3));
|
|
})();
|
|
|
|
material.transparent = true;
|
|
material.depthWrite = false;
|
|
material.side = THREE.BackSide;
|
|
|
|
return new THREE.Mesh(geometry, material);
|
|
}
|
|
|
|
function createStars() {
|
|
const starsGeometry = new THREE.BufferGeometry();
|
|
const starCount = 2000;
|
|
|
|
const positions = new Float32Array(starCount * 3);
|
|
const colors = new Float32Array(starCount * 3);
|
|
|
|
for (let i = 0; i < starCount; i++) {
|
|
// Random position on sphere
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos(Math.random() * 2 - 1);
|
|
const radius = 50 + Math.random() * 50;
|
|
|
|
positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
|
|
positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
|
positions[i * 3 + 2] = radius * Math.cos(phi);
|
|
|
|
// Slight color variation
|
|
const brightness = 0.5 + Math.random() * 0.5;
|
|
colors[i * 3] = brightness;
|
|
colors[i * 3 + 1] = brightness;
|
|
colors[i * 3 + 2] = brightness + Math.random() * 0.2;
|
|
}
|
|
|
|
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
starsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
|
|
const starsMaterial = new THREE.PointsNodeMaterial();
|
|
starsMaterial.colorNode = Fn(() => {
|
|
return vec3(1.0);
|
|
})();
|
|
starsMaterial.sizeNode = float(2.0);
|
|
starsMaterial.vertexColors = true;
|
|
|
|
const stars = new THREE.Points(starsGeometry, starsMaterial);
|
|
scene.add(stars);
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
// Rotate Earth slowly
|
|
earth.rotation.y += 0.001;
|
|
clouds.rotation.y += 0.0012;
|
|
|
|
// Animate sun direction (optional - creates day/night cycle)
|
|
// const angle = time.value * 0.1;
|
|
// sunDirection.value.set(Math.cos(angle), 0.2, Math.sin(angle)).normalize();
|
|
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
init();
|
|
|
|
// Export for external control
|
|
export { sunDirection, atmosphereDayColor, atmosphereTwilightColor, cloudSpeed, cityLightIntensity };
|