@reearth/core 0.0.7-alpha.44 → 0.0.7-alpha.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reearth/core",
3
- "version": "0.0.7-alpha.44",
3
+ "version": "0.0.7-alpha.45",
4
4
  "author": "Re:Earth contributors <community@reearth.io>",
5
5
  "license": "Apache-2.0",
6
6
  "description": "A library that abstracts a map engine as one common API.",
@@ -0,0 +1,73 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { Globe as CesiumGlobe } from "resium";
3
+
4
+ import type { ViewerProperty } from "../../..";
5
+ import { toColor } from "../../common";
6
+
7
+ import useTerrainProviderPromise from "./useTerrainProviderPromise";
8
+
9
+ export type Props = {
10
+ property?: ViewerProperty;
11
+ cesiumIonAccessToken?: string;
12
+ onTerrainProviderChange?: () => void;
13
+ };
14
+
15
+ export default function Globe({
16
+ property,
17
+ cesiumIonAccessToken,
18
+ onTerrainProviderChange,
19
+ }: Props): JSX.Element | null {
20
+ const providerPromise = useTerrainProviderPromise({
21
+ terrain: property?.terrain?.enabled,
22
+ terrainType: property?.terrain?.type,
23
+ normal: property?.terrain?.normal,
24
+ ionAccessToken: property?.assets?.cesium?.terrain?.ionAccessToken || cesiumIonAccessToken,
25
+ ionAsset: property?.assets?.cesium?.terrain?.ionAsset,
26
+ ionUrl: property?.assets?.cesium?.terrain?.ionUrl,
27
+ });
28
+
29
+ const baseColor = useMemo(
30
+ () => toColor(property?.globe?.baseColor),
31
+ [property?.globe?.baseColor],
32
+ );
33
+
34
+ const lastResolvedProviderRef = useRef<any>(null);
35
+
36
+ useEffect(() => {
37
+ let isCancelled = false;
38
+
39
+ providerPromise
40
+ .then(resolvedProvider => {
41
+ if (isCancelled) return;
42
+
43
+ // Only trigger callback if the resolved provider is actually different
44
+ if (lastResolvedProviderRef.current !== resolvedProvider) {
45
+ lastResolvedProviderRef.current = resolvedProvider;
46
+ onTerrainProviderChange?.();
47
+ }
48
+ })
49
+ .catch(error => {
50
+ if (!isCancelled) {
51
+ console.warn("Terrain provider failed to load:", error);
52
+ }
53
+ });
54
+
55
+ return () => {
56
+ isCancelled = true;
57
+ };
58
+ }, [providerPromise, onTerrainProviderChange]);
59
+
60
+ return (
61
+ <CesiumGlobe
62
+ baseColor={baseColor}
63
+ enableLighting={!!property?.globe?.enableLighting}
64
+ showGroundAtmosphere={property?.globe?.atmosphere?.enabled ?? true}
65
+ atmosphereLightIntensity={property?.globe?.atmosphere?.lightIntensity}
66
+ atmosphereSaturationShift={property?.globe?.atmosphere?.saturationShift}
67
+ atmosphereHueShift={property?.globe?.atmosphere?.hueShift}
68
+ atmosphereBrightnessShift={property?.globe?.atmosphere?.brightnessShift}
69
+ terrainProvider={providerPromise}
70
+ depthTestAgainstTerrain={!!property?.globe?.depthTestAgainstTerrain}
71
+ />
72
+ );
73
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ ArcGISTiledElevationTerrainProvider,
3
+ CesiumTerrainProvider,
4
+ EllipsoidTerrainProvider,
5
+ IonResource,
6
+ TerrainProvider,
7
+ } from "cesium";
8
+ import { useMemo, useRef } from "react";
9
+
10
+ import { TerrainProperty } from "../../..";
11
+ import { AssetsCesiumProperty } from "../../../../Map";
12
+
13
+ type TerrainType = NonNullable<TerrainProperty["type"]>;
14
+
15
+ type ProviderOpts = Pick<TerrainProperty, "normal"> &
16
+ AssetsCesiumProperty["terrain"] & {
17
+ terrain?: boolean;
18
+ terrainType?: TerrainType | null | undefined;
19
+ ionAccessToken?: string | undefined;
20
+ };
21
+
22
+ export default function useTerrainProviderPromise(opts: ProviderOpts) {
23
+ // Cache promises so we don’t recreate providers on every toggle
24
+ const cacheRef = useRef(new Map<string, Promise<TerrainProvider>>());
25
+ const ellipsoidRef = useRef<TerrainProvider>();
26
+
27
+ return useMemo<Promise<TerrainProvider>>(() => {
28
+ if (!opts.terrain) {
29
+ // single shared ellipsoid provider
30
+ if (!ellipsoidRef.current) ellipsoidRef.current = new EllipsoidTerrainProvider();
31
+ return Promise.resolve(ellipsoidRef.current);
32
+ }
33
+
34
+ const kind = (opts.terrainType ?? "cesium") as TerrainType;
35
+ const key = makeKey(kind, opts);
36
+ let p = cacheRef.current.get(key);
37
+ if (!p) {
38
+ p = createProvider(kind, opts);
39
+ cacheRef.current.set(key, p);
40
+ }
41
+ return p;
42
+ }, [opts]);
43
+ }
44
+
45
+ function makeKey(type: TerrainType, opts: ProviderOpts) {
46
+ // Key should change when output provider would differ
47
+ const asset = opts.ionAsset ?? "";
48
+ const url = opts.ionUrl ?? "";
49
+ const normal = String(!!opts.normal);
50
+ return `${type}|asset:${asset}|url:${url}|normal:${normal}`;
51
+ }
52
+
53
+ function createProvider(type: TerrainType, opts: ProviderOpts): Promise<TerrainProvider> {
54
+ switch (type) {
55
+ case "cesium":
56
+ return CesiumTerrainProvider.fromUrl(
57
+ IonResource.fromAssetId(1, { accessToken: opts.ionAccessToken }),
58
+ { requestVertexNormals: !!opts.normal, requestWaterMask: false },
59
+ ) as Promise<TerrainProvider>;
60
+
61
+ case "arcgis":
62
+ return ArcGISTiledElevationTerrainProvider.fromUrl(
63
+ "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer",
64
+ ) as Promise<TerrainProvider>;
65
+
66
+ case "cesiumion":
67
+ if (!opts.ionAsset && !opts.ionUrl) {
68
+ // Fallback to ellipsoid when misconfigured
69
+ return Promise.resolve(new EllipsoidTerrainProvider());
70
+ }
71
+ return CesiumTerrainProvider.fromUrl(
72
+ opts.ionUrl ??
73
+ IonResource.fromAssetId(parseInt(String(opts.ionAsset), 10), {
74
+ accessToken: opts.ionAccessToken,
75
+ }),
76
+ { requestVertexNormals: !!opts.normal },
77
+ ) as Promise<TerrainProvider>;
78
+ }
79
+ }
@@ -63,6 +63,12 @@ function makeGlobeShadersDirty(globe: Globe): void {
63
63
  // reset surface shader source to the initial state (assuming that we never
64
64
  // use custom material on globe).
65
65
  // ref: https://github.com/CesiumGS/cesium/blob/1.106/packages/engine/Source/Scene/Globe.js#L562-L572
66
+
67
+ // Safety check: ensure globe's internal properties exist before manipulation
68
+ if (!globe || globe.isDestroyed() || !(globe as any)._surface) {
69
+ return;
70
+ }
71
+
66
72
  const material = globe.material;
67
73
  if (material == null) {
68
74
  globe.material = Material.fromType("Color");
@@ -73,6 +79,18 @@ function makeGlobeShadersDirty(globe: Globe): void {
73
79
  }
74
80
  }
75
81
 
82
+ function withUniforms(globe: PrivateCesiumGlobe, add?: Record<string, () => any>) {
83
+ if (!globe._surface?._tileProvider) return;
84
+ const mm = (globe._surface._tileProvider as any)?.materialUniformMap ?? {};
85
+ (globe._surface._tileProvider as any).materialUniformMap = { ...mm, ...(add ?? {}) };
86
+ }
87
+
88
+ function removeUniforms(globe: PrivateCesiumGlobe, keys: string[]) {
89
+ const tp = globe._surface?._tileProvider as any;
90
+ if (!tp?.materialUniformMap) return;
91
+ for (const k of keys) delete tp.materialUniformMap[k];
92
+ }
93
+
76
94
  const useIBL = ({
77
95
  sphericalHarmonicCoefficients,
78
96
  globeImageBasedLighting,
@@ -184,6 +202,18 @@ const useTerrainHeatmap = ({
184
202
  return { isCustomHeatmapEnabled, shaderForTerrainHeatmap };
185
203
  };
186
204
 
205
+ async function waitTerrainReady(scene: any) {
206
+ const t = scene.terrain;
207
+ if (t?.ready) return;
208
+ await new Promise<void>(resolve => {
209
+ if (!t) return resolve();
210
+ const off = t.readyEvent.addEventListener(() => {
211
+ off();
212
+ resolve();
213
+ });
214
+ });
215
+ }
216
+
187
217
  export const useOverrideGlobeShader = ({
188
218
  cesium,
189
219
  sphericalHarmonicCoefficients,
@@ -201,6 +231,8 @@ export const useOverrideGlobeShader = ({
201
231
  enableLighting?: boolean;
202
232
  terrain: TerrainProperty | undefined;
203
233
  }) => {
234
+ const applyingRef = useRef(false);
235
+
204
236
  const { uniformMapForIBL, isIBLEnabled, shaderForIBL } = useIBL({
205
237
  sphericalHarmonicCoefficients,
206
238
  globeImageBasedLighting,
@@ -223,75 +255,85 @@ export const useOverrideGlobeShader = ({
223
255
 
224
256
  const needUpdateGlobeRef = useRef(false);
225
257
 
226
- const handleGlobeShader = useCallback(() => {
258
+ const handleGlobeShader = useCallback(async () => {
227
259
  // NOTE: Support the spherical harmonic coefficient only when the terrain normal is enabled.
228
260
  // Because it's difficult to control the shader for the entire globe.
229
261
  // ref: https://github.com/CesiumGS/cesium/blob/af4e2bebbef25259f049b05822adf2958fce11ff/packages/engine/Source/Shaders/GlobeFS.glsl#L408
230
262
  if (!cesium.current?.cesiumElement || !needUpdateGlobeRef.current) return;
231
263
 
232
- const globe = cesium.current.cesiumElement.scene.globe as PrivateCesiumGlobe;
264
+ if (applyingRef.current) return;
265
+ applyingRef.current = true;
233
266
 
234
- const surfaceShaderSet = globe._surfaceShaderSet;
235
- if (!surfaceShaderSet) {
236
- if (import.meta.env.DEV) {
237
- throw new Error("`globe._surfaceShaderSet` could not found");
238
- }
239
- return;
240
- }
267
+ try {
268
+ const { scene } = cesium.current.cesiumElement;
269
+ await waitTerrainReady(scene);
241
270
 
242
- const baseFragmentShaderSource = surfaceShaderSet.baseFragmentShaderSource;
271
+ const globe = cesium.current.cesiumElement.scene.globe as PrivateCesiumGlobe;
243
272
 
244
- const GlobeFS = baseFragmentShaderSource?.sources[baseFragmentShaderSource.sources.length - 1];
273
+ // Reset shaders first so we patch the freshest base
274
+ makeGlobeShadersDirty(globe);
245
275
 
246
- if (!GlobeFS || !baseFragmentShaderSource) {
247
- if (import.meta.env.DEV) {
248
- throw new Error("GlobeFS could not find.");
276
+ const surfaceShaderSet = globe._surfaceShaderSet;
277
+ if (!surfaceShaderSet) {
278
+ if (import.meta.env.DEV) {
279
+ throw new Error("`globe._surfaceShaderSet` could not found");
280
+ }
281
+ return;
249
282
  }
250
- return;
251
- }
252
283
 
253
- const matchers: StringMatcher[] = [];
254
- const shaders: string[] = [];
255
- if (isIBLEnabled && globe.enableLighting && globe.terrainProvider.hasVertexNormals) {
256
- matchers.push(shaderForIBL);
257
- shaders.push(IBLFS);
258
- }
284
+ const baseFragmentShaderSource = surfaceShaderSet.baseFragmentShaderSource;
285
+ const GlobeFS =
286
+ baseFragmentShaderSource?.sources[baseFragmentShaderSource.sources.length - 1];
259
287
 
260
- if (isCustomHeatmapEnabled) {
261
- // This will log the variables needed in the shader below.
262
- // we need the minHeight, maxHeight and logarithmic
263
- matchers.push(shaderForTerrainHeatmap);
264
- shaders.push(HeatmapForTerrainFS);
265
- }
288
+ if (!GlobeFS || !baseFragmentShaderSource) {
289
+ if (import.meta.env.DEV) {
290
+ throw new Error("GlobeFS could not find.");
291
+ }
292
+ return;
293
+ }
266
294
 
267
- // This means there is no overridden shader.
268
- if (!matchers.length) return;
295
+ const matchers: StringMatcher[] = [];
296
+ const shaders: string[] = [];
297
+ const terrainHasNormals = !!(globe.terrainProvider as any)?.hasVertexNormals;
269
298
 
270
- needUpdateGlobeRef.current = false;
299
+ if (isIBLEnabled && globe.enableLighting && terrainHasNormals) {
300
+ matchers.push(shaderForIBL);
301
+ shaders.push(IBLFS);
302
+ }
271
303
 
272
- if (!globe?._surface?._tileProvider) {
273
- if (import.meta.env.DEV) {
274
- throw new Error("`globe._surface._tileProvider.materialUniformMap` could not found");
304
+ if (isCustomHeatmapEnabled && terrainHasNormals) {
305
+ // This will log the variables needed in the shader below.
306
+ // we need the minHeight, maxHeight and logarithmic
307
+ matchers.push(shaderForTerrainHeatmap);
308
+ shaders.push(HeatmapForTerrainFS);
275
309
  }
276
- return;
277
- }
278
310
 
279
- makeGlobeShadersDirty(globe);
311
+ // This means there is no overridden shader.
312
+ if (!matchers.length) return;
280
313
 
281
- const replacedGlobeFS = defaultMatcher.concat(...matchers).execute(GlobeFS);
314
+ needUpdateGlobeRef.current = false;
282
315
 
283
- globe._surface._tileProvider.materialUniformMap = {
284
- ...(globe._surface._tileProvider.materialUniformMap ?? {}),
285
- ...uniformMapForIBL,
286
- };
316
+ if (!globe?._surface?._tileProvider) {
317
+ if (import.meta.env.DEV) {
318
+ throw new Error("`globe._surface._tileProvider.materialUniformMap` could not found");
319
+ }
320
+ return;
321
+ }
287
322
 
288
- surfaceShaderSet.baseFragmentShaderSource = new ShaderSource({
289
- sources: [
290
- ...baseFragmentShaderSource.sources.slice(0, -1),
291
- GlobeFSDefinitions + replacedGlobeFS + shaders.join(""),
292
- ],
293
- defines: baseFragmentShaderSource.defines,
294
- });
323
+ const replacedGlobeFS = defaultMatcher.concat(...matchers).execute(GlobeFS);
324
+
325
+ withUniforms(globe, isIBLEnabled ? uniformMapForIBL : undefined);
326
+
327
+ surfaceShaderSet.baseFragmentShaderSource = new ShaderSource({
328
+ sources: [
329
+ ...baseFragmentShaderSource.sources.slice(0, -1),
330
+ GlobeFSDefinitions + replacedGlobeFS + shaders.join(""),
331
+ ],
332
+ defines: baseFragmentShaderSource.defines,
333
+ });
334
+ } finally {
335
+ applyingRef.current = false;
336
+ }
295
337
  }, [
296
338
  cesium,
297
339
  isCustomHeatmapEnabled,
@@ -317,6 +359,10 @@ export const useOverrideGlobeShader = ({
317
359
 
318
360
  return () => {
319
361
  if (!globe.isDestroyed()) {
362
+ removeUniforms(globe, [
363
+ "u_reearth_sphericalHarmonicCoefficients",
364
+ "u_reearth_globeImageBasedLighting",
365
+ ]);
320
366
  // Reset customized shader to default
321
367
  makeGlobeShadersDirty(globe);
322
368
  }
@@ -1,113 +0,0 @@
1
- import {
2
- ArcGISTiledElevationTerrainProvider,
3
- CesiumTerrainProvider,
4
- EllipsoidTerrainProvider,
5
- IonResource,
6
- TerrainProvider,
7
- } from "cesium";
8
- import { useEffect, useMemo } from "react";
9
- import { Globe as CesiumGlobe } from "resium";
10
-
11
- import type { ViewerProperty, TerrainProperty } from "../..";
12
- import { AssetsCesiumProperty } from "../../../Map";
13
- import { toColor } from "../common";
14
-
15
- export type Props = {
16
- property?: ViewerProperty;
17
- cesiumIonAccessToken?: string;
18
- onTerrainProviderChange?: () => void;
19
- };
20
-
21
- export default function Globe({
22
- property,
23
- cesiumIonAccessToken,
24
- onTerrainProviderChange,
25
- }: Props): JSX.Element | null {
26
- const terrainProperty = useMemo(
27
- (): TerrainProperty => ({
28
- ...property?.terrain,
29
- }),
30
- [property?.terrain],
31
- );
32
-
33
- const terrainProvider = useMemo((): Promise<TerrainProvider> | TerrainProvider | undefined => {
34
- const opts = {
35
- terrain: terrainProperty?.enabled,
36
- terrainType: terrainProperty?.type,
37
- normal: terrainProperty?.normal,
38
- ionAccessToken: property?.assets?.cesium?.terrain?.ionAccessToken || cesiumIonAccessToken,
39
- ionAsset: property?.assets?.cesium?.terrain?.ionAsset,
40
- ionUrl: property?.assets?.cesium?.terrain?.ionUrl,
41
- };
42
- const provider = opts.terrain ? terrainProviders[opts.terrainType || "cesium"] : undefined;
43
- return (typeof provider === "function" ? provider(opts) : provider) ?? defaultTerrainProvider;
44
- }, [
45
- terrainProperty?.enabled,
46
- terrainProperty?.type,
47
- terrainProperty?.normal,
48
- property?.assets?.cesium?.terrain?.ionAccessToken,
49
- property?.assets?.cesium?.terrain?.ionAsset,
50
- property?.assets?.cesium?.terrain?.ionUrl,
51
- cesiumIonAccessToken,
52
- ]);
53
-
54
- const baseColor = useMemo(
55
- () => toColor(property?.globe?.baseColor),
56
- [property?.globe?.baseColor],
57
- );
58
-
59
- useEffect(() => {
60
- onTerrainProviderChange?.();
61
- }, [terrainProvider, onTerrainProviderChange]);
62
-
63
- return (
64
- <CesiumGlobe
65
- baseColor={baseColor}
66
- enableLighting={!!property?.globe?.enableLighting}
67
- showGroundAtmosphere={property?.globe?.atmosphere?.enabled ?? true}
68
- atmosphereLightIntensity={property?.globe?.atmosphere?.lightIntensity}
69
- atmosphereSaturationShift={property?.globe?.atmosphere?.saturationShift}
70
- atmosphereHueShift={property?.globe?.atmosphere?.hueShift}
71
- atmosphereBrightnessShift={property?.globe?.atmosphere?.brightnessShift}
72
- terrainProvider={terrainProvider}
73
- depthTestAgainstTerrain={!!property?.globe?.depthTestAgainstTerrain}
74
- />
75
- );
76
- }
77
-
78
- const defaultTerrainProvider = new EllipsoidTerrainProvider();
79
-
80
- const terrainProviders: {
81
- [k in NonNullable<TerrainProperty["type"]>]:
82
- | TerrainProvider
83
- | ((
84
- opts: Pick<TerrainProperty, "normal"> & AssetsCesiumProperty["terrain"],
85
- ) => Promise<TerrainProvider> | TerrainProvider | null);
86
- } = {
87
- cesium: ({ ionAccessToken, normal }) =>
88
- CesiumTerrainProvider.fromUrl(
89
- IonResource.fromAssetId(1, {
90
- accessToken: ionAccessToken,
91
- }),
92
- {
93
- requestVertexNormals: normal,
94
- requestWaterMask: false,
95
- },
96
- ),
97
- arcgis: () =>
98
- ArcGISTiledElevationTerrainProvider.fromUrl(
99
- "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer",
100
- ),
101
- cesiumion: ({ ionAccessToken, ionAsset, ionUrl, normal }) =>
102
- ionAsset
103
- ? CesiumTerrainProvider.fromUrl(
104
- ionUrl ||
105
- IonResource.fromAssetId(parseInt(ionAsset, 10), {
106
- accessToken: ionAccessToken,
107
- }),
108
- {
109
- requestVertexNormals: normal,
110
- },
111
- )
112
- : null,
113
- };