entangled-worlds-orbiters-shared 3.1.0 → 3.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/README.md +1 -1
- package/dist/star/framing.d.ts +17 -6
- package/dist/star/framing.js +16 -52
- package/dist/star/index.d.ts +1 -1
- package/dist/star/index.js +1 -1
- package/dist/star/layout.js +1 -2
- package/dist/star/params.js +3 -2
- package/dist/types/config/env.d.ts +12 -0
- package/dist/types/config/index.d.ts +2 -0
- package/dist/types/config/textureConfig.d.ts +121 -0
- package/dist/types/core/index.d.ts +2 -0
- package/dist/types/core/lightFresnel.d.ts +5 -0
- package/dist/types/core/lights.d.ts +58 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/loaders/bodyTextureLoader.d.ts +12 -0
- package/dist/types/loaders/cubeMapLoader.d.ts +70 -0
- package/dist/types/loaders/glbLoader.d.ts +24 -0
- package/dist/types/loaders/index.d.ts +5 -0
- package/dist/types/loaders/ktx2.d.ts +16 -0
- package/dist/types/moons/index.d.ts +2 -0
- package/dist/types/moons/moonsGeneration.d.ts +72 -0
- package/dist/types/moons/moonsManager.d.ts +27 -0
- package/dist/types/render/frameBudget.d.ts +29 -0
- package/dist/types/render/index.d.ts +2 -0
- package/dist/types/render/viewportCompositor.d.ts +58 -0
- package/dist/types/utils/dispose.d.ts +2 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/materials.d.ts +2 -0
- package/dist/types/world/cloudShell.d.ts +25 -0
- package/dist/types/world/index.d.ts +6 -0
- package/dist/types/world/worldGLB.d.ts +20 -0
- package/dist/types/world/worldManager.d.ts +63 -0
- package/dist/types/world/worldNormal.d.ts +43 -0
- package/dist/types/world/worldTexture.d.ts +80 -0
- package/package.json +35 -10
- package/src/config/env.js +8 -0
- package/src/core/lights.js +12 -0
- package/src/loaders/bodyTextureLoader.js +7 -0
- package/src/loaders/cubeMapLoader.js +27 -0
- package/src/loaders/glbLoader.js +7 -1
- package/src/loaders/index.js +3 -1
- package/src/moons/moonsGeneration.js +32 -2
- package/src/moons/moonsManager.js +1 -1
- package/src/star/framing.ts +17 -55
- package/src/star/index.ts +1 -1
- package/src/star/layout.ts +1 -2
- package/src/star/params.ts +3 -2
- package/src/world/cloudShell.js +13 -0
- package/src/world/index.js +3 -1
- package/src/world/worldGLB.js +12 -1
- package/src/world/worldManager.js +32 -1
- package/src/world/worldNormal.js +19 -0
- package/src/world/worldTexture.js +41 -0
package/src/star/framing.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Camera framing helpers (pure, no three): how far a camera must sit to fit a focus sphere, and a
|
|
2
|
-
//
|
|
2
|
+
// default placement for the star that lights the planet's side facing the camera.
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { DEFAULT_STAR_PARAMS, STAR_PARAM_RANGES } from './params.js';
|
|
4
|
+
import { DEFAULT_STAR_PARAMS } from './params.js';
|
|
6
5
|
|
|
7
6
|
const DEG = Math.PI / 180;
|
|
8
7
|
|
|
@@ -35,62 +34,25 @@ export interface StarPlacementInput {
|
|
|
35
34
|
export interface StarPlacement {
|
|
36
35
|
azimuth: number;
|
|
37
36
|
elevation: number;
|
|
38
|
-
/**
|
|
37
|
+
/** Always true (kept for older hosts): the spot is behind any camera closer than ~0.79 × the star's distance (≥ 15.8 units). */
|
|
39
38
|
fits: boolean;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* The opening light: the star over the camera's right shoulder (behind it, a little up and to the
|
|
43
|
+
* right), so the planet opens on its day side — about 90% of the face the camera sees is lit, the
|
|
44
|
+
* night edge on the left — and the star itself shows when the viewer orbits round. 145° / 15° is
|
|
45
|
+
* ~38° off the view axis. It is `DEFAULT_STAR_PARAMS`' direction, so a host with no camera gets it too.
|
|
47
46
|
*/
|
|
48
|
-
export
|
|
49
|
-
const fov = Number.isFinite(input.fovDeg) && input.fovDeg > 0 ? input.fovDeg : 45;
|
|
50
|
-
const aspect = Number.isFinite(input.aspect) && input.aspect > 0 ? input.aspect : 1;
|
|
51
|
-
const cd = Number.isFinite(input.cameraDistance) && input.cameraDistance > 0 ? input.cameraDistance : 4;
|
|
52
|
-
const pr = Math.max(0, Number.isFinite(input.planetRadius) ? input.planetRadius : 1);
|
|
53
|
-
const starRadius = input.starRadius ?? DEFAULT_STAR_PARAMS.radius;
|
|
54
|
-
const [dLo, dHi] = STAR_PARAM_RANGES.distance;
|
|
55
|
-
const D = Math.min(dHi, Math.max(dLo, input.distance ?? DEFAULT_STAR_PARAMS.distance));
|
|
47
|
+
export const OVER_THE_SHOULDER = { azimuth: DEFAULT_STAR_PARAMS.azimuth, elevation: DEFAULT_STAR_PARAMS.elevation } as const;
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let x = 0;
|
|
67
|
-
let y = Number.isFinite(rho) ? rho : maxY;
|
|
68
|
-
let fits = false;
|
|
69
|
-
for (let phiDeg = 30; phiDeg <= 90; phiDeg += 2) {
|
|
70
|
-
const px = rho * Math.cos(phiDeg * DEG);
|
|
71
|
-
const py = rho * Math.sin(phiDeg * DEG);
|
|
72
|
-
if (px <= maxX && py <= maxY) {
|
|
73
|
-
x = px;
|
|
74
|
-
y = py;
|
|
75
|
-
fits = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Ray from the camera through (x, y) on the image plane, hit on the sphere |P| = D.
|
|
81
|
-
const len = Math.hypot(x, y, 1);
|
|
82
|
-
const ux = x / len;
|
|
83
|
-
const uy = y / len;
|
|
84
|
-
const uz = -1 / len;
|
|
85
|
-
const cu = cd * uz; // C · u with C = (0, 0, cd)
|
|
86
|
-
const t = -cu + Math.sqrt(Math.max(0, cu * cu - cd * cd + D * D));
|
|
87
|
-
const px = ux * t;
|
|
88
|
-
const py = uy * t;
|
|
89
|
-
const pz = cd + uz * t;
|
|
90
|
-
const round = (v: number) => Math.round(v * 10) / 10;
|
|
91
|
-
return {
|
|
92
|
-
azimuth: round(Math.atan2(px, -pz) / DEG),
|
|
93
|
-
elevation: round(Math.asin(Math.max(-1, Math.min(1, py / D))) / DEG),
|
|
94
|
-
fits,
|
|
95
|
-
};
|
|
49
|
+
/**
|
|
50
|
+
* Azimuth / elevation of the default star for a camera on +Z looking at the origin: over the
|
|
51
|
+
* camera's shoulder (`OVER_THE_SHOULDER`), so the world opens lit. The star sits ≥ 0.79 × its
|
|
52
|
+
* distance (≥ 15.8 units) out on +Z, behind any host camera, so the frame never changes it. Hosts
|
|
53
|
+
* use it only when the author has not set azimuth/elevation; `DEFAULT_STAR_PARAMS` stays the
|
|
54
|
+
* host-agnostic fallback.
|
|
55
|
+
*/
|
|
56
|
+
export function defaultStarPlacement(_input?: StarPlacementInput): StarPlacement {
|
|
57
|
+
return { azimuth: OVER_THE_SHOULDER.azimuth, elevation: OVER_THE_SHOULDER.elevation, fits: true };
|
|
96
58
|
}
|
package/src/star/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { StarParams, StarMultiplicity, SpectralClass } from './params.js';
|
|
|
19
19
|
export { blackbodyColor } from './blackbody.js';
|
|
20
20
|
export type { RGB } from './blackbody.js';
|
|
21
21
|
export { starLayout, starPositions, starSystemPosition, starFocusRadius, flareAt } from './layout.js';
|
|
22
|
-
export { framingDistance, defaultStarPlacement } from './framing.js';
|
|
22
|
+
export { framingDistance, defaultStarPlacement, OVER_THE_SHOULDER } from './framing.js';
|
|
23
23
|
export type { StarPlacement, StarPlacementInput } from './framing.js';
|
|
24
24
|
export type { StarLayout, LaidOutStar, StarOrbit, StarRole, FlareState } from './layout.js';
|
|
25
25
|
export {
|
package/src/star/layout.ts
CHANGED
|
@@ -203,8 +203,7 @@ export function starsReach(layout: Pick<StarLayout, 'stars' | 'pairOrbit'>): num
|
|
|
203
203
|
function distantFor(p: StarParams, reach: number): StarLayout['distant'] {
|
|
204
204
|
if (!p.distantCompanion) return null;
|
|
205
205
|
const r = createLcg(`${p.seed}|distant`);
|
|
206
|
-
// Default:
|
|
207
|
-
// behind it, and inside a typical frame when the host itself is placed beside the planet.
|
|
206
|
+
// Default: a little above the host and swung towards the scene's −Z axis — never on top of it.
|
|
208
207
|
const inward = p.azimuth >= 0 ? -1 : 1;
|
|
209
208
|
let azimuth = p.distantAzimuth ?? wrapDeg(p.azimuth + inward * lerp(12, 22, r()));
|
|
210
209
|
let elevation = p.distantElevation ?? Math.min(60, p.elevation + lerp(6, 11, r()));
|
package/src/star/params.ts
CHANGED
|
@@ -55,8 +55,9 @@ export const DEFAULT_STAR_PARAMS: Readonly<StarParams> = Object.freeze({
|
|
|
55
55
|
separation: 2,
|
|
56
56
|
seed: 1,
|
|
57
57
|
distance: 20,
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Over the camera's shoulder (`OVER_THE_SHOULDER` in framing.ts): worlds open on their day side.
|
|
59
|
+
azimuth: 145,
|
|
60
|
+
elevation: 15,
|
|
60
61
|
companionTemperatureK: 0,
|
|
61
62
|
companionRadius: 0,
|
|
62
63
|
distantCompanion: false,
|
package/src/world/cloudShell.js
CHANGED
|
@@ -53,6 +53,19 @@ async function makeTextureMaterial({
|
|
|
53
53
|
return mat;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} [opts]
|
|
58
|
+
* @param {import('three').BufferGeometry} [opts.baseGeometry] cloned for the shell; when absent a
|
|
59
|
+
* sphere is built from `baseRadius` and the segment counts.
|
|
60
|
+
* @param {number} [opts.baseRadius=0.5]
|
|
61
|
+
* @param {number} [opts.widthSegments=64]
|
|
62
|
+
* @param {number} [opts.heightSegments=64]
|
|
63
|
+
* @param {string} [opts.textureUrl]
|
|
64
|
+
* @param {number} [opts.color=0xffffff]
|
|
65
|
+
* @param {number} [opts.opacity=0.6]
|
|
66
|
+
* @param {number} [opts.speed=0.015]
|
|
67
|
+
* @param {import('three').WebGLRenderer | null} [opts.renderer]
|
|
68
|
+
*/
|
|
56
69
|
export async function createCloudShell({
|
|
57
70
|
baseGeometry,
|
|
58
71
|
baseRadius = 0.5,
|
package/src/world/index.js
CHANGED
|
@@ -3,4 +3,6 @@ export { WorldManager } from './worldManager.js';
|
|
|
3
3
|
export { makeSphere, EARTH_RADIUS_UNITS } from './worldTexture.js';
|
|
4
4
|
export { makeNormalSphere } from './worldNormal.js';
|
|
5
5
|
export { makeGLB } from './worldGLB.js';
|
|
6
|
-
export { createCloudShell } from './cloudShell.js';
|
|
6
|
+
export { createCloudShell } from './cloudShell.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('./worldManager.js').WorldCreator} WorldCreator */
|
package/src/world/worldGLB.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { loadGLB } from '../loaders/glbLoader.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* `emissiveIntensity`: textured self-glow (1 = old flat-lit look; ≈0.15 under a star light).
|
|
5
|
+
*
|
|
6
|
+
* @param {object} [opts]
|
|
7
|
+
* @param {string} opts.modelUrl REQUIRED; throws without it.
|
|
8
|
+
* @param {object} [opts.flags] material flags (`forceOpaque`, `wireframe`).
|
|
9
|
+
* @param {number} [opts.retries=3]
|
|
10
|
+
* @param {boolean} [opts.withCredentials=false]
|
|
11
|
+
* @param {import('three').WebGLRenderer | null} [opts.renderer] REQUIRED at runtime (`loadGLB` throws
|
|
12
|
+
* without one); nullable here because `WorldManager` threads its own, which may be null.
|
|
13
|
+
* @param {number} [opts.emissiveIntensity]
|
|
14
|
+
*/
|
|
4
15
|
export async function makeGLB({ modelUrl, flags, retries = 3, withCredentials = false, renderer, emissiveIntensity } = {}) {
|
|
5
16
|
if (!modelUrl) throw new Error('modelUrl is required');
|
|
6
17
|
const obj = await loadGLB(modelUrl, { retries, applyFlags: flags, withCredentials, renderer, emissiveIntensity });
|
|
@@ -3,7 +3,22 @@ import { makeGLB } from './worldGLB.js';
|
|
|
3
3
|
import { makeSphere } from './worldTexture.js';
|
|
4
4
|
import { makeNormalSphere } from './worldNormal.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Builds a world object for a mode registered with `registerWorldCreator`. It receives the manager's
|
|
8
|
+
* flags, the `setMode` args and the renderer, and may return the object or a promise of it (the
|
|
9
|
+
* manager awaits either).
|
|
10
|
+
* @typedef {(args: { flags: object, renderer: import('three').WebGLRenderer | null, [key: string]: unknown }) => import('three').Object3D | Promise<import('three').Object3D>} WorldCreator
|
|
11
|
+
*/
|
|
12
|
+
|
|
6
13
|
export class WorldManager {
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('three').Scene} scene
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {() => void} [opts.onLoadStart]
|
|
18
|
+
* @param {() => void} [opts.onLoadEnd]
|
|
19
|
+
* @param {import('three').WebGLRenderer | null} [opts.renderer]
|
|
20
|
+
* @param {boolean} [opts.externalTick=false]
|
|
21
|
+
*/
|
|
7
22
|
constructor(scene, { onLoadStart, onLoadEnd, renderer, externalTick = false } = {}) {
|
|
8
23
|
this.scene = scene;
|
|
9
24
|
// The renderer this world belongs to — threaded to the texture loaders so each view's textures
|
|
@@ -16,6 +31,7 @@ export class WorldManager {
|
|
|
16
31
|
// defeating the one-loop compositor.
|
|
17
32
|
this.externalTick = Boolean(externalTick);
|
|
18
33
|
this.current = null;
|
|
34
|
+
/** @type {Record<string, any>} */
|
|
19
35
|
this.flags = {};
|
|
20
36
|
this.onLoadStart = onLoadStart;
|
|
21
37
|
this.onLoadEnd = onLoadEnd;
|
|
@@ -24,7 +40,10 @@ export class WorldManager {
|
|
|
24
40
|
this.customCreators = new Map();
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Run the current world's per-frame updater once (externalTick hosts only; no-op otherwise).
|
|
45
|
+
* @param {number} timeSec
|
|
46
|
+
*/
|
|
28
47
|
tick(timeSec) {
|
|
29
48
|
if (!this.externalTick || !this.current) return;
|
|
30
49
|
const updater = this.current.userData?.update;
|
|
@@ -38,6 +57,7 @@ export class WorldManager {
|
|
|
38
57
|
}
|
|
39
58
|
}
|
|
40
59
|
|
|
60
|
+
/** @param {object} flags merged over the current flags */
|
|
41
61
|
setFlags(flags) {
|
|
42
62
|
this.flags = { ...this.flags, ...flags };
|
|
43
63
|
}
|
|
@@ -51,10 +71,21 @@ export class WorldManager {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
// Register custom world creators for app-specific world types
|
|
74
|
+
/**
|
|
75
|
+
* @param {number} mode
|
|
76
|
+
* @param {WorldCreator} creatorFunction
|
|
77
|
+
*/
|
|
54
78
|
registerWorldCreator(mode, creatorFunction) {
|
|
55
79
|
this.customCreators.set(mode, creatorFunction);
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Build and show the world for `mode` (a registered creator, 2 = GLB, 3 = normal sphere, else the
|
|
84
|
+
* textured sphere), replacing the current one. A newer call supersedes one still loading.
|
|
85
|
+
* @param {number} mode
|
|
86
|
+
* @param {Record<string, any>} [args]
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
58
89
|
async setMode(mode, args = {}) {
|
|
59
90
|
const myToken = ++this.token;
|
|
60
91
|
this.onLoadStart?.();
|
package/src/world/worldNormal.js
CHANGED
|
@@ -9,6 +9,25 @@ import { resolvePublicUrl } from '../config/env.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Create a simple sphere using THREE.MeshNormalMaterial.
|
|
11
11
|
* Accepts the same basic geometry params as textured sphere so the manager API is consistent.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [opts]
|
|
14
|
+
* @param {number} [opts.sphereRadius]
|
|
15
|
+
* @param {number} [opts.sphereWidthSegments]
|
|
16
|
+
* @param {number} [opts.sphereHeightSegments]
|
|
17
|
+
* @param {object} [opts.flags] material flags (`forceOpaque`, `wireframe`).
|
|
18
|
+
* @param {boolean} [opts.glow] add the fresnel halo.
|
|
19
|
+
* @param {number} [opts.glowScale=1.18]
|
|
20
|
+
* @param {number} [opts.glowRimHex=0x66ccff]
|
|
21
|
+
* @param {number} [opts.glowFacingHex=0x000000]
|
|
22
|
+
* @param {number} [opts.glowBias=0.02]
|
|
23
|
+
* @param {number} [opts.glowFresnelScale=1]
|
|
24
|
+
* @param {number} [opts.glowPower=3.5]
|
|
25
|
+
* @param {boolean} [opts.cloudsEnabled]
|
|
26
|
+
* @param {string} [opts.cloudsTextureUrl]
|
|
27
|
+
* @param {number} [opts.cloudsColor=0xffffff]
|
|
28
|
+
* @param {number} [opts.cloudsOpacity=0.6]
|
|
29
|
+
* @param {number} [opts.cloudsSpeed=0.015]
|
|
30
|
+
* @param {import('three').WebGLRenderer | null} [opts.renderer]
|
|
12
31
|
*/
|
|
13
32
|
export async function makeNormalSphere({
|
|
14
33
|
sphereRadius,
|
|
@@ -50,6 +50,47 @@ function isLikelyEquirectangular(img) {
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The textured world sphere. Every option is optional. The offset, repeat, rotation, flip and sphere
|
|
55
|
+
* geometry options are read only when they have their type; the rest are used as given (`glow`,
|
|
56
|
+
* `cloudsEnabled` and `mirrorX` by truthiness).
|
|
57
|
+
*
|
|
58
|
+
* @param {object} [opts]
|
|
59
|
+
* @param {string} [opts.textureUrl]
|
|
60
|
+
* @param {import('three').WebGLRenderer | null} [opts.renderer] REQUIRED for a KTX2 texture.
|
|
61
|
+
* @param {object} [opts.flags] material flags (`forceOpaque`, `wireframe`).
|
|
62
|
+
* @param {number} [opts.uOffset] horizontal texture offset, 0..1 (wins over `textureOffsetDeg`).
|
|
63
|
+
* @param {number} [opts.textureOffsetDeg] horizontal texture offset in degrees.
|
|
64
|
+
* @param {boolean} [opts.mirrorX]
|
|
65
|
+
* @param {boolean} [opts.mirrorDuplicateX] explicit override of the equirectangular detection.
|
|
66
|
+
* @param {boolean} [opts.autoDetectEquirectangular=true]
|
|
67
|
+
* @param {boolean} [opts.forceEquirectangular=false]
|
|
68
|
+
* @param {boolean} [opts.forceRectangular=false]
|
|
69
|
+
* @param {string} [opts.wrapS='repeat'] `'clamp'`, `'repeat'` or `'mirror'`.
|
|
70
|
+
* @param {string} [opts.wrapT='clamp'] `'clamp'`, `'repeat'` or `'mirror'`.
|
|
71
|
+
* @param {number} [opts.repeatX]
|
|
72
|
+
* @param {number} [opts.repeatY]
|
|
73
|
+
* @param {number} [opts.rotationDeg]
|
|
74
|
+
* @param {number[]} [opts.center=[0.5, 0.5]] texture rotation centre, `[u, v]`.
|
|
75
|
+
* @param {boolean} [opts.flipY]
|
|
76
|
+
* @param {number} [opts.sphereRadius]
|
|
77
|
+
* @param {number} [opts.sphereWidthSegments]
|
|
78
|
+
* @param {number} [opts.sphereHeightSegments]
|
|
79
|
+
* @param {number} [opts.materialRoughness] accepted from saved worlds; no longer changes the look.
|
|
80
|
+
* @param {number} [opts.materialMetalness] accepted from saved worlds; no longer changes the look.
|
|
81
|
+
* @param {boolean} [opts.glow=true]
|
|
82
|
+
* @param {number} [opts.glowRimHex=0x66ccff]
|
|
83
|
+
* @param {number} [opts.glowFacingHex=0x000000]
|
|
84
|
+
* @param {number} [opts.glowScale=1.18]
|
|
85
|
+
* @param {number} [opts.glowBias=0.02]
|
|
86
|
+
* @param {number} [opts.glowFresnelScale=1]
|
|
87
|
+
* @param {number} [opts.glowPower=3.5]
|
|
88
|
+
* @param {boolean} [opts.cloudsEnabled=false]
|
|
89
|
+
* @param {string} [opts.cloudsTextureUrl]
|
|
90
|
+
* @param {number} [opts.cloudsColor=0xffffff]
|
|
91
|
+
* @param {number} [opts.cloudsOpacity=0.6]
|
|
92
|
+
* @param {number} [opts.cloudsSpeed=0.015]
|
|
93
|
+
*/
|
|
53
94
|
export async function makeSphere({
|
|
54
95
|
textureUrl,
|
|
55
96
|
renderer,
|