Building Interactive 3D Energy Storage Visualization with Three.js and WebGL

Overview

Energy storage systems play a critical role in modern smart grids—balancing supply and demand, smoothing load peaks, and enabling distributed renewable integration. This article demonstrates how to build a production-grade 3D visualization system for energy storage stations using Three.js and WebGL, emphasizing modularity, real-time data binding, and intuitive interaction.

The solution combines parametric modeling, dynamic scene management, live telemetry integration, and responsive UI components—without relying on pre-baked 3D assets. All geometry is generated programmatically for minimal payload size and maximum runtime flexibility.


Core Visualization Components

1. Scene Composition & Camera Control

A layered scene architecture separates infrastructure, equipment, and overlays:

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f2f5);

// Skybox for ambient lighting
const skybox = createSkybox();
scene.add(skybox);

// Directional light mimicking sun position
const sunLight = new THREE.DirectionalLight(0xffffff, 1);
sunLight.position.set(-742, 1024, 357);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
scene.add(sunLight);

Camera transitions are eased using THREE.AnimationMixer for smooth navigation between views:

function transitionTo(targetPosition, targetLookAt, duration = 1000) {
  const mixer = new THREE.AnimationMixer(camera);
  const clip = new THREE.VectorKeyframeTrack(
    '.position',
    [0, duration / 1000],
    [camera.position.toArray(), targetPosition.toArray()]
  );
  const lookAtClip = new THREE.VectorKeyframeTrack(
    '.lookAt',
    [0, duration / 1000],
    [camera.position.toArray(), targetLookAt.toArray()]
  );
  const action = mixer.clipAction(clip);
  action.play();
  // ... similar for lookAt
}

2. Parametric Equipment Modeling

Instead of loading external .glb files, all core assets are built procedurally:

  • Storage Cabinet Shell — Extruded polygon mesh with semi-transparent skin:
function createCabinetShell() {
  const shape = new THREE.Shape();
  shape.moveTo(0, 0);
  shape.lineTo(0, 23);
  shape.bezierCurveTo(3, 23, 4, 27, 8, 27);
  shape.lineTo(56, 23);
  shape.lineTo(56, 9);
  shape.bezierCurveTo(52, 8, 52, 5, 48, 5);
  shape.lineTo(24, 0);
  
  const extrudeSettings = {
    steps: 1,
    amount: 10,
    bevelEnabled: true,
    bevelThickness: 2,
    bevelSize: 2
  };
  
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  const material = new THREE.MeshPhongMaterial({
    color: 0x345a9d,
    transparent: true,
    opacity: 0.35,
    side: THREE.DoubleSide
  });
  
  return new THREE.Mesh(geometry, material);
}
  • Battery Module Array — Grid-based instantiation with dynamic scaling:
function createBatteryArray(rows = 4, cols = 6) {
  const group = new THREE.Group();
  const spacing = { x: 120, z: 80 };
  
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const battery = createCylindricalCell();
      battery.position.set(
        (c - cols / 2) * spacing.x,
        -1165 + r * 25,
        (r - rows / 2) * spacing.z
      );
      battery.scale.set(0.5, 1.2, 0.5);
      group.add(battery);
    }
  }
  
  return group;
}

3. Real-Time Data Integration

Telemetry is fetched via standardized REST endpoints and mapped to visual properties:

class DataConnector {
  constructor(baseURL = '/api/v1') {
    this.endpoints = {
      temperature: `${baseURL}/temperatures`,
      soc: `${baseURL}/soc`,
      powerFlow: `${baseURL}/dis-charge`,
      status: `${baseURL}/battery-basic`
    };
  }

  async fetchTemperatureGrid() {
    const res = await fetch(this.endpoints.temperature);
    const data = await res.json();
    return data.map(point => ({
      x: point.x,
      y: point.y,
      z: point.z,
      value: Math.max(15, Math.min(45, point.temp))
    }));
  }
}

Thermal maps are rendered as dynamic texture overlays using THREE.CanvasTexture:

function updateHeatmap(dataPoints, canvas) {
  const ctx = canvas.getContext('2d');
  const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 10);
  gradient.addColorStop(0, '#00ffff');
  gradient.addColorStop(0.5, '#ffff00');
  gradient.addColorStop(1, '#ff0000');

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  dataPoints.forEach(p => {
    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.arc(p.x * 10, p.y * 10, 8 + p.value * 0.3, 0, Math.PI * 2);
    ctx.fill();
  });

  return new THREE.CanvasTexture(canvas);
}

4. Interactive Behaviors

Event-driven interactions use raycasting against object hierarchies:

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

window.addEventListener('click', (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(storageCabinets);
  
  if (intersects.length > 0) {
    const cabinet = intersects[0].object;
    highlightCabinet(cabinet);
    showCabinetDetails(cabinet.userData.id);
  }
});

function highlightCabinet(mesh) {
  mesh.material.emissive.setHex(0x44aaff);
  mesh.material.emissiveIntensity = 0.8;
  
  // Animate scale pulse
  new TWEEN.Tween(mesh.scale)
    .to({ x: 1.03, y: 1.03, z: 1.03 }, 300)
    .yoyo(true)
    .repeat(1)
    .start();
}

Double-click triggers drill-down into internal structure, fading non-target objects:

window.addEventListener('dblclick', () => {
  const visibleObjects = scene.children.filter(o => o.visible);
  visibleObjects.forEach(obj => {
    if (obj !== activeCabinet && obj.type !== 'Light') {
      new TWEEN.Tween(obj.material)
        .to({ opacity: 0.15 }, 400)
        .start();
    }
  });
  
  // Load inner model asynchronously
  loadInnerModel(activeCabinet.userData.innerModelPath);
});

5. Animated Operational States

Battery charge/discharge cycles are visualized using morph targets and animated scaling:

function animateBatteryCharge(cell, progress) {
  // Scale Z axis to simulate swelling during charging
  cell.scale.z = 0.9 + progress * 0.2;
  
  // Pulse glow intensity based on SOC
  const intensity = 0.2 + Math.sin(Date.now() * 0.002) * 0.15;
  cell.material.emissiveIntensity = intensity;
  
  // Update color gradient
  const hue = Math.max(180, 240 - progress * 120); // blue → yellow → red
  cell.material.emissive.setHSL(hue / 360, 0.8, 0.5);
}

6. Dashboard Integration

ECharts panels are embedded as HTML overlays positioned in world space using CSS3DRenderer:

const cssRenderer = new CSS3DRenderer();
cssRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(cssRenderer.domElement);

function createChartPanel(title, chartOptions) {
  const div = document.createElement('div');
  div.className = 'chart-panel';
  div.innerHTML = `<h3>${title}</h3><div class="echart-container"></div>`;
  
  const container = div.querySelector('.echart-container');
  const chart = echarts.init(container);
  chart.setOption(chartOptions);
  
  const object = new CSS3DObject(div);
  object.position.set(-500, 200, -2000);
  object.rotation.y = Math.PI / 12;
  
  return { object, chart };
}

Weather feed is integrated via iframe overlay aligned to corner viewport:

<iframe 
  src="https://i.tianqi.com/?c=code&a=getcode&id=9&py=changshou&icon=1&color=white" 
  width="100%" 
  height="9vh" 
  frameborder="0" 
  style="margin-left:1.02vh;"
></iframe>

System Architecture

  • Frontend: Three.js + Webpack + ECharts + Tween.js
  • Data Layer: REST API with JSON telemetry payloads
  • Rendering Pipeline: WebGL context with shadow mapping, environment maps, and post-processing effects
  • Interaction Model: Raycast-driven selection, state-aware navigation, and progressive disclosure

No third-party 3D modeling tools are required—the entire scene is constructed from code, ensuring version control, reproducibility, and zero asset bundling overhead.

Tags: Three.js WebGL energy-storage real-time-visualization iot-dashboard

Posted on Sat, 05 Sep 2026 16:12:12 +0000 by [n00b]