Files
space-game/apps/viewer/src/viewerPresentation.ts

141 lines
6.4 KiB
TypeScript

import * as THREE from "three";
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes";
import { rawObject } from "./viewerScenePrimitives";
export const MIN_ICON_PIXELS = 25;
export const MAX_ICON_PIXELS = 50;
export function iconWorldScale(distToCamera: number, camera: THREE.PerspectiveCamera, pixels: number): number {
return pixels * distToCamera * 2 * Math.tan((camera.fov * Math.PI / 180) / 2) / window.innerHeight;
}
export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) {
const elapsedMs = now - visual.receivedAtMs;
const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1);
return new THREE.Vector3().lerpVectors(visual.startPosition, visual.authoritativePosition, blendT);
}
export function resolveShipHeading(visual: ShipVisual, worldPosition: THREE.Vector3, orbitYaw: number) {
const desiredHeading = visual.targetPosition.clone().sub(worldPosition);
if (desiredHeading.lengthSq() > 0.01) {
return desiredHeading;
}
if (visual.velocity.lengthSq() > 0.01) {
return visual.velocity.clone();
}
return new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
}
export function updatePlanetPresentation(
world: WorldState | undefined,
worldTimeSyncMs: number,
planetVisuals: PlanetVisual[],
systemCamera: THREE.PerspectiveCamera,
) {
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
// In systemScene all positions use scaleLocalVector * ACTIVE_SYSTEM_DETAIL_SCALE.
// Star is always at origin (0,0,0); orbits are centered there.
for (const visual of planetVisuals) {
const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds))
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
visual.mesh.setPosition(position);
visual.icon.setPosition(position);
const iconWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
const distToIcon = systemCamera.position.distanceTo(iconWorldPos);
const t = THREE.MathUtils.clamp(distToIcon / 300, 0, 1);
const rawScale = visual.iconBaseScale * t * Math.sqrt(t);
const planetIconScale = THREE.MathUtils.clamp(rawScale, iconWorldScale(distToIcon, systemCamera, MIN_ICON_PIXELS), iconWorldScale(distToIcon, systemCamera, MAX_ICON_PIXELS));
visual.icon.setScaleScalar(planetIconScale);
if (visual.ring) {
visual.ring.setPosition(position);
}
const distToPlanet = systemCamera.position.distanceTo(position);
const moonOrbitOpacity = THREE.MathUtils.clamp(1 - distToPlanet / 500, 0, 1) * 0.18;
const clusterVisible = distToPlanet < 300;
for (const [moonIndex, moon] of visual.moons.entries()) {
const moonPos = position.clone().add(
scaleLocalVector(computeMoonLocalPosition(visual.planet.moons[moonIndex], nowSeconds))
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
);
moon.mesh.setPosition(moonPos);
moon.mesh.setVisible(clusterVisible);
moon.icon.setPosition(moonPos);
moon.icon.setVisible(clusterVisible);
if (clusterVisible) {
const iconWorldPos = moon.icon.getWorldPosition(new THREE.Vector3());
const moonDist = systemCamera.position.distanceTo(iconWorldPos);
const t = THREE.MathUtils.clamp(moonDist / 120, 0, 1);
const rawMoonScale = moon.iconBaseScale * t * Math.sqrt(t);
const moonIconScale = THREE.MathUtils.clamp(rawMoonScale, iconWorldScale(moonDist, systemCamera, MIN_ICON_PIXELS), iconWorldScale(moonDist, systemCamera, MAX_ICON_PIXELS));
moon.icon.setScaleScalar(moonIconScale);
}
moon.orbit.setPosition(position);
const orbitObj = rawObject(moon.orbit);
if (orbitObj instanceof THREE.LineLoop) {
(orbitObj.material as THREE.LineBasicMaterial).opacity = moonOrbitOpacity;
}
}
}
}
export function updateSystemStarPresentation(
systemVisuals: Map<string, SystemVisual>,
activeSystemId: string | undefined,
galaxyCamera: THREE.PerspectiveCamera,
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void,
) {
const activeSystem = activeSystemId ? systemVisuals.get(activeSystemId) : undefined;
for (const [systemId, visual] of systemVisuals.entries()) {
// galaxyRoot is always at the galaxy position of this system
visual.galaxyRoot.setPosition(visual.galaxyPosition);
visual.shellReticle.setScaleScalar(visual.shellReticleBaseScale);
if (!activeSystem) {
// Galaxy view: show star dot, hide shell reticle
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
visual.icon.setVisible(true);
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
visual.shellReticle.setVisible(false);
setShellReticleOpacity(visual.shellReticle, 0);
const dotWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
visual.icon.setScaleScalar(galaxyCamera.position.distanceTo(dotWorldPos) * 0.01);
continue;
}
if (systemId !== activeSystemId) {
// Other systems in galaxy view while a system is active: show shell reticle projected to edge
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
visual.icon.setVisible(false);
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
visual.shellReticle.setVisible(true);
setShellReticleOpacity(visual.shellReticle, 1);
const direction = visual.galaxyPosition.clone().sub(activeSystem.galaxyPosition);
if (direction.lengthSq() > 0.0001) {
visual.galaxyRoot.setPosition(
activeSystem.galaxyPosition.clone().add(direction.normalize().multiplyScalar(PROJECTED_GALAXY_RADIUS)),
);
}
const reticleWorldPosition = visual.galaxyRoot.getWorldPosition(new THREE.Vector3());
const reticleDistance = galaxyCamera.position.distanceTo(reticleWorldPosition);
const reticleScale = Math.max(900, reticleDistance * 0.032);
visual.shellReticle.setScaleScalar(reticleScale);
continue;
}
// Active system in galaxy view: show star dot, hide shell reticle
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
visual.icon.setVisible(true);
visual.shellReticle.setVisible(false);
setShellReticleOpacity(visual.shellReticle, 0);
}
}