feat: improved visualisation and x4 data import

This commit is contained in:
2026-03-18 20:58:17 -04:00
parent 358122a74a
commit f98c47a8a7
45 changed files with 32840 additions and 1482 deletions

View File

@@ -1,5 +1,6 @@
import * as THREE from "three";
import {
ACTIVE_SYSTEM_DETAIL_SCALE,
MOON_RENDER_SCALE,
PLANET_RENDER_SCALE,
STAR_RENDER_SCALE,
@@ -8,6 +9,7 @@ import type {
CelestialSnapshot,
ClaimSnapshot,
ConstructionSiteSnapshot,
MoonSnapshot,
PlanetSnapshot,
ResourceNodeSnapshot,
ShipSnapshot,
@@ -17,10 +19,9 @@ import type {
import type { MoonVisual } from "./viewerTypes";
import {
celestialRenderRadius,
computeMoonOrbitRadius,
computeMoonLocalPosition,
computeMoonRenderRadius,
computePlanetLocalPosition,
scaleLocalScalar,
scaleLocalVector,
starHaloOpacity,
toThreeVector,
@@ -84,45 +85,34 @@ export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): Scen
export function createStarCluster(system: SystemSnapshot): SceneNode {
const root = new THREE.Group();
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62);
const offsets = system.starCount > 1
? [new THREE.Vector3(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)]
: [new THREE.Vector3(0, 0, 0)];
for (const [index, offset] of offsets.entries()) {
const sizeScale = index === 0 ? 1 : 0.72;
const star = new THREE.Mesh(
new THREE.SphereGeometry(renderedStarSize * sizeScale, 24, 24),
new THREE.MeshBasicMaterial({ color: system.starColor }),
for (const [index, star] of system.stars.entries()) {
const renderedSize = celestialRenderRadius(star.size, 0.00018, 40, 0.62);
const offset = system.stars.length > 1
? (index === 0
? new THREE.Vector3(-renderedSize * 0.55, 0, 0)
: new THREE.Vector3(renderedSize * 0.75, renderedSize * 0.08, 0))
: new THREE.Vector3(0, 0, 0);
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(renderedSize, 24, 24),
new THREE.MeshBasicMaterial({ color: star.color }),
);
const halo = new THREE.Mesh(
new THREE.SphereGeometry(renderedStarSize * sizeScale * 1.45, 20, 20),
new THREE.SphereGeometry(renderedSize * 1.45, 20, 20),
new THREE.MeshBasicMaterial({
color: system.starColor,
color: star.color,
transparent: true,
opacity: starHaloOpacity(system.starKind),
opacity: starHaloOpacity(star.kind),
side: THREE.BackSide,
}),
);
star.position.copy(offset);
mesh.position.copy(offset);
halo.position.copy(offset);
root.add(star, halo);
root.add(mesh, halo);
}
return createSceneNode(root);
}
export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
const points = Array.from({ length: 120 }, (_, index) => {
const phaseDegrees = (index / 120) * 360;
return scaleLocalVector(computePlanetLocalPosition(planet, 0, phaseDegrees));
});
return createSceneNode(new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({ color: 0x17314d, transparent: true, opacity: 0.22 }),
));
}
export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
@@ -140,41 +130,74 @@ export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
return createSceneNode(ring);
}
export function createMoonVisuals(planet: PlanetSnapshot, seed: number): MoonVisual[] {
const moonCount = Math.min(planet.moonCount, 12);
const moons: MoonVisual[] = [];
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
const orbitRadius = scaleLocalScalar(computeMoonOrbitRadius(planet, moonIndex, seed));
const orbit = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(
Array.from({ length: 48 }, (_, index) => {
const angle = (index / 48) * Math.PI * 2;
return new THREE.Vector3(
Math.cos(angle) * orbitRadius,
0,
Math.sin(angle) * orbitRadius,
);
}),
),
new THREE.LineBasicMaterial({ color: 0x3b5065, transparent: true, opacity: 0.1 }),
function createMoonOrbit(moon: MoonSnapshot): SceneNode {
const segments = 64;
const period = (2 * Math.PI) / Math.max(Math.abs(moon.orbitSpeed), 1e-6);
const points: THREE.Vector3[] = [];
for (let i = 0; i <= segments; i++) {
points.push(
scaleLocalVector(computeMoonLocalPosition(moon, (i / segments) * period))
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
);
orbit.rotation.x = THREE.MathUtils.degToRad(planet.orbitInclination * 0.35);
}
return createSceneNode(new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({
color: moon.color,
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: false,
}),
));
}
const moonSize = computeMoonRenderRadius(planet, moonIndex, seed);
export function createMoonVisuals(planet: PlanetSnapshot, documentRef: Document): MoonVisual[] {
return planet.moons.map((moon, moonIndex) => {
const moonSize = computeMoonRenderRadius(moon);
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(moonSize, 12, 12),
new THREE.MeshStandardMaterial({
color: new THREE.Color(planet.color).lerp(new THREE.Color("#d9dee7"), 0.55),
color: moon.color,
roughness: 0.96,
metalness: 0.02,
}),
);
const baseColor = new THREE.Color(moon.color);
const hsl = { h: 0, s: 0, l: 0 };
baseColor.getHSL(hsl);
const iconColor = new THREE.Color().setHSL(hsl.h, Math.max(hsl.s, 0.4), 0.72).getStyle();
const iconBaseScale = 72;
const icon = createTacticalIcon(documentRef, iconColor, iconBaseScale);
return {
systemId: "",
planetIndex: -1,
moonIndex,
mesh: createSceneNode(mesh),
icon,
iconBaseScale,
orbit: createMoonOrbit(moon),
};
});
}
moons.push({ systemId: "", planetIndex: -1, mesh: createSceneNode(mesh), orbit: createSceneNode(orbit) });
export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
const segments = 96;
const points: THREE.Vector3[] = [];
for (let i = 0; i <= segments; i++) {
const phase = (i / segments) * 360;
points.push(scaleLocalVector(computePlanetLocalPosition(planet, 0, phase)).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
}
return moons;
return createSceneNode(new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({
color: planet.color,
transparent: true,
opacity: 0.22,
depthWrite: false,
depthTest: false,
}),
));
}
export function createStationMesh(station: StationSnapshot): SceneNode {
@@ -201,32 +224,160 @@ export function createShipMesh(ship: ShipSnapshot, size: number, length: number,
return createSceneNode(mesh);
}
export function createBackdropStars(): THREE.Points {
const starCount = 1800;
const radius = 36000;
function createStarGlowTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create star glow texture");
}
const gradient = context.createRadialGradient(64, 64, 0, 64, 64, 64);
gradient.addColorStop(0, "rgba(255,255,255,1)");
gradient.addColorStop(0.14, "rgba(255,255,255,0.95)");
gradient.addColorStop(0.35, "rgba(255,255,255,0.42)");
gradient.addColorStop(0.68, "rgba(180,205,255,0.1)");
gradient.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = gradient;
context.fillRect(0, 0, 128, 128);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function createStarSparkleTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create star sparkle texture");
}
context.clearRect(0, 0, 128, 128);
context.translate(64, 64);
context.lineCap = "round";
const bloom = context.createRadialGradient(0, 0, 0, 0, 0, 48);
bloom.addColorStop(0, "rgba(255,255,255,0.95)");
bloom.addColorStop(0.3, "rgba(255,255,255,0.24)");
bloom.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = bloom;
context.beginPath();
context.arc(0, 0, 48, 0, Math.PI * 2);
context.fill();
context.strokeStyle = "rgba(255,255,255,0.75)";
context.lineWidth = 4;
context.beginPath();
context.moveTo(-38, 0);
context.lineTo(38, 0);
context.moveTo(0, -38);
context.lineTo(0, 38);
context.stroke();
context.rotate(Math.PI / 4);
context.strokeStyle = "rgba(255,255,255,0.35)";
context.lineWidth = 2;
context.beginPath();
context.moveTo(-28, 0);
context.lineTo(28, 0);
context.moveTo(0, -28);
context.lineTo(0, 28);
context.stroke();
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function createMilkyWayTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 1024;
canvas.height = 256;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create milky way texture");
}
const background = context.createLinearGradient(0, 0, 1024, 0);
background.addColorStop(0, "rgba(0,0,0,0)");
background.addColorStop(0.1, "rgba(150,110,255,0.08)");
background.addColorStop(0.32, "rgba(120,210,255,0.14)");
background.addColorStop(0.5, "rgba(255,240,220,0.28)");
background.addColorStop(0.68, "rgba(255,165,210,0.16)");
background.addColorStop(0.88, "rgba(115,155,255,0.08)");
background.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = background;
context.fillRect(0, 0, 1024, 256);
for (let index = 0; index < 220; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(78);
const radiusX = THREE.MathUtils.randFloat(40, 180);
const radiusY = THREE.MathUtils.randFloat(8, 28);
const alpha = THREE.MathUtils.randFloat(0.025, 0.09);
const hue = THREE.MathUtils.randFloat(0.52, 0.76);
const color = new THREE.Color().setHSL(hue, THREE.MathUtils.randFloat(0.25, 0.6), THREE.MathUtils.randFloat(0.72, 0.9));
const puff = context.createRadialGradient(x, y, 0, x, y, radiusX);
puff.addColorStop(0, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha})`);
puff.addColorStop(0.55, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha * 0.45})`);
puff.addColorStop(1, "rgba(0,0,0,0)");
context.save();
context.translate(x, y);
context.scale(1, radiusY / radiusX);
context.fillStyle = puff;
context.beginPath();
context.arc(0, 0, radiusX, 0, Math.PI * 2);
context.fill();
context.restore();
}
for (let index = 0; index < 540; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(54);
const alpha = THREE.MathUtils.randFloat(0.12, 0.65);
const size = THREE.MathUtils.randFloat(0.8, 2.4);
context.fillStyle = `rgba(255,255,255,${alpha})`;
context.fillRect(x, y, size, size);
}
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function sampleBackdropStarColor(): THREE.Color {
const roll = Math.random();
if (roll < 0.1) {
return new THREE.Color().setHSL(0.08, THREE.MathUtils.randFloat(0.65, 0.9), THREE.MathUtils.randFloat(0.78, 0.9));
}
if (roll < 0.28) {
return new THREE.Color().setHSL(0.58, THREE.MathUtils.randFloat(0.28, 0.55), THREE.MathUtils.randFloat(0.78, 0.9));
}
if (roll < 0.92) {
return new THREE.Color().setHSL(0.61, THREE.MathUtils.randFloat(0.08, 0.3), THREE.MathUtils.randFloat(0.84, 0.97));
}
return new THREE.Color().setHSL(0.76, THREE.MathUtils.randFloat(0.25, 0.48), THREE.MathUtils.randFloat(0.78, 0.88));
}
function createStarPointLayer(radius: number, starCount: number, size: number, opacity: number): THREE.Points {
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
const color = new THREE.Color();
for (let index = 0; index < starCount; index += 1) {
const direction = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.82, 1));
).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.83, 1));
const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.55, 1.2));
positions[index * 3] = direction.x;
positions[index * 3 + 1] = direction.y;
positions[index * 3 + 2] = direction.z;
const tint = THREE.MathUtils.randFloat(0, 1);
color.setRGB(
THREE.MathUtils.lerp(0.68, 1, tint),
THREE.MathUtils.lerp(0.76, 0.94, tint),
THREE.MathUtils.lerp(0.9, 1, tint),
);
if (Math.random() < 0.08) {
color.lerp(new THREE.Color(0xffd6a0), 0.45);
}
colors[index * 3] = color.r;
colors[index * 3 + 1] = color.g;
colors[index * 3 + 2] = color.b;
@@ -239,77 +390,244 @@ export function createBackdropStars(): THREE.Points {
return new THREE.Points(
geometry,
new THREE.PointsMaterial({
size: 2.2,
size,
sizeAttenuation: false,
vertexColors: true,
transparent: true,
opacity: 0.9,
opacity,
depthWrite: false,
blending: THREE.AdditiveBlending,
fog: false,
}),
);
}
export function createBackdropStars(documentRef: Document): THREE.Group {
const radius = 36000;
const root = new THREE.Group();
root.add(
createStarPointLayer(radius, 2800, 1.15, 0.5),
createStarPointLayer(radius, 900, 1.9, 0.85),
createStarPointLayer(radius, 240, 3.1, 0.95),
);
const glowTexture = createStarGlowTexture(documentRef);
const sparkleTexture = createStarSparkleTexture(documentRef);
for (let index = 0; index < 72; index += 1) {
const direction = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.84, 0.98));
const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.9, 1.45));
const glow = new THREE.Sprite(new THREE.SpriteMaterial({
map: glowTexture,
color,
transparent: true,
opacity: THREE.MathUtils.randFloat(0.5, 0.95),
depthWrite: false,
blending: THREE.AdditiveBlending,
fog: false,
}));
const sparkle = new THREE.Sprite(new THREE.SpriteMaterial({
map: sparkleTexture,
color: color.clone().lerp(new THREE.Color(0xffffff), 0.35),
transparent: true,
opacity: THREE.MathUtils.randFloat(0.2, 0.55),
depthWrite: false,
blending: THREE.AdditiveBlending,
fog: false,
}));
const glowScale = THREE.MathUtils.randFloat(120, 260);
glow.position.copy(direction);
glow.scale.set(glowScale, glowScale, 1);
sparkle.position.copy(direction);
sparkle.material.rotation = THREE.MathUtils.randFloat(0, Math.PI);
sparkle.scale.set(glowScale * THREE.MathUtils.randFloat(0.9, 1.4), glowScale * THREE.MathUtils.randFloat(0.9, 1.4), 1);
root.add(glow, sparkle);
}
return root;
}
export function createPlanetTexture(color: string, seed: number, documentRef: Document): THREE.CanvasTexture {
const W = 256, H = 128;
const canvas = documentRef.createElement("canvas");
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Unable to create planet texture");
const imageData = ctx.createImageData(W, H);
const base = new THREE.Color(color);
function hash(x: number, y: number): number {
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.3) * 43758.5453;
return n - Math.floor(n);
}
function smoothNoise(x: number, y: number): number {
const ix = Math.floor(x), iy = Math.floor(y);
const fx = x - ix, fy = y - iy;
const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
const a = hash(ix, iy), b = hash(ix + 1, iy);
const c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
}
function fbm(x: number, y: number): number {
let v = 0, amp = 0.5, freq = 1;
for (let i = 0; i < 5; i++) { v += smoothNoise(x * freq, y * freq) * amp; amp *= 0.5; freq *= 2; }
return v;
}
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const nx = (x / W) * 5, ny = (y / H) * 3;
const turb = fbm(nx + 0.1, ny + 0.1) - 0.5;
const band = Math.sin((y / H * 10 + turb * 3) * Math.PI);
const light = 0.62 + band * 0.38;
const idx = (y * W + x) * 4;
imageData.data[idx] = Math.min(255, base.r * 255 * light);
imageData.data[idx + 1] = Math.min(255, base.g * 255 * light);
imageData.data[idx + 2] = Math.min(255, base.b * 255 * light);
imageData.data[idx + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.needsUpdate = true;
return texture;
}
export function createNebulaTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 256;
canvas.height = 256;
canvas.width = 512;
canvas.height = 512;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create nebula texture");
}
const gradient = context.createRadialGradient(128, 128, 18, 128, 128, 118);
gradient.addColorStop(0, "rgba(255,255,255,0.95)");
gradient.addColorStop(0.2, "rgba(255,255,255,0.48)");
gradient.addColorStop(0.55, "rgba(140,180,255,0.14)");
gradient.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = gradient;
context.fillRect(0, 0, 256, 256);
const palettes = [
["rgba(80,220,255,0.24)", "rgba(120,110,255,0.18)", "rgba(255,255,255,0.14)"],
["rgba(255,130,205,0.24)", "rgba(110,170,255,0.16)", "rgba(255,240,255,0.12)"],
["rgba(120,255,205,0.2)", "rgba(100,160,255,0.18)", "rgba(255,255,255,0.1)"],
];
for (let index = 0; index < 10; index += 1) {
const x = THREE.MathUtils.randFloat(30, 226);
const y = THREE.MathUtils.randFloat(30, 226);
const radius = THREE.MathUtils.randFloat(24, 72);
const puff = context.createRadialGradient(x, y, 0, x, y, radius);
puff.addColorStop(0, "rgba(255,255,255,0.16)");
puff.addColorStop(0.45, "rgba(255,255,255,0.08)");
puff.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = puff;
context.clearRect(0, 0, 512, 512);
for (let layer = 0; layer < palettes.length; layer += 1) {
for (let index = 0; index < 18; index += 1) {
const x = THREE.MathUtils.randFloat(40, 472);
const y = THREE.MathUtils.randFloat(40, 472);
const radius = THREE.MathUtils.randFloat(55, 180);
const [core, mid, edge] = palettes[layer];
const puff = context.createRadialGradient(x, y, 0, x, y, radius);
puff.addColorStop(0, core);
puff.addColorStop(0.4, mid);
puff.addColorStop(0.78, edge);
puff.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = puff;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
}
}
for (let index = 0; index < 36; index += 1) {
const x = THREE.MathUtils.randFloat(50, 462);
const y = THREE.MathUtils.randFloat(50, 462);
const radius = THREE.MathUtils.randFloat(18, 60);
const glow = context.createRadialGradient(x, y, 0, x, y, radius);
glow.addColorStop(0, "rgba(255,255,255,0.12)");
glow.addColorStop(0.4, "rgba(255,255,255,0.05)");
glow.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = glow;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
}
// Feather the entire texture toward the borders so large sprites do not show a card-like cutoff.
const edgeFade = context.createRadialGradient(256, 256, 86, 256, 256, 256);
edgeFade.addColorStop(0, "rgba(255,255,255,1)");
edgeFade.addColorStop(0.58, "rgba(255,255,255,0.96)");
edgeFade.addColorStop(0.82, "rgba(255,255,255,0.42)");
edgeFade.addColorStop(1, "rgba(255,255,255,0)");
context.globalCompositeOperation = "destination-in";
context.fillStyle = edgeFade;
context.fillRect(0, 0, 512, 512);
context.globalCompositeOperation = "source-over";
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] {
const directions = [
new THREE.Vector3(0.74, 0.34, -0.58),
new THREE.Vector3(-0.62, 0.18, -0.77),
new THREE.Vector3(0.22, -0.44, -0.87),
new THREE.Vector3(-0.38, 0.56, 0.73),
const seeds = [
{ direction: new THREE.Vector3(0.76, 0.28, -0.58), color: "#5bd4ff", scale: 24000, opacity: 0.22, rotation: 0.18 },
{ direction: new THREE.Vector3(0.7, 0.34, -0.54), color: "#93b3ff", scale: 18000, opacity: 0.16, rotation: -0.22 },
{ direction: new THREE.Vector3(-0.58, 0.24, -0.78), color: "#ff8cc6", scale: 22000, opacity: 0.2, rotation: 0.34 },
{ direction: new THREE.Vector3(-0.48, 0.14, -0.86), color: "#8a8dff", scale: 16000, opacity: 0.14, rotation: -0.4 },
{ direction: new THREE.Vector3(0.24, -0.46, -0.85), color: "#79ffd6", scale: 20000, opacity: 0.17, rotation: 0.52 },
{ direction: new THREE.Vector3(-0.34, 0.58, 0.74), color: "#79b7ff", scale: 26000, opacity: 0.16, rotation: -0.12 },
];
return directions.map((direction, index) => {
return seeds.map((seed, index) => {
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0.14,
opacity: seed.opacity,
depthWrite: false,
color: ["#6dc7ff", "#ff9ec8", "#8e7dff", "#7ce0c3"][index] ?? "#6dc7ff",
color: seed.color,
blending: THREE.AdditiveBlending,
fog: false,
}));
sprite.position.copy(direction.normalize().multiplyScalar(25000 + index * 2600));
const scale = 15000 + index * 2400;
sprite.scale.set(scale, scale * 0.62, 1);
sprite.position.copy(seed.direction.normalize().multiplyScalar(23000 + index * 1800));
sprite.material.rotation = seed.rotation;
sprite.scale.set(seed.scale, seed.scale * THREE.MathUtils.randFloat(0.52, 0.78), 1);
return sprite;
});
}
export function createMilkyWayBand(documentRef: Document): THREE.Group {
const radius = 33800;
const texture = createMilkyWayTexture(documentRef);
const root = new THREE.Group();
const planeNormal = new THREE.Vector3(0.24, 0.92, -0.3).normalize();
const tangent = new THREE.Vector3().crossVectors(planeNormal, new THREE.Vector3(0, 0, 1));
if (tangent.lengthSq() < 1e-6) {
tangent.set(1, 0, 0);
}
tangent.normalize();
const bitangent = new THREE.Vector3().crossVectors(planeNormal, tangent).normalize();
for (let index = 0; index < 8; index += 1) {
const angle = (index / 8) * Math.PI * 2;
const direction = tangent.clone().multiplyScalar(Math.cos(angle)).add(bitangent.clone().multiplyScalar(Math.sin(angle)));
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: index % 2 === 0 ? 0.22 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
color: index % 3 === 0 ? "#ffd3f1" : index % 3 === 1 ? "#c8d8ff" : "#ffffff",
fog: false,
}));
sprite.position.copy(direction.multiplyScalar(radius));
sprite.scale.set(16500, 4300 + (index % 3) * 800, 1);
sprite.material.rotation = angle + Math.PI / 2;
root.add(sprite);
}
return root;
}
export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
const canvas = documentRef.createElement("canvas");
canvas.width = 64;
@@ -325,12 +643,6 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
context.beginPath();
context.arc(32, 32, 18, 0, Math.PI * 2);
context.stroke();
context.beginPath();
context.moveTo(32, 8);
context.lineTo(32, 56);
context.moveTo(8, 32);
context.lineTo(56, 32);
context.stroke();
const texture = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({