tosijs-3d 0.10.2 → 0.10.3

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/b3d-planet.d.ts +47 -1
  3. package/dist/b3d-planet.d.ts.map +1 -1
  4. package/dist/b3d-planet.js +202 -49
  5. package/dist/b3d-planet.js.map +1 -1
  6. package/dist/b3d-terrain.d.ts +30 -0
  7. package/dist/b3d-terrain.d.ts.map +1 -1
  8. package/dist/b3d-terrain.js +48 -0
  9. package/dist/b3d-terrain.js.map +1 -1
  10. package/dist/biome-plugin.d.ts +2 -1
  11. package/dist/biome-plugin.d.ts.map +1 -1
  12. package/dist/biome-plugin.js +46 -12
  13. package/dist/biome-plugin.js.map +1 -1
  14. package/dist/gas-giant-material.d.ts +2 -0
  15. package/dist/gas-giant-material.d.ts.map +1 -1
  16. package/dist/gas-giant-material.js +1 -1
  17. package/dist/gas-giant-material.js.map +1 -1
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/planet-clouds.d.ts +34 -0
  23. package/dist/planet-clouds.d.ts.map +1 -0
  24. package/dist/planet-clouds.js +280 -0
  25. package/dist/planet-clouds.js.map +1 -0
  26. package/dist/planet-map-material.d.ts +4 -0
  27. package/dist/planet-map-material.d.ts.map +1 -0
  28. package/dist/planet-map-material.js +74 -0
  29. package/dist/planet-map-material.js.map +1 -0
  30. package/dist/planet-map.d.ts +35 -0
  31. package/dist/planet-map.d.ts.map +1 -0
  32. package/dist/planet-map.js +271 -0
  33. package/dist/planet-map.js.map +1 -0
  34. package/dist/sphere-map.d.ts +74 -0
  35. package/dist/sphere-map.d.ts.map +1 -0
  36. package/dist/sphere-map.js +208 -0
  37. package/dist/sphere-map.js.map +1 -0
  38. package/package.json +1 -1
  39. package/static/attributes.json +51 -1
  40. package/static/attributes.txt +14 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,60 @@ All notable changes to **tosijs-3d**. This project is pre-1.0 (`0.x`), so minor
6
6
  versions may carry breaking peer-dependency changes — each is called out in a
7
7
  **⚠️ Breaking** block in its version section below, with what a consumer must do.
8
8
 
