Designing a Layer-Based Globe
The 2020 BFCM Globe was built around a modular layer architecture, where each layer operates like an isolated React component with minimal shared state. This approach made it possible to reuse code across both the BFCM Globe and the daily Live View experience for merchants. By keeping layers self-contained, the team could quickly prototype with three.js and ship within a tight two-month window.
For realistic visuals, the EarthRealistic layer relies on physically based rendering via three.js MeshPhysicalMaterial or MeshStandardMaterial. Lighting comes from a 32bit EXR environment map, which provides smooth image based lighting without gradient banding. Because 8bit JPGs and PNGs would introduce visible color stepping, the higher bit depth was essential to preserving realism across the globe’s surface.
Building the Carbon Offset Bubble Material
The carbon offset visualization needed to replicate a bubble’s optical behavior, where transparency varies based on light intensity and angle. To achieve this, a custom material was created on top of MeshStandardMaterial. The process follows three steps:
- Create a custom material class extending
MeshStandardMaterial. - Write a custom vertex or fragment shader and define uniforms for the shader program.
- Override
onBeforeCompile(shader: Shader, _renderer: WebGLRenderer): voidto pass in the custom shaders and uniforms.
The custom ShieldMaterial reads the environment map to simulate bubble lighting. In the fragment shader, two lines calculate the brightness of the outgoing pixel and adjust its alpha accordingly. Here, the brightness is determined with the GLSL length function on the RGB vec3 value of outgoingLight. The alpha is then interpolated between the baseline diffuseColor.a and a custom maxOpacity uniform using mix. This lets artists tune the visual range between minimum and maximum opacity.
The shader file itself can look intimidating because three.js materials handle substantial functionality internally. Extending a material requires pulling the original shader source from the src/renderers/shaders/ShaderLib/ folder in the three.js repo and adding custom calculations before setting gl_FragColor. A simpler way to inspect the shader is to log the shader.fragmentShader and shader.vertexShader strings exposed in onBeforeCompile. This hook runs right before the GPU shader program is created, allowing overrides on both shaders and uniforms. The abstraction in CustomMeshStandardMaterial manages uniform lifecycle and exposes setCustomUniform and getCustomUniform helpers, which are used to adjust maxOpacity dynamically.
Order Visualization with Particles
The central feature of the BFCM Globe is real-time order display. Earlier attempts used arcs to connect buyer and merchant locations, but with thousands of orders per minute, arcs quickly created visual clutter and frame rate drops. Capping the number of arcs would have limited visibility to only a tiny fraction of orders. Instead, the team explored a particle-based approach with these goals:
- Display thousands of simultaneous orders on screen.
- Maintain 60 fps on low-end devices.
- Customize style and animation per order, including differentiating local and international orders.
Rendering individual geometry for every order wouldn’t scale. Using three.js Points allowed drawing with dots rather than triangles. Each particle’s data is stored in custom attributes on a BufferGeometry. A custom ShaderMaterial with vertex and fragment shaders handles rendering and animation. Most of the logic lives in the vertex shader, where each particle undergoes several transformations.
Each particle has latitude and longitude start and end positions. A geo interpolation function generates a path along the surface, ensuring particles don’t clip through the globe. To simulate height along this path, a parabola equation based on time warps the straight interpolation into a curved trajectory. The combined height and path data are then converted into a vector position used as gl_Position. A time uniform drives additional animations for size and color, and the fragment shader composites the final pixel with its animated alpha.
Performance considerations mattered with potentially 10,000 particles in transit at any moment. Updating all attributes every frame would be processor-intensive. The solution was to use BufferAttribute’s updateRange to modify only specific attribute subsets per frame. With these optimizations, the visualization handled upwards of 150,000 particles simultaneously without noticeable degradation.
An FPS-Driven Auto Optimizer
Game engines often scale quality dynamically based on device capability, but replicating that aggressive texture or geometry reduction wasn’t feasible within the development timeline. Instead, the team built an auto optimizer that adjusts canvas resolution based on measured performance.
The base quality level was initially tied to display resolution, but that punished capable devices with low-resolution screens while still struggling on older phones. The final design monitors frame rate: if it drops below 55 fps for longer than 2 seconds, the app lowers its quality. This lets a high-end device like the iPhone 12 Pro Max run at maximum fidelity, while an iPhone 7+ drops to lower quality but maintains a consistent high framerate.
The simplest effective adjustment was shrinking the <canvas> element’s HTML size, which reduces the pixel count and typically cuts WebGL’s workload by 2x or 3x. WebGLRenderer initialization uses setPixelRatio based on window.devicePixelRatio. When performance lags, the ratio is dropped to 1x. This raises less visible artifacts (only slight aliasing in some edges) while providing significant gains. Additionally, environment map resolution from PMREMGenerator is reduced, though the devicePixelRatio drop is the more broadly applicable technique.
Reusable Foundation, Future Globes
Shipping two globes in rapid succession validated the team’s focus on what mattered most while still delivering a high level of quality. The more significant outcome of this work is the internal library that now houses the globe implementation, ready to be reused for future data visualization projects. The architecture built for 2020 is already being used as the starting point for the next iteration.
Short on Time, Strong on Technique
Building a real-time globe that is both visually stunning and performant requires making deliberate trade-offs. The team had to balance several technical priorities to render a high-quality 3D visualization without compromising the device experience.
- Earth textures from Visible Earth NASA provided the base layer for the planet's surface, with Natural Earth data supporting geographic references.
- three.js materials were customized for the visual effect, sticking to a combination of
MeshPhysicalMaterialandMeshStandardMaterialfor the core rendering. - Custom shader work through
Material.onBeforeCompilegave the team direct control over how the globe would react to light and color. PointsandBufferGeometryhandled the massive data plotting, supported byShaderMaterialandBufferAttributeto stream the sales data into the view effectively.- OpenGL functions such as
lengthandmixwere referenced for the math behind the visual transitions and color gradients that make the data feel alive.
Performance at the Core
The underlying goal was not just to make the globe look good but to ensure it ran smoothly across a broad range of devices. The WebGLRenderer from three.js was configured with a careful eye on the device, using setPixelRatio to limit how many physical pixels are drawn. This avoids overloading the GPU, particularly on high-density mobile screens where the cost of rendering can escalate fast.
Handling environment map lighting also mattered. The PMREMGenerator pre-processes the lighting information that reaches the globe's surface material, guaranteeing that the quality of reflections stays realistic even as the globe spins into different angles.
These light-but-effective techniques look familiar, but they carry nuance when you have one shot to render a data stream at scale. Keeping the vertex count down, managing memory with strict buffers, and controlling draw calls made it possible to pull off the whole experience without dropping the frame rate.
Acknowledgments
The work was carried out by a seasoned team. Development Manager Mikko Haapoja, based in Toronto, focuses on the 3D/AR/VR side and was the driving force behind getting the project out. Staff Developer Stephan Leroux, centered on AR/VR intersections with commerce, provided the technical depth. Both dedicated their effort to turning a dense set of event data into a user-friendly visual framework.
Looking Ahead
This year also brought with it a large-scale hiring push; the company is set to bring on 2,021 new technical staff in order to double the engineering department.



