entangled-worlds-orbiters-shared 4.2.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/shortcuts/catalogue.d.ts +72 -0
- package/dist/shortcuts/catalogue.js +118 -0
- package/dist/shortcuts/chord.d.ts +24 -2
- package/dist/shortcuts/chord.js +86 -8
- package/dist/shortcuts/format.d.ts +7 -4
- package/dist/shortcuts/format.js +40 -14
- package/dist/shortcuts/index.d.ts +4 -8
- package/dist/shortcuts/index.js +8 -11
- package/dist/shortcuts/match.d.ts +14 -6
- package/dist/shortcuts/match.js +14 -8
- package/dist/shortcuts/source.d.ts +62 -0
- package/dist/shortcuts/source.js +139 -0
- package/dist/sky/geometry.d.ts +31 -0
- package/dist/sky/geometry.js +84 -0
- package/dist/sky/index.d.ts +6 -0
- package/dist/sky/index.js +6 -0
- package/dist/sky/params.d.ts +44 -0
- package/dist/sky/params.js +52 -0
- package/dist/sky/shaders.d.ts +14 -0
- package/dist/sky/shaders.js +149 -0
- package/dist/sky/sky.d.ts +24 -0
- package/dist/sky/sky.js +268 -0
- package/dist/sky/worldSkyFile.d.ts +16 -0
- package/dist/sky/worldSkyFile.js +51 -0
- package/package.json +8 -2
- package/src/shortcuts/catalogue.ts +172 -0
- package/src/shortcuts/chord.ts +96 -6
- package/src/shortcuts/format.ts +44 -17
- package/src/shortcuts/index.ts +31 -28
- package/src/shortcuts/match.ts +25 -13
- package/src/shortcuts/source.ts +204 -0
- package/src/sky/geometry.ts +99 -0
- package/src/sky/index.ts +16 -0
- package/src/sky/params.ts +89 -0
- package/src/sky/shaders.ts +159 -0
- package/src/sky/sky.ts +330 -0
- package/src/sky/worldSkyFile.ts +60 -0
- package/dist/shortcuts/entangledWorlds.d.ts +0 -21
- package/dist/shortcuts/entangledWorlds.js +0 -40
- package/dist/shortcuts/labels.d.ts +0 -10
- package/dist/shortcuts/labels.js +0 -130
- package/dist/shortcuts/orbiters.d.ts +0 -55
- package/dist/shortcuts/orbiters.js +0 -109
- package/dist/shortcuts/overrides.d.ts +0 -30
- package/dist/shortcuts/overrides.js +0 -81
- package/dist/shortcuts/sections.d.ts +0 -23
- package/dist/shortcuts/sections.js +0 -19
- package/dist/shortcuts/site.d.ts +0 -9
- package/dist/shortcuts/site.js +0 -25
- package/src/shortcuts/entangledWorlds.ts +0 -51
- package/src/shortcuts/labels.ts +0 -139
- package/src/shortcuts/orbiters.ts +0 -132
- package/src/shortcuts/overrides.ts +0 -110
- package/src/shortcuts/sections.ts +0 -36
- package/src/shortcuts/site.ts +0 -33
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ShortcutCatalogue } from './catalogue.js';
|
|
2
|
+
import { type ShortcutAction, type ShortcutOverrides } from './match.js';
|
|
3
|
+
/** The overrides in a stored value, or `null` when it is not a list of `{ id, keys }`. */
|
|
4
|
+
export declare function parseOverrides(value: unknown): ShortcutOverrides | null;
|
|
5
|
+
/**
|
|
6
|
+
* How the app reads the signed-in person's settings: `null` when no one is signed in (the
|
|
7
|
+
* default keys); `undefined` when the read failed (the overrides in force are kept: a blip must not
|
|
8
|
+
* swap someone's keys for the defaults).
|
|
9
|
+
*/
|
|
10
|
+
export type ReadAccountSettings = (options: {
|
|
11
|
+
fresh: boolean;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
shortcuts?: unknown;
|
|
14
|
+
} | null | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* How the app reads the catalogue (`{ version, sections }`); `undefined` or a throw when the read
|
|
17
|
+
* failed (the catalogue in force is kept). There is no copy in the app: until the first read
|
|
18
|
+
* answers, no key does anything.
|
|
19
|
+
*/
|
|
20
|
+
export type ReadShortcutCatalogue = (options: {
|
|
21
|
+
fresh: boolean;
|
|
22
|
+
}) => Promise<unknown>;
|
|
23
|
+
export interface ShortcutSource {
|
|
24
|
+
/** The catalogue in force (empty until it is read). */
|
|
25
|
+
catalogue(): ShortcutCatalogue;
|
|
26
|
+
/** The actions of these sections, the first named winning a shared key; the same list until the catalogue changes. */
|
|
27
|
+
actions(...sectionIds: string[]): readonly ShortcutAction[];
|
|
28
|
+
/** The person's overrides in force. */
|
|
29
|
+
overrides(): ShortcutOverrides;
|
|
30
|
+
/** A list of actions with their keys now (kept until the catalogue or the overrides change). */
|
|
31
|
+
bindings(actions: readonly ShortcutAction[]): Map<string, readonly string[]>;
|
|
32
|
+
/** Called whenever the catalogue or the overrides change; returns the unsubscribe. */
|
|
33
|
+
subscribe(listener: () => void): () => void;
|
|
34
|
+
/** Read both again (`fresh`: past the app's own cached read). The newest read of each wins, whatever order they end in. */
|
|
35
|
+
refresh(options?: {
|
|
36
|
+
fresh?: boolean;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/** Read now, and again whenever the page comes back to the front; once per source. */
|
|
39
|
+
bind(): void;
|
|
40
|
+
/** For tests: set the overrides directly. */
|
|
41
|
+
setOverrides(overrides: ShortcutOverrides): void;
|
|
42
|
+
/** For tests: set the catalogue directly. */
|
|
43
|
+
setCatalogue(catalogue: ShortcutCatalogue): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where a device keeps the last catalogue it read: offline (the desktop app with no connection) the
|
|
47
|
+
* keys are the ones it last had, and on every start they answer before the read comes back. A copy of
|
|
48
|
+
* what was served, never a list of its own.
|
|
49
|
+
*/
|
|
50
|
+
export interface ShortcutCatalogueStore {
|
|
51
|
+
load(): unknown;
|
|
52
|
+
save(value: unknown): void;
|
|
53
|
+
}
|
|
54
|
+
export declare const CATALOGUE_STORAGE_KEY = "ps.shortcutCatalogue";
|
|
55
|
+
/** This browser's localStorage, when there is one and it may be used; otherwise nothing is kept. */
|
|
56
|
+
export declare function localShortcutCatalogueStore(): ShortcutCatalogueStore | null;
|
|
57
|
+
export declare function createShortcutSource({ readCatalogue, readAccount, store, }: {
|
|
58
|
+
readCatalogue: ReadShortcutCatalogue;
|
|
59
|
+
readAccount: ReadAccountSettings;
|
|
60
|
+
/** Where the last catalogue read is kept (this browser's localStorage by default; `null`: nowhere). */
|
|
61
|
+
store?: ShortcutCatalogueStore | null;
|
|
62
|
+
}): ShortcutSource;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Where an app's keys come from, read the same way by every app: the catalogue (every action and its
|
|
2
|
+
// default keys, from the backend) and the person's own changes (their account's settings; someone
|
|
3
|
+
// signed out has the defaults). Both are read at start and again whenever the page comes back to the
|
|
4
|
+
// front, so a key changed in the catalogue or on the settings page applies on return, without a reload.
|
|
5
|
+
//
|
|
6
|
+
// Framework-free: the app hands in how it reads each (its own API client) and gets the catalogue, the
|
|
7
|
+
// overrides, and each list of actions' resolved keys back, kept until either changes.
|
|
8
|
+
import { EMPTY_CATALOGUE, parseCatalogue, sectionActions } from './catalogue.js';
|
|
9
|
+
import { resolveBindings } from './match.js';
|
|
10
|
+
/** The overrides in a stored value, or `null` when it is not a list of `{ id, keys }`. */
|
|
11
|
+
export function parseOverrides(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return null;
|
|
14
|
+
return value.filter((entry) => !!entry && typeof entry.id === 'string' && Array.isArray(entry.keys) && entry.keys.every((k) => typeof k === 'string'));
|
|
15
|
+
}
|
|
16
|
+
export const CATALOGUE_STORAGE_KEY = 'ps.shortcutCatalogue';
|
|
17
|
+
/** This browser's localStorage, when there is one and it may be used; otherwise nothing is kept. */
|
|
18
|
+
export function localShortcutCatalogueStore() {
|
|
19
|
+
const storage = globalThis.localStorage;
|
|
20
|
+
if (!storage)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
load() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = storage.getItem(CATALOGUE_STORAGE_KEY);
|
|
26
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
save(value) {
|
|
33
|
+
try {
|
|
34
|
+
storage.setItem(CATALOGUE_STORAGE_KEY, JSON.stringify(value));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Full or refused (a private window): the next start reads the backend first, as without a store.
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Coming back to the front twice in a row (visible, then focus) reads once. */
|
|
43
|
+
const REFRESH_GAP_MS = 2000;
|
|
44
|
+
/** Keeps the answer of the newest read that answered: an older one ending later says nothing new. */
|
|
45
|
+
function newestRead(read, apply) {
|
|
46
|
+
let latest = 0;
|
|
47
|
+
let applied = 0;
|
|
48
|
+
return async (fresh) => {
|
|
49
|
+
const ticket = ++latest;
|
|
50
|
+
let value;
|
|
51
|
+
try {
|
|
52
|
+
value = await read(fresh);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
value = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (value === undefined || ticket < applied)
|
|
58
|
+
return;
|
|
59
|
+
applied = ticket;
|
|
60
|
+
apply(value);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function createShortcutSource({ readCatalogue, readAccount, store = localShortcutCatalogueStore(), }) {
|
|
64
|
+
let catalogue = (store && parseCatalogue(store.load())) || EMPTY_CATALOGUE;
|
|
65
|
+
let overrides = [];
|
|
66
|
+
let resolved = new WeakMap();
|
|
67
|
+
let lastRefreshAt = -Infinity;
|
|
68
|
+
let bound = false;
|
|
69
|
+
const listeners = new Set();
|
|
70
|
+
const changed = () => {
|
|
71
|
+
resolved = new WeakMap();
|
|
72
|
+
listeners.forEach((listener) => listener());
|
|
73
|
+
};
|
|
74
|
+
const applyOverrides = (next) => {
|
|
75
|
+
if (JSON.stringify(next) === JSON.stringify(overrides))
|
|
76
|
+
return;
|
|
77
|
+
overrides = next;
|
|
78
|
+
changed();
|
|
79
|
+
};
|
|
80
|
+
const applyCatalogue = (next) => {
|
|
81
|
+
if (next === catalogue || (next.version && next.version === catalogue.version && next.sections.length === catalogue.sections.length))
|
|
82
|
+
return;
|
|
83
|
+
catalogue = next;
|
|
84
|
+
changed();
|
|
85
|
+
};
|
|
86
|
+
const refreshCatalogue = newestRead(async (fresh) => {
|
|
87
|
+
const value = await readCatalogue({ fresh });
|
|
88
|
+
const read = value === undefined ? null : parseCatalogue(value);
|
|
89
|
+
if (!read)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (read.version !== catalogue.version)
|
|
92
|
+
store?.save(value);
|
|
93
|
+
return read;
|
|
94
|
+
}, applyCatalogue);
|
|
95
|
+
const refreshAccount = newestRead(async (fresh) => {
|
|
96
|
+
const settings = await readAccount({ fresh });
|
|
97
|
+
return settings === undefined ? undefined : settings === null ? [] : (parseOverrides(settings.shortcuts) ?? []);
|
|
98
|
+
}, applyOverrides);
|
|
99
|
+
const refresh = async ({ fresh = false } = {}) => {
|
|
100
|
+
lastRefreshAt = Date.now();
|
|
101
|
+
await Promise.all([refreshCatalogue(fresh), refreshAccount(fresh)]);
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
catalogue: () => catalogue,
|
|
105
|
+
actions: (...sectionIds) => sectionActions(catalogue, ...sectionIds),
|
|
106
|
+
overrides: () => overrides,
|
|
107
|
+
bindings(actions) {
|
|
108
|
+
let map = resolved.get(actions);
|
|
109
|
+
if (!map) {
|
|
110
|
+
map = resolveBindings(actions, overrides);
|
|
111
|
+
resolved.set(actions, map);
|
|
112
|
+
}
|
|
113
|
+
return map;
|
|
114
|
+
},
|
|
115
|
+
subscribe(listener) {
|
|
116
|
+
listeners.add(listener);
|
|
117
|
+
return () => {
|
|
118
|
+
listeners.delete(listener);
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
refresh,
|
|
122
|
+
bind() {
|
|
123
|
+
const win = globalThis;
|
|
124
|
+
if (bound || typeof win.addEventListener !== 'function' || !win.document?.addEventListener)
|
|
125
|
+
return;
|
|
126
|
+
bound = true;
|
|
127
|
+
void refresh();
|
|
128
|
+
const onFront = () => {
|
|
129
|
+
if (win.document?.visibilityState !== 'visible' || Date.now() - lastRefreshAt < REFRESH_GAP_MS)
|
|
130
|
+
return;
|
|
131
|
+
void refresh({ fresh: true });
|
|
132
|
+
};
|
|
133
|
+
win.document.addEventListener('visibilitychange', onFront);
|
|
134
|
+
win.addEventListener('focus', onFront);
|
|
135
|
+
},
|
|
136
|
+
setOverrides: applyOverrides,
|
|
137
|
+
setCatalogue: applyCatalogue,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { WorldSky } from './worldSkyFile.js';
|
|
3
|
+
/** The band's frame: `basis` maps band space (x toward the core, y the band's pole) to the scene. */
|
|
4
|
+
export interface BandFrame {
|
|
5
|
+
basis: THREE.Matrix4;
|
|
6
|
+
core: THREE.Vector3;
|
|
7
|
+
}
|
|
8
|
+
/** An equatorial direction in the scene frame (y = celestial north). */
|
|
9
|
+
export declare function equatorial(raDeg: number, decDeg: number, out?: THREE.Vector3): THREE.Vector3;
|
|
10
|
+
/** A seeded band, tilted `tiltDeg` from the equator, its core at a seeded longitude. */
|
|
11
|
+
export declare function seededBandFrame(seed: number, tiltDeg: number): BandFrame;
|
|
12
|
+
/**
|
|
13
|
+
* The Milky Way: the galactic plane (J2000 north pole and centre). From every catalogued host it
|
|
14
|
+
* is the same plane to well under a degree, so it holds for every world sky.
|
|
15
|
+
*/
|
|
16
|
+
export declare function galacticBandFrame(): BandFrame;
|
|
17
|
+
/**
|
|
18
|
+
* The star buffer, exactly `count` stars (the device's star budget). A world sky first: its
|
|
19
|
+
* catalogue stars copied as they are (brightest first, so a small budget keeps the brightest), then
|
|
20
|
+
* its stand-ins, then seeded faint fill; a seeded sky is `count` seeded stars. Stars gather into the
|
|
21
|
+
* band by `band`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildStarGeometry(opts: {
|
|
24
|
+
seed: number;
|
|
25
|
+
count: number;
|
|
26
|
+
band: number;
|
|
27
|
+
frame: BandFrame;
|
|
28
|
+
world?: WorldSky | null;
|
|
29
|
+
}): THREE.BufferGeometry;
|
|
30
|
+
/** The constellation figures of a world sky, straight from the file. */
|
|
31
|
+
export declare function buildLineGeometry(world: WorldSky): THREE.BufferGeometry;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Star and line buffers. One layout for every sky: direction `Int16 × 3` (normalised) + colour and
|
|
2
|
+
// brightness `Uint8 × 4` (normalised) = 10 bytes a star, the world-sky file's own layout, so a world
|
|
3
|
+
// sky's catalogue stars are copied in as they are and only the seeded stars are worked out here.
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { blackbodyColor } from '../star/blackbody.js';
|
|
6
|
+
import { createLcg } from '../star/layout.js';
|
|
7
|
+
const INT16 = 32767;
|
|
8
|
+
/** An equatorial direction in the scene frame (y = celestial north). */
|
|
9
|
+
export function equatorial(raDeg, decDeg, out = new THREE.Vector3()) {
|
|
10
|
+
const a = THREE.MathUtils.degToRad(raDeg);
|
|
11
|
+
const d = THREE.MathUtils.degToRad(decDeg);
|
|
12
|
+
return out.set(Math.cos(d) * Math.cos(a), Math.sin(d), -Math.cos(d) * Math.sin(a));
|
|
13
|
+
}
|
|
14
|
+
/** A seeded band, tilted `tiltDeg` from the equator, its core at a seeded longitude. */
|
|
15
|
+
export function seededBandFrame(seed, tiltDeg) {
|
|
16
|
+
const basis = new THREE.Matrix4().makeRotationX(THREE.MathUtils.degToRad(tiltDeg));
|
|
17
|
+
const lon = createLcg(seed ^ 0x5bd1e995)() * Math.PI * 2;
|
|
18
|
+
basis.multiply(new THREE.Matrix4().makeRotationY(-lon));
|
|
19
|
+
return { basis, core: new THREE.Vector3(1, 0, 0).applyMatrix4(basis).normalize() };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The Milky Way: the galactic plane (J2000 north pole and centre). From every catalogued host it
|
|
23
|
+
* is the same plane to well under a degree, so it holds for every world sky.
|
|
24
|
+
*/
|
|
25
|
+
export function galacticBandFrame() {
|
|
26
|
+
const pole = equatorial(192.85948, 27.12825);
|
|
27
|
+
const centre = equatorial(266.40499, -28.93617);
|
|
28
|
+
const x = centre.sub(pole.clone().multiplyScalar(centre.dot(pole))).normalize();
|
|
29
|
+
const z = new THREE.Vector3().crossVectors(x, pole);
|
|
30
|
+
return { basis: new THREE.Matrix4().makeBasis(x, pole, z), core: x.clone() };
|
|
31
|
+
}
|
|
32
|
+
/** Naked-eye stand-ins reach this brightness (0..1), under the glow tier so only real stars glow; faint fill stays under the catalogue's limit. */
|
|
33
|
+
const STAND_IN_BRIGHTNESS = 0.7;
|
|
34
|
+
const FILL_BRIGHTNESS = 0.22;
|
|
35
|
+
/**
|
|
36
|
+
* The star buffer, exactly `count` stars (the device's star budget). A world sky first: its
|
|
37
|
+
* catalogue stars copied as they are (brightest first, so a small budget keeps the brightest), then
|
|
38
|
+
* its stand-ins, then seeded faint fill; a seeded sky is `count` seeded stars. Stars gather into the
|
|
39
|
+
* band by `band`.
|
|
40
|
+
*/
|
|
41
|
+
export function buildStarGeometry(opts) {
|
|
42
|
+
const { seed, band, frame, world } = opts;
|
|
43
|
+
const total = Math.max(0, opts.count);
|
|
44
|
+
const worldStars = world ? Math.min(world.starCount, total) : 0;
|
|
45
|
+
const standIns = world ? Math.min(world.standIns, total - worldStars) : 0;
|
|
46
|
+
const dir = new Int16Array(total * 3);
|
|
47
|
+
const cb = new Uint8Array(total * 4);
|
|
48
|
+
if (world) {
|
|
49
|
+
dir.set(world.directions.subarray(0, worldStars * 3));
|
|
50
|
+
cb.set(world.colourBrightness.subarray(0, worldStars * 4));
|
|
51
|
+
}
|
|
52
|
+
const rand = createLcg(seed);
|
|
53
|
+
const v = new THREE.Vector3();
|
|
54
|
+
const bandShare = 0.1 + 0.5 * band;
|
|
55
|
+
for (let i = worldStars; i < total; i++) {
|
|
56
|
+
const inBand = rand() < bandShare;
|
|
57
|
+
const lon = rand() * Math.PI * 2;
|
|
58
|
+
const lat = inBand ? (rand() + rand() + rand() - 1.5) * 0.2 : Math.asin(rand() * 2 - 1);
|
|
59
|
+
v.set(Math.cos(lat) * Math.cos(lon), Math.sin(lat), Math.cos(lat) * Math.sin(lon)).applyMatrix4(frame.basis);
|
|
60
|
+
dir[i * 3] = Math.round(v.x * INT16);
|
|
61
|
+
dir[i * 3 + 1] = Math.round(v.y * INT16);
|
|
62
|
+
dir[i * 3 + 2] = Math.round(v.z * INT16);
|
|
63
|
+
// Many faint, few bright. Under a world sky: stand-ins at naked-eye brightness, then faint fill.
|
|
64
|
+
const scale = !world ? 1 : i < worldStars + standIns ? STAND_IN_BRIGHTNESS : FILL_BRIGHTNESS;
|
|
65
|
+
const c = blackbodyColor(2800 + Math.pow(rand(), 2.2) * 12000);
|
|
66
|
+
cb[i * 4] = Math.round(c.r * 255);
|
|
67
|
+
cb[i * 4 + 1] = Math.round(c.g * 255);
|
|
68
|
+
cb[i * 4 + 2] = Math.round(c.b * 255);
|
|
69
|
+
cb[i * 4 + 3] = Math.round(Math.pow(rand(), 4) * scale * 255);
|
|
70
|
+
}
|
|
71
|
+
const geo = new THREE.BufferGeometry();
|
|
72
|
+
geo.setAttribute('position', new THREE.BufferAttribute(dir, 3, true));
|
|
73
|
+
geo.setAttribute('aColourBrightness', new THREE.BufferAttribute(cb, 4, true));
|
|
74
|
+
// Stars sit at infinity in every direction: never frustum-cull them.
|
|
75
|
+
geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity);
|
|
76
|
+
return geo;
|
|
77
|
+
}
|
|
78
|
+
/** The constellation figures of a world sky, straight from the file. */
|
|
79
|
+
export function buildLineGeometry(world) {
|
|
80
|
+
const geo = new THREE.BufferGeometry();
|
|
81
|
+
geo.setAttribute('position', new THREE.BufferAttribute(world.lines, 3, true));
|
|
82
|
+
geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity);
|
|
83
|
+
return geo;
|
|
84
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createSky } from './sky.js';
|
|
2
|
+
export type { Sky, CreateSkyOptions } from './sky.js';
|
|
3
|
+
export { DEFAULT_SKY_PARAMS, SKY_PARAM_RANGES, SKY_LOOK_KEYS, SKY_SHAPE_KEYS, normalizeSkyParams, } from './params.js';
|
|
4
|
+
export type { SkyParams, SkySource } from './params.js';
|
|
5
|
+
export { parseWorldSky, WORLD_SKY_MAGIC, WORLD_SKY_FORMAT } from './worldSkyFile.js';
|
|
6
|
+
export type { WorldSky } from './worldSkyFile.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// ./sky — the sky behind a world: a parametric star field (seeded, or the world's own sky as seen
|
|
2
|
+
// from its host star, read from the backend's precomputed file), an optional nebula + Milky Way
|
|
3
|
+
// baked once per change, and constellation figures. Replaces the six-face painted skycube.
|
|
4
|
+
export { createSky } from './sky.js';
|
|
5
|
+
export { DEFAULT_SKY_PARAMS, SKY_PARAM_RANGES, SKY_LOOK_KEYS, SKY_SHAPE_KEYS, normalizeSkyParams, } from './params.js';
|
|
6
|
+
export { parseWorldSky, WORLD_SKY_MAGIC, WORLD_SKY_FORMAT } from './worldSkyFile.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Where the stars come from: a seed, or the world's own sky (the backend's file for its host star). */
|
|
2
|
+
export type SkySource = 'seeded' | 'world';
|
|
3
|
+
export interface SkyParams {
|
|
4
|
+
source: SkySource;
|
|
5
|
+
/** Same seed → same sky. Also seeds the stand-in stars and the nebula under a world sky. */
|
|
6
|
+
seed: number;
|
|
7
|
+
/** Stars drawn: the device's star budget. A world sky spends it on its catalogue stars first
|
|
8
|
+
* (brightest first), then its stand-ins, then faint fill. */
|
|
9
|
+
count: number;
|
|
10
|
+
/** Star brightness multiplier. */
|
|
11
|
+
brightness: number;
|
|
12
|
+
/** Star size in pixels at a 1080-pixel-tall frame, for the brightest ordinary star. */
|
|
13
|
+
size: number;
|
|
14
|
+
/** 0 = every star white, 1 = full blackbody colour. */
|
|
15
|
+
colour: number;
|
|
16
|
+
/** 0 = steady, 1 = full twinkle. */
|
|
17
|
+
twinkle: number;
|
|
18
|
+
/** Nebula amount, 0 = no nebula layer at all (the cheapest sky). */
|
|
19
|
+
nebula: number;
|
|
20
|
+
/** Nebula hue, degrees. */
|
|
21
|
+
hue: number;
|
|
22
|
+
/** How far the nebula's hue wanders around `hue`, degrees. */
|
|
23
|
+
hueSpread: number;
|
|
24
|
+
/** How much stars and clouds gather into a Milky Way band, 0..1. */
|
|
25
|
+
band: number;
|
|
26
|
+
/** Band tilt, degrees. Seeded skies only: a world sky's band is the galactic plane. */
|
|
27
|
+
bandTilt: number;
|
|
28
|
+
/** Constellation figures' strength, 0 = hidden. World skies only. */
|
|
29
|
+
constellations: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const DEFAULT_SKY_PARAMS: Readonly<SkyParams>;
|
|
32
|
+
type NumericKey = Exclude<keyof SkyParams, 'source'>;
|
|
33
|
+
/** Valid range of every numeric param, for UIs and for `normalizeSkyParams`. */
|
|
34
|
+
export declare const SKY_PARAM_RANGES: Readonly<Record<NumericKey, {
|
|
35
|
+
min: number;
|
|
36
|
+
max: number;
|
|
37
|
+
}>>;
|
|
38
|
+
/** Params that only change uniforms: no rebuild, no re-bake. */
|
|
39
|
+
export declare const SKY_LOOK_KEYS: readonly ["brightness", "size", "colour", "twinkle", "constellations", "nebula", "hue", "hueSpread"];
|
|
40
|
+
/** Params that rebuild the star buffer (at most once a frame); all but `count` also re-bake the glow's shape. */
|
|
41
|
+
export declare const SKY_SHAPE_KEYS: readonly ["source", "seed", "count", "band", "bandTilt"];
|
|
42
|
+
/** A complete, in-range set from anything stored: missing or invalid fields take the default. */
|
|
43
|
+
export declare function normalizeSkyParams(input: unknown): SkyParams;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// What a world sets on its sky. Plain data, so it persists as-is; `normalizeSkyParams` is the one
|
|
2
|
+
// boundary that turns whatever was stored (or nothing) into a complete, in-range set.
|
|
3
|
+
export const DEFAULT_SKY_PARAMS = Object.freeze({
|
|
4
|
+
source: 'seeded',
|
|
5
|
+
seed: 1,
|
|
6
|
+
count: 40000,
|
|
7
|
+
brightness: 1,
|
|
8
|
+
size: 3,
|
|
9
|
+
colour: 0.7,
|
|
10
|
+
twinkle: 0.4,
|
|
11
|
+
nebula: 0.6,
|
|
12
|
+
hue: 255,
|
|
13
|
+
hueSpread: 50,
|
|
14
|
+
band: 0.6,
|
|
15
|
+
bandTilt: 60,
|
|
16
|
+
constellations: 0,
|
|
17
|
+
});
|
|
18
|
+
/** Valid range of every numeric param, for UIs and for `normalizeSkyParams`. */
|
|
19
|
+
export const SKY_PARAM_RANGES = Object.freeze({
|
|
20
|
+
seed: { min: 0, max: 2 ** 31 - 1 },
|
|
21
|
+
count: { min: 0, max: 100000 },
|
|
22
|
+
brightness: { min: 0, max: 3 },
|
|
23
|
+
size: { min: 0.5, max: 5 },
|
|
24
|
+
colour: { min: 0, max: 1 },
|
|
25
|
+
twinkle: { min: 0, max: 1 },
|
|
26
|
+
nebula: { min: 0, max: 1 },
|
|
27
|
+
hue: { min: 0, max: 360 },
|
|
28
|
+
hueSpread: { min: 0, max: 180 },
|
|
29
|
+
band: { min: 0, max: 1 },
|
|
30
|
+
bandTilt: { min: 0, max: 180 },
|
|
31
|
+
constellations: { min: 0, max: 1 },
|
|
32
|
+
});
|
|
33
|
+
/** Params that only change uniforms: no rebuild, no re-bake. */
|
|
34
|
+
export const SKY_LOOK_KEYS = ['brightness', 'size', 'colour', 'twinkle', 'constellations', 'nebula', 'hue', 'hueSpread'];
|
|
35
|
+
/** Params that rebuild the star buffer (at most once a frame); all but `count` also re-bake the glow's shape. */
|
|
36
|
+
export const SKY_SHAPE_KEYS = ['source', 'seed', 'count', 'band', 'bandTilt'];
|
|
37
|
+
/** A complete, in-range set from anything stored: missing or invalid fields take the default. */
|
|
38
|
+
export function normalizeSkyParams(input) {
|
|
39
|
+
const raw = (input && typeof input === 'object' ? input : {});
|
|
40
|
+
const out = { ...DEFAULT_SKY_PARAMS };
|
|
41
|
+
out.source = raw.source === 'world' ? 'world' : 'seeded';
|
|
42
|
+
for (const key of Object.keys(SKY_PARAM_RANGES)) {
|
|
43
|
+
const v = raw[key];
|
|
44
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
45
|
+
continue;
|
|
46
|
+
const { min, max } = SKY_PARAM_RANGES[key];
|
|
47
|
+
out[key] = Math.min(max, Math.max(min, v));
|
|
48
|
+
}
|
|
49
|
+
out.seed = Math.round(out.seed);
|
|
50
|
+
out.count = Math.round(out.count);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Ordinary stars are never drawn bigger than this (pixels at a 1080-pixel-tall frame); glow stars 3.2×. */
|
|
2
|
+
export declare const MAX_STAR_SIZE = 5;
|
|
3
|
+
/** Brightness (0..1) above which a star joins the glow tier: ~1% of a seeded sky; on a world sky,
|
|
4
|
+
* every catalogue star brighter than apparent magnitude ~1. */
|
|
5
|
+
export declare const GLOW_SEEDED = 0.965;
|
|
6
|
+
export declare const GLOW_WORLD = 0.73;
|
|
7
|
+
export declare const STAR_VERTEX: string;
|
|
8
|
+
export declare const STAR_FRAGMENT = "\n varying vec3 vColour;\n varying float vAlpha;\n varying float vGlow;\n void main() {\n vec2 d = gl_PointCoord - 0.5;\n float r2 = dot(d, d) * 4.0;\n // Ordinary star: a soft core. Glow star: a tighter core plus a wide halo and a thin cross, so a\n // handful of stars read as bright without a bloom pass.\n float core = exp(-r2 * mix(3.5, 11.0, vGlow));\n float halo = vGlow * (0.4 * exp(-r2 * 3.0) + 0.3 * exp(-abs(d.x * d.y) * 700.0) * (1.0 - r2));\n // No discard (it defeats early depth on tile GPUs); additive black adds nothing.\n gl_FragColor = vec4(vColour * (core + max(halo, 0.0)) * vAlpha, 1.0);\n }\n";
|
|
9
|
+
export declare const NEBULA_VERTEX = "\n uniform float uFar;\n varying vec3 vDir;\n void main() {\n vDir = position;\n gl_Position = projectionMatrix * vec4(mat3(modelViewMatrix) * position * uFar * 0.5, 1.0);\n }\n";
|
|
10
|
+
export declare const NEBULA_FRAGMENT = "\n varying vec3 vDir;\n uniform samplerCube uNebula;\n uniform float uAmount;\n uniform float uHue;\n uniform float uSpread;\n vec3 hsv(float h, float s, float v) {\n vec3 k = clamp(abs(mod(h / 60.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return v * mix(vec3(1.0), k, s);\n }\n void main() {\n vec3 f = textureCube(uNebula, vDir).rgb;\n vec3 c = hsv(uHue + uSpread * (f.g - 0.5) * 3.0, 0.7 - 0.3 * f.b, 1.0) * f.r * uAmount;\n // Under a pixel of dither (interleaved gradient noise): the 8-bit screen would otherwise band\n // the glow's last faint steps into black.\n float dither = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5;\n gl_FragColor = vec4(max(c + dither / 255.0, 0.0), 1.0);\n }\n";
|
|
11
|
+
export declare const LINE_VERTEX = "\n uniform float uFar;\n void main() {\n gl_Position = projectionMatrix * vec4(mat3(modelViewMatrix) * normalize(position) * uFar * 0.9, 1.0);\n }\n";
|
|
12
|
+
export declare const LINE_FRAGMENT = "\n uniform float uLines;\n void main() { gl_FragColor = vec4(vec3(0.45, 0.6, 0.9) * uLines * 0.35, 1.0); }\n";
|
|
13
|
+
export declare const BAKE_VERTEX = "\n varying vec3 vDir;\n void main() {\n vDir = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n";
|
|
14
|
+
export declare const BAKE_FRAGMENT = "\n varying vec3 vDir;\n uniform float uSeed;\n uniform float uBand;\n uniform mat3 uToBand;\n uniform vec3 uCoreDir;\n float hash(vec3 p) {\n p = fract(p * 0.3183099 + vec3(0.71, 0.113, 0.419) * (1.0 + uSeed * 0.137));\n p *= 17.0;\n return fract(p.x * p.y * p.z * (p.x + p.y + p.z));\n }\n float noise(vec3 x) {\n vec3 i = floor(x);\n vec3 f = fract(x);\n f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),\n mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y), f.z);\n }\n float fbm(vec3 p) {\n float a = 0.5, s = 0.0;\n for (int i = 0; i < 7; i++) { s += a * noise(p); p = p * 2.03 + vec3(1.7, 9.2, 4.1); a *= 0.5; }\n return s;\n }\n void main() {\n vec3 d = normalize(vDir);\n vec3 q = d * 2.2 + vec3(uSeed * 3.17, uSeed * 1.31, uSeed * 2.07);\n vec3 w = vec3(fbm(q), fbm(q + vec3(5.2, 1.3, 2.8)), fbm(q + vec3(9.1, 4.7, 7.3)));\n float n = fbm(q + 1.9 * w);\n float lat = (uToBand * d).y;\n float bandMask = exp(-lat * lat / 0.06);\n float density = smoothstep(0.42, 0.9, n) * mix(1.0, 0.25 + 1.6 * bandMask, uBand);\n float core = exp(-(1.0 - dot(d, uCoreDir)) * 5.0) * bandMask * uBand;\n density *= 1.0 + 1.8 * core;\n float dust = smoothstep(0.5, 0.75, fbm(q * 2.7 + w * 1.3 + 3.0));\n density *= 1.0 - 0.75 * dust * bandMask * uBand;\n density += 0.12 * uBand * bandMask * (0.6 + 0.4 * n);\n gl_FragColor = vec4(density * 0.42, w.x, bandMask * uBand, 1.0);\n }\n";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The sky's shaders. Only the nebula bake runs noise, and only once per change; every per-frame
|
|
2
|
+
// shader is a handful of instructions.
|
|
3
|
+
/** Ordinary stars are never drawn bigger than this (pixels at a 1080-pixel-tall frame); glow stars 3.2×. */
|
|
4
|
+
export const MAX_STAR_SIZE = 5;
|
|
5
|
+
/** Brightness (0..1) above which a star joins the glow tier: ~1% of a seeded sky; on a world sky,
|
|
6
|
+
* every catalogue star brighter than apparent magnitude ~1. */
|
|
7
|
+
export const GLOW_SEEDED = 0.965;
|
|
8
|
+
export const GLOW_WORLD = 0.73;
|
|
9
|
+
export const STAR_VERTEX = /* glsl */ `
|
|
10
|
+
attribute vec4 aColourBrightness;
|
|
11
|
+
uniform float uTime;
|
|
12
|
+
uniform float uBrightness;
|
|
13
|
+
uniform float uSize;
|
|
14
|
+
uniform float uColour;
|
|
15
|
+
uniform float uTwinkle;
|
|
16
|
+
uniform float uPixelScale;
|
|
17
|
+
uniform float uGlowAt;
|
|
18
|
+
varying vec3 vColour;
|
|
19
|
+
varying float vAlpha;
|
|
20
|
+
varying float vGlow;
|
|
21
|
+
float hash(float n) { return fract(sin(n) * 43758.5453); }
|
|
22
|
+
void main() {
|
|
23
|
+
// Rotation only: the sky is at infinity. xyww pins depth to the far plane, so bodies in front
|
|
24
|
+
// hide the stars behind them before their fragments run. (Points cannot streak at w = 0.)
|
|
25
|
+
vec4 p = projectionMatrix * vec4(mat3(modelViewMatrix) * normalize(position), 1.0);
|
|
26
|
+
gl_Position = p.xyww;
|
|
27
|
+
float mag = aColourBrightness.a;
|
|
28
|
+
float h = hash(float(gl_VertexID) * 1.618);
|
|
29
|
+
// Twinkle reads the playback clock, so a recording is the same every time.
|
|
30
|
+
float tw = 1.0 + uTwinkle * 0.45 * sin(uTime * (1.3 + 3.0 * h) + h * 6.2831) * (0.4 + 0.6 * (1.0 - mag));
|
|
31
|
+
vGlow = step(uGlowAt, mag);
|
|
32
|
+
float px = (0.8 + (uSize - 0.8) * pow(mag, 0.6)) * uPixelScale;
|
|
33
|
+
px = min(px, ${MAX_STAR_SIZE.toFixed(1)} * uPixelScale);
|
|
34
|
+
// Glow stars: a bigger sprite whose rim is the halo; the core stays star-sized.
|
|
35
|
+
px *= 1.0 + 2.2 * vGlow;
|
|
36
|
+
// Sub-pixel stars: draw one pixel and fade, so energy stays and nothing shimmers.
|
|
37
|
+
float fade = clamp(px, 0.0, 1.0);
|
|
38
|
+
gl_PointSize = max(px, 1.0);
|
|
39
|
+
float lum = dot(aColourBrightness.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
40
|
+
vColour = mix(vec3(lum), aColourBrightness.rgb, uColour) / max(lum, 0.2);
|
|
41
|
+
vAlpha = uBrightness * fade * tw * (0.55 + 0.45 * mag);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
export const STAR_FRAGMENT = /* glsl */ `
|
|
45
|
+
varying vec3 vColour;
|
|
46
|
+
varying float vAlpha;
|
|
47
|
+
varying float vGlow;
|
|
48
|
+
void main() {
|
|
49
|
+
vec2 d = gl_PointCoord - 0.5;
|
|
50
|
+
float r2 = dot(d, d) * 4.0;
|
|
51
|
+
// Ordinary star: a soft core. Glow star: a tighter core plus a wide halo and a thin cross, so a
|
|
52
|
+
// handful of stars read as bright without a bloom pass.
|
|
53
|
+
float core = exp(-r2 * mix(3.5, 11.0, vGlow));
|
|
54
|
+
float halo = vGlow * (0.4 * exp(-r2 * 3.0) + 0.3 * exp(-abs(d.x * d.y) * 700.0) * (1.0 - r2));
|
|
55
|
+
// No discard (it defeats early depth on tile GPUs); additive black adds nothing.
|
|
56
|
+
gl_FragColor = vec4(vColour * (core + max(halo, 0.0)) * vAlpha, 1.0);
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
// Lines and the nebula box sit at a real radius just inside the far plane, not at xyww: pinning z to
|
|
60
|
+
// w breaks clipping at w = 0, and an edge that crosses behind the camera then streaks across the
|
|
61
|
+
// frame. The box's corners are √3 out, hence 0.5 of the far plane.
|
|
62
|
+
export const NEBULA_VERTEX = /* glsl */ `
|
|
63
|
+
uniform float uFar;
|
|
64
|
+
varying vec3 vDir;
|
|
65
|
+
void main() {
|
|
66
|
+
vDir = position;
|
|
67
|
+
gl_Position = projectionMatrix * vec4(mat3(modelViewMatrix) * position * uFar * 0.5, 1.0);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
// Colours the baked glow field: R its strength, G where its hue wanders, B how much it is the
|
|
71
|
+
// band. Glow, hue and hue spread are uniforms here, so dragging them never re-bakes.
|
|
72
|
+
export const NEBULA_FRAGMENT = /* glsl */ `
|
|
73
|
+
varying vec3 vDir;
|
|
74
|
+
uniform samplerCube uNebula;
|
|
75
|
+
uniform float uAmount;
|
|
76
|
+
uniform float uHue;
|
|
77
|
+
uniform float uSpread;
|
|
78
|
+
vec3 hsv(float h, float s, float v) {
|
|
79
|
+
vec3 k = clamp(abs(mod(h / 60.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
|
|
80
|
+
return v * mix(vec3(1.0), k, s);
|
|
81
|
+
}
|
|
82
|
+
void main() {
|
|
83
|
+
vec3 f = textureCube(uNebula, vDir).rgb;
|
|
84
|
+
vec3 c = hsv(uHue + uSpread * (f.g - 0.5) * 3.0, 0.7 - 0.3 * f.b, 1.0) * f.r * uAmount;
|
|
85
|
+
// Under a pixel of dither (interleaved gradient noise): the 8-bit screen would otherwise band
|
|
86
|
+
// the glow's last faint steps into black.
|
|
87
|
+
float dither = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5;
|
|
88
|
+
gl_FragColor = vec4(max(c + dither / 255.0, 0.0), 1.0);
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
export const LINE_VERTEX = /* glsl */ `
|
|
92
|
+
uniform float uFar;
|
|
93
|
+
void main() {
|
|
94
|
+
gl_Position = projectionMatrix * vec4(mat3(modelViewMatrix) * normalize(position) * uFar * 0.9, 1.0);
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
export const LINE_FRAGMENT = /* glsl */ `
|
|
98
|
+
uniform float uLines;
|
|
99
|
+
void main() { gl_FragColor = vec4(vec3(0.45, 0.6, 0.9) * uLines * 0.35, 1.0); }
|
|
100
|
+
`;
|
|
101
|
+
export const BAKE_VERTEX = /* glsl */ `
|
|
102
|
+
varying vec3 vDir;
|
|
103
|
+
void main() {
|
|
104
|
+
vDir = position;
|
|
105
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
// Runs once per bake over the cube's texels: domain-warped fbm clouds, gathered into the band,
|
|
109
|
+
// brighter toward its core, cut by dust lanes. It bakes the glow's shape only (strength, hue
|
|
110
|
+
// driver, band share); NEBULA_FRAGMENT colours it.
|
|
111
|
+
export const BAKE_FRAGMENT = /* glsl */ `
|
|
112
|
+
varying vec3 vDir;
|
|
113
|
+
uniform float uSeed;
|
|
114
|
+
uniform float uBand;
|
|
115
|
+
uniform mat3 uToBand;
|
|
116
|
+
uniform vec3 uCoreDir;
|
|
117
|
+
float hash(vec3 p) {
|
|
118
|
+
p = fract(p * 0.3183099 + vec3(0.71, 0.113, 0.419) * (1.0 + uSeed * 0.137));
|
|
119
|
+
p *= 17.0;
|
|
120
|
+
return fract(p.x * p.y * p.z * (p.x + p.y + p.z));
|
|
121
|
+
}
|
|
122
|
+
float noise(vec3 x) {
|
|
123
|
+
vec3 i = floor(x);
|
|
124
|
+
vec3 f = fract(x);
|
|
125
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
126
|
+
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
|
|
127
|
+
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y), f.z);
|
|
128
|
+
}
|
|
129
|
+
float fbm(vec3 p) {
|
|
130
|
+
float a = 0.5, s = 0.0;
|
|
131
|
+
for (int i = 0; i < 7; i++) { s += a * noise(p); p = p * 2.03 + vec3(1.7, 9.2, 4.1); a *= 0.5; }
|
|
132
|
+
return s;
|
|
133
|
+
}
|
|
134
|
+
void main() {
|
|
135
|
+
vec3 d = normalize(vDir);
|
|
136
|
+
vec3 q = d * 2.2 + vec3(uSeed * 3.17, uSeed * 1.31, uSeed * 2.07);
|
|
137
|
+
vec3 w = vec3(fbm(q), fbm(q + vec3(5.2, 1.3, 2.8)), fbm(q + vec3(9.1, 4.7, 7.3)));
|
|
138
|
+
float n = fbm(q + 1.9 * w);
|
|
139
|
+
float lat = (uToBand * d).y;
|
|
140
|
+
float bandMask = exp(-lat * lat / 0.06);
|
|
141
|
+
float density = smoothstep(0.42, 0.9, n) * mix(1.0, 0.25 + 1.6 * bandMask, uBand);
|
|
142
|
+
float core = exp(-(1.0 - dot(d, uCoreDir)) * 5.0) * bandMask * uBand;
|
|
143
|
+
density *= 1.0 + 1.8 * core;
|
|
144
|
+
float dust = smoothstep(0.5, 0.75, fbm(q * 2.7 + w * 1.3 + 3.0));
|
|
145
|
+
density *= 1.0 - 0.75 * dust * bandMask * uBand;
|
|
146
|
+
density += 0.12 * uBand * bandMask * (0.6 + 0.4 * n);
|
|
147
|
+
gl_FragColor = vec4(density * 0.42, w.x, bandMask * uBand, 1.0);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type SkyParams } from './params.js';
|
|
3
|
+
import type { WorldSky } from './worldSkyFile.js';
|
|
4
|
+
export interface CreateSkyOptions {
|
|
5
|
+
/** The world's sky (`parseWorldSky`), used when `params.source` is `'world'`. */
|
|
6
|
+
world?: WorldSky | null;
|
|
7
|
+
/** Nebula cube face size: 512 on desktops, 256 on phones (the clouds are soft either way). */
|
|
8
|
+
nebulaSize?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface Sky {
|
|
11
|
+
/** Add to the scene. Rotate it to turn the sky. */
|
|
12
|
+
readonly object: THREE.Group;
|
|
13
|
+
/** Applies a partial change: uniforms only for look params, a re-bake for nebula params, a rebuild for shape params. */
|
|
14
|
+
setParams(next: Partial<SkyParams>): void;
|
|
15
|
+
/** Swaps the world sky (a new host, or none). Rebuilds when the source is `'world'`. */
|
|
16
|
+
setWorld(world: WorldSky | null): void;
|
|
17
|
+
/** Call once a frame before rendering: clock time in seconds (the playback clock) and the drawing
|
|
18
|
+
* buffer's height in pixels. Runs a pending nebula bake. */
|
|
19
|
+
update(timeSec: number, drawingBufferHeight: number): void;
|
|
20
|
+
/** Bytes the sky holds on the GPU. */
|
|
21
|
+
gpuBytes(): number;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createSky(renderer: THREE.WebGLRenderer, initial?: Partial<SkyParams>, options?: CreateSkyOptions): Sky;
|