跪拜 Guibai
← All articles
Frontend

Building a 3D Geo-Viz Engine in Vanilla Three.js: 12 Pitfalls from GeoJSON to Shader Flylines

By 漏刻有时 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Chart libraries like ECharts abstract away the geometry, shaders, and coordinate systems that make custom 3D map effects possible. This engine's 12 documented pitfalls—especially the global UV rewrite, TubeGeometry vertex layout, and coordinate-system rotation—are the exact problems a developer hits when breaking free of those abstractions, and the solutions apply to any Three.js geographic project.

Summary

Starting from a single HTML file, the LockScope engine renders GeoJSON as extruded 3D city blocks with a continuous satellite texture, conical light pillars mapped to economic data, and flowing flylines animated via custom fragment shaders. A CSS variable and JS color dual-track system enables hot-switching between three color presets without a page reload. The entire stack avoids npm, frameworks, and build tools, shipping as a directory of static files that runs on any HTTP server. The project's 12-version iteration surfaced a dozen concrete pitfalls in Three.js, from ExtrudeGeometry UV fragmentation and TubeGeometry vertex structure to strict-mode recursion and memory leaks during scene rebuilding. Each pitfall is documented with its root cause and the specific fix applied, forming a practical reference for anyone building custom 3D map visualizations outside the constraints of charting libraries.

Takeaways
ExtrudeGeometry generates independent 0–1 UVs per Shape, shattering a shared satellite texture; a two-pass global bounding-box rewrite maps all blocks into a single continuous UV space.
THREE.Line's lineWidth is fixed at 1px in most WebGL implementations, making TubeGeometry the practical alternative for thick, Shader-animated flylines.
QuadraticBezierCurve3 expects (start, controlPoint, end); swapping the last two arguments produces a wildly oversized arc that flies past the target and doubles back.
TubeGeometry creates radialSegments + 1 vertices per ring, not radialSegments; miscounting this misaligns custom Shader attributes across the mesh.
Building the map in a Z-up Group and rotating the root by -π/2 keeps geographic intuition (Z is height) while staying compatible with Three.js's Y-up OrbitControls.
UnrealBloomPass threshold at 0.55 prevents the satellite texture from washing out while still letting AdditiveBlending light pillars and flylines glow.
Hot-switching color schemes requires a dual-track system: Three.js materials read numeric colors directly, while DOM elements sync via CSS variables through an applyCSSColors() call.
Scene rebuilding without a page refresh demands recursive disposal of geometries and materials, with special handling for Array materials and shared textures to avoid memory leaks.
Separating hoveredMesh from selectedMesh in the Raycaster interaction prevents hover from clearing a clicked city's highlight state.
Procedural Canvas textures capped at 512², a pixelRatio clamped to 1.5, and zero shadow maps keep the engine at 60fps on integrated laptop graphics.
Conclusions

ExtrudeGeometry's per-Shape UV generation is a documented but underappreciated trap: the fix requires understanding the geometry's vertex layout well enough to walk rings and rewrite the UV buffer by hand.

The TubeGeometry vertex-count detail (radialSegments + 1) is easy to miss because the constructor parameter is named radialSegments, not verticesPerRing, and the docs don't emphasize the +1.

Hardcoding flyline color to a single gold that works across all three color presets is a pragmatic design choice that avoids the complexity of per-scheme Shader uniform updates.

Shipping as a directory of static files with zero build steps is a deliberate constraint that forces simplicity; it also makes the engine trivially deployable in air-gapped or restricted environments common for government data dashboards.

The 12 pitfalls are not generic debugging anecdotes—each is a Three.js API behavior that is either counterintuitive or poorly documented, making the list a useful pre-flight checklist for similar projects.

Concepts & terms
ExtrudeGeometry UV fragmentation
Three.js's ExtrudeGeometry assigns each Shape its own 0–1 UV range. When multiple Shapes (e.g., city blocks) share a single texture, the texture repeats on every block instead of spanning the whole scene. The fix rewrites UVs using a global bounding box so the texture maps continuously across all geometry.
TubeGeometry vertex layout
TubeGeometry creates a tubular mesh with a given number of tubular segments and radial segments. Each ring of vertices contains radialSegments + 1 vertices (the extra vertex closes the ring). Custom Shader attributes must account for this +1, or the data will misalign across the mesh.
Z-up to Y-up coordinate conversion
Geographic data naturally uses Z as height (Z-up), but Three.js defaults to Y-up. Wrapping all map geometry in a Group rotated by -π/2 around the X axis lets the map logic stay in Z-up while the rendered scene conforms to Three.js's Y-up world and OrbitControls.
UnrealBloomPass threshold
A post-processing pass that adds glow to bright areas of a scene. The threshold controls which luminance values trigger the bloom. A value around 0.55 ensures only emissive or additively blended elements (light pillars, flylines) glow, while the diffuse satellite texture remains unaffected.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