跪拜 Guibai
← Back to the summary

A Typhoon Tracker Gets a 3D Globe and a Blender-Built Storm Model

Experiencing Typhoon Bavi from a New Perspective on a 3D Globe

image.png

image.png

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:

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:

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:

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:

  1. Three.js + Mapbox Custom Layer
  2. Mapbox GL JS native model Layer

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:

The limitations to accept are also clear:

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:

image.png

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:

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:

  1. Select the main cloud mass and determine the eye or visual center of the cloud mass.
  2. Move the 3D Cursor to this position.
  3. Set the Origin of the combined object to the 3D Cursor.
  4. Move the entire assembly to the Blender world origin (0, 0, 0).
  5. 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:

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:

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:

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:

  1. Can the file be opened by an independent glTF Viewer.
  2. Is the model origin still at the center of the cloud mass.
  3. Are materials and transparency correct.
  4. Does the file contain external linked textures.
  5. 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:

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:

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 , 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:

  1. Global or regional perspective: Shows the typhoon's location.
  2. Medium shot: Displays the path direction and surrounding cities.
  3. Close-up: Highlights the cloud system model, wind circles, and terrain.
  4. 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:

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:

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:

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:

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:

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:

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.

Comments

Top 2 of 3 from juejin.cn, machine-translated. The original thread is authoritative.

夜聆风 1 likes

Awesome. Such a good article and no one's liking it. The world's going downhill.

LukeSuperCoder

Haha, it's not that serious. Seeing it is fate enough [heart].

aspray

I made something globe-related too, https://mairastar.cn/ and also developed a Pico VR app.