@johnmorrisdotca/wakusei-planets 0.0.0-stage → 0.3.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/NOTICE.md +48 -0
- package/README.md +214 -2
- package/dist/bodies/all.d.ts +16 -0
- package/dist/bodies/all.js +16 -0
- package/dist/bodies/earth.d.ts +1 -0
- package/dist/bodies/earth.js +23 -0
- package/dist/bodies/jupiter.d.ts +1 -0
- package/dist/bodies/jupiter.js +22 -0
- package/dist/bodies/mars.d.ts +1 -0
- package/dist/bodies/mars.js +25 -0
- package/dist/bodies/mercury.d.ts +1 -0
- package/dist/bodies/mercury.js +9 -0
- package/dist/bodies/moons-earth.d.ts +1 -0
- package/dist/bodies/moons-earth.js +37 -0
- package/dist/bodies/moons-jupiter.d.ts +1 -0
- package/dist/bodies/moons-jupiter.js +29 -0
- package/dist/bodies/moons-mars.d.ts +1 -0
- package/dist/bodies/moons-mars.js +4 -0
- package/dist/bodies/moons-neptune.d.ts +1 -0
- package/dist/bodies/moons-neptune.js +11 -0
- package/dist/bodies/moons-pluto.d.ts +1 -0
- package/dist/bodies/moons-pluto.js +5 -0
- package/dist/bodies/moons-saturn.d.ts +1 -0
- package/dist/bodies/moons-saturn.js +34 -0
- package/dist/bodies/moons-uranus.d.ts +1 -0
- package/dist/bodies/moons-uranus.js +7 -0
- package/dist/bodies/neptune.d.ts +1 -0
- package/dist/bodies/neptune.js +19 -0
- package/dist/bodies/pluto.d.ts +1 -0
- package/dist/bodies/pluto.js +22 -0
- package/dist/bodies/saturn.d.ts +1 -0
- package/dist/bodies/saturn.js +14 -0
- package/dist/bodies/sun.d.ts +1 -0
- package/dist/bodies/sun.js +20 -0
- package/dist/bodies/uranus.d.ts +1 -0
- package/dist/bodies/uranus.js +15 -0
- package/dist/bodies/venus.d.ts +1 -0
- package/dist/bodies/venus.js +10 -0
- package/dist/data/orbits.d.ts +20 -0
- package/dist/data/orbits.js +27 -0
- package/dist/element.d.ts +88 -0
- package/dist/element.js +603 -0
- package/dist/ephemeris.d.ts +189 -0
- package/dist/ephemeris.js +340 -0
- package/dist/facts.d.ts +213 -0
- package/dist/facts.js +198 -0
- package/dist/gate.d.ts +37 -0
- package/dist/gate.js +54 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/line.d.ts +82 -0
- package/dist/line.js +280 -0
- package/dist/live.d.ts +42 -0
- package/dist/live.js +62 -0
- package/dist/pose.d.ts +62 -0
- package/dist/pose.js +69 -0
- package/dist/scene/body.d.ts +68 -0
- package/dist/scene/body.js +337 -0
- package/dist/scene/glsl.d.ts +13 -0
- package/dist/scene/glsl.js +186 -0
- package/dist/scene/look.d.ts +106 -0
- package/dist/scene/look.js +35 -0
- package/dist/scene/recipes.d.ts +49 -0
- package/dist/scene/recipes.js +76 -0
- package/dist/scene/rings.d.ts +34 -0
- package/dist/scene/rings.js +64 -0
- package/dist/scene/shared.d.ts +24 -0
- package/dist/scene/shared.js +55 -0
- package/dist/scene/store.d.ts +19 -0
- package/dist/scene/store.js +8 -0
- package/dist/textures/callisto-1024.webp +0 -0
- package/dist/textures/callisto-512.webp +0 -0
- package/dist/textures/enceladus-1024.webp +0 -0
- package/dist/textures/enceladus-512.webp +0 -0
- package/dist/textures/europa-1024.webp +0 -0
- package/dist/textures/europa-512.webp +0 -0
- package/dist/textures/ganymede-1024.webp +0 -0
- package/dist/textures/ganymede-512.webp +0 -0
- package/dist/textures/io-1024.webp +0 -0
- package/dist/textures/io-512.webp +0 -0
- package/dist/textures/jupiter-1024.webp +0 -0
- package/dist/textures/jupiter-512.webp +0 -0
- package/dist/textures/mars-1024.webp +0 -0
- package/dist/textures/mars-512.webp +0 -0
- package/dist/textures/mercury-1024.webp +0 -0
- package/dist/textures/mercury-512.webp +0 -0
- package/dist/textures/mimas-1024.webp +0 -0
- package/dist/textures/mimas-512.webp +0 -0
- package/dist/textures/moon-1024.webp +0 -0
- package/dist/textures/moon-512.webp +0 -0
- package/dist/textures/neptune-1024.webp +0 -0
- package/dist/textures/neptune-512.webp +0 -0
- package/dist/textures/pluto-1024.webp +0 -0
- package/dist/textures/pluto-512.webp +0 -0
- package/dist/textures/rhea-1024.webp +0 -0
- package/dist/textures/rhea-512.webp +0 -0
- package/dist/textures/saturn-1024.webp +0 -0
- package/dist/textures/saturn-512.webp +0 -0
- package/dist/textures/saturn-ring-2048.webp +0 -0
- package/dist/textures/sun-1024.webp +0 -0
- package/dist/textures/sun-512.webp +0 -0
- package/dist/textures/titan-1024.webp +0 -0
- package/dist/textures/titan-512.webp +0 -0
- package/dist/textures/uranus-1024.webp +0 -0
- package/dist/textures/uranus-512.webp +0 -0
- package/dist/textures/venus-1024.webp +0 -0
- package/dist/textures/venus-512.webp +0 -0
- package/dist/vec.d.ts +19 -0
- package/dist/vec.js +32 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +236 -4
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { BODIES, ringReach } from "../facts.js";
|
|
2
|
+
import { cross } from "../vec.js";
|
|
3
|
+
import { BALL_FRAGMENT, bodyFragment, GLOW_FRAGMENT, GLOW_VERTEX, PAINTED_SURFACE, RING_FRAGMENT, RING_VERTEX, VERTEX } from "./glsl.js";
|
|
4
|
+
import { lookOf } from "./look.js";
|
|
5
|
+
import { ringStrip } from "./rings.js";
|
|
6
|
+
/** A body that has no registered look is still drawn: a lit ball in its mean colour. */
|
|
7
|
+
export function fallbackLook(facts) {
|
|
8
|
+
const c = [1, 3, 5].map((i) => (parseInt(facts.colour.slice(i, i + 2), 16) / 255).toFixed(3)).join(", ");
|
|
9
|
+
return { id: facts.id, surface: `vec4 surface(vec3 p) { float n = fbm(p * 5.0); return vec4(vec3(${c}) * (0.85 + 0.3 * n), 0.0); }` };
|
|
10
|
+
}
|
|
11
|
+
const col3 = (T, rgb) => new T.Vector3(rgb[0], rgb[1], rgb[2]);
|
|
12
|
+
const v3 = (T, v) => new T.Vector3(v[0], v[1], v[2]);
|
|
13
|
+
function makeBodyMaterial(T, look, painted) {
|
|
14
|
+
const a = look.atmosphere;
|
|
15
|
+
return new T.ShaderMaterial({
|
|
16
|
+
vertexShader: VERTEX,
|
|
17
|
+
fragmentShader: bodyFragment(painted ? PAINTED_SURFACE : look.surface, painted),
|
|
18
|
+
uniforms: {
|
|
19
|
+
uLight: { value: new T.Vector3(-0.6, 0.2, 0.77) },
|
|
20
|
+
uRelief: { value: (painted ? (look.painted?.relief ?? 0) : (look.relief ?? 0.6)) },
|
|
21
|
+
uAmbient: { value: look.ambient ?? 0.04 },
|
|
22
|
+
uLimb: { value: look.limb ?? 0 },
|
|
23
|
+
uWrap: { value: look.wrap ?? (a === undefined ? 0.04 : 0.3) },
|
|
24
|
+
uAtmos: { value: col3(T, a?.colour ?? [0, 0, 0]) },
|
|
25
|
+
uAtmosPow: { value: a?.power ?? 3 },
|
|
26
|
+
uEmissive: { value: look.emissive === true ? 1 : 0 },
|
|
27
|
+
uRingN: { value: new T.Vector3(0, 1, 0) },
|
|
28
|
+
uRingR: { value: new T.Vector2(1, 2) },
|
|
29
|
+
uRingOn: { value: 0 },
|
|
30
|
+
uRingTex: { value: null },
|
|
31
|
+
uMap: { value: null },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Set an object's matrix from three axes (columns), without letting three.js overwrite it. */
|
|
36
|
+
function setBasis(T, object, x, y, z, sx = 1, sy = 1, sz = 1) {
|
|
37
|
+
object.matrixAutoUpdate = false;
|
|
38
|
+
object.matrixWorldAutoUpdate = false;
|
|
39
|
+
object.matrix.makeBasis(new T.Vector3(x[0] * sx, x[1] * sx, x[2] * sx), new T.Vector3(y[0] * sy, y[1] * sy, y[2] * sy), new T.Vector3(z[0] * sz, z[1] * sz, z[2] * sz));
|
|
40
|
+
object.matrixWorld.copy(object.matrix);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The shared-renderer item for a body: what `renderer.view(host, item)` shows. `state()` is read when the scene is built and `apply` moves it afterwards.
|
|
44
|
+
*
|
|
45
|
+
* @param T - The three.js module (the element passes the page's own).
|
|
46
|
+
* @param options - The body, the look, the key and the callbacks.
|
|
47
|
+
* @returns The item for kazoku-ui's shared renderer.
|
|
48
|
+
*/
|
|
49
|
+
export function createBodyItem(options) {
|
|
50
|
+
const facts = BODIES[options.id];
|
|
51
|
+
return {
|
|
52
|
+
key: options.key,
|
|
53
|
+
async build({ three: T, assets, width, height }) {
|
|
54
|
+
const look = lookOf(options.id) ?? fallbackLook(facts);
|
|
55
|
+
const wantPainted = options.look === "painted" && look.painted !== undefined;
|
|
56
|
+
const geometry = assets.acquire("wk:sphere", () => new T.SphereGeometry(1, 128, 64), (g) => g.dispose());
|
|
57
|
+
const shader = assets.acquire(`wk:body:${options.id}:r`, () => makeBodyMaterial(T, look, false), (m) => m.dispose());
|
|
58
|
+
const paintedMaterial = wantPainted ? assets.acquire(`wk:body:${options.id}:p`, () => makeBodyMaterial(T, look, true), (m) => m.dispose()) : undefined;
|
|
59
|
+
const scene = new T.Scene();
|
|
60
|
+
const camera = new T.OrthographicCamera(-1, 1, 1, -1, 0.1, 400);
|
|
61
|
+
camera.position.set(0, 0, 200);
|
|
62
|
+
const mesh = new T.Mesh(geometry, shader);
|
|
63
|
+
mesh.frustumCulled = false;
|
|
64
|
+
scene.add(mesh);
|
|
65
|
+
// rings: a picture of them when the look has one (painted), else the table's strip
|
|
66
|
+
const loadImage = (url) => new Promise((resolve, reject) => {
|
|
67
|
+
const loader = new T.TextureLoader();
|
|
68
|
+
loader.setCrossOrigin("anonymous");
|
|
69
|
+
loader.load(url, (t) => resolve(t), undefined, (e) => reject(e instanceof Error ? e : new Error("texture failed")));
|
|
70
|
+
});
|
|
71
|
+
const table = ringStrip(facts, 1024);
|
|
72
|
+
const photo = wantPainted && table !== null ? look.painted?.rings : undefined;
|
|
73
|
+
let photoTexture;
|
|
74
|
+
if (photo !== undefined) {
|
|
75
|
+
try {
|
|
76
|
+
photoTexture = await loadImage(`${options.textureBase()}${photo.map}-${photo.width}.webp`);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
photoTexture = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const ringInner = photo !== undefined && photoTexture !== undefined ? photo.from / facts.radiusKm : (table?.inner ?? 1);
|
|
83
|
+
const ringOuter = photo !== undefined && photoTexture !== undefined ? photo.to / facts.radiusKm : (table?.outer ?? 2);
|
|
84
|
+
const ringKey = photoTexture !== undefined ? `${options.id}:p` : options.id;
|
|
85
|
+
let ringMesh;
|
|
86
|
+
let ringTexture;
|
|
87
|
+
let ringMaterial;
|
|
88
|
+
if (table !== null && look.emissive !== true) {
|
|
89
|
+
ringTexture = assets.acquire(`wk:ringtex:${ringKey}`, () => {
|
|
90
|
+
if (photoTexture !== undefined) {
|
|
91
|
+
photoTexture.magFilter = T.LinearFilter;
|
|
92
|
+
photoTexture.minFilter = T.LinearFilter;
|
|
93
|
+
photoTexture.generateMipmaps = false;
|
|
94
|
+
photoTexture.wrapS = T.ClampToEdgeWrapping;
|
|
95
|
+
photoTexture.colorSpace = T.NoColorSpace;
|
|
96
|
+
return photoTexture;
|
|
97
|
+
}
|
|
98
|
+
const tex = new T.DataTexture(table.data, table.width, 1, T.RGBAFormat);
|
|
99
|
+
tex.magFilter = T.LinearFilter;
|
|
100
|
+
tex.minFilter = T.LinearFilter;
|
|
101
|
+
tex.wrapS = T.ClampToEdgeWrapping;
|
|
102
|
+
tex.needsUpdate = true;
|
|
103
|
+
return tex;
|
|
104
|
+
}, (t) => t.dispose());
|
|
105
|
+
if (photoTexture !== undefined && ringTexture !== photoTexture)
|
|
106
|
+
photoTexture.dispose();
|
|
107
|
+
const ringGeometry = assets.acquire(`wk:ring:${ringKey}`, () => new T.RingGeometry(ringInner, ringOuter, 256, 1), (g) => g.dispose());
|
|
108
|
+
ringMaterial = assets.acquire(`wk:ringmat:${ringKey}`, () => new T.ShaderMaterial({
|
|
109
|
+
vertexShader: RING_VERTEX,
|
|
110
|
+
fragmentShader: RING_FRAGMENT,
|
|
111
|
+
side: T.DoubleSide,
|
|
112
|
+
transparent: true,
|
|
113
|
+
depthWrite: false,
|
|
114
|
+
uniforms: { uRingTex: { value: ringTexture }, uRingR: { value: new T.Vector2(ringInner, ringOuter) }, uLight: { value: new T.Vector3(0, 0, 1) }, uRingN: { value: new T.Vector3(0, 1, 0) } },
|
|
115
|
+
}), (m) => m.dispose());
|
|
116
|
+
ringMesh = new T.Mesh(ringGeometry, ringMaterial);
|
|
117
|
+
ringMesh.frustumCulled = false;
|
|
118
|
+
scene.add(ringMesh);
|
|
119
|
+
}
|
|
120
|
+
// the glow of a star
|
|
121
|
+
let glow;
|
|
122
|
+
if (look.emissive === true) {
|
|
123
|
+
const glowGeometry = assets.acquire("wk:glow", () => new T.PlaneGeometry(6, 6), (g) => g.dispose());
|
|
124
|
+
const glowMaterial = assets.acquire("wk:glowmat", () => new T.ShaderMaterial({ vertexShader: GLOW_VERTEX, fragmentShader: GLOW_FRAGMENT, transparent: true, depthWrite: false, blending: T.AdditiveBlending, uniforms: { uColour: { value: new T.Vector3(1, 0.7, 0.3) } } }), (m) => m.dispose());
|
|
125
|
+
glow = new T.Mesh(glowGeometry, glowMaterial);
|
|
126
|
+
glow.position.z = -5;
|
|
127
|
+
glow.scale.setScalar(1);
|
|
128
|
+
glow.frustumCulled = false;
|
|
129
|
+
scene.add(glow);
|
|
130
|
+
}
|
|
131
|
+
// moons, made on first use
|
|
132
|
+
const moonMeshes = new Map();
|
|
133
|
+
const orbitLines = new Map();
|
|
134
|
+
const orbitMaterial = new T.LineBasicMaterial({ color: 0x8fa1b8, transparent: true, opacity: 0.4 });
|
|
135
|
+
let viewWidth = width;
|
|
136
|
+
let viewHeight = height;
|
|
137
|
+
// the picture
|
|
138
|
+
let textureWidth = 0;
|
|
139
|
+
let texture;
|
|
140
|
+
let loading = false;
|
|
141
|
+
let failed = false;
|
|
142
|
+
const tried = new Set();
|
|
143
|
+
let latest = options.state();
|
|
144
|
+
const loadTexture = (w) => {
|
|
145
|
+
const far = look.painted.far !== undefined && w >= look.painted.far;
|
|
146
|
+
return loadImage(`${far ? options.farBase() : options.textureBase()}${look.painted.map}-${w}.webp`);
|
|
147
|
+
};
|
|
148
|
+
const use = (t, w) => {
|
|
149
|
+
t.anisotropy = 4;
|
|
150
|
+
t.wrapS = T.RepeatWrapping;
|
|
151
|
+
t.colorSpace = T.NoColorSpace;
|
|
152
|
+
const previous = texture;
|
|
153
|
+
texture = t;
|
|
154
|
+
textureWidth = w;
|
|
155
|
+
paintedMaterial.uniforms.uMap.value = t;
|
|
156
|
+
mesh.material = paintedMaterial;
|
|
157
|
+
previous?.dispose();
|
|
158
|
+
options.report?.({ painted: true, width: w });
|
|
159
|
+
};
|
|
160
|
+
const fail = () => {
|
|
161
|
+
failed = true;
|
|
162
|
+
mesh.material = shader;
|
|
163
|
+
textureWidth = 0;
|
|
164
|
+
options.report?.({ painted: false, width: 0, failed: true });
|
|
165
|
+
};
|
|
166
|
+
if (wantPainted && paintedMaterial !== undefined) {
|
|
167
|
+
const widths = look.painted.widths;
|
|
168
|
+
try {
|
|
169
|
+
use(await loadTexture(widths[0]), widths[0]);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
fail();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
options.report?.({ painted: false, width: 0 });
|
|
177
|
+
}
|
|
178
|
+
const orient = (state) => {
|
|
179
|
+
const { pole, meridian } = state.pose;
|
|
180
|
+
const east = cross(pole, meridian);
|
|
181
|
+
const ax = facts.axesKm;
|
|
182
|
+
const mean = facts.radiusKm;
|
|
183
|
+
const scaleX = ax ? ax[1] / mean : 1;
|
|
184
|
+
const scaleY = ax ? ax[2] / mean : 1 - facts.flattening;
|
|
185
|
+
const scaleZ = ax ? ax[0] / mean : 1;
|
|
186
|
+
setBasis(T, mesh, east, pole, meridian, scaleX, scaleY, scaleZ);
|
|
187
|
+
if (ringMesh !== undefined)
|
|
188
|
+
setBasis(T, ringMesh, meridian, east, pole);
|
|
189
|
+
};
|
|
190
|
+
const frame = (state) => {
|
|
191
|
+
const aspect = viewWidth / Math.max(1, viewHeight);
|
|
192
|
+
const half = state.extent / Math.max(0.05, state.zoom);
|
|
193
|
+
const halfW = aspect >= 1 ? half * aspect : half;
|
|
194
|
+
const halfH = aspect >= 1 ? half : half / aspect;
|
|
195
|
+
const cx = state.panX * halfW;
|
|
196
|
+
const cy = state.panY * halfH;
|
|
197
|
+
camera.left = cx - halfW;
|
|
198
|
+
camera.right = cx + halfW;
|
|
199
|
+
camera.top = cy + halfH;
|
|
200
|
+
camera.bottom = cy - halfH;
|
|
201
|
+
camera.updateProjectionMatrix();
|
|
202
|
+
};
|
|
203
|
+
const moonFor = (id) => {
|
|
204
|
+
let m = moonMeshes.get(id);
|
|
205
|
+
if (m !== undefined)
|
|
206
|
+
return m;
|
|
207
|
+
const f = BODIES[id];
|
|
208
|
+
const c = [1, 3, 5].map((i) => parseInt(f.colour.slice(i, i + 2), 16) / 255);
|
|
209
|
+
const material = assets.acquire(`wk:ball:${id}`, () => new T.ShaderMaterial({ vertexShader: VERTEX, fragmentShader: BALL_FRAGMENT, uniforms: { uLight: { value: new T.Vector3(0, 0, 1) }, uColour: { value: col3(T, c) } } }), (x) => x.dispose());
|
|
210
|
+
m = new T.Mesh(geometry, material);
|
|
211
|
+
m.frustumCulled = false;
|
|
212
|
+
moonMeshes.set(id, m);
|
|
213
|
+
scene.add(m);
|
|
214
|
+
return m;
|
|
215
|
+
};
|
|
216
|
+
const place = (state) => {
|
|
217
|
+
const moons = state.moons ?? [];
|
|
218
|
+
const aspect = viewWidth / Math.max(1, viewHeight);
|
|
219
|
+
const half = state.extent / Math.max(0.05, state.zoom);
|
|
220
|
+
const unitsPerPx = (2 * half) / Math.max(1, aspect >= 1 ? viewHeight : viewWidth);
|
|
221
|
+
const minR = (state.minMoonPx ?? 2.5) * unitsPerPx;
|
|
222
|
+
const live = new Set(moons.map((m) => m.id));
|
|
223
|
+
for (const [id, m] of moonMeshes)
|
|
224
|
+
if (!live.has(id)) {
|
|
225
|
+
scene.remove(m);
|
|
226
|
+
moonMeshes.delete(id);
|
|
227
|
+
}
|
|
228
|
+
for (const [id, o] of orbitLines)
|
|
229
|
+
if (!live.has(id)) {
|
|
230
|
+
scene.remove(o.line);
|
|
231
|
+
o.line.geometry.dispose();
|
|
232
|
+
orbitLines.delete(id);
|
|
233
|
+
}
|
|
234
|
+
for (const moon of moons) {
|
|
235
|
+
const f = BODIES[moon.id];
|
|
236
|
+
const mesh2 = moonFor(moon.id);
|
|
237
|
+
const r = Math.max(f.radiusKm / facts.radiusKm, minR);
|
|
238
|
+
mesh2.position.set(moon.x, moon.y, moon.z);
|
|
239
|
+
mesh2.scale.setScalar(r);
|
|
240
|
+
mesh2.updateMatrix();
|
|
241
|
+
mesh2.updateMatrixWorld(true);
|
|
242
|
+
const mat = mesh2.material;
|
|
243
|
+
mat.uniforms.uLight.value.copy(v3(T, state.pose.light));
|
|
244
|
+
if (moon.orbit !== undefined) {
|
|
245
|
+
const held = orbitLines.get(moon.id);
|
|
246
|
+
if (held === undefined || held.source !== moon.orbit) {
|
|
247
|
+
if (held !== undefined) {
|
|
248
|
+
scene.remove(held.line);
|
|
249
|
+
held.line.geometry.dispose();
|
|
250
|
+
}
|
|
251
|
+
const g = new T.BufferGeometry();
|
|
252
|
+
g.setAttribute("position", new T.BufferAttribute(moon.orbit, 3));
|
|
253
|
+
const line = new T.Line(g, orbitMaterial);
|
|
254
|
+
line.frustumCulled = false;
|
|
255
|
+
scene.add(line);
|
|
256
|
+
orbitLines.set(moon.id, { line, source: moon.orbit });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (state.orbitColour !== undefined)
|
|
261
|
+
orbitMaterial.color.set(state.orbitColour);
|
|
262
|
+
};
|
|
263
|
+
const apply = (state) => {
|
|
264
|
+
latest = state;
|
|
265
|
+
orient(state);
|
|
266
|
+
frame(state);
|
|
267
|
+
place(state);
|
|
268
|
+
if (ringMaterial !== undefined) {
|
|
269
|
+
const light = v3(T, state.pose.light);
|
|
270
|
+
const pole = v3(T, state.pose.pole);
|
|
271
|
+
ringMaterial.uniforms.uLight.value.copy(light);
|
|
272
|
+
ringMaterial.uniforms.uRingN.value.copy(pole);
|
|
273
|
+
for (const mat of [shader, paintedMaterial]) {
|
|
274
|
+
if (mat === undefined)
|
|
275
|
+
continue;
|
|
276
|
+
mat.uniforms.uRingOn.value = 1;
|
|
277
|
+
mat.uniforms.uRingN.value.copy(pole);
|
|
278
|
+
mat.uniforms.uRingR.value.set(ringInner, ringOuter);
|
|
279
|
+
mat.uniforms.uRingTex.value = ringTexture;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const mat of [shader, paintedMaterial])
|
|
283
|
+
mat?.uniforms.uLight.value.copy(v3(T, state.pose.light));
|
|
284
|
+
if (glow !== undefined)
|
|
285
|
+
glow.scale.setScalar(1);
|
|
286
|
+
// a sharper picture, when the disc is big enough to show it
|
|
287
|
+
if (wantPainted && !failed && !loading && look.painted !== undefined) {
|
|
288
|
+
const aspect = viewWidth / Math.max(1, viewHeight);
|
|
289
|
+
const discPx = (aspect >= 1 ? viewHeight : viewWidth) * (state.zoom / state.extent);
|
|
290
|
+
const wanted = options.wantedWidth(discPx);
|
|
291
|
+
const next = look.painted.widths.find((w) => w >= wanted) ?? look.painted.widths[look.painted.widths.length - 1];
|
|
292
|
+
if (next > textureWidth && !tried.has(next)) {
|
|
293
|
+
tried.add(next);
|
|
294
|
+
loading = true;
|
|
295
|
+
void loadTexture(next).then((t) => {
|
|
296
|
+
loading = false;
|
|
297
|
+
use(t, next);
|
|
298
|
+
options.redraw();
|
|
299
|
+
}, () => {
|
|
300
|
+
loading = false;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
apply(latest);
|
|
306
|
+
void ringReach;
|
|
307
|
+
return {
|
|
308
|
+
scene,
|
|
309
|
+
camera,
|
|
310
|
+
apply,
|
|
311
|
+
get textureWidth() {
|
|
312
|
+
return textureWidth;
|
|
313
|
+
},
|
|
314
|
+
get painted() {
|
|
315
|
+
return textureWidth > 0;
|
|
316
|
+
},
|
|
317
|
+
resize(w, h) {
|
|
318
|
+
viewWidth = w;
|
|
319
|
+
viewHeight = h;
|
|
320
|
+
frame(latest);
|
|
321
|
+
place(latest);
|
|
322
|
+
},
|
|
323
|
+
update() {
|
|
324
|
+
return false;
|
|
325
|
+
},
|
|
326
|
+
dispose() {
|
|
327
|
+
texture?.dispose();
|
|
328
|
+
texture = undefined;
|
|
329
|
+
for (const o of orbitLines.values())
|
|
330
|
+
o.line.geometry.dispose();
|
|
331
|
+
orbitMaterial.dispose();
|
|
332
|
+
scene.clear();
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const VERTEX = "\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\nvoid main() {\n vObj = vec3(position.z, position.x, position.y);\n vN = normalize(normalMatrix * normal);\n vec4 w = modelMatrix * vec4(position, 1.0);\n vW = w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
|
|
2
|
+
/** Noise, craters and the band palette, for the surface code of a body to call. */
|
|
3
|
+
export declare const PRELUDE = "\nfloat hash13(vec3 p) { p = fract(p * 0.1031); p += dot(p, p.zyx + 31.32); return fract((p.x + p.y) * p.z); }\nvec3 hash33(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }\nfloat vnoise(vec3 p) {\n vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(hash13(i), hash13(i + vec3(1,0,0)), f.x), mix(hash13(i + vec3(0,1,0)), hash13(i + vec3(1,1,0)), f.x), f.y),\n mix(mix(hash13(i + vec3(0,0,1)), hash13(i + vec3(1,0,1)), f.x), mix(hash13(i + vec3(0,1,1)), hash13(i + vec3(1,1,1)), f.x), f.y), f.z);\n}\nfloat fbm(vec3 p) { float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * vnoise(p); p = p * 2.03 + vec3(1.7, 9.2, 3.1); a *= 0.5; } return s; }\n// Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl.\nfloat craters(vec3 p, float freq, float amp, float seed) {\n vec3 q = p * freq + seed; vec3 ip = floor(q); float h = 0.0;\n for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) {\n vec3 c = ip + vec3(float(x), float(y), float(z));\n float on = step(0.52, hash13(c + 3.7));\n vec3 ctr = c + hash33(c);\n float rad = mix(0.12, 0.36, hash13(c + 17.3));\n float d = length(q - ctr) / rad;\n float bowl = d < 1.0 ? (d * d - 1.0) : 0.0;\n float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 : 0.0;\n h += on * amp * rad * (bowl + rim);\n }\n return h;\n}\nfloat lon(vec3 p) { return atan(p.y, p.x); }\nfloat lat(vec3 p) { return asin(clamp(p.z, -1.0, 1.0)); }\nfloat wrapPi(float a) { return a - 6.2831853 * floor((a + 3.1415927) / 6.2831853); }\nvec3 dirOf(float lonDeg, float latDeg) { float lo = radians(lonDeg); float la = radians(latDeg); return vec3(cos(la) * cos(lo), cos(la) * sin(lo), sin(la)); }\n// One big round bowl with a rim, by longitude, latitude and angular radius in degrees. Negative inside.\nfloat bowl(vec3 p, float lonDeg, float latDeg, float radDeg, float depth) {\n float d = acos(clamp(dot(p, dirOf(lonDeg, latDeg)), -1.0, 1.0)) / radians(radDeg);\n return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3;\n}\n// A soft oval on the body, 1 inside and 0 outside, by longitude and latitude in degrees.\nfloat oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {\n float dx = wrapPi(lon(p) - radians(lon0)) * cos(radians(lat0)) / radians(rx);\n float dy = (lat(p) - radians(lat0)) / radians(ry);\n return 1.0 - smoothstep(1.0 - soft, 1.0, sqrt(dx * dx + dy * dy));\n}";
|
|
4
|
+
/** The fragment shader of a body: the surface code goes between the prelude and `main`. */
|
|
5
|
+
export declare function bodyFragment(surface: string, painted: boolean): string;
|
|
6
|
+
/** A painted body's surface code: the map by longitude and latitude, with the seam at 180 degrees left alone. */
|
|
7
|
+
export declare const PAINTED_SURFACE = "\nvec4 surface(vec3 p) {\n float u1 = atan(p.y, p.x) / 6.2831853 + 0.5;\n float u2 = fract(u1 + 0.5) - 0.5;\n float v = asin(clamp(p.z, -1.0, 1.0)) / 3.1415927 + 0.5;\n vec2 a = vec2(u1, v); vec2 b = vec2(u2, v);\n vec2 ax = dFdx(a); vec2 ay = dFdy(a); vec2 bx = dFdx(b); vec2 by = dFdy(b);\n bool useA = dot(ax, ax) + dot(ay, ay) <= dot(bx, bx) + dot(by, by);\n vec3 c = textureGrad(uMap, useA ? a : b, useA ? ax : bx, useA ? ay : by).rgb;\n return vec4(c, dot(c, vec3(0.3, 0.59, 0.11)));\n}";
|
|
8
|
+
export declare const RING_FRAGMENT = "\nprecision highp float;\nuniform sampler2D uRingTex;\nuniform vec2 uRingR;\nuniform vec3 uLight;\nuniform vec3 uRingN;\nvarying vec3 vL;\nvarying vec3 vW;\nvoid main() {\n float u = (length(vL.xy) - uRingR.x) / (uRingR.y - uRingR.x);\n if (u < 0.0 || u > 1.0) discard;\n vec4 t = texture2D(uRingTex, vec2(u, 0.5));\n float nl = dot(uRingN, uLight);\n float side = nl * uRingN.z;\n float bright = 0.16 + 0.84 * clamp(abs(nl) * 2.0, 0.0, 1.0);\n if (side < 0.0) bright *= 0.38;\n float b = dot(vW, uLight);\n float closest = sqrt(max(dot(vW, vW) - b * b, 0.0));\n float shadow = b < 0.0 ? smoothstep(1.02, 0.97, closest) : 0.0;\n vec3 c = pow(t.rgb, vec3(2.2)) * bright * (1.0 - 0.93 * shadow);\n gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), t.a);\n}";
|
|
9
|
+
export declare const RING_VERTEX = "\nvarying vec3 vL;\nvarying vec3 vW;\nvoid main() {\n vL = position;\n vec4 w = modelMatrix * vec4(position, 1.0);\n vW = w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
|
|
10
|
+
export declare const GLOW_VERTEX = "\nvarying vec2 vP;\nvoid main() { vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }";
|
|
11
|
+
export declare const GLOW_FRAGMENT = "\nprecision highp float;\nvarying vec2 vP;\nuniform vec3 uColour;\nvoid main() {\n float r = length(vP);\n if (r < 0.98) discard;\n float a = exp(-(r - 1.0) * 2.4) * 0.62;\n float streak = 0.82 + 0.18 * sin(atan(vP.y, vP.x) * 9.0 + r * 3.0);\n a *= streak * smoothstep(3.0, 1.6, r);\n gl_FragColor = vec4(uColour * a, a);\n}";
|
|
12
|
+
/** A cheap lit ball for a moon seen small beside its planet. */
|
|
13
|
+
export declare const BALL_FRAGMENT = "\nprecision highp float;\nuniform vec3 uLight;\nuniform vec3 uColour;\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\n\nfloat hash13(vec3 p) { p = fract(p * 0.1031); p += dot(p, p.zyx + 31.32); return fract((p.x + p.y) * p.z); }\nvec3 hash33(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }\nfloat vnoise(vec3 p) {\n vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(hash13(i), hash13(i + vec3(1,0,0)), f.x), mix(hash13(i + vec3(0,1,0)), hash13(i + vec3(1,1,0)), f.x), f.y),\n mix(mix(hash13(i + vec3(0,0,1)), hash13(i + vec3(1,0,1)), f.x), mix(hash13(i + vec3(0,1,1)), hash13(i + vec3(1,1,1)), f.x), f.y), f.z);\n}\nfloat fbm(vec3 p) { float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * vnoise(p); p = p * 2.03 + vec3(1.7, 9.2, 3.1); a *= 0.5; } return s; }\n// Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl.\nfloat craters(vec3 p, float freq, float amp, float seed) {\n vec3 q = p * freq + seed; vec3 ip = floor(q); float h = 0.0;\n for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) {\n vec3 c = ip + vec3(float(x), float(y), float(z));\n float on = step(0.52, hash13(c + 3.7));\n vec3 ctr = c + hash33(c);\n float rad = mix(0.12, 0.36, hash13(c + 17.3));\n float d = length(q - ctr) / rad;\n float bowl = d < 1.0 ? (d * d - 1.0) : 0.0;\n float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 : 0.0;\n h += on * amp * rad * (bowl + rim);\n }\n return h;\n}\nfloat lon(vec3 p) { return atan(p.y, p.x); }\nfloat lat(vec3 p) { return asin(clamp(p.z, -1.0, 1.0)); }\nfloat wrapPi(float a) { return a - 6.2831853 * floor((a + 3.1415927) / 6.2831853); }\nvec3 dirOf(float lonDeg, float latDeg) { float lo = radians(lonDeg); float la = radians(latDeg); return vec3(cos(la) * cos(lo), cos(la) * sin(lo), sin(la)); }\n// One big round bowl with a rim, by longitude, latitude and angular radius in degrees. Negative inside.\nfloat bowl(vec3 p, float lonDeg, float latDeg, float radDeg, float depth) {\n float d = acos(clamp(dot(p, dirOf(lonDeg, latDeg)), -1.0, 1.0)) / radians(radDeg);\n return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3;\n}\n// A soft oval on the body, 1 inside and 0 outside, by longitude and latitude in degrees.\nfloat oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {\n float dx = wrapPi(lon(p) - radians(lon0)) * cos(radians(lat0)) / radians(rx);\n float dy = (lat(p) - radians(lat0)) / radians(ry);\n return 1.0 - smoothstep(1.0 - soft, 1.0, sqrt(dx * dx + dy * dy));\n}\nvoid main() {\n vec3 n = normalize(vN);\n float shade = 0.82 + 0.3 * (fbm(normalize(vObj) * 4.0) - 0.5);\n float diff = max(dot(n, uLight), 0.0);\n vec3 c = pow(uColour * shade, vec3(2.2)) * (0.04 + 1.1 * diff);\n gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), 1.0);\n}";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The shader code shared by every body: noise, craters and bands for the realistic look, the lighting, and the ring and glow shaders. Strings only.
|
|
2
|
+
export const VERTEX = /* glsl */ `
|
|
3
|
+
varying vec3 vObj;
|
|
4
|
+
varying vec3 vN;
|
|
5
|
+
varying vec3 vW;
|
|
6
|
+
void main() {
|
|
7
|
+
vObj = vec3(position.z, position.x, position.y);
|
|
8
|
+
vN = normalize(normalMatrix * normal);
|
|
9
|
+
vec4 w = modelMatrix * vec4(position, 1.0);
|
|
10
|
+
vW = w.xyz;
|
|
11
|
+
gl_Position = projectionMatrix * viewMatrix * w;
|
|
12
|
+
}`;
|
|
13
|
+
/** Noise, craters and the band palette, for the surface code of a body to call. */
|
|
14
|
+
export const PRELUDE = /* glsl */ `
|
|
15
|
+
float hash13(vec3 p) { p = fract(p * 0.1031); p += dot(p, p.zyx + 31.32); return fract((p.x + p.y) * p.z); }
|
|
16
|
+
vec3 hash33(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }
|
|
17
|
+
float vnoise(vec3 p) {
|
|
18
|
+
vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
|
19
|
+
return mix(mix(mix(hash13(i), hash13(i + vec3(1,0,0)), f.x), mix(hash13(i + vec3(0,1,0)), hash13(i + vec3(1,1,0)), f.x), f.y),
|
|
20
|
+
mix(mix(hash13(i + vec3(0,0,1)), hash13(i + vec3(1,0,1)), f.x), mix(hash13(i + vec3(0,1,1)), hash13(i + vec3(1,1,1)), f.x), f.y), f.z);
|
|
21
|
+
}
|
|
22
|
+
float fbm(vec3 p) { float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * vnoise(p); p = p * 2.03 + vec3(1.7, 9.2, 3.1); a *= 0.5; } return s; }
|
|
23
|
+
// Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl.
|
|
24
|
+
float craters(vec3 p, float freq, float amp, float seed) {
|
|
25
|
+
vec3 q = p * freq + seed; vec3 ip = floor(q); float h = 0.0;
|
|
26
|
+
for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) {
|
|
27
|
+
vec3 c = ip + vec3(float(x), float(y), float(z));
|
|
28
|
+
float on = step(0.52, hash13(c + 3.7));
|
|
29
|
+
vec3 ctr = c + hash33(c);
|
|
30
|
+
float rad = mix(0.12, 0.36, hash13(c + 17.3));
|
|
31
|
+
float d = length(q - ctr) / rad;
|
|
32
|
+
float bowl = d < 1.0 ? (d * d - 1.0) : 0.0;
|
|
33
|
+
float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 : 0.0;
|
|
34
|
+
h += on * amp * rad * (bowl + rim);
|
|
35
|
+
}
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
float lon(vec3 p) { return atan(p.y, p.x); }
|
|
39
|
+
float lat(vec3 p) { return asin(clamp(p.z, -1.0, 1.0)); }
|
|
40
|
+
float wrapPi(float a) { return a - 6.2831853 * floor((a + 3.1415927) / 6.2831853); }
|
|
41
|
+
vec3 dirOf(float lonDeg, float latDeg) { float lo = radians(lonDeg); float la = radians(latDeg); return vec3(cos(la) * cos(lo), cos(la) * sin(lo), sin(la)); }
|
|
42
|
+
// One big round bowl with a rim, by longitude, latitude and angular radius in degrees. Negative inside.
|
|
43
|
+
float bowl(vec3 p, float lonDeg, float latDeg, float radDeg, float depth) {
|
|
44
|
+
float d = acos(clamp(dot(p, dirOf(lonDeg, latDeg)), -1.0, 1.0)) / radians(radDeg);
|
|
45
|
+
return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3;
|
|
46
|
+
}
|
|
47
|
+
// A soft oval on the body, 1 inside and 0 outside, by longitude and latitude in degrees.
|
|
48
|
+
float oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {
|
|
49
|
+
float dx = wrapPi(lon(p) - radians(lon0)) * cos(radians(lat0)) / radians(rx);
|
|
50
|
+
float dy = (lat(p) - radians(lat0)) / radians(ry);
|
|
51
|
+
return 1.0 - smoothstep(1.0 - soft, 1.0, sqrt(dx * dx + dy * dy));
|
|
52
|
+
}`;
|
|
53
|
+
/** The fragment shader of a body: the surface code goes between the prelude and `main`. */
|
|
54
|
+
export function bodyFragment(surface, painted) {
|
|
55
|
+
return /* glsl */ `
|
|
56
|
+
precision highp float;
|
|
57
|
+
uniform vec3 uLight;
|
|
58
|
+
uniform float uRelief;
|
|
59
|
+
uniform float uAmbient;
|
|
60
|
+
uniform float uLimb;
|
|
61
|
+
uniform float uWrap;
|
|
62
|
+
uniform vec3 uAtmos;
|
|
63
|
+
uniform float uAtmosPow;
|
|
64
|
+
uniform float uEmissive;
|
|
65
|
+
uniform vec3 uRingN;
|
|
66
|
+
uniform vec2 uRingR;
|
|
67
|
+
uniform float uRingOn;
|
|
68
|
+
uniform sampler2D uRingTex;
|
|
69
|
+
${painted ? "uniform sampler2D uMap;" : ""}
|
|
70
|
+
varying vec3 vObj;
|
|
71
|
+
varying vec3 vN;
|
|
72
|
+
varying vec3 vW;
|
|
73
|
+
${PRELUDE}
|
|
74
|
+
${surface}
|
|
75
|
+
void main() {
|
|
76
|
+
vec3 p = normalize(vObj);
|
|
77
|
+
vec4 s = surface(p);
|
|
78
|
+
vec3 n = normalize(vN);
|
|
79
|
+
float mu = clamp(n.z, 0.0, 1.0);
|
|
80
|
+
if (uEmissive > 0.5) {
|
|
81
|
+
float edge = pow(mu, uLimb);
|
|
82
|
+
vec3 c = s.rgb * mix(vec3(1.0, 0.55, 0.25), vec3(1.0), edge) * (0.55 + 0.45 * edge);
|
|
83
|
+
gl_FragColor = vec4(c, 1.0);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
float dg = dot(n, uLight);
|
|
87
|
+
vec3 dpx = dFdx(vW); vec3 dpy = dFdy(vW);
|
|
88
|
+
float hx = dFdx(s.a); float hy = dFdy(s.a);
|
|
89
|
+
vec3 grad = cross(dpy, n) * hx + cross(n, dpx) * hy;
|
|
90
|
+
float det = dot(dpx, cross(dpy, n));
|
|
91
|
+
vec3 nb = normalize(abs(det) * n - sign(det) * uRelief * grad * 22.0);
|
|
92
|
+
float d = dot(nb, uLight);
|
|
93
|
+
float diff = clamp((d + uWrap) / (1.0 + uWrap), 0.0, 1.0) * smoothstep(-0.02 - uWrap * 0.6, 0.1 + uWrap * 0.8, dg);
|
|
94
|
+
if (uRingOn > 0.5) {
|
|
95
|
+
float den = dot(uLight, uRingN);
|
|
96
|
+
if (abs(den) > 1e-3) {
|
|
97
|
+
float t = -dot(vW, uRingN) / den;
|
|
98
|
+
if (t > 0.0) {
|
|
99
|
+
vec3 hp = vW + t * uLight;
|
|
100
|
+
float u = (length(hp) - uRingR.x) / (uRingR.y - uRingR.x);
|
|
101
|
+
if (u > 0.0 && u < 1.0) diff *= 1.0 - 0.88 * texture2D(uRingTex, vec2(u, 0.5)).a;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
vec3 alb = pow(s.rgb, vec3(2.2));
|
|
106
|
+
vec3 lit = alb * (uAmbient + (1.0 - uAmbient) * diff * 1.18);
|
|
107
|
+
float rim = pow(1.0 - mu, uAtmosPow);
|
|
108
|
+
lit += pow(uAtmos, vec3(2.2)) * rim * (0.12 + 0.88 * clamp(dg * 1.6 + 0.35, 0.0, 1.0));
|
|
109
|
+
lit *= mix(1.0, pow(mu, 0.45), uLimb);
|
|
110
|
+
gl_FragColor = vec4(pow(lit, vec3(1.0 / 2.2)), 1.0);
|
|
111
|
+
}`;
|
|
112
|
+
}
|
|
113
|
+
/** A painted body's surface code: the map by longitude and latitude, with the seam at 180 degrees left alone. */
|
|
114
|
+
export const PAINTED_SURFACE = /* glsl */ `
|
|
115
|
+
vec4 surface(vec3 p) {
|
|
116
|
+
float u1 = atan(p.y, p.x) / 6.2831853 + 0.5;
|
|
117
|
+
float u2 = fract(u1 + 0.5) - 0.5;
|
|
118
|
+
float v = asin(clamp(p.z, -1.0, 1.0)) / 3.1415927 + 0.5;
|
|
119
|
+
vec2 a = vec2(u1, v); vec2 b = vec2(u2, v);
|
|
120
|
+
vec2 ax = dFdx(a); vec2 ay = dFdy(a); vec2 bx = dFdx(b); vec2 by = dFdy(b);
|
|
121
|
+
bool useA = dot(ax, ax) + dot(ay, ay) <= dot(bx, bx) + dot(by, by);
|
|
122
|
+
vec3 c = textureGrad(uMap, useA ? a : b, useA ? ax : bx, useA ? ay : by).rgb;
|
|
123
|
+
return vec4(c, dot(c, vec3(0.3, 0.59, 0.11)));
|
|
124
|
+
}`;
|
|
125
|
+
export const RING_FRAGMENT = /* glsl */ `
|
|
126
|
+
precision highp float;
|
|
127
|
+
uniform sampler2D uRingTex;
|
|
128
|
+
uniform vec2 uRingR;
|
|
129
|
+
uniform vec3 uLight;
|
|
130
|
+
uniform vec3 uRingN;
|
|
131
|
+
varying vec3 vL;
|
|
132
|
+
varying vec3 vW;
|
|
133
|
+
void main() {
|
|
134
|
+
float u = (length(vL.xy) - uRingR.x) / (uRingR.y - uRingR.x);
|
|
135
|
+
if (u < 0.0 || u > 1.0) discard;
|
|
136
|
+
vec4 t = texture2D(uRingTex, vec2(u, 0.5));
|
|
137
|
+
float nl = dot(uRingN, uLight);
|
|
138
|
+
float side = nl * uRingN.z;
|
|
139
|
+
float bright = 0.16 + 0.84 * clamp(abs(nl) * 2.0, 0.0, 1.0);
|
|
140
|
+
if (side < 0.0) bright *= 0.38;
|
|
141
|
+
float b = dot(vW, uLight);
|
|
142
|
+
float closest = sqrt(max(dot(vW, vW) - b * b, 0.0));
|
|
143
|
+
float shadow = b < 0.0 ? smoothstep(1.02, 0.97, closest) : 0.0;
|
|
144
|
+
vec3 c = pow(t.rgb, vec3(2.2)) * bright * (1.0 - 0.93 * shadow);
|
|
145
|
+
gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), t.a);
|
|
146
|
+
}`;
|
|
147
|
+
export const RING_VERTEX = /* glsl */ `
|
|
148
|
+
varying vec3 vL;
|
|
149
|
+
varying vec3 vW;
|
|
150
|
+
void main() {
|
|
151
|
+
vL = position;
|
|
152
|
+
vec4 w = modelMatrix * vec4(position, 1.0);
|
|
153
|
+
vW = w.xyz;
|
|
154
|
+
gl_Position = projectionMatrix * viewMatrix * w;
|
|
155
|
+
}`;
|
|
156
|
+
export const GLOW_VERTEX = /* glsl */ `
|
|
157
|
+
varying vec2 vP;
|
|
158
|
+
void main() { vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
|
|
159
|
+
export const GLOW_FRAGMENT = /* glsl */ `
|
|
160
|
+
precision highp float;
|
|
161
|
+
varying vec2 vP;
|
|
162
|
+
uniform vec3 uColour;
|
|
163
|
+
void main() {
|
|
164
|
+
float r = length(vP);
|
|
165
|
+
if (r < 0.98) discard;
|
|
166
|
+
float a = exp(-(r - 1.0) * 2.4) * 0.62;
|
|
167
|
+
float streak = 0.82 + 0.18 * sin(atan(vP.y, vP.x) * 9.0 + r * 3.0);
|
|
168
|
+
a *= streak * smoothstep(3.0, 1.6, r);
|
|
169
|
+
gl_FragColor = vec4(uColour * a, a);
|
|
170
|
+
}`;
|
|
171
|
+
/** A cheap lit ball for a moon seen small beside its planet. */
|
|
172
|
+
export const BALL_FRAGMENT = /* glsl */ `
|
|
173
|
+
precision highp float;
|
|
174
|
+
uniform vec3 uLight;
|
|
175
|
+
uniform vec3 uColour;
|
|
176
|
+
varying vec3 vObj;
|
|
177
|
+
varying vec3 vN;
|
|
178
|
+
varying vec3 vW;
|
|
179
|
+
${PRELUDE}
|
|
180
|
+
void main() {
|
|
181
|
+
vec3 n = normalize(vN);
|
|
182
|
+
float shade = 0.82 + 0.3 * (fbm(normalize(vObj) * 4.0) - 0.5);
|
|
183
|
+
float diff = max(dot(n, uLight), 0.0);
|
|
184
|
+
vec3 c = pow(uColour * shade, vec3(2.2)) * (0.04 + 1.1 * diff);
|
|
185
|
+
gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), 1.0);
|
|
186
|
+
}`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { BodyId } from "../facts.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A rim of air: the colour it scatters (0 to 1 each), how strong it is and how thin the rim is.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { registerLook, type Atmosphere } from "@johnmorrisdotca/wakusei-planets/look";
|
|
8
|
+
*
|
|
9
|
+
* const air: Atmosphere = { colour: [0.5, 0.7, 1], strength: 0.4 };
|
|
10
|
+
* registerLook({ id: "venus", surface: "vec4 surface(vec3 p) { return vec4(vec3(0.9), 0.0); }", atmosphere: air });
|
|
11
|
+
* console.log(air.strength);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export interface Atmosphere {
|
|
15
|
+
colour: readonly [number, number, number];
|
|
16
|
+
strength: number;
|
|
17
|
+
/** How thin the rim is: higher is thinner. Default 3. */
|
|
18
|
+
power?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The painted look of a body: a surface map in the package's `textures` folder, as widths from the smallest up.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { type PaintedMap } from "@johnmorrisdotca/wakusei-planets/look";
|
|
26
|
+
*
|
|
27
|
+
* const map: PaintedMap = { map: "mars", widths: [512, 1024, 2048] };
|
|
28
|
+
* console.log(map.widths.length);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export interface PaintedMap {
|
|
32
|
+
/** The file name before `-<width>.webp`, for example `mars`. */
|
|
33
|
+
map: string;
|
|
34
|
+
/** The widths made, smallest first: the smallest is drawn first and sharper ones are fetched when the view is zoomed in. */
|
|
35
|
+
widths: readonly number[];
|
|
36
|
+
/** The smallest width that is not in the npm package: it is fetched from the repository's own files (see `texture-far-base`). */
|
|
37
|
+
far?: number;
|
|
38
|
+
/** How much of the map's brightness is also relief. Default 0. */
|
|
39
|
+
relief?: number;
|
|
40
|
+
/** A picture of the body's rings for this look, one pixel high: colour and opacity from the inner edge to the outer, `width` samples. Used instead of the table's strip when it loads. */
|
|
41
|
+
rings?: {
|
|
42
|
+
map: string;
|
|
43
|
+
width: number;
|
|
44
|
+
from: number;
|
|
45
|
+
to: number;
|
|
46
|
+
};
|
|
47
|
+
/** The painted pictures' credit line, for the page. */
|
|
48
|
+
credit?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What a body looks like in three.js: the shader code of its surface, how it glows, its atmosphere and its painted map.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { registerLook, type BodyLook } from "@johnmorrisdotca/wakusei-planets/look";
|
|
56
|
+
*
|
|
57
|
+
* const look: BodyLook = { id: "moon", surface: "vec4 surface(vec3 p) { return vec4(vec3(0.7), 0.0); }", ambient: 0.02 };
|
|
58
|
+
* console.log(registerLook(look).id);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export interface BodyLook {
|
|
62
|
+
id: BodyId;
|
|
63
|
+
/** GLSL that defines `vec4 surface(vec3 p)`: `p` is the unit position on the body (x toward longitude 0 on the equator, y toward 90 degrees east, z the north pole); return the colour in `rgb` and the height in `a`. */
|
|
64
|
+
surface: string;
|
|
65
|
+
/** A glowing body (the Sun): no shading from outside light. */
|
|
66
|
+
emissive?: boolean;
|
|
67
|
+
atmosphere?: Atmosphere;
|
|
68
|
+
/** How much the disc darkens toward the limb, 0 to 1. Default 0. */
|
|
69
|
+
limb?: number;
|
|
70
|
+
/** How strongly the height shows as relief, 0 to 2. Default 0.6. */
|
|
71
|
+
relief?: number;
|
|
72
|
+
/** How far light wraps round the terminator: 0 is a hard edge (no air), 0.3 a soft one. Default 0.04, or 0.3 with an atmosphere. */
|
|
73
|
+
wrap?: number;
|
|
74
|
+
/** How much of the night side shows (0 to 1). Default 0.04. */
|
|
75
|
+
ambient?: number;
|
|
76
|
+
painted?: PaintedMap;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Teach the package how to draw a body in three.js. Each body's entry point (`@johnmorrisdotca/wakusei-planets/jupiter`) calls this once as it loads; call it yourself to add or replace a look.
|
|
80
|
+
*
|
|
81
|
+
* @param look - The body's name and its shader code, atmosphere and map.
|
|
82
|
+
* @returns The look, as it was given.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { registerLook, lookOf } from "@johnmorrisdotca/wakusei-planets/look";
|
|
87
|
+
*
|
|
88
|
+
* registerLook({ id: "moon", surface: "vec4 surface(vec3 p) { return vec4(vec3(0.6), 0.0); }" });
|
|
89
|
+
* console.log(lookOf("moon")?.id);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function registerLook(look: BodyLook): BodyLook;
|
|
93
|
+
/**
|
|
94
|
+
* The look registered for a body, or `undefined` when its entry point has not been imported.
|
|
95
|
+
*
|
|
96
|
+
* @param id - The body's name.
|
|
97
|
+
* @returns The registered look, or `undefined` for a body with none.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { lookOf } from "@johnmorrisdotca/wakusei-planets/look";
|
|
102
|
+
*
|
|
103
|
+
* console.log(lookOf("jupiter") === undefined);
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare const lookOf: (id: BodyId) => BodyLook | undefined;
|