SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
6.6 KB · 187 lines tsx
Raw Blame History
1'use client';2/**3 * Vector Earth: dark sphere shaded by the real sun direction (day/night terminator), fresnel atmosphere, and4 * Natural Earth 110 m land + country outlines drawn as GPU line segments (no raster texture — a premium, data-first look).5 */6import { useFrame } from '@react-three/fiber';7import { useMemo, useRef } from 'react';8import * as THREE from 'three';9import { mesh as topoMesh } from 'topojson-client';10import type { Topology } from 'topojson-specification';11import countries110 from 'world-atlas/countries-110m.json';12import land110 from 'world-atlas/land-110m.json';13import { llaToXyz, prefersReducedMotion, sunDirection, token } from './geo';1415const R_LAND = 1.003;1617type MultiLine = { type: string; coordinates: number[][][] };1819function linesToSegments(ml: MultiLine, r: number): Float32Array {20  let segs = 0;21  for (const line of ml.coordinates) segs += Math.max(0, line.length - 1);22  const out = new Float32Array(segs * 6);23  let o = 0;24  const a = new Float32Array(3);25  const b = new Float32Array(3);26  for (const line of ml.coordinates) {27    for (let i = 0; i < line.length - 1; i++) {28      const p = line[i]!;29      const q = line[i + 1]!;30      llaToXyz(p[1] ?? 0, p[0] ?? 0, r, a);31      llaToXyz(q[1] ?? 0, q[0] ?? 0, r, b);32      out.set(a, o);33      out.set(b, o + 3);34      o += 6;35    }36  }37  return out;38}3940function graticule(stepDeg: number, r: number): Float32Array {41  const pts: number[] = [];42  const v = new Float32Array(3);43  const push = (lat: number, lon: number) => {44    llaToXyz(lat, lon, r, v);45    pts.push(v[0]!, v[1]!, v[2]!);46  };47  for (let lon = -180; lon < 180; lon += stepDeg) {48    for (let lat = -90; lat < 90; lat += 3) {49      push(lat, lon);50      push(lat + 3, lon);51    }52  }53  for (let lat = -60; lat <= 60; lat += stepDeg) {54    for (let lon = -180; lon < 180; lon += 3) {55      push(lat, lon);56      push(lat, lon + 3);57    }58  }59  return new Float32Array(pts);60}6162function lineGeometry(arr: Float32Array): THREE.BufferGeometry {63  const g = new THREE.BufferGeometry();64  g.setAttribute('position', new THREE.BufferAttribute(arr, 3));65  return g;66}6768const EARTH_VERT = /* glsl */ `69  varying vec3 vNormalW;70  varying vec3 vPosW;71  void main() {72    vNormalW = normalize(mat3(modelMatrix) * normal);73    vPosW = (modelMatrix * vec4(position, 1.0)).xyz;74    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);75  }76`;77const EARTH_FRAG = /* glsl */ `78  uniform vec3 uSun;79  uniform vec3 uNight;80  uniform vec3 uDay;81  uniform vec3 uRim;82  varying vec3 vNormalW;83  varying vec3 vPosW;84  void main() {85    vec3 n = normalize(vNormalW);86    float l = dot(n, uSun);87    float day = smoothstep(-0.12, 0.28, l);88    vec3 col = mix(uNight, uDay, day);89    vec3 viewDir = normalize(cameraPosition - vPosW);90    float rim = pow(1.0 - max(dot(n, viewDir), 0.0), 3.0);91    col += uRim * rim * 0.35;92    gl_FragColor = vec4(col, 1.0);93  }94`;95const ATMO_VERT = /* glsl */ `96  varying vec3 vNormalW;97  varying vec3 vPosW;98  void main() {99    vNormalW = normalize(mat3(modelMatrix) * normal);100    vPosW = (modelMatrix * vec4(position, 1.0)).xyz;101    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);102  }103`;104const ATMO_FRAG = /* glsl */ `105  uniform vec3 uColor;106  varying vec3 vNormalW;107  varying vec3 vPosW;108  void main() {109    vec3 viewDir = normalize(cameraPosition - vPosW);110    float f = pow(max(dot(normalize(vNormalW), viewDir), 0.0), 2.6);111    gl_FragColor = vec4(uColor, f * 0.55);112  }113`;114115export function Earth({ quality = 'high' }: { quality?: 'high' | 'low' }) {116  const sunRef = useRef(new THREE.Vector3(1, 0, 0));117  const lastSun = useRef(0);118  const reduced = prefersReducedMotion();119120  const { landGeo, borderGeo, gratGeo, earthMat, atmoMat, landColor, borderColor, gratColor } = useMemo(() => {121    const landTopo = land110 as unknown as Topology;122    const cTopo = countries110 as unknown as Topology;123    const land = topoMesh(landTopo, landTopo.objects.land as never) as unknown as MultiLine;124    const borders = topoMesh(cTopo, cTopo.objects.countries as never, ((a: { id: string }, b: { id: string }) => a !== b) as never) as unknown as MultiLine;125    const accent = new THREE.Color(token('--accent'));126    const night = new THREE.Color(token('--plane')).multiplyScalar(0.55);127    const day = new THREE.Color(token('--plane-3')).lerp(new THREE.Color(token('--accent')), 0.05);128    const rim = new THREE.Color(token('--accent')).multiplyScalar(0.6);129    return {130      landGeo: lineGeometry(linesToSegments(land, R_LAND)),131      borderGeo: lineGeometry(linesToSegments(borders, R_LAND)),132      gratGeo: lineGeometry(graticule(30, 1.001)),133      earthMat: new THREE.ShaderMaterial({134        vertexShader: EARTH_VERT,135        fragmentShader: EARTH_FRAG,136        uniforms: { uSun: { value: sunRef.current }, uNight: { value: night }, uDay: { value: day }, uRim: { value: rim } },137      }),138      atmoMat: new THREE.ShaderMaterial({139        vertexShader: ATMO_VERT,140        fragmentShader: ATMO_FRAG,141        uniforms: { uColor: { value: accent } },142        transparent: true,143        side: THREE.BackSide,144        depthWrite: false,145        blending: THREE.AdditiveBlending,146      }),147      landColor: new THREE.Color(token('--accent')).lerp(new THREE.Color('#ffffff'), 0.25),148      borderColor: new THREE.Color(token('--ink-3')),149      gratColor: new THREE.Color(token('--ink-3')),150    };151  }, []);152153  useFrame(() => {154    const now = Date.now();155    if (now - lastSun.current > (reduced ? 60_000 : 1_000)) {156      lastSun.current = now;157      sunDirection(new Date(now), sunRef.current);158    }159  });160161  const seg = quality === 'high' ? 96 : 48;162  return (163    <group>164      <mesh material={earthMat} renderOrder={0}>165        <sphereGeometry args={[1, seg, seg / 2]} />166      </mesh>167      <mesh material={atmoMat} renderOrder={1} scale={1.045}>168        <sphereGeometry args={[1, 64, 32]} />169      </mesh>170      <lineSegments geometry={gratGeo} renderOrder={2}>171        <lineBasicMaterial color={gratColor} transparent opacity={0.16} depthWrite={false} />172      </lineSegments>173      <lineSegments geometry={landGeo} renderOrder={3}>174        <lineBasicMaterial color={landColor} transparent opacity={0.85} depthWrite={false} />175      </lineSegments>176      <lineSegments geometry={borderGeo} renderOrder={3}>177        <lineBasicMaterial color={borderColor} transparent opacity={0.35} depthWrite={false} />178      </lineSegments>179      {/* Equator hairline helps read the GEO ring geometry */}180      <mesh rotation={[Math.PI / 2, 0, 0]} renderOrder={2}>181        <ringGeometry args={[1.0005, 1.0035, 128]} />182        <meshBasicMaterial color={gratColor} transparent opacity={0.25} side={THREE.DoubleSide} depthWrite={false} />183      </mesh>184    </group>185  );186}187