feat: 3 scene rendering setup

This commit is contained in:
2026-03-18 08:49:51 -04:00
parent 933c6afd08
commit 358122a74a
33 changed files with 1094 additions and 1132 deletions

View File

@@ -2,49 +2,15 @@ import * as THREE from "three";
import {
MAX_CAMERA_DISTANCE,
MIN_CAMERA_DISTANCE,
ZOOM_DISTANCE,
NAV_DISTANCE,
} from "./viewerConstants";
import { createViewerHud } from "./viewerHud";
import {
classifyZoomLevel,
computeZoomBlend,
formatBytes,
inventoryAmount,
smoothBand,
} from "./viewerMath";
import { updatePanFromKeyboard } from "./viewerCamera";
import {
createCirclePoints,
shipLength,
shipPresentationColor,
shipSize,
spatialNodeColor,
} from "./viewerSceneAppearance";
import {
createBackdropStars,
createNebulaClouds,
createNebulaTexture,
} from "./viewerSceneFactory";
import {
setShellReticleOpacity,
} from "./viewerControls";
import {
recordPerformanceStats,
updateNetworkPanel as renderNetworkPanel,
updatePerformancePanel as renderPerformancePanel,
} from "./viewerTelemetry";
import { setShellReticleOpacity } from "./viewerControls";
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
import { updatePlanetPresentation } from "./viewerPresentation";
import {
renderRecentEvents,
updateGameStatus,
updateSystemSummaries,
updateWorldPresentation,
} from "./viewerWorldPresentation";
import {
resolveFocusedBubbleId,
} from "./viewerSelection";
import { describeSelectionParent, updateSystemPanel } from "./viewerPanels";
import { updateSystemStarPresentation } from "./viewerPresentation";
import { resolveFocusedCelestialId } from "./viewerSelection";
import { describeSelectionParent } from "./viewerPanels";
import {
createInitialNetworkStats,
createInitialPerformanceStats,
@@ -55,68 +21,65 @@ import { ViewerNavigationController } from "./viewerNavigationController";
import { ViewerSceneDataController } from "./viewerSceneDataController";
import { ViewerPresentationController } from "./viewerPresentationController";
import { createViewerControllers, wireViewerEvents } from "./viewerControllerFactory";
import type { SceneNode } from "./viewerScenePrimitives";
import type { FactionSnapshot, ShipSnapshot } from "./contracts";
import { toDisplayLocalPosition, getSystemCameraFocus } from "./viewerCamera";
import { UniverseLayer } from "./viewerUniverseLayer";
import { GalaxyLayer } from "./viewerGalaxyLayer";
import { SystemLayer } from "./viewerSystemLayer";
import { LocalLayer } from "./viewerLocalLayer";
import type { FactionSnapshot } from "./contracts";
import type {
BubbleVisual,
CelestialVisual,
CameraMode,
ClaimVisual,
ConstructionSiteVisual,
DragMode,
HistoryWindowState,
MoonVisual,
NetworkStats,
NodeVisual,
OrbitLineVisual,
OrbitalAnchor,
PerformanceStats,
PlanetVisual,
PresentationEntry,
Selectable,
ShipVisual,
SpatialNodeVisual,
StructureVisual,
SystemSummaryVisual,
SystemVisual,
WorldState,
ZoomLevel,
PovLevel,
} from "./viewerTypes";
export class ViewerAppController {
private readonly container: HTMLElement;
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
private readonly scene = new THREE.Scene();
private readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 160000);
// ── Three independent rendering layers ───────────────────────────────────
private readonly universeLayer = new UniverseLayer();
private readonly galaxyLayer = new GalaxyLayer();
private readonly systemLayer = new SystemLayer();
private readonly localLayer = new LocalLayer();
private readonly clock = new THREE.Clock();
private readonly raycaster = new THREE.Raycaster();
private readonly mouse = new THREE.Vector2();
private readonly galaxyFocus = new THREE.Vector3(2200, 0, 300);
private readonly systemFocusLocal = new THREE.Vector3();
// ── Galaxy-space anchor ───────────────────────────────────────────────────
private readonly galaxyAnchor = new THREE.Vector3(2200, 0, 300);
// ── System-space anchor ───────────────────────────────────────────────────
private readonly systemAnchor = new THREE.Vector3();
private readonly cameraOffset = new THREE.Vector3();
private readonly keyState = new Set<string>();
private readonly systemGroup = new THREE.Group();
private readonly spatialNodeGroup = new THREE.Group();
private readonly bubbleGroup = new THREE.Group();
private readonly nodeGroup = new THREE.Group();
private readonly stationGroup = new THREE.Group();
private readonly claimGroup = new THREE.Group();
private readonly constructionSiteGroup = new THREE.Group();
private readonly shipGroup = new THREE.Group();
private readonly ambienceGroup = new THREE.Group();
private readonly gamePanelEl: HTMLDivElement;
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
private readonly presentationEntries: PresentationEntry[] = [];
private readonly nodeVisuals = new Map<string, NodeVisual>();
private readonly spatialNodeVisuals = new Map<string, SpatialNodeVisual>();
private readonly bubbleVisuals = new Map<string, BubbleVisual>();
private readonly celestialVisuals = new Map<string, CelestialVisual>();
private readonly stationVisuals = new Map<string, StructureVisual>();
private readonly claimVisuals = new Map<string, ClaimVisual>();
private readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
private readonly shipVisuals = new Map<string, ShipVisual>();
private readonly systemVisuals = new Map<string, SystemVisual>();
private readonly systemSummaryVisuals = new Map<string, SystemSummaryVisual>();
private readonly planetVisuals: PlanetVisual[] = [];
private readonly nodeVisuals = new Map<string, NodeVisual>();
private readonly planetVisuals: any[] = [];
private readonly orbitLines: OrbitLineVisual[] = [];
private readonly statusEl: HTMLDivElement;
private readonly gameSummaryEl: HTMLSpanElement;
private readonly systemPanelEl: HTMLDivElement;
@@ -145,9 +108,9 @@ export class ViewerAppController {
private selectedItems: Selectable[] = [];
private worldSignature = "";
private zoomLevel: ZoomLevel = "system";
private currentDistance = ZOOM_DISTANCE.system;
private desiredDistance = ZOOM_DISTANCE.system;
private povLevel: PovLevel = "system";
private currentDistance = NAV_DISTANCE.system;
private desiredDistance = NAV_DISTANCE.system;
private orbitYaw = -2.3;
private orbitPitch = 0.62;
private cameraMode: CameraMode = "tactical";
@@ -181,23 +144,7 @@ export class ViewerAppController {
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.scene.background = new THREE.Color(0x040912);
this.scene.fog = new THREE.FogExp2(0x040912, 0.00011);
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
keyLight.position.set(1000, 1200, 800);
this.scene.add(keyLight);
this.scene.add(
this.ambienceGroup,
this.systemGroup,
this.spatialNodeGroup,
this.bubbleGroup,
this.nodeGroup,
this.stationGroup,
this.claimGroup,
this.constructionSiteGroup,
this.shipGroup,
);
const hud = createViewerHud(document);
this.gamePanelEl = hud.gamePanelEl;
this.statusEl = hud.statusEl;
@@ -263,12 +210,11 @@ export class ViewerAppController {
return this.sceneDataController.createWorldPresentationContext({
world: this.world,
activeSystemId: this.activeSystemId,
zoomLevel: this.zoomLevel,
povLevel: this.povLevel,
orbitYaw: this.orbitYaw,
camera: this.camera,
systemFocusLocal: this.systemFocusLocal,
toDisplayLocalPosition: this.toDisplayLocalPosition.bind(this),
updateSystemDetailVisibility: () => this.navigationController.updateSystemDetailVisibility(),
systemCamera: this.systemLayer.camera,
systemAnchor: this.systemAnchor,
toDisplayLocalPosition: (localPosition) => toDisplayLocalPosition(localPosition),
setShellReticleOpacity: (sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
});
}
@@ -285,8 +231,14 @@ export class ViewerAppController {
renderFrame({
clock: this.clock,
renderer: this.renderer,
scene: this.scene,
camera: this.camera,
universeScene: this.universeLayer.scene,
galaxyScene: this.galaxyLayer.scene,
galaxyCamera: this.galaxyLayer.camera,
systemScene: this.systemLayer.scene,
systemCamera: this.systemLayer.camera,
localScene: this.localLayer.scene,
localCamera: this.localLayer.camera,
getPovLevel: () => this.povLevel,
updateCamera: (delta) => this.updateCamera(delta),
updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
updatePlanetPresentation: () => this.presentationController.updatePlanetPresentation(),
@@ -298,10 +250,13 @@ export class ViewerAppController {
});
}
private updateAmbience(delta: number) {
this.ambienceGroup.position.copy(this.camera.position);
this.ambienceGroup.rotation.y += delta * 0.005;
this.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
private computeOrbitOffset(): THREE.Vector3 {
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
return new THREE.Vector3(
Math.cos(this.orbitYaw) * horizontalDistance,
this.currentDistance * Math.sin(this.orbitPitch),
Math.sin(this.orbitYaw) * horizontalDistance,
);
}
private updateCamera(delta: number) {
@@ -312,25 +267,38 @@ export class ViewerAppController {
delta,
});
this.currentDistance = nextState.currentDistance;
this.zoomLevel = nextState.zoomLevel;
this.povLevel = nextState.povLevel;
this.orbitPitch = nextState.orbitPitch;
this.navigationController.updateActiveSystem();
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
// Follow camera directly controls systemLayer.camera in updateFollowCamera.
// Still update galaxy camera independently.
const orbitOffset = this.computeOrbitOffset();
this.galaxyLayer.updateCamera(this.galaxyAnchor, orbitOffset);
return;
}
this.updatePanFromKeyboard(delta);
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
const focus = this.navigationController.getCameraFocusWorldPosition();
this.cameraOffset.set(
Math.cos(this.orbitYaw) * horizontalDistance,
this.currentDistance * Math.sin(this.orbitPitch),
Math.sin(this.orbitYaw) * horizontalDistance,
const orbitOffset = this.computeOrbitOffset();
this.galaxyLayer.updateCamera(this.galaxyAnchor, orbitOffset);
if (this.activeSystemId) {
this.systemLayer.updateCamera(getSystemCameraFocus(this.systemAnchor), orbitOffset);
}
this.localLayer.updateCamera(orbitOffset);
// Update star dot scales in galaxy scene
updateSystemStarPresentation(
this.systemVisuals,
this.activeSystemId,
this.galaxyLayer.camera,
(sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
);
this.camera.position.copy(focus).add(this.cameraOffset);
this.camera.lookAt(focus);
}
private updatePanFromKeyboard(delta: number) {
@@ -338,10 +306,10 @@ export class ViewerAppController {
this.keyState,
this.orbitYaw,
this.currentDistance,
this.zoomLevel,
this.povLevel,
this.activeSystemId,
this.systemFocusLocal,
this.galaxyFocus,
this.systemAnchor,
this.galaxyAnchor,
delta,
MIN_CAMERA_DISTANCE,
MAX_CAMERA_DISTANCE,
@@ -352,16 +320,6 @@ export class ViewerAppController {
this.presentationController.updateSystemSummaries();
}
private registerPresentation(
detail: SceneNode,
icon: SceneNode,
hideDetailInUniverse: boolean,
hideIconInUniverse = false,
systemId?: string,
) {
this.presentationEntries.push({ detail, icon, systemId, hideDetailInUniverse, hideIconInUniverse });
}
private renderRecentEvents(entityKind: string, entityId: string) {
return this.presentationController.renderRecentEvents(entityKind, entityId);
}
@@ -378,14 +336,16 @@ export class ViewerAppController {
this.interactionController.refreshHistoryWindows();
}
private resolveFocusedBubbleId() {
return resolveFocusedBubbleId(this.world, this.selectedItems);
private resolveFocusedCelestialId() {
return resolveFocusedCelestialId(this.world, this.selectedItems);
}
private onResize = () => {
resizeViewer({
renderer: this.renderer,
camera: this.camera,
galaxyCamera: this.galaxyLayer.camera,
systemCamera: this.systemLayer.camera,
localCamera: this.localLayer.camera,
});
};
@@ -398,7 +358,7 @@ export class ViewerAppController {
}
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
return this.navigationController.toDisplayLocalPosition(localPosition, systemId);
return toDisplayLocalPosition(localPosition);
}
private updateSystemPanel() {