9
+ ## 0.10.3
10
+
11
+ ### Added
12
+
13
+ - **One world, two views.** A terrain in `surfaceType="sphere"` can hand out
14
+ its own surface as a globe (`terrain.planetSurface()`), and
15
+ `<tosi-b3d-planet>` draws from it (`planet.surface`, with `relief` to
16
+ exaggerate the heights). The planet you see from orbit is then the ground
17
+ you land on: the same height function, checked to agree to the centimetre.
18
+ - **Places.** `terrain.latLonAt(x, z)` and `terrain.groundAt(lat, lon)`, and
19
+ the pure `sphere-map` module behind them (ground, latitude and longitude,
20
+ and direction on the globe, each from the others), so a pin on the globe is
21
+ a spot on the ground. On the ground +x is east and +z is north.
22
+ - **Real worlds.** `<tosi-b3d-planet heightMap colorMap>` draws a planet
23
+ from its maps: the Moon, Mars and the Earth from NASA's and NOAA's data on
24
+ the CDN (`nasa/moon`, `nasa/mars`, `nasa/earth`, `noaa/earth`). `relief`
25
+ exaggerates the heights, `flatSea` fills the oceans level, `bodyRadiusKm`
26
+ is the real body's radius. The new `planet-map` module reads the 16-bit
27
+ height PNGs itself (a browser's own decoding keeps eight bits):
28
+ `loadPlanetMap`, `decodeGray16Png`, `mapSurface`.
29
+ - **Weather from orbit.** `<tosi-b3d-planet clouds="0.5">` puts a shell of
30
+ cloud inside the air: one coverage dial, winds that reverse with latitude,
31
+ more cloud along the equator and the storm tracks, and a shadow on the
32
+ ground beneath. `cloudColor`, `cloudSpeed`. `planet.storms` places up to
33
+ four cyclones by latitude and longitude, each with an eye, turning the
34
+ right way for its hemisphere, with lightning inside. Provisional: this is
35
+ the weather from orbit only, its own field and its own storms; it does not
36
+ yet agree with the cloud deck and weather cells on that world's ground.
37
+ - `planet.rootNode` is public, for placing a planet.
38
+
39
+ ### Fixed
40
+
41
+ - **(behaviour) Planets turn west to east.** `rotationSpeed` spun them the
42
+ other way, so the Earth ran backwards. A negative `rotationSpeed` turns a
43
+ planet the old way (it used to be ignored).
44
+ - **A planet that has been moved is shaded where it is.** The biome shader
45
+ measured altitude and latitude from the world origin.
46
+ - **A rotating planet's land no longer shimmers.** The biome shader read its
47
+ noise by world position, so a planet turned under its own climate. On a
48
+ planet it now works in the planet's own frame, and reads its climate noise
49
+ in 3D (it was smeared down the sides of the sphere).
50
+ - **Volcanism on a planet is planet-sized.** The volcanic patterns were sized
51
+ in metres of terrain, so on a globe the regions were one value everywhere
52
+ (the dial flipped the whole planet) and the plates were a ninth of its
53
+ width. They scale with the planet now: regions about a twentieth of the
54
+ way round, plates you can see.
55
+ - **A planet's air no longer pinches at the poles.** Its clouds were 2D noise
56
+ over latitude and longitude, which converges to a point at each pole and has
57
+ a seam down the back. They are 3D noise read on the sphere itself now, and
58
+ in the planet's own frame, so they turn with a rotating planet where they
59
+ used to stand still while it turned beneath them.
60
+
61
+ Size: barrel (min, packages external) 1005.5 KB raw / 340.6 KB gz, +1.8% on 0.10.2.
62
+
9
63
  ## 0.10.2
10
64
 
11
65
  ### Added
@@ -1,5 +1,7 @@
1
1
  import { B3dChild } from './b3d-utils.js';
2
2
  import { BiomePlugin } from './biome-plugin.js';
3
+ import type { PlanetSurface } from './sphere-map.js';
4
+ import { type PlanetStorm } from './planet-clouds.js';
3
5
  import * as BABYLON from '@babylonjs/core';
4
6
  import type { B3d } from './tosi-b3d.js';
5
7
  import type { GradientFilter } from './gradient-filter.js';
@@ -13,6 +15,11 @@ export declare class B3dPlanet extends B3dChild {
13
15
  static initAttributes: {
14
16
  biome: "on" | "off";
15
17
  gas: string;
18
+ heightMap: string;
19
+ colorMap: string;
20
+ bodyRadiusKm: number;
21
+ flatSea: "on" | "off";
22
+ relief: number;
16
23
  bands: number;
17
24
  turbulence: number;
18
25
  storm: number;
@@ -31,22 +38,44 @@ export declare class B3dPlanet extends B3dChild {
31
38
  atmosphere: number;
32
39
  atmosphereColor: string;
33
40
  atmosphereTurbulence: number;
41
+ clouds: number;
42
+ cloudColor: string;
43
+ cloudSpeed: number;
34
44
  ocean: number;
35
45
  rings: number;
36
46
  wireframe: boolean;
37
47
  rotationSpeed: number;
38
48
  };
39
49
  owner: B3d | null;
50
+ /**
51
+ * ANOTHER WORLD'S SURFACE, to draw this planet from: set it to a terrain's
52
+ * `planetSurface()` and the globe is that terrain seen from orbit (then
53
+ * `regenerate()`). Its heights are true metres on a planet of its own
54
+ * radius; `relief` says how much to exaggerate them, because real relief
55
+ * is invisible at this size. With a surface set, the planet's own noise
56
+ * settings are unused and the sea sits at the surface's sea level.
57
+ */
58
+ surface: PlanetSurface | null;
59
+ /**
60
+ * STORMS placed on the weather (needs `clouds` above 0): up to four, each
61
+ * `{ lat, lon, size, intensity, lightning }`. A storm is a cyclone that
62
+ * turns the right way for its hemisphere, with an eye, and lightning
63
+ * flickering inside it, which on the night side is all you see of it.
64
+ * Read every frame, so move one by changing its `lat` and `lon`.
65
+ */
66
+ storms: PlanetStorm[];
40
67
  grossFilter: GradientFilter;
41
68
  detailFilter: GradientFilter;
42
69
  private noise;
43
70
  private planetMesh;
44
71
  private atmosphereMesh;
45
72
  private oceanMesh;
73
+ private cloudMesh;
46
74
  /** Live-tunable biome shader parameters (biome="on") — see biome-plugin. */
47
75
  biomePlugin: BiomePlugin | null;
48
76
  private ringMesh;
49
- private rootNode;
77
+ /** The node everything of this planet hangs from: move it to place the planet. */
78
+ rootNode: BABYLON.TransformNode | null;
50
79
  private registered;
51
80
  private _beforeRender;
52
81
  private vertexHeights;
@@ -55,9 +84,26 @@ export declare class B3dPlanet extends B3dChild {
55
84
  sceneDispose(): void;
56
85
  private update;
57
86
  private buildPlanet;
87
+ private _heightMapLoaded;
88
+ private _surfaceFromMap;
89
+ /**
90
+ * Fetch `heightMap` (once per URL and sea setting) and rebuild from it. The
91
+ * planet shows as a smooth ball, in its colours, until the heights arrive.
92
+ */
93
+ private _loadHeightMap;
94
+ /** A vertex's height above the base sphere, in this planet's own units. */
95
+ private _heightFor;
58
96
  /** Get the height at a given percentile (0..1) of all vertex heights */
59
97
  private heightPercentile;
60
98
  private buildAtmosphere;
99
+ /** The highest ground above the base sphere, in this planet's units. */
100
+ private _maxHeight;
101
+ /**
102
+ * The weather, seen from orbit (planet-clouds): a shell just above the
103
+ * highest ground and INSIDE the air shell, which stays outside it as the
104
+ * glow at the limb.
105
+ */
106
+ private buildClouds;
61
107
  private buildOcean;
62
108
  private buildRings;
63
109
  private heightAt;
@@ -1 +1 @@
1
- {"version":3,"file":"b3d-planet.d.ts","sourceRoot":"","sources":["../src/b3d-planet.ts"],"names":[],"mappings":"AAuKA,OAAO,EAAS,QAAQ,EAAc,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGlE,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAGxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAoB1D,qBAAa,SAAU,SAAQ,QAAQ;IACrC,MAAM,CAAC,gBAAgB,SAAoB;IAE3C,MAAM,CAAC,eAAe;;;;MAIrB;IAED,MAAM,CAAC,cAAc;eAIH,IAAI,GAAG,KAAK;;;;;;;;;;;;;;;;;;;;;;;;MA4B7B;IAED,KAAK,EAAE,GAAG,GAAG,IAAI,CAAO;IACxB,WAAW,EAAE,cAAc,CAA8B;IACzD,YAAY,EAAE,cAAc,CAA8B;IAE1D,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,SAAS,CAA4B;IAC7C,4EAA4E;IAC5E,WAAW,EAAE,WAAW,GAAG,IAAI,CAAO;IACtC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;IAEjD,OAAO,eAAW;IAElB,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK;IAgB3C,YAAY;IAaZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,WAAW;IA0JnB,wEAAwE;IACxE,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,eAAe;IAkJvB,OAAO,CAAC,UAAU;IA0BlB,OAAO,CAAC,UAAU;IAgIlB,OAAO,CAAC,QAAQ;IAqBhB,sDAAsD;IACtD,UAAU;IAyBV,iDAAiD;IACjD,aAAa;CAoBd;AAED,eAAO,MAAM,SAAS,4CAA6B,CAAA"}
1
+ {"version":3,"file":"b3d-planet.d.ts","sourceRoot":"","sources":["../src/b3d-planet.ts"],"names":[],"mappings":"AAmLA,OAAO,EAAS,QAAQ,EAAc,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIzE,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAGxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAoB1D,qBAAa,SAAU,SAAQ,QAAQ;IACrC,MAAM,CAAC,gBAAgB,SAAoB;IAE3C,MAAM,CAAC,eAAe;;;;MAIrB;IAED,MAAM,CAAC,cAAc;eAIH,IAAI,GAAG,KAAK;;;;;iBAaV,IAAI,GAAG,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;MAgC/B;IAED,KAAK,EAAE,GAAG,GAAG,IAAI,CAAO;IACxB;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,GAAG,IAAI,CAAO;IACpC;;;;;;OAMG;IACH,MAAM,EAAE,WAAW,EAAE,CAAK;IAC1B,WAAW,EAAE,cAAc,CAA8B;IACzD,YAAY,EAAE,cAAc,CAA8B;IAE1D,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,SAAS,CAA4B;IAC7C,4EAA4E;IAC5E,WAAW,EAAE,WAAW,GAAG,IAAI,CAAO;IACtC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAC,aAAa,GAAG,IAAI,CAAO;IAC7C,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;IAEjD,OAAO,eAAW;IAElB,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK;IAkB3C,YAAY;IAgBZ,OAAO,CAAC,MAAM;IAed,OAAO,CAAC,WAAW;IA8KnB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,eAAe,CAAQ;IAC/B;;;OAGG;IACH,OAAO,CAAC,cAAc;IA4BtB,2EAA2E;IAC3E,OAAO,CAAC,UAAU;IAYlB,wEAAwE;IACxE,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,eAAe;IA+HvB,wEAAwE;IACxE,OAAO,CAAC,UAAU;IASlB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA+BnB,OAAO,CAAC,UAAU;IA6BlB,OAAO,CAAC,UAAU;IAgIlB,OAAO,CAAC,QAAQ;IAqBhB,sDAAsD;IACtD,UAAU;IA2BV,iDAAiD;IACjD,aAAa;CAqBd;AAED,eAAO,MAAM,SAAS,4CAA6B,CAAA"}
@@ -22,6 +22,7 @@ const { demo } = tosi({
22
22
  grossAmplitude: 5,
23
23
  detailAmplitude: 1,
24
24
  atmosphere: 0.08,
25
+ clouds: 0.45,
25
26
  ocean: 0.6,
26
27
  wireframe: false,
27
28
  rotationSpeed: 0.05,
@@ -39,6 +40,7 @@ const planet = b3dPlanet({
39
40
  grossAmplitude: demo.grossAmplitude,
40
41
  detailAmplitude: demo.detailAmplitude,
41
42
  atmosphere: demo.atmosphere,
43
+ clouds: demo.clouds,
42
44
  ocean: demo.ocean,
43
45
  wireframe: demo.wireframe,
44
46
  rotationSpeed: demo.rotationSpeed,
@@ -55,6 +57,7 @@ const scene = b3d(
55
57
  slider3d({ label: 'detail amp', value: demo.detailAmplitude, min: 0, max: 5, step: 0.1 }),
56
58
  slider3d({ label: 'rotation', value: demo.rotationSpeed, min: 0, max: 0.5, step: 0.01 }),
57
59
  slider3d({ label: 'atmosphere', value: demo.atmosphere, min: 0, max: 0.2, step: 0.01 }),
60
+ slider3d({ label: 'clouds', value: demo.clouds, min: 0, max: 1, step: 0.05 }),
58
61
  slider3d({ label: 'ocean', value: demo.ocean, min: 0, max: 1, step: 0.05 }),
59
62
  toggle3d({ label: 'wireframe', value: demo.wireframe }),
60
63
  // The biome climate, in the terms you think in: temperature at the
@@ -110,7 +113,7 @@ for (const key of ['grossScale', 'detailScale', 'grossAmplitude', 'detailAmplitu
110
113
  planet.regenerate()
111
114
  })
112
115
  }
113
- for (const key of ['atmosphere', 'ocean', 'wireframe', 'rotationSpeed']) {
116
+ for (const key of ['atmosphere', 'clouds', 'ocean', 'wireframe', 'rotationSpeed']) {
114
117
  demo[key].observe(() => {
115
118
  planet.updateOptions()
116
119
  })
@@ -142,6 +145,11 @@ tosi-b3d {
142
145
  | --- | --- | --- |
143
146
  | `seed` | `12345` | Noise seed |
144
147
  | `radius` | `50` | Base sphere radius |
148
+ | `heightMap` | `''` | A real world's HEIGHTS: a 16-bit greyscale PNG, 2:1, `metres = value − 32768` (see [planet-map](/planet-map/)). A full URL, or a path resolved by `assetUrl`: call `setAssetBase('https://cdn.tosijs.net')` once and `nasa/moon/height-2k.png`, `nasa/mars/…` and `noaa/earth/…` are there. Sets `surface` when it has loaded. Those maps are NASA's and NOAA's and free to use with credit, which is yours to show |
149
+ | `bodyRadiusKm` | `0` | The real body's radius in km, which is what the heights are a fraction of (Moon 1737.4, Mars 3389.5, Earth 6371). `0` reads the height map's `body-radius-km` header, which a browser only shows to a page on the same origin unless the server exposes it: set this for a map from another host |
150
+ | `colorMap` | `''` | A real world's COLOUR: a 2:1 image, longitude −180° to 180° left to right. `nasa/moon/color-2k.jpg` and so on. Replaces the biome shading and the sea sphere |
151
+ | `flatSea` | `'off'` | `'on'` fills everything below 0 m level, for a world whose colour map shows water (the Earth) |
152
+ | `relief` | `1` | With a `heightMap` or a `surface` set: how much its true relief is exaggerated. `1` is to scale, which on a globe is flat to the eye; 20 to 60 reads as a relief map |
145
153
  | `gas` | `''` | Make it a GAS GIANT: `'jupiter'`, `'saturn'`, `'uranus'` or `'neptune'`. A smooth banded sphere with winds and a storm in place of terrain, sea and air shell; `seed` gives a different planet of the same kind and `rings` still applies. See [gas-giant](/gas-giant/) |
146
154
  | `bands` | `0` | Gas giant: stripes from pole to pole. `0` = the preset's |
147
155
  | `turbulence` | `-1` | Gas giant: how torn the band edges are, `0…1`. `-1` = the preset's |
@@ -157,16 +165,24 @@ tosi-b3d {
157
165
  | `grossAmplitude` | `5` | Gross height multiplier |
158
166
  | `detailAmplitude` | `1` | Detail height multiplier |
159
167
  | `atmosphere` | `0.08` | Atmosphere thickness (fraction of radius, 0=none) |
168
+ | `clouds` | `0` | (Provisional: this is the weather from ORBIT only. It is its own field, and does not yet agree with the cloud deck or the weather cells on that world's ground.) Weather seen from orbit: how much of the sky is cloud, `0…1`. A shell inside the air, with winds that reverse with latitude, more cloud along the equator and the storm tracks, and a shadow on the ground beneath. `0` = none |
169
+ | `cloudColor` | `'#ffffff'` | The cloud's colour where lit |
170
+ | `storms` (property) | `[]` | Storms placed on the weather, up to four: `planet.storms = [{ lat: 22, lon: -60, size: 8, intensity: 1, lightning: 1 }]`. Each is a cyclone with an eye, turning the right way for its hemisphere, with lightning inside; `cyclone: 0` makes it a thunderstorm complex that sits and flashes. Needs `clouds` above 0. Provisional: a storm here is not yet a weather cell on the ground, and the shape of this may change when it is |
171
+ | `cloudSpeed` | `1` | How fast the weather moves. `0` holds it still |
160
172
  | `ocean` | `0.6` | Ocean coverage (0=none, 0.6=60% of surface underwater) |
161
173
  | `wireframe` | `false` | Debug: render as wireframe |
162
- | `rotationSpeed` | `0` | Auto-rotation speed (radians/sec) |
174
+ | `rotationSpeed` | `0` | Auto-rotation speed (radians/sec), west to east as planets turn. Negative turns it the other way |
163
175
 
164
176
  */
165
177
  /*{ "parent": "space", "order": 40 }*/
166
178
  import { Color } from 'tosijs';
167
179
  import { isOff, B3dChild, sceneDelta } from './b3d-utils.js';
168
180
  import { attachBiomePlugin } from './biome-plugin.js';
169
- import { gasGiantRgb, resolveGasGiant } from './gas-giant.js';
181
+ import { GAS_GIANT_GLSL, gasGiantRgb, resolveGasGiant } from './gas-giant.js';
182
+ import { loadPlanetMap } from './planet-map.js';
183
+ import { createPlanetClouds } from './planet-clouds.js';
184
+ import { createMappedPlanetMaterial } from './planet-map-material.js';
185
+ import { assetUrl } from './asset-url.js';
170
186
  import { createGasGiantMaterial } from './gas-giant-material.js';
171
187
  import * as BABYLON from '@babylonjs/core';
172
188
  import { PerlinNoise } from './perlin-noise.js';
@@ -203,6 +219,19 @@ export class B3dPlanet extends B3dChild {
203
219
  // A gas giant: 'jupiter' | 'saturn' | 'uranus' | 'neptune'. A smooth,
204
220
  // banded sphere in place of terrain and sea (see gas-giant).
205
221
  gas: '',
222
+ // A real world from its maps (planet-map): a 16-bit height PNG and an
223
+ // equirectangular colour image. Paths resolve through `assetUrl`.
224
+ heightMap: '',
225
+ colorMap: '',
226
+ // The real body's radius, km: what the heights are measured against.
227
+ // 0 = the height map's own `body-radius-km` header, where it can be read.
228
+ bodyRadiusKm: 0,
229
+ // Fill the seas level (heights below 0 read as 0): for a world whose
230
+ // colour map shows water.
231
+ flatSea: 'off',
232
+ // With `surface` set: how much its true relief is exaggerated (1 = to
233
+ // scale, which on a globe is flat to the eye).
234
+ relief: 1,
206
235
  // Gas giant overrides; 0 / -1 / '' mean the preset's own.
207
236
  bands: 0,
208
237
  turbulence: -1,
@@ -223,21 +252,44 @@ export class B3dPlanet extends B3dChild {
223
252
  atmosphere: 0.08,
224
253
  atmosphereColor: 'rgba(77,128,230,0.15)',
225
254
  atmosphereTurbulence: 0.5,
255
+ // Weather seen from orbit: how much of the sky is cloud, 0 = none.
256
+ clouds: 0,
257
+ cloudColor: '#ffffff',
258
+ cloudSpeed: 1,
226
259
  ocean: 0.6,
227
260
  rings: 0,
228
261
  wireframe: false,
229
262
  rotationSpeed: 0,
230
263
  };
231
264
  owner = null;
265
+ /**
266
+ * ANOTHER WORLD'S SURFACE, to draw this planet from: set it to a terrain's
267
+ * `planetSurface()` and the globe is that terrain seen from orbit (then
268
+ * `regenerate()`). Its heights are true metres on a planet of its own
269
+ * radius; `relief` says how much to exaggerate them, because real relief
270
+ * is invisible at this size. With a surface set, the planet's own noise
271
+ * settings are unused and the sea sits at the surface's sea level.
272
+ */
273
+ surface = null;
274
+ /**
275
+ * STORMS placed on the weather (needs `clouds` above 0): up to four, each
276
+ * `{ lat, lon, size, intensity, lightning }`. A storm is a cyclone that
277
+ * turns the right way for its hemisphere, with an eye, and lightning
278
+ * flickering inside it, which on the night side is all you see of it.
279
+ * Read every frame, so move one by changing its `lat` and `lon`.
280
+ */
281
+ storms = [];
232
282
  grossFilter = new PiecewiseLinearFilter();
233
283
  detailFilter = new PiecewiseLinearFilter();
234
284
  noise;
235
285
  planetMesh = null;
236
286
  atmosphereMesh = null;
237
287
  oceanMesh = null;
288
+ cloudMesh = null;
238
289
  /** Live-tunable biome shader parameters (biome="on") — see biome-plugin. */
239
290
  biomePlugin = null;
240
291
  ringMesh = null;
292
+ /** The node everything of this planet hangs from: move it to place the planet. */
241
293
  rootNode = null;
242
294
  registered = false;
243
295
  _beforeRender = null;
@@ -248,9 +300,11 @@ export class B3dPlanet extends B3dChild {
248
300
  const attrs = this;
249
301
  this.noise = new PerlinNoise(attrs.seed);
250
302
  this.rootNode = new BABYLON.TransformNode('planet-root', scene);
303
+ this._loadHeightMap();
251
304
  this.buildPlanet();
252
305
  this.buildAtmosphere();
253
306
  this.buildOcean();
307
+ this.buildClouds();
254
308
  this.buildRings();
255
309
  this._beforeRender = () => this.update();
256
310
  scene.registerBeforeRender(this._beforeRender);
@@ -264,7 +318,10 @@ export class B3dPlanet extends B3dChild {
264
318
  this.planetMesh = null;
265
319
  this.atmosphereMesh = null;
266
320
  this.oceanMesh = null;
321
+ this.cloudMesh = null;
267
322
  this.ringMesh = null;
323
+ // So a planet put back in a scene fetches its heights again.
324
+ this._heightMapLoaded = '';
268
325
  this.owner = null;
269
326
  }
270
327
  update() {
@@ -272,9 +329,14 @@ export class B3dPlanet extends B3dChild {
272
329
  return;
273
330
  const attrs = this;
274
331
  const speed = attrs.rotationSpeed;
275
- if (speed > 0) {
332
+ // Negative runs it the other way (the direction it had before 0.10.3).
333
+ if (speed !== 0) {
276
334
  const dt = sceneDelta(this.owner.scene);
277
- this.rootNode.rotation.y += speed * dt;
335
+ // WEST TO EAST, as planets turn: seen from above the north pole,
336
+ // anticlockwise. East is +longitude, toward +Z from +X, and in
337
+ // Babylon's left-handed frame that is a NEGATIVE turn about Y. (It was
338
+ // positive, and the Earth ran backwards.)
339
+ this.rootNode.rotation.y -= speed * dt;
278
340
  }
279
341
  }
280
342
  buildPlanet() {
@@ -331,7 +393,7 @@ export class B3dPlanet extends B3dChild {
331
393
  const ny = cy / len;
332
394
  const nz = cz / len;
333
395
  // Height from noise
334
- const h = this.heightAt(nx, ny, nz);
396
+ const h = this._heightFor(nx, ny, nz);
335
397
  heights[vertOffset] = h;
336
398
  const r = radius + h;
337
399
  const vi = vertOffset * 3;
@@ -364,6 +426,21 @@ export class B3dPlanet extends B3dChild {
364
426
  vertexData.indices = indices;
365
427
  vertexData.normals = normals;
366
428
  vertexData.applyToMesh(mesh, true);
429
+ if (attrs.colorMap) {
430
+ // A REAL WORLD'S COLOUR: the map, looked up by direction (planet-map).
431
+ const mapped = createMappedPlanetMaterial(this.owner.scene, assetUrl(String(attrs.colorMap)));
432
+ mapped.wireframe = attrs.wireframe;
433
+ mesh.material = mapped;
434
+ mesh.parent = this.rootNode;
435
+ this.planetMesh = mesh;
436
+ this.vertexHeights = heights;
437
+ this.biomePlugin = null;
438
+ if (!this.registered) {
439
+ this.registered = true;
440
+ this.owner.register({ meshes: [mesh] });
441
+ }
442
+ return;
443
+ }
367
444
  const mat = new BABYLON.StandardMaterial('planet-mat', this.owner.scene);
368
445
  mat.diffuseColor = new BABYLON.Color3(0.6, 0.75, 0.45);
369
446
  mat.specularColor = new BABYLON.Color3(0.05, 0.05, 0.05);
@@ -376,7 +453,8 @@ export class B3dPlanet extends B3dChild {
376
453
  // crawls if the planet SPINS — fine for a static planet; a rotating one
377
454
  // wants object-space sampling (a step-5 concern with triplanar).
378
455
  const seaRadius = attrs.ocean > 0
379
- ? attrs.radius + this.heightPercentile(attrs.ocean)
456
+ ? attrs.radius +
457
+ (this.surface != null ? 0 : this.heightPercentile(attrs.ocean))
380
458
  : 0.001;
381
459
  this.biomePlugin = attachBiomePlugin(mat, {
382
460
  seaRadius,
@@ -404,6 +482,53 @@ export class B3dPlanet extends B3dChild {
404
482
  this.owner.register({ meshes: [mesh] });
405
483
  }
406
484
  }
485
+ _heightMapLoaded = '';
486
+ _surfaceFromMap = false;
487
+ /**
488
+ * Fetch `heightMap` (once per URL and sea setting) and rebuild from it. The
489
+ * planet shows as a smooth ball, in its colours, until the heights arrive.
490
+ */
491
+ _loadHeightMap() {
492
+ const attrs = this;
493
+ const url = attrs.heightMap ? assetUrl(String(attrs.heightMap)) : '';
494
+ const key = `${url}|${attrs.flatSea}|${attrs.bodyRadiusKm}`;
495
+ if (key === this._heightMapLoaded)
496
+ return;
497
+ // A change of map (or none) drops the heights of the last one: the planet
498
+ // is its own noise, in the new colours, until the new heights arrive.
499
+ if (this._surfaceFromMap) {
500
+ this.surface = null;
501
+ this._surfaceFromMap = false;
502
+ }
503
+ this._heightMapLoaded = key;
504
+ if (url === '')
505
+ return;
506
+ loadPlanetMap(url, {
507
+ ...(isOff(attrs.flatSea) ? {} : { floor: 0 }),
508
+ ...(attrs.bodyRadiusKm > 0 ? { radius: attrs.bodyRadiusKm * 1000 } : {}),
509
+ })
510
+ .then((surface) => {
511
+ if (this.owner == null || key !== this._heightMapLoaded)
512
+ return;
513
+ this.surface = surface;
514
+ this._surfaceFromMap = true;
515
+ this.regenerate();
516
+ })
517
+ .catch((err) => {
518
+ console.warn(`b3d-planet: could not load heightMap "${url}"`, err);
519
+ });
520
+ }
521
+ /** A vertex's height above the base sphere, in this planet's own units. */
522
+ _heightFor(nx, ny, nz) {
523
+ const s = this.surface;
524
+ if (s == null)
525
+ return this.heightAt(nx, ny, nz);
526
+ const attrs = this;
527
+ const relief = attrs.relief > 0 ? attrs.relief : 1;
528
+ return (((s.heightAt({ x: nx, y: ny, z: nz }) - s.seaLevel) / s.radius) *
529
+ attrs.radius *
530
+ relief);
531
+ }
407
532
  /** Get the height at a given percentile (0..1) of all vertex heights */
408
533
  heightPercentile(p) {
409
534
  if (this.vertexHeights == null)
@@ -420,9 +545,16 @@ export class B3dPlanet extends B3dChild {
420
545
  if (atmoFrac <= 0 || attrs.gas)
421
546
  return;
422
547
  const scene = this.owner.scene;
423
- // Register atmosphere shader once
424
- if (!BABYLON.Effect.ShadersStore['planetAtmoVertexShader']) {
425
- BABYLON.Effect.ShadersStore['planetAtmoVertexShader'] = `
548
+ /*
549
+ Register the atmosphere shader once. Its clouds are 3D noise read ON THE
550
+ SPHERE, at the point itself, in the planet's own frame. They used to be
551
+ 2D noise over latitude and longitude, which pinches to a point at each
552
+ pole and has a seam down the back; and they were read from the WORLD
553
+ normal, so they stood still while a rotating planet turned under them.
554
+ The winds are the point turned about the axis before the noise is read.
555
+ */
556
+ if (!BABYLON.Effect.ShadersStore['planetAtmo2VertexShader']) {
557
+ BABYLON.Effect.ShadersStore['planetAtmo2VertexShader'] = `
426
558
  precision highp float;
427
559
  attribute vec3 position;
428
560
  attribute vec3 normal;
@@ -430,62 +562,36 @@ export class B3dPlanet extends B3dChild {
430
562
  uniform mat4 world;
431
563
  varying vec3 vWorldPos;
432
564
  varying vec3 vWorldNormal;
565
+ varying vec3 vBody;
433
566
  void main() {
434
567
  gl_Position = worldViewProjection * vec4(position, 1.0);
435
568
  vWorldPos = (world * vec4(position, 1.0)).xyz;
436
569
  vWorldNormal = normalize((world * vec4(normal, 0.0)).xyz);
570
+ vBody = position;
437
571
  }
438
572
  `;
439
- BABYLON.Effect.ShadersStore['planetAtmoFragmentShader'] = `
573
+ BABYLON.Effect.ShadersStore['planetAtmo2FragmentShader'] = `
440
574
  precision highp float;
441
575
  varying vec3 vWorldPos;
442
576
  varying vec3 vWorldNormal;
577
+ varying vec3 vBody;
443
578
  uniform vec3 cameraPosition;
444
579
  uniform vec3 atmoColor;
445
580
  uniform float atmoOpacity;
446
581
  uniform float time;
447
582
  uniform float turbulence;
448
-
449
- float hash(vec2 p) {
450
- vec3 p3 = fract(vec3(p.xyx) * 0.1031);
451
- p3 += dot(p3, p3.yzx + 33.33);
452
- return fract((p3.x + p3.y) * p3.z);
453
- }
454
-
455
- float noise(vec2 p) {
456
- vec2 i = floor(p);
457
- vec2 f = fract(p);
458
- f = f * f * (3.0 - 2.0 * f);
459
- float a = hash(i);
460
- float b = hash(i + vec2(1.0, 0.0));
461
- float c = hash(i + vec2(0.0, 1.0));
462
- float d = hash(i + vec2(1.0, 1.0));
463
- return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
464
- }
465
-
466
- float fbm(vec2 p) {
467
- float v = 0.0;
468
- float amp = 0.5;
469
- for (int i = 0; i < 4; i++) {
470
- v += amp * noise(p);
471
- p *= 2.0;
472
- amp *= 0.5;
473
- }
474
- return v;
475
- }
476
-
583
+ ${GAS_GIANT_GLSL}
477
584
  void main() {
478
585
  vec3 viewDir = normalize(cameraPosition - vWorldPos);
479
586
  float facing = abs(dot(viewDir, vWorldNormal));
480
587
  // Transparent at edges (dot→0), visible when facing camera
481
588
  float glow = pow(facing, 0.5);
482
589
 
483
- // Turbulent cloud patterns
484
- vec3 n = normalize(vWorldNormal);
485
- float lat = asin(n.y) * 3.0;
486
- float lon = atan(n.z, n.x) * 3.0;
487
- float turb = fbm(vec2(lon + time * 0.02, lat) * 3.0) * turbulence;
488
- turb += fbm(vec2(lon - time * 0.01, lat * 1.5 + time * 0.005) * 5.0) * turbulence * 0.5;
590
+ // Turbulent cloud patterns: two layers drifting opposite ways.
591
+ vec3 d = normalize(vBody);
592
+ float turb = b3dGgFbm(b3dGgTurn(d, time * 0.02) * 9.0) * turbulence;
593
+ turb += b3dGgFbm(b3dGgTurn(d, -time * 0.01) * vec3(15.0, 22.0, 15.0)
594
+ + vec3(0.0, time * 0.025, 3.1)) * turbulence * 0.5;
489
595
 
490
596
  float alpha = glow * atmoOpacity + turb * 0.3;
491
597
  alpha = clamp(alpha, 0.0, 1.0);
@@ -508,7 +614,7 @@ export class B3dPlanet extends B3dChild {
508
614
  }
509
615
  const atmoRadius = attrs.radius + maxH + atmoFrac * attrs.radius;
510
616
  const mesh = BABYLON.MeshBuilder.CreateSphere('atmosphere', { diameter: atmoRadius * 2, segments: 32 }, scene);
511
- const mat = new BABYLON.ShaderMaterial('atmo-shader-mat', scene, { vertex: 'planetAtmo', fragment: 'planetAtmo' }, {
617
+ const mat = new BABYLON.ShaderMaterial('atmo-shader-mat', scene, { vertex: 'planetAtmo2', fragment: 'planetAtmo2' }, {
512
618
  attributes: ['position', 'normal'],
513
619
  uniforms: [
514
620
  'worldViewProjection',
@@ -539,16 +645,60 @@ export class B3dPlanet extends B3dChild {
539
645
  mesh.parent = this.rootNode;
540
646
  this.atmosphereMesh = mesh;
541
647
  }
648
+ /** The highest ground above the base sphere, in this planet's units. */
649
+ _maxHeight() {
650
+ const attrs = this;
651
+ if (this.vertexHeights == null || this.vertexHeights.length === 0)
652
+ return attrs.gas ? 0 : attrs.grossAmplitude + attrs.detailAmplitude;
653
+ let max = -Infinity;
654
+ for (const h of this.vertexHeights)
655
+ if (h > max)
656
+ max = h;
657
+ return max;
658
+ }
659
+ /**
660
+ * The weather, seen from orbit (planet-clouds): a shell just above the
661
+ * highest ground and INSIDE the air shell, which stays outside it as the
662
+ * glow at the limb.
663
+ */
664
+ buildClouds() {
665
+ if (this.cloudMesh) {
666
+ this.cloudMesh.dispose(false, true);
667
+ this.cloudMesh = null;
668
+ }
669
+ if (this.owner == null || this.rootNode == null)
670
+ return;
671
+ const attrs = this;
672
+ if (!(attrs.clouds > 0) || attrs.gas)
673
+ return;
674
+ let color;
675
+ try {
676
+ color = BABYLON.Color3.FromHexString(String(attrs.cloudColor || '#ffffff').slice(0, 7));
677
+ }
678
+ catch {
679
+ color = new BABYLON.Color3(1, 1, 1);
680
+ }
681
+ const lift = Math.max(0.004, Math.min(0.02, attrs.atmosphere * 0.3));
682
+ this.cloudMesh = createPlanetClouds(this.owner.scene, this.rootNode, attrs.radius + Math.max(0, this._maxHeight()) + lift * attrs.radius, {
683
+ coverage: Math.min(1, attrs.clouds),
684
+ color,
685
+ speed: attrs.cloudSpeed,
686
+ seed: 1 + (Math.abs(attrs.seed) % 89),
687
+ storms: () => this.storms,
688
+ });
689
+ }
542
690
  buildOcean() {
543
691
  if (this.owner == null || this.rootNode == null)
544
692
  return;
545
693
  const attrs = this;
546
694
  const oceanPct = attrs.ocean;
547
- if (oceanPct <= 0 || attrs.gas)
695
+ // A colour map already shows its seas.
696
+ if (oceanPct <= 0 || attrs.gas || attrs.colorMap)
548
697
  return;
549
698
  // Ocean radius: height at the given percentile of vertices
550
699
  // e.g. ocean=0.6 means 60% of surface is underwater
551
- const oceanHeight = this.heightPercentile(oceanPct);
700
+ // A borrowed surface says where its sea is: the datum, height 0 here.
701
+ const oceanHeight = this.surface != null ? 0 : this.heightPercentile(oceanPct);
552
702
  const oceanRadius = attrs.radius + oceanHeight;
553
703
  const mesh = BABYLON.MeshBuilder.CreateSphere('ocean', { diameter: oceanRadius * 2, segments: 32 }, this.owner.scene);
554
704
  const mat = new BABYLON.StandardMaterial('ocean-mat', this.owner.scene);
@@ -702,9 +852,11 @@ export class B3dPlanet extends B3dChild {
702
852
  this.ringMesh = null;
703
853
  }
704
854
  this.registered = false;
855
+ this._loadHeightMap();
705
856
  this.buildPlanet();
706
857
  this.buildAtmosphere();
707
858
  this.buildOcean();
859
+ this.buildClouds();
708
860
  this.buildRings();
709
861
  }
710
862
  /** Update atmosphere/ocean/wireframe/rotation */
@@ -721,6 +873,7 @@ export class B3dPlanet extends B3dChild {
721
873
  }
722
874
  this.buildAtmosphere();
723
875
  this.buildOcean();
876
+ this.buildClouds();
724
877
  if (this.planetMesh?.material) {
725
878
  ;
726
879
  this.planetMesh.material.wireframe =