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.