Building an Interactive 3D Globe with Three.js, GLSL Shaders, and Clickable Countries
This tutorial explains how to create a visually striking, interactive 3D globe using three.js, covering texture mapping, GLSL shader programming, click interaction logic, flight‑line effects, performance considerations, and a complete example of vertex and fragment shaders with accompanying JavaScript code.
Developers familiar with data‑visualization will learn how to build a cool, interactive 3D globe in about a week using three.js, Vue, webpack, and related libraries.
Core Topics
Implementation of the earth sphere using THREE.ShaderMaterial and multiple textures.
Clickable interaction logic to identify countries.
Flight‑line visualization with THREE.CubicBezierCurve3 .
Performance optimizations and known drawbacks.
Key Knowledge Points
GLSL shader usage in 3D objects.
THREE.ShaderMaterial and THREE.Texture integration.
THREE.CubicBezierCurve3 for 3‑D Bezier curves.
Dynamic THREE.CylinderGeometry coloring based on data.
Technology Stack
Vue
webpack
three.js
antv
d3.js
Earth Implementation
The earth uses five textures: mapIndex , lookup , outline , textTexture , and depthTexture . Each texture serves a specific purpose such as country indexing, selection highlighting, border outlining, label rendering, and ocean depth shading.
Texture Details
Texture 1: mapIndex
Provides per‑pixel country indices (1‑255) and ocean (0). Used for click detection and GLSL‑based coloring.
Texture 2: lookup
const lookupCanvas = document.createElement('canvas');
lookupCanvas.width = 256;
lookupCanvas.height = 1;
const lookupTexture = new THREE.Texture(lookupCanvas);
lookupTexture.magFilter = THREE.NearestFilter;
lookupTexture.minFilter = THREE.NearestFilter;
lookupTexture.needsUpdate = true;The canvas holds a 1×256 index map that is updated at runtime to reflect selected countries.
Texture 3: outline
Contains country borders; lines are kept ≤1 px to avoid blurring.
Texture 4: textTexture
Renders country names; due to polar distortion, text near the poles appears stretched.
Texture 5: depthTexture
Encodes ocean depth; mixed with white to brighten water surfaces.
Uniforms and Flags
Besides textures, the shader defines five colors (surface, selected, line, lineSelected, light) and a boolean flag indicating whether a click interaction is in progress.
Complete Shader Code
Fragment Shader
uniform sampler2D mapIndex;
uniform sampler2D lookup;
uniform sampler2D outline;
uniform sampler2D textTexture;
uniform sampler2D depthTexture;
uniform float outlineLevel;
uniform vec3 surfaceColor;
uniform vec3 lineColor;
uniform vec3 lineSelectedColor;
uniform vec3 selectedColor;
uniform vec3 u_lightColor;
uniform float flag;
vec3 u_lightDirection = vec3(0.0, 1.0, 0.0);
varying vec3 vNormal;
varying vec2 vUv;
void main() {
vec4 mapColor = texture2D(mapIndex, vUv);
vec4 text = texture2D(textTexture, vUv);
vec4 depth = texture2D(depthTexture, vUv);
float indexedColor = mapColor.x;
vec4 lookupColor = texture2D(lookup, vec2(indexedColor, 0.0));
float outlineColor = texture2D(outline, vUv).x;
float diffuse = lookupColor.x + indexedColor + outlineColor;
vec4 earthColor = vec4(0.0);
if (flag == 1.0) {
if (lookupColor.x == 1.0) { // country
if (outlineColor > 0.3) {
earthColor = vec4(lineColor, 0.8);
} else {
earthColor = vec4(mix(surfaceColor, vec3(indexedColor), 0.0), 1.0);
}
} else if (lookupColor.x == 0.0) { // ocean
earthColor = vec4(mix(vec3(1.0), depth.xyz, 0.86), 1.0);
vec3 faceNormal = normalize(vNormal);
float nDotL = max(dot(u_lightDirection, faceNormal), 0.0);
vec4 AmbientColor = vec4(u_lightColor, 1.0);
vec4 diffuseColor = vec4(u_lightColor, 1.0) * nDotL;
earthColor = earthColor * (AmbientColor + diffuseColor);
} else if (lookupColor.x == 0.8) { // selected
if (outlineColor > 0.0) {
earthColor = vec4(lineSelectedColor, 1);
} else {
earthColor = vec4(selectedColor, 1);
}
}
if (text.w > 0.3) {
earthColor = vec4(0.7, 0.7, 0.7, 1);
}
} else {
earthColor = vec4(vec3(diffuse), 1.0);
}
gl_FragColor = earthColor;
}Vertex Shader
varying vec2 vUv;
varying vec3 vNormal;
void main() {
vUv = uv;
vNormal = normal;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}Click Interaction Logic
Listen for mouse click.
Render the scene to a black off‑screen buffer with flag = 0 .
Read pixel color at the click position using gl.readPixels .
Map the color (1‑255) to a country ID via a lookup table.
Update the lookup texture at the corresponding index to #CCCCCC (float 0.8) to mark selection.
Restore normal rendering.
The lookup table is a JavaScript object mapping numeric IDs to ISO country codes (e.g., {1:'PE', 2:'BF', ...} ).
Drawbacks
The interaction relies on a manual off‑screen render; rapid clicks cause multiple renders and frame‑rate drops.
On low‑performance devices a single click may still cause noticeable stutter.
Mousemove events cannot be used for continuous hover because they may fire more often than the render loop.
Flight‑Line Visualization
Flight lines are created with THREE.CubicBezierCurve3 and rendered as points whose size and opacity are driven by a custom vertex shader.
Path Calculation
/**
* Convert longitude/latitude to 3D sphere coordinates.
*/
function getSpherePosition(lng = 0, lat = 0, radius = 100) {
if (lng < 0) lng += 360;
if (lat > 0) lat += 2;
const y = radius * Math.sin((lat * Math.PI) / 180);
const zx = radius * Math.cos((lat * Math.PI) / 180);
const x = zx * Math.sin((lng * Math.PI) / 180);
const z = zx * Math.cos((lng * Math.PI) / 180);
return new THREE.Vector3(x, y, z);
}
getSpherePosition(116.3, 39.9, 100); // => {x:66.73, y:66.78, z:-32.98}Bezier Curve Generation
// v0 start, v3 end, v1/v2 control points
let v0, v1, v2, v3;
v0 = getSpherePosition(start_lng, start_lat, 100);
v3 = getSpherePosition(end_lng, end_lat, 100);
const angle = v0.angleTo(v3);
let vtop = v0.clone().add(v3).normalize().multiplyScalar(100);
let n;
if (angle <= 1) {
n = (params.globeRadius / 5) * angle;
} else if (angle > 1 && angle < 2) {
n = (params.globeRadius / 5) * Math.pow(angle, 2);
} else {
n = (params.globeRadius / 5) * Math.pow(angle, 1.5);
}
v1 = v0.clone().add(vtop).normalize().multiplyScalar(100 + n);
v2 = v3.clone().add(vtop).normalize().multiplyScalar(100 + n);
const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3);
const points = curve.getPoints(500);
const geometry = new THREE.Geometry().setFromPoints(points);
const { length } = points;
const percents = new Float32Array(length);
for (let i = 0; i < length; i++) {
percents[i] = i / length;
}
geometry.addAttribute('percent', new THREE.BufferAttribute(percents, 1));Vertex Shader for Flight Lines
attribute float percent;
uniform float time;
uniform float number;
uniform float speed;
uniform float length;
uniform float size;
varying float opacity;
void main() {
float l = clamp(1.0 - length, 0.0, 1.0);
gl_PointSize = clamp(fract(percent * number + l - time * number * speed) - l, 0.0, 1.0) * size * (1.0 / length);
opacity = gl_PointSize / size;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}Fragment Shader for Flight Lines
varying float opacity;
uniform vec3 color;
void main() {
if (opacity <= 0.2) discard;
gl_FragColor = vec4(color, 1.0);
}Flight‑Line Drawbacks
Insufficient vertex density leads to visible gaps.
Point size is screen‑space; zooming does not scale the line, causing artifacts.
Solutions: increase point density or switch to a mesh‑based line library such as meshline .
Conclusion
The chapter demonstrates how textures, uniforms, and attributes work together in GLSL to build an interactive globe, covering index textures for country picking, depth shading for oceans, and animated flight lines, while also discussing performance pitfalls and possible improvements.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.