A fully integrated WebGIS viewer with all layers combined
Over the course of all 11 chapters, we have built from scratch the internal architecture of web mapping libraries that GIS professionals have been using as a "black box." Starting from Scene/Camera/Renderer initialization, we experienced every element of a WebGIS engine: coordinate transformations, tile mesh generation, LOD control, integration of raster, vector, and 3D tiles, and styling with custom shaders.
In this chapter, we take a bird's-eye view of the GlobeViewer architecture, examine performance bottlenecks and optimization strategies, review advanced topics not covered in this book, and outline the next steps. This is the starting point for future extensions.
The layers implemented in Chapters 7 through 9 are passed to GlobeViewer as an array in +page.svelte.
The canvas on the left displays two layers combined: raster tiles and 3D Tiles. Adding a VectorTileLayer would enable a three-layer configuration.
const viewer = new GlobeViewer({
canvas: canvasEl,
layers: [
new RasterTileLayer(),
new Tiles3DLayer({
url: '...tileset.json'
})
]
});
viewer.start();Layers are added to the scene in array order, and the depth buffer ensures correct front-to-back rendering.
Each layer has its own independent THREE.Group and does not interfere with other layers' resources. This independence allows you to freely change layer combinations.
The core of GlobeViewer is the RenderLoop. Within the callback invoked every frame via requestAnimationFrame,
it executes update() for all layers in sequence, then performs the final rendering.
requestAnimationFrame callback
│
├── RasterTileLayer.update(ctx)
│ ├── Camera change check
│ ├── Throttling (200ms limit)
│ ├── SSE tile selection
│ ├── Diff calculation (add / remove)
│ └── Start texture loading
│
├── VectorTileLayer.update(ctx)
│ ├── Build queue processing (frame-distributed)
│ ├── Destroy queue processing (budget-aware)
│ ├── Camera change check
│ ├── Throttling (200ms limit)
│ ├── SSE tile selection
│ ├── Diff calculation (add / remove)
│ └── MVT load → enqueue to build queue
│
├── Tiles3DLayer.update(ctx)
│ └── adapter.update(camera, renderer)
│ ├── processDisposalQueue()
│ ├── Camera change check
│ ├── traverseTileset()
│ │ ├── Bounding volume → SceneBounds
│ │ ├── Horizon + frustum culling
│ │ ├── SSE calculation + refinement decision
│ │ └── Generate desired / toRemove / toCancel
│ ├── Cancel + remove from scene
│ ├── Cache hit → add to scene
│ └── Cache miss → load request
│
└── ThreeRenderer.render()
├── Zoom interpolation (ellipsoid radius + surface dist)
├── Target transition (surface ↔ origin)
├── Rotation speed adjustment
├── Dynamic near/far adjustment (horizon dist)
├── controls.update()
└── renderer.render(scene, camera)The update → render order is critical. Layers first add and remove tiles, and then ThreeRenderer draws the result.
const loop = new RenderLoop(() => {
const ctx = createContext(
performance.now()
);
for (const layer of layers) {
layer.update(ctx);
}
threeRenderer.render();
});The LayerContext passed to each layer's update() contains all the information a layer needs for rendering:
selectVisibleTiles has a built-in caching mechanism. When called with the same parameters from multiple layers, it reuses the same result within a single frame. This allows RasterTileLayer and VectorTileLayer to share the same SSE selection results, reducing the CPU cost of quadtree traversal.
This implementation includes simplifications made for educational purposes that could become bottlenecks in production use.
computeTileGeometry recalculates vertices every time. Since tile geometry is immutable (same tile coordinates produce the same mesh), an LRU cache can be used to retain results and avoid recomputation.
MVT Protocol Buffers decoding and coordinate transformation (including trigonometric functions) run on the main thread (UI thread). Tiles with a large number of features can cause frame drops. The 200ms throttling in VectorTileLayer mitigates this, but a fundamental solution requires off-thread processing with Web Workers.
Since each tile has its own Mesh or Group, draw calls increase proportionally with the number of visible tiles. Batching with InstancedMesh or Texture Atlas can reduce this, but it is not implemented in this book.
| Problem | Solution |
|---|---|
| Mesh recomputation | LRU cache for geometry |
| Main thread load | Decoding and transformation in Web Workers |
| Draw calls | Batching with InstancedMesh |
| Texture memory | Consolidation via Texture Atlas |
| MVT rendering quality | earcut triangulation after spherical subdivision (improved polar accuracy) |
Camera controls in this book are limited to OrbitControls rotation and custom zoom; tilting the camera toward the horizon (pitch/tilt) is not supported. To look across the cityscape near the ground — as in Google Maps, CesiumJS, or MapLibre GL — you would need to relax OrbitControls' polar angle limits or implement a custom camera controller. Because the frustum shape changes significantly with tilt angle, the tile selection algorithm (SSE calculation) and near/far clip plane adjustments must also be revisited accordingly.
The tile meshes in this book are generated on the ellipsoid surface at elevation 0. The computeTileGeometry heightFn parameter is prepared for elevation support, but the implementation is not included. To achieve terrain meshes, you need to load elevation tiles (e.g., Terrain-RGB), supply elevation values to heightFn, and recalculate normals (cross product from adjacent vertices).
The atmospheric scattering effect along the Earth's limb when viewed from space is achieved with custom shaders. The custom shader knowledge from Chapter 10 is directly applicable here. CesiumJS's atmospheric scattering shader is a useful reference.
MVT label layers (such as place_labels) are currently rendered only as points. Rendering text labels requires SDF textures (Signed
Distance Field) or Canvas-based texture generation.
The viewer in this book only supports camera controls; click selection of objects and popup display are not implemented. This can be achieved using Three.js's Raycaster combined with feature attribute linking.
Three.js's WebGPU backend is available as WebGPURenderer, but the ShaderMaterial and onBeforeCompile used in this book are not supported by WebGPURenderer. Migration requires rewriting to node materials using TSL (Three.js Shading
Language).
┌──────────────────────────────────────────┐ │ +page.svelte │ │ GlobeViewer (Facade) │ │ ├── ThreeRenderer (rendering, camera, │ │ │ zoom, pinch-to-zoom) │ │ ├── RenderLoop (rAF) │ │ └── Layer[] │ │ ├── RasterTileLayer │ │ │ ├── selectVisibleTiles (SSE) │ │ │ ├── TileTextureLoader (LRU) │ │ │ └── createTileMesh │ │ ├── VectorTileLayer │ │ │ ├── selectVisibleTiles (shared)│ │ │ ├── MvtTileLoader (PBF) │ │ │ └── createMvtGroup │ │ └── Tiles3DLayer │ │ └── Tiles3DAdapter │ │ ├── tileset-parser │ │ │ (tree construction) │ │ ├── tile-traversal │ │ │ (SSE traversal) │ │ ├── bounding-volume │ │ │ (coordinate transform) │ │ ├── tile-cache │ │ │ (LRU + request mgmt) │ │ └── content-loader │ │ (B3DM/GLB parsing) │ ├──────────────────────────────────────────┤ │ core/ (Three.js independent) │ │ ellipsoid → coordinates → projection │ │ → tiles │ │ tile-geometry → tile-tree │ └──────────────────────────────────────────┘
core/ made unit testing of coordinate transformations straightforwardLayer abstract class makes it easy to add new layer typesselectVisibleTiles is shared between Raster and Vector, ensuring consistent LOD controltile-layer-utils.ts eliminated duplicate diff-update logic between Raster and VectorThe entire project is composed of modules with clearly separated responsibilities:
Pure mathematical functions with no Three.js dependency. Provides coordinate transformations (geodetic/ECEF/ENU/scene), WGS84 ellipsoid constants, tile coordinate calculations, and quadtree traversal algorithms. Easy to unit test and portable to other rendering engines.
Three.js-dependent rendering infrastructure. Handles WebGLRenderer, PerspectiveCamera, OrbitControls, custom zoom (wheel and pinch-to-zoom), and lighting setup and management. Also includes dynamic near/far adjustment and target transition. Includes custom shaders (custom-shader.ts), tile mesh generation, and MVT mesh generation.
Six modules for the custom 3D Tiles loader. types.ts (type definitions), tileset-parser.ts (tree construction), tile-traversal.ts (SSE traversal), bounding-volume.ts (coordinate transformation), tile-cache.ts (LRU cache + request management), content-loader.ts (B3DM/GLB parsing + GLTFLoader). tiles3d-adapter.ts serves as the control layer that integrates all of these.
Layer lifecycle management. Based on the abstract Layer class, Raster/Vector/Tiles3D layers each manage rendering through the initialize/update/dispose pattern. tile-layer-utils.ts provides shared diff-update logic.
Render loop and frame budget management. Provides requestAnimationFrame abstraction and per-layer processing time control.
Using this book's implementation as a starting point, you can extend it in the following directions:
heightFn foundation was implemented in Chapter 5)From Chapter 1 through Chapter 11, we progressively built up the following elements:
| Ch. | Topic | Core Techniques |
|---|---|---|
| 1 | Three.js Setup | Logarithmic depth buffer, lighting |
| 2 | Camera Controls | Surface-distance-based zoom, ellipsoid directional radius |
| 3 | Coordinate Transforms | WGS84 ellipsoid, ECEF, ENU, axis conversion |
| 4 | Projection and Tiles | Web Mercator, Slippy Map Tiles |
| 5 | Spherical Mesh | Mercator Y interpolation, UV coordinates |
| 6 | LOD Control | SSE, Quadtree, Frustum culling |
| 7 | Raster Tiles | LRU cache, diff updates, parent-child transitions |
| 8 | Vector Tiles | MVT decoding, earcut triangulation |
| 9 | 3D Tiles | B3DM parser, SSE traversal, LRU cache, coordinate transform matrices |
| 10 | Custom Shaders | ShaderMaterial, onBeforeCompile, uniform |
| 11 | Full Integration | Facade, frame budget, extensible design |
The internals of a WebGIS engine are an intricate interplay of coordinate transformation mathematics, GPU rendering techniques, and asynchronous resource management architecture. We hope this book has helped you open that black box.