feat: 3 scene rendering setup
This commit is contained in:
@@ -1,42 +1,40 @@
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
DISPLAY_UNITS_PER_KILOMETER,
|
||||
DISPLAY_UNITS_PER_LIGHT_YEAR,
|
||||
KILOMETERS_PER_AU,
|
||||
computeMoonLocalPosition,
|
||||
computeMoonSize,
|
||||
computePlanetLocalPosition,
|
||||
currentWorldTimeSeconds,
|
||||
resolveOrbitalAnchorPosition,
|
||||
toDisplayGalaxyVector,
|
||||
toThreeVector,
|
||||
} from "./viewerMath";
|
||||
import { describeActiveSpace } from "./viewerSelection";
|
||||
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection";
|
||||
import {
|
||||
resolveShipHeading,
|
||||
updateSystemStarPresentation,
|
||||
updateSystemSummaryPresentation,
|
||||
getAnimatedShipLocalPosition,
|
||||
} from "./viewerPresentation";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
import type {
|
||||
LocalBubbleDelta,
|
||||
LocalBubbleSnapshot,
|
||||
ResourceNodeDelta,
|
||||
ResourceNodeSnapshot,
|
||||
ShipSnapshot,
|
||||
} from "./contracts";
|
||||
import type {
|
||||
BubbleVisual,
|
||||
CelestialVisual,
|
||||
ClaimVisual,
|
||||
Selectable,
|
||||
ConstructionSiteVisual,
|
||||
NodeVisual,
|
||||
OrbitalAnchor,
|
||||
ShipVisual,
|
||||
SpatialNodeVisual,
|
||||
StructureVisual,
|
||||
SystemSummaryVisual,
|
||||
SystemVisual,
|
||||
WorldState,
|
||||
ZoomLevel,
|
||||
PovLevel,
|
||||
CameraMode,
|
||||
} from "./viewerTypes";
|
||||
|
||||
@@ -47,23 +45,22 @@ export interface WorldOrbitalContext {
|
||||
worldTimeSyncMs: number;
|
||||
worldSeed: number;
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
spatialNodeVisuals: Map<string, SpatialNodeVisual>;
|
||||
bubbleVisuals: Map<string, BubbleVisual>;
|
||||
celestialVisuals: Map<string, CelestialVisual>;
|
||||
stationVisuals: Map<string, StructureVisual>;
|
||||
}
|
||||
|
||||
export interface WorldPresentationContext extends WorldOrbitalContext {
|
||||
activeSystemId?: string;
|
||||
zoomLevel: ZoomLevel;
|
||||
povLevel: PovLevel;
|
||||
orbitYaw: number;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
systemFocusLocal: THREE.Vector3;
|
||||
systemAnchor: THREE.Vector3;
|
||||
shipVisuals: Map<string, ShipVisual>;
|
||||
claimVisuals: Map<string, ClaimVisual>;
|
||||
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||
systemVisuals: Map<string, SystemVisual>;
|
||||
systemSummaryVisuals: Map<string, SystemSummaryVisual>;
|
||||
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
||||
systemSummaryVisuals: Map<string, any>;
|
||||
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
|
||||
updateSystemDetailVisibility: () => void;
|
||||
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void;
|
||||
}
|
||||
@@ -74,15 +71,17 @@ export interface GameStatusParams {
|
||||
world?: WorldState;
|
||||
activeSystemId?: string;
|
||||
cameraMode: CameraMode;
|
||||
zoomLevel: ZoomLevel;
|
||||
povLevel: PovLevel;
|
||||
selectedItems: Selectable[];
|
||||
mode: string;
|
||||
galaxyAnchor?: THREE.Vector3;
|
||||
systemAnchor?: THREE.Vector3;
|
||||
}
|
||||
|
||||
export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
const now = performance.now();
|
||||
const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
|
||||
const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.zoomLevel);
|
||||
const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.povLevel);
|
||||
|
||||
for (const [shipId, visual] of context.shipVisuals.entries()) {
|
||||
const ship = context.world?.ships.get(shipId);
|
||||
@@ -91,7 +90,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
||||
const displayPosition = resolveShipWorldPosition(context, ship, visual, worldPosition);
|
||||
const displayPosition = context.toDisplayLocalPosition(worldPosition);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
|
||||
@@ -105,67 +104,51 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
|
||||
for (const visual of context.nodeVisuals.values()) {
|
||||
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.spatialNodeVisuals.values()) {
|
||||
const animatedLocalPosition = computeSpatialNodeLocalPosition(context, visual, worldTimeSeconds);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
for (const visual of context.celestialVisuals.values()) {
|
||||
const animatedLocalPosition = computeCelestialLocalPosition(context, visual, worldTimeSeconds);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.bubbleVisuals.values()) {
|
||||
const animatedLocalPosition = resolveBubbleAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.stationVisuals.values()) {
|
||||
const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.claimVisuals.values()) {
|
||||
const animatedLocalPosition = computeSpatialNodeLocalPositionById(context, visual.nodeId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.constructionSiteVisuals.values()) {
|
||||
const animatedLocalPosition = computeSpatialNodeLocalPositionById(context, visual.nodeId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
updateSystemStarPresentation(
|
||||
context.systemVisuals,
|
||||
context.activeSystemId,
|
||||
context.systemFocusLocal,
|
||||
context.camera,
|
||||
context.setShellReticleOpacity,
|
||||
);
|
||||
context.updateSystemDetailVisibility();
|
||||
updateSystemSummaryPresentation(context.systemSummaryVisuals, context.camera, context.activeSystemId);
|
||||
}
|
||||
|
||||
type RenderSpaceMode = "galaxy" | "system" | "local";
|
||||
|
||||
function resolveRenderSpaceMode(activeSystemId: string | undefined, zoomLevel: ZoomLevel): RenderSpaceMode {
|
||||
if (!activeSystemId || zoomLevel === "universe") {
|
||||
function resolveRenderSpaceMode(activeSystemId: string | undefined, povLevel: PovLevel): RenderSpaceMode {
|
||||
if (!activeSystemId || povLevel === "galaxy") {
|
||||
return "galaxy";
|
||||
}
|
||||
|
||||
return zoomLevel === "local" ? "local" : "system";
|
||||
return povLevel === "local" ? "local" : "system";
|
||||
}
|
||||
|
||||
function isShipVisible(mode: RenderSpaceMode, activeSystemId: string | undefined, ship: ShipSnapshot) {
|
||||
@@ -186,22 +169,11 @@ export function resolveShipWorldPosition(
|
||||
visual: ShipVisual,
|
||||
animatedLocalPosition = getAnimatedShipLocalPosition(visual),
|
||||
) {
|
||||
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||
const destinationNode = destinationNodeId ? context.world?.spatialNodes.get(destinationNodeId) : undefined;
|
||||
const originSystem = context.world?.systems.get(ship.systemId);
|
||||
const destinationSystem = destinationNode ? context.world?.systems.get(destinationNode.systemId) : undefined;
|
||||
if (originSystem && destinationSystem) {
|
||||
const origin = toDisplayGalaxyVector(originSystem.galaxyPosition);
|
||||
const destination = toDisplayGalaxyVector(destinationSystem.galaxyPosition);
|
||||
return origin.lerp(destination, THREE.MathUtils.clamp(ship.spatialState.transit?.progress ?? 0, 0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
return context.toDisplayLocalPosition(animatedLocalPosition, ship.systemId);
|
||||
// FTL ships are invisible in system scene; just return their last known local position.
|
||||
return context.toDisplayLocalPosition(animatedLocalPosition);
|
||||
}
|
||||
|
||||
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, SystemSummaryVisual>) {
|
||||
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) {
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
@@ -275,26 +247,50 @@ export function renderRecentEvents(world: WorldState | undefined, entityKind: st
|
||||
.join("<br>");
|
||||
}
|
||||
|
||||
function fmtVec(v: THREE.Vector3, digits: number) {
|
||||
return `${v.x.toFixed(digits)} ${v.y.toFixed(digits)} ${v.z.toFixed(digits)}`;
|
||||
}
|
||||
|
||||
export function updateGameStatus(params: GameStatusParams) {
|
||||
const { statusEl, summaryEl, world, activeSystemId, cameraMode, zoomLevel, selectedItems, mode } = params;
|
||||
const { statusEl, summaryEl, world, activeSystemId, cameraMode, povLevel, selectedItems, mode, galaxyAnchor, systemAnchor } = params;
|
||||
const sequence = world?.sequence ?? 0;
|
||||
const generatedAt = world?.generatedAtUtc
|
||||
? new Date(world.generatedAtUtc).toLocaleTimeString()
|
||||
: "n/a";
|
||||
const displayZoomLevel = activeSystemId ? zoomLevel : "universe";
|
||||
const activeSpace = describeActiveSpace(world, displayZoomLevel, activeSystemId, selectedItems);
|
||||
const displayPovLevel = activeSystemId ? povLevel : "galaxy";
|
||||
const activeSpace = describeActiveSpace(world, displayPovLevel, activeSystemId, selectedItems);
|
||||
const cameraModeLabel = cameraMode === "follow" ? "follow" : "map";
|
||||
|
||||
// Galaxy space: galaxyAnchor in light-years — changes only during galaxy navigation
|
||||
const galPos = galaxyAnchor
|
||||
? `gal pos: ${fmtVec(galaxyAnchor.clone().divideScalar(DISPLAY_UNITS_PER_LIGHT_YEAR), 2)} ly`
|
||||
: "";
|
||||
// System space: systemAnchor in AU — changes only during system navigation
|
||||
const sysPos = systemAnchor
|
||||
? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
|
||||
: "";
|
||||
// Local space: position relative to the focused celestial's orbital anchor in km
|
||||
const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||
const celestialAnchor = focusedCelestialId
|
||||
? world?.celestials.get(focusedCelestialId)?.orbitalAnchor
|
||||
: undefined;
|
||||
const locPos = systemAnchor && celestialAnchor
|
||||
? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
|
||||
: "";
|
||||
|
||||
statusEl.textContent = [
|
||||
`mode: ${mode}`,
|
||||
`camera: ${cameraModeLabel}`,
|
||||
`zoom: ${displayZoomLevel}`,
|
||||
`zoom: ${displayPovLevel}`,
|
||||
`space: ${activeSpace}`,
|
||||
galPos,
|
||||
sysPos,
|
||||
locPos,
|
||||
`sequence: ${sequence}`,
|
||||
`snapshot: ${generatedAt}`,
|
||||
].join("\n");
|
||||
].filter(Boolean).join("\n");
|
||||
if (summaryEl) {
|
||||
summaryEl.textContent = `${mode} | ${displayZoomLevel} | ${activeSpace}`;
|
||||
summaryEl.textContent = `${mode} | ${displayPovLevel} | ${activeSpace}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,58 +368,54 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
|
||||
return bestAnchor;
|
||||
}
|
||||
|
||||
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, nodeId?: string | null) {
|
||||
if (nodeId) {
|
||||
const spatialNode = context.world?.spatialNodes.get(nodeId);
|
||||
if (spatialNode) {
|
||||
return toThreeVector(spatialNode.localPosition);
|
||||
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null) {
|
||||
if (celestialId) {
|
||||
const celestial = context.world?.celestials.get(celestialId);
|
||||
if (celestial) {
|
||||
return toThreeVector(celestial.orbitalAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
return new THREE.Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
export function resolveBubblePosition(context: WorldOrbitalContext, bubble: LocalBubbleSnapshot | LocalBubbleDelta) {
|
||||
return resolvePointPosition(context, bubble.systemId, bubble.nodeId);
|
||||
export function computeCelestialLocalPosition(context: WorldOrbitalContext, visual: CelestialVisual, timeSeconds: number) {
|
||||
return computeCelestialLocalPositionById(context, visual.id, timeSeconds) ?? visual.orbitalAnchor.clone();
|
||||
}
|
||||
|
||||
export function computeSpatialNodeLocalPosition(context: WorldOrbitalContext, visual: SpatialNodeVisual, timeSeconds: number) {
|
||||
return computeSpatialNodeLocalPositionById(context, visual.id, timeSeconds) ?? visual.localPosition.clone();
|
||||
}
|
||||
|
||||
export function computeSpatialNodeLocalPositionById(
|
||||
export function computeCelestialLocalPositionById(
|
||||
context: WorldOrbitalContext,
|
||||
nodeId: string,
|
||||
celestialId: string,
|
||||
timeSeconds: number,
|
||||
visiting = new Set<string>(),
|
||||
): THREE.Vector3 | undefined {
|
||||
if (!context.world || visiting.has(nodeId)) {
|
||||
if (!context.world || visiting.has(celestialId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const node = context.world.spatialNodes.get(nodeId);
|
||||
if (!node) {
|
||||
const celestial = context.world.celestials.get(celestialId);
|
||||
if (!celestial) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const basePosition = toThreeVector(node.localPosition);
|
||||
if (!node.parentNodeId) {
|
||||
const basePosition = toThreeVector(celestial.orbitalAnchor);
|
||||
if (!celestial.parentNodeId) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
const parentNode = context.world.spatialNodes.get(node.parentNodeId);
|
||||
if (!parentNode) {
|
||||
const parentCelestial = context.world.celestials.get(celestial.parentNodeId);
|
||||
if (!parentCelestial) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
visiting.add(nodeId);
|
||||
const parentCurrentPosition = computeSpatialNodeLocalPositionById(context, node.parentNodeId, timeSeconds, visiting);
|
||||
visiting.delete(nodeId);
|
||||
visiting.add(celestialId);
|
||||
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting);
|
||||
visiting.delete(celestialId);
|
||||
if (!parentCurrentPosition) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
const parentInitialPosition = toThreeVector(parentNode.localPosition);
|
||||
const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor);
|
||||
const relativeOffset = basePosition.clone().sub(parentInitialPosition);
|
||||
const initialAngle = Math.atan2(parentInitialPosition.z, parentInitialPosition.x);
|
||||
const currentAngle = Math.atan2(parentCurrentPosition.z, parentCurrentPosition.x);
|
||||
@@ -431,13 +423,6 @@ export function computeSpatialNodeLocalPositionById(
|
||||
return parentCurrentPosition.clone().add(rotatedOffset);
|
||||
}
|
||||
|
||||
export function setBubbleVisualState(visual: BubbleVisual, bubble: LocalBubbleSnapshot | LocalBubbleDelta) {
|
||||
const intensity = bubble.occupantShipIds.length + bubble.occupantStationIds.length + bubble.occupantConstructionSiteIds.length;
|
||||
const material = (rawObject(visual.mesh) as THREE.LineLoop).material as THREE.LineBasicMaterial;
|
||||
material.opacity = THREE.MathUtils.clamp(0.18 + intensity * 0.05, 0.18, 0.72);
|
||||
material.color.set(intensity > 0 ? "#7fffd4" : "#6ed6ff");
|
||||
}
|
||||
|
||||
function drawCountIcon(
|
||||
context: CanvasRenderingContext2D,
|
||||
kind: SummaryIconKind,
|
||||
@@ -504,24 +489,15 @@ function getOrbitalAnchorPosition(context: WorldOrbitalContext, systemId: string
|
||||
return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds, context.worldSeed);
|
||||
}
|
||||
|
||||
function resolveBubbleAnimatedLocalPosition(context: WorldOrbitalContext, visual: BubbleVisual, timeSeconds: number) {
|
||||
const bubble = context.world?.localBubbles.get(visual.id);
|
||||
if (!bubble) {
|
||||
return visual.localPosition.clone();
|
||||
}
|
||||
|
||||
return computeSpatialNodeLocalPositionById(context, bubble.nodeId, timeSeconds) ?? visual.localPosition.clone();
|
||||
}
|
||||
|
||||
function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) {
|
||||
if (!context.world) {
|
||||
return visual.localPosition.clone();
|
||||
}
|
||||
|
||||
const station = context.world.stations.get(visual.id);
|
||||
if (!station?.nodeId) {
|
||||
if (!station?.celestialId) {
|
||||
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
|
||||
}
|
||||
|
||||
return computeSpatialNodeLocalPositionById(context, station.nodeId, timeSeconds) ?? visual.localPosition.clone();
|
||||
return computeCelestialLocalPositionById(context, station.celestialId, timeSeconds) ?? visual.localPosition.clone();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user