A Typhoon Tracker Gets a 3D Globe and a Blender-Built Storm Model
Experiencing Typhoon Bavi from a New Perspective on a 3D Globe
GitHub: lukeSuperCoder/typhoon-tracker-3d Live Demo: typhoon-tracker-3d.lc970820.workers.dev Access Note: The project currently does not have an independent domain name and uses the default Cloudflare Workers domain. Direct connections from mainland China may not be possible, and a network proxy might be required.
Foreword: Project Origin and Acknowledgements
This project was not built from scratch.
Its inspiration and business foundation came from a Juejin article:
- Original Author: HiSt
- Original Juejin Article
- Original Project Repository: Trade-Offf/typhoon-bavi-tracker
The original project had already implemented a very complete typhoon information product capability, including real-time typhoon paths, multi-agency forecast comparisons, 7/10/12-level wind circles, city impact countdowns, emergency guides, news aggregation, PWA, as well as Cloudflare Workers data proxy, caching, and dual data source disaster recovery.
After studying the original article and code, a new idea emerged: Could we switch to a more intuitive, spatially-aware perspective, allowing people to more directly feel the scale, power, and potential danger of a typhoon?
Traditional 2D flat maps are excellent for accurately displaying paths, wind circles, and city locations, but they struggle to convey the coverage area of typhoon cloud systems, the direction of movement, and the sense of pressure as it approaches land. For the average user, a line and a few circles on a map are sometimes just abstract data, making it difficult to immediately build an awareness of the disaster's scale.
If we place a typhoon on a 3D globe that can be rotated, tilted, and followed, letting the cloud mass move along its real path, could people more intuitively understand how big it is, where it's heading, and why early precautions are necessary?
This was the main motivation for developing the 3D upgraded version. 3D is not just about making the page look flashier, but about leveraging stronger spatial hierarchy and visual expression to transform abstract path data into an easier-to-understand disaster process, ensuring users maintain sufficient attention and vigilance towards the typhoon when viewing information (ps: there's also a bit of showing off involved 😄).
The project retains the core business capabilities of the original repository, focusing on upgrading the map engine and typhoon model, and adds:
- Mapbox GL JS Globe 3D Earth
- Starfield, atmosphere, and optional DEM terrain
- Blender-processed GLB typhoon cloud system model
- Model synchronized movement along the typhoon path
- Three camera modes: Free, Follow, Presentation
- Unified timeline for historical path, forecast path, wind circles, model, and HUD
- Three performance tiers: High Quality, Balanced, Power Saving
- 2D eye-of-the-storm fallback upon model load failure
This article will not repeat the business features already introduced in the original article, but will instead focus on the most share-worthy parts of this 3D upgrade process: How to prepare a Blender model suitable for WebGIS, how to place a GLB onto a Mapbox Globe, and how to synchronize the model, path, and multi-perspective camera.
1. Why Upgrade from a 2D Map to a 3D Globe
2D maps are excellent for accurately reading latitude, longitude, wind circles, and city distances, but a typhoon itself is a weather system with significant spatial scale and direction of movement.
In a 2D top-down view, users can see "where it is"; switching to a 3D perspective further allows them to feel:
- The scale of the typhoon cloud mass relative to the Earth's curvature
- The spatial direction of historical and predicted paths
- The movement trend when the camera follows from behind
- The layering between clouds, terrain, and wind circles when observed from a low pitch angle
Therefore, this upgrade is not simply about adding a pitch to the map, but about re-dividing the rendering layers:
Cloudflare Worker / Typhoon Data
│
▼
Standard TyphoonData
│
▼
PlaybackEngine
│
▼
TrackState
│
┌───────┼─────────┐
▼ ▼ ▼
HUD 3D Model Camera Control
│
┌───────┼─────────┐
▼ ▼ ▼
Path Wind Circles Forecast Layers
The key principle is just one sentence:
The model, path, wind circles, information panel, and camera must not each use their own timing; they must all consume the same
TrackState.
If the model uses its own requestAnimationFrame and the camera uses another timer, after running for a while, issues with position, wind circles, and HUD desynchronization will inevitably occur.
2. Technology Choice: Why Use Mapbox's Native model Layer
There are two common approaches for loading GLB in a 3D map:
- Three.js + Mapbox Custom Layer
- Mapbox GL JS native
modelLayer
Three.js offers higher degrees of freedom, suitable for skeletal animations, particles, and complex materials; however, this project uses Globe projection and needs to consider mobile devices, layer lifecycle, and rapid deployment. To reduce the cost of converting between two 3D coordinate systems, I ultimately prioritized choosing the Mapbox native model layer.
The benefits of this approach are:
- Model position directly uses GeoJSON latitude and longitude
- The model naturally follows the Globe projection
- No need to maintain an additional Three.js camera
- Styles, layer visibility, and map lifecycle can be managed uniformly
- GLB is loaded and parsed by a Mapbox Worker
The limitations to accept are also clear:
- Not suitable for complex internal model animations
- Material and lighting adjustment capabilities are less than a full Three.js scene
- Model origin, dimensions, and axes must be handled properly during the Blender stage
Therefore, this project adopts a division of labor: "Blender is responsible for asset composition, Mapbox is responsible for overall spatial movement."
3. The Most Time-Consuming Part: Processing a Generic GLB into a Map-Suitable Typhoon Model
3.1 Model Source
Model assets were browsed and downloaded as GLB files via the Sketchfab Feed, then imported into Blender for secondary processing, combination, and export.
A special reminder is necessary here: Different models on Sketchfab have different licensing agreements. Before downloading and using, you should confirm on the specific model's detail page whether downloading, modification, and public display are allowed, and retain the author's attribution according to the corresponding license. This article only introduces the technical process and does not imply that any arbitrary Sketchfab model can be directly used in a project.
My goal was not to create a meteorologically accurate simulation of real cloud volumes, but to obtain a "typhoon visual symbol" that is still easily recognizable during map zooming, rotation, and low-pitch observation.
The final model is composed of various visual elements:
- Main cloud mass
- Surrounding raindrops
- Local lightning or highlighted structures
- Visual whitespace in the central area
3.2 Don't Rush to Export After Importing into Blender
Downloaded GLB files are often prepared for model viewers and are not necessarily suitable for direct placement into WebGIS. Common issues include:
- The model's center point is not at the geometric center
- Objects are far from the world origin
- Each child object has different scaling and rotation
- The model's actual dimensions span too large a range
- Too many materials, textures too large
- Raindrops or lightning extending downwards get obscured by the terrain when placed on the surface
- Looks good from a top view, but layers become chaotic when the map is tilted
My processing order is:
Step 1: Clean the Scene
Delete cameras, lights, ground planes, backgrounds, and hidden objects unrelated to the final display, keeping only the parts that need to be exported, such as cloud masses, raindrops, and lightning.
Object naming is also organized at this step. Names don't directly affect Mapbox rendering, but clear names make subsequent troubleshooting of materials, bounding boxes, and anomalous nodes much easier.
Step 2: Unify Axes and Transforms
Confirm the model's vertical direction in Blender, and for objects that need to be retained, execute:
Object > Apply > Rotation & Scale
The significance of applying transforms is to make the exported GLB treat the current visual state as the model's base state. Otherwise, when using model-scale or model-rotation on the web side later, unexpected results can easily accumulate.
Step 3: Reorganize Composition Around the Typhoon Center
In WebGIS, a model's positioning point is a single latitude and longitude, so the model's origin must correspond to the typhoon center.
The specific approach is:
- Select the main cloud mass and determine the eye or visual center of the cloud mass.
- Move the 3D Cursor to this position.
- Set the Origin of the combined object to the 3D Cursor.
- Move the entire assembly to the Blender world origin
(0, 0, 0). - Check the relative positions of raindrops, lightning, and cloud layers from top, front, and perspective views.
This point is very critical. An origin offset might not be obvious in Blender, but when placed on a map and rotated or scaled, the model will spin around the wrong position, making it look like the typhoon is "drifting" beside its path.
Step 4: Handle Vertical Structure
The typhoon cloud system on the map needs to be observed from tilted camera angles. If all elements are pressed into the same plane, it will look like a sticker from a low angle.
So, in Blender, I preserved a certain height difference among the cloud mass, raindrops, and lightning. But the height difference cannot be too large, otherwise, the map close-up will look like a cloud pillar.
A visual ratio, rather than a real meteorological ratio, is used here:
- Horizontal scale is used to express the typhoon cloud system's range
- Vertical scale is only responsible for establishing layering
- The actual geographically meaningful impact range is still expressed by the 7/10/12-level wind circles
In other words, the GLB is a visual symbol, while the wind circles are the spatial data.
Step 5: Material Merging and Color Check
The lighting environment in a browser map is different from that in a Sketchfab model viewer. After importing materials, key checks are needed for:
- Whether the Base Color is too white
- Whether Roughness causes the clouds to appear black
- Whether Emissive is too strong
- Whether Alpha Blend causes sorting issues
- Whether multiple materials can be merged
The current project retains the warm gray material inside the GLB and sets in Mapbox:
"model-color-mix-intensity": 0
If the map layer color is forced to override the model, the cloud mass can easily revert to a flat, pure white block without depth.
Step 6: Control Volume and Export
Models on a web page cannot only consider image quality; initial load time and mobile parsing time must also be considered.
The goals I set for the model were:
- GLB should be less than 3 MB
- Maximum not exceeding 5 MB
- Textures not exceeding 2048×2048
- Minimize materials and independent objects
- No dependency on skeletal animation
- Enable Draco mesh compression
The final cloud.glb in the project is about 1.4 MB. The exporter used was Blender's Khronos glTF 2.0 export plugin, with Draco compression enabled.
Blender export settings:
File
└─ Export
└─ glTF 2.0
├─ Format: glTF Binary (.glb)
├─ Include: Selected Objects
├─ Transform: +Y Up
├─ Apply Modifiers
└─ Compression: Draco
After exporting, a minimal verification is also needed:
- Can the file be opened by an independent glTF Viewer.
- Is the model origin still at the center of the cloud mass.
- Are materials and transparency correct.
- Does the file contain external linked textures.
- Does it clip into the ground surface when observed from a low pitch angle in Mapbox.
4. Placing the GLB onto the Mapbox Globe
4.1 Representing Model Position with a GeoJSON Point
Mapbox's native model layer still requires a Source. A single-point GeoJSON is used here, with the model URL placed in the Feature properties:
const SOURCE_ID = "storm-model-source";
const LAYER_ID = "storm-model-layer";
const MODEL_URL = new URL(
"models/cloud.glb?v=cloud-effects-centered-v5",
document.baseURI,
).href;
function modelData(modelUri: string, lng = 0, lat = 0) {
return {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { modelUri },
geometry: {
type: "Point",
coordinates: [lng, lat],
},
}],
};
}
A Blob URL is not used here; instead, the Mapbox Worker directly requests the same-origin GLB. In actual testing, same-origin URLs proved more stable in Worker parsing, browser caching, and Service Worker environments.
The version number after the URL is used to bypass three layers of caching:
- Browser HTTP cache
- Service Worker cache
- Mapbox Worker internal model cache
If you replace the model but the page looks completely unchanged, the first thing to do is modify this version number.
4.2 Creating the Native model Layer
The core layer code is as follows:
map.addSource(SOURCE_ID, {
type: "geojson",
data: modelData(MODEL_URL, lng, lat),
});
map.addLayer({
id: LAYER_ID,
type: "model",
slot: "middle",
source: SOURCE_ID,
layout: {
"model-id": ["get", "modelUri"],
visibility: "visible",
},
paint: {
"model-type": "common-3d",
"model-scale": [28_000, 28_000, 18_000],
"model-translation": [0, 0, 120_000],
"model-rotation": [0, 0, 0],
"model-color-mix-intensity": 0,
"model-opacity": 1,
"model-emissive-strength": 0,
"model-cast-shadows": false,
"model-receive-shadows": false,
},
});
A few parameters are worth explaining individually.
4.3 Why the Model Needs to be Scaled Up Tens of Thousands of Times
The unit scale of a Blender model is not the same as meters on Earth. The current model's horizontal dimensions are only a dozen or so model units; if placed directly on the map, it would be almost invisible.
Therefore, this is used:
"model-scale": [28_000, 28_000, 18_000]
This makes the model's horizontal visual size approach several hundred kilometers. The Z-axis scaling is smaller to prevent the cloud mass from being too thick.
This is not a physical simulation, but a visual mapping in information visualization. The actual impact radius is still handled by the wind circles calculated in kilometers.
4.4 Why Elevate by 120 Kilometers
The model contains raindrops and lightning extending downwards. If the model origin is placed directly on the surface, they would enter the Earth's interior or be obscured by DEM terrain.
Therefore, this is used:
"model-translation": [0, 0, 120_000]
This elevates the entire model. This parameter needs to be adjusted in conjunction with the model's origin and lowest point in Blender, as well as the web-side pitch, and cannot be judged solely from a top-down view.
4.5 Model Load Failure Must Have a Fallback
A 3D model might fail to load due to network issues, browser capabilities, GLB parsing, or caching problems, but the typhoon path and disaster prevention information cannot disappear because of this.
The project listens for map errors, handling only exceptions related to the GLB:
private readonly handleMapError = (
event: { error?: Error },
): void => {
const message = event.error?.message ?? "";
if (!/cloud\.glb|gltf|model/i.test(message)) return;
this.failed = true;
this.available = false;
this.onAvailabilityChange(false);
};
When the model is unavailable, the map continues to display the original DOM eye-of-the-storm Marker. The 3D model is an enhancement layer, not a single point of failure for business functions.
5. Making the Model Move Smoothly Along the Path
Moving the model itself is not complex: update the GeoJSON Point every frame.
update(state: TrackState): void {
this.lng = state.lng;
this.lat = state.lat;
const source = this.map.getSource(
SOURCE_ID,
) as mapboxgl.GeoJSONSource | undefined;
source?.setData(
modelData(MODEL_URL, state.lng, state.lat),
);
}
The real difficulty lies in where state.lng and state.lat come from.
The typhoon API provides discrete observation points. If the model jumps directly from one point to the next, it will visibly teleport. Therefore, it's necessary to find the two adjacent nodes based on the current playback time and interpolate by time ratio:
const ratio = (currentTime - pointA.time) /
(pointB.time - pointA.time);
const lng = interpolateLongitude(
pointA.lng,
pointB.lng,
ratio,
);
const lat = pointA.lat +
(pointB.lat - pointA.lat) * ratio;
Longitude cannot always use simple linear interpolation because the path might cross the 180°/-180° date line. The shortest direction must be chosen:
function interpolateLongitude(
from: number,
to: number,
ratio: number,
): number {
let delta = to - from;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
const value = from + delta * ratio;
return ((value + 540) % 360) - 180;
}
This unified state is also simultaneously handed to:
StormModelLayer- Historical path progress
- 7/10/12-level wind circles
- Typhoon intensity and pressure HUD
CameraController
Thus, whether pausing, dragging the timeline, or changing the speed, all visual elements can stop at the same moment.
6. Multi-Perspective Switching: Free, Follow, and Presentation Cameras
If the model just moves, the user still needs to constantly drag the map manually. The key to a real "cinematic feel" in 3D display is making the camera understand the typhoon's motion state.
The project consolidates the camera into three modes:
export type CameraMode =
| "free"
| "follow"
| "presentation";
6.1 Free Mode
Free mode is the simplest:
if (this.mode === "free") return;
The playback engine continues to advance, but the program no longer modifies the map camera. The user can freely rotate, zoom, and tilt the globe.
6.2 Follow Mode: Using a Third-Person Perspective to Orient the Camera Towards the Typhoon's Forward Direction
Follow mode is not simply about centering the typhoon on the screen.
First, calculate the movement bearing based on the last two positions; if the displacement is too small, fall back to the movement direction provided by the API, such as "Northeast, Northward":
const measuredBearing =
lastState &&
haversineKm(
lastState.lng,
lastState.lat,
state.lng,
state.lat,
) > 0.02
? bearingBetween(
lastState.lng,
lastState.lat,
state.lng,
state.lat,
)
: bearingFromMoveDirection(state.moveDir);
Next, you cannot directly interpolate the bearing from 359° to 1°, otherwise the camera will rotate 358° in the opposite direction. Interpolation along the shortest angle is needed:
function lerpBearing(
from: number,
to: number,
amount: number,
): number {
const delta = ((to - from + 540) % 360) - 180;
return (from + delta * amount + 360) % 360;
}
Finally, use easeTo to update the camera:
map.easeTo({
center: [state.lng, state.lat],
zoom: Math.max(map.getZoom(), 6.2),
pitch: 68,
bearing: followBearing ?? map.getBearing(),
offset: [0, Math.round(height * 0.15)],
duration: force ? 900 : 360,
essential: false,
});
There's also a small design detail here that's easy to overlook:
offset: [0, height * 0.15]
This places the typhoon cloud mass slightly lower on the screen, leaving more view for the forward direction. This way, the user sees not an icon forever locked dead center, but a follow shot where "there is still road ahead."
6.3 Presentation Mode: Switching Shots Based on Progress
Presentation mode is suitable for auto-playback or recording demo videos. It divides the complete process into four segments based on playback progress:
const step = Math.min(
3,
Math.floor(progress * 4),
);
const shots = [
{ zoom: 3.8, pitch: 28, bearing: -12 },
{ zoom: 5.2, pitch: 48, bearing: 18 },
{ zoom: 6.2, pitch: 60, bearing: -28 },
{ zoom: 4.6, pitch: 42, bearing: 8 },
];
map.easeTo({
center: [state.lng, state.lat],
...shots[step],
duration: force ? 1000 : 1400,
essential: false,
});
The four stages serve different purposes:
- Global or regional perspective: Shows the typhoon's location.
- Medium shot: Displays the path direction and surrounding cities.
- Close-up: Highlights the cloud system model, wind circles, and terrain.
- Pull back: Re-shows the overall path and predicted trend.
This approach does not introduce a complex keyframe system, yet achieves relatively natural shot changes.
6.4 Why Camera Updates Need Throttling
Playback might approach 60 FPS, but there's no need to call easeTo every frame. Frequently initiating camera animations can cause jitter and increase main thread and GPU pressure.
The project limits camera updates to roughly once every 220ms:
const now = performance.now();
if (!force && now - this.lastMove < 220) return;
this.lastMove = now;
The model can move at high frequency; the camera only needs to follow smoothly. The two have different update frequencies but use the same state source.
7. Globe Scene: Atmosphere, Satellite Basemap, and Terrain
Map initialization uses Globe projection:
const map = new mapboxgl.Map({
container,
style: AMAP_SATELLITE_STYLE,
projection: "globe",
center: [138, 18],
zoom: 3.5,
pitch: 35,
fadeDuration: 0,
refreshExpiredTiles: false,
});
The basemap continues to use AMap satellite imagery and Chinese annotations. Mapbox handles the Globe, atmosphere, layer rendering, and optional DEM terrain.
Atmosphere configuration is as follows:
map.setFog({
color: "rgb(18, 29, 54)",
"high-color": "rgb(25, 54, 108)",
"horizon-blend": 0.035,
"space-color": "rgb(4, 7, 15)",
"star-intensity": 0.55,
});
On high-performance desktop devices, if a Mapbox Token exists, DEM can be further enabled:
map.addSource("mapbox-dem", {
type: "raster-dem",
url: "mapbox://mapbox.mapbox-terrain-dem-v1",
tileSize: 512,
maxzoom: 14,
});
map.setTerrain({
source: "mapbox-dem",
exaggeration: 1.12,
});
Terrain is not a mandatory item. On low-performance devices, mobile, or without a Token, the project can still use the basic Globe, satellite imagery, paths, and GLB model.
8. Performance Optimization: Don't Let 3D Effects Drag Down Business
After the 3D upgrade, the performance budget is mainly consumed by:
- Globe projection
- Satellite tiles
- DEM terrain
- GLB parsing
- Continuous model and wind circle updates
- Camera animation
The project adopts a layered update strategy:
| Content | Update Strategy |
|---|---|
| Model Position | Follows active animation frames |
| HUD | Updates when current state changes |
| Wind Circles | Throttled to approx. 10–15 FPS |
| Historical Path | Rebuilt after crossing nodes/thresholds |
| Follow Camera | Updates approx. every 220ms |
| DEM Terrain | Disabled on low-performance devices |
| Page Hidden | Pauses animation progression |
Three performance tiers are also provided: High Quality, Balanced, and Power Saving. Power Saving mode can disable terrain and some dynamic effects, and retain the 2D eye-of-the-storm as a model replacement.
The experience here is:
3D display cannot become a prerequisite for business correctness.
Even if the GLB, terrain, or lighting effects all fail to load, the path, wind circles, forecasts, countdowns, and disaster prevention guides should still function normally.
9. A Few Pitfalls Encountered During Migration
9.1 Centered in Blender Doesn't Mean Truly Centered in Mapbox
Need to simultaneously check:
- Combined object Origin
- Local transforms of all child objects
- Exported glTF node Transform
- Mapbox's
model-translation
Only moving the visible geometry without adjusting the Origin will still result in an offset on the web side.
9.2 A Model Too Realistic Might Actually Be Unusable
A real typhoon's scale is very large, while its vertical scale is relatively very small. Modeling entirely to real proportions might make it invisible from a global perspective, or it might obscure the map in a close-up.
The final choice was:
- GLB uses a visual ratio
- Wind circles use real kilometer radii
- HUD displays real meteorological data
Separating the responsibilities of these three makes the picture clearer.
9.3 style.load Will Clear Custom Layers
After the map style reloads, custom Sources and Layers need to be re-registered. Therefore, the model layer's add() must be idempotent:
if (map.getLayer(LAYER_ID)) return;
The scene layer also needs to restore paths, wind circles, and model positions based on cached data and the current state.
9.4 Caching Will Make You Doubt Whether Blender Exported Successfully
The GLB might be cached simultaneously by the browser, Service Worker, and Mapbox Worker. After replacing a file with the same name, the page might still display the old model.
The simplest way to handle this is to add a version parameter to the model URL:
models/cloud.glb?v=cloud-effects-centered-v5
9.5 Model Load Success Doesn't Mean the User Can See It
The model might have loaded, but is just:
- Too small in size
- Height is below the surface
- Material is nearly transparent
- Layer is obscured by other layers
- Coordinates are still stuck at
[0, 0] - Current zoom level is unsuitable
When debugging, it's recommended to first use exaggerated scaling and height to confirm the model's existence, then gradually adjust back to appropriate proportions.
10. Current Project Structure
Core files related to the 3D map:
src/
├── animation/
│ ├── PlaybackEngine.ts # Unified playback timeline
│ └── playbackTypes.ts
├── map/
│ ├── CameraController.ts # Free/Follow/Presentation camera
│ ├── PerformanceController.ts
│ ├── StormModelLayer.ts # Mapbox native GLB model layer
│ └── contracts.ts
├── geo.ts # Interpolation, distance, bearing, and wind circles
└── map.ts # Globe scene and business layers
public/
└── models/
└── cloud.glb # Blender-processed cloud system model
Local Run:
git clone https://github.com/lukeSuperCoder/typhoon-tracker-3d.git
cd typhoon-tracker-3d
npm install
npm run dev
Build and Deploy:
npm run typecheck
npm run test
npm run build
npm run deploy
11. Future Plans
The current 3D model still primarily relies on whole-model motion. The next steps to try include:
- Dynamically adjusting model size based on typhoon intensity
- Further decoupling cloud mass rotation from path movement
- Adding more natural camera keyframes and transition curves
- Providing a lighter low-poly model for mobile devices
- Adding multi-typhoon switching and simultaneous comparison
- Exploring particle wind fields and satellite cloud image time series
- Registering an independent domain and improving domestic access
If future compatibility between Mapbox Globe and custom 3D rendering becomes more mature, further attempts with Three.js particle cloud layers could be made. But in the current version, the native model layer already strikes a good balance between effect, complexity, and stability.
Summary
The most important takeaway from this upgrade is not "placing a GLB onto a map," but understanding the boundaries of several types of state in 3D WebGIS:
- Blender determines the model's origin, axes, layering, materials, and asset volume
- Mapbox determines the model's position, scale, height, and scene relationships on the Earth
- PlaybackEngine determines which point in time all business elements are at
- CameraController determines from what angle the user understands this motion
- Wind circles and paths are responsible for expressing real spatial data, while the GLB enhances visual cognition
When these responsibilities are separated, the 3D model is no longer just a decoration overlaid on the map, but truly becomes part of the typhoon path narrative.
Finally, thanks again to the original article and project author for the inspiration and open-source foundation:
My 3D upgraded version:
If this article was helpful to you, feel free to Star, open an Issue, or discuss WebGIS, Mapbox, Blender, and meteorological visualization together.
Disclaimer: The data displayed in the project comes from public meteorological service interfaces. City impact times are algorithmically estimated and do not constitute official forecasts or warnings. All disaster prevention decisions should be based on information released by the Central Meteorological Observatory and local governments.
Top 2 of 3 from juejin.cn, machine-translated. The original thread is authoritative.
Awesome. Such a good article and no one's liking it. The world's going downhill.
Haha, it's not that serious. Seeing it is fate enough [heart].
I made something globe-related too, https://mairastar.cn/ and also developed a Pico VR app.