Ein Babylon.js Digital Twin bietet Ihnen eine voll ausgestattete 3D-Engine im Browser mit integriertem Picking, Highlight-Effekten und einem Inspector, wodurch sich interaktive Zwillinge schnell bauen lassen. In diesem Tutorial erstellen Sie ein kleines Kühlmodul (eine Pumpe, einen Tank und ein Manometer), das über Secure WebSocket auf Live-MQTT-Telemetrie reagiert, bei hohem Druck rot leuchtet und sich per Klick auf die Pumpe starten oder stoppen lässt. Alle Daten laufen über den öffentlichen MQTT-Broker broker.connectmq.com, und ein Python-Simulator übernimmt die Rolle der realen Anlage.
Warum Babylon.js für einen Digital Twin
Babylon.js und Three.js rendern beide WebGL-Szenen im Browser und arbeiten beide gut mit MQTT zusammen. Der Unterschied liegt darin, was bereits mitgeliefert wird:
| Funktion | Babylon.js | Three.js |
|---|---|---|
| Picking und Pointer-Aktionen | Integriert (ActionManager, scene.pick) |
Raycaster, Events verdrahten Sie selbst |
| Glow- und Highlight-Effekte | HighlightLayer, GlowLayer integriert |
Post-Processing-Passes zusammenstellen |
| Debug-Inspector | scene.debugLayer.show() |
Tools von Drittanbietern |
| Bundle-Größe und API-Stil | Größer, mehr „Batteries included“ | Kleiner, modularer |
| glTF-Laden | Loaders-Paket | GLTFLoader-Addon |
Wenn Sie eine schlankere Bibliothek bevorzugen, baut unser Tutorial zum Three.js Digital Twin eine ähnliche Szene. Geht es beim Zwilling vor allem um Interaktion (Maschine anklicken, Details sehen, Befehl senden), spart Babylon.js viel Verdrahtungsarbeit. Die übergreifenden Konzepte behandelt unser Leitfaden zur Digital-Twin-Technologie.
Topics und Payloads
Der Zwilling nutzt drei Topics unter einem eindeutigen Präfix. Ersetzen Sie bjs-7f3a durch ein eigenes zufälliges Suffix:
| Topic | Publisher | Payload | Verhalten des Zwillings |
|---|---|---|---|
connectmq/demo/bjs-7f3a/skid1/telemetry |
Anlage | {"pressure":3.2,"level":0.55,"running":true} |
Manometerzeiger, Tankfüllstand, Pumpendrehung |
connectmq/demo/bjs-7f3a/skid1/cmd |
Zwilling (bei Klick) | START oder STOP |
Simulator ändert den Pumpenzustand |
connectmq/demo/bjs-7f3a/skid1/status |
Anlage, retained, Last Will | online / offline |
Ausgrauen, wenn offline |
Der Druck wird in bar angegeben, der Füllstand als Anteil von 0 bis 1. Feste und dokumentierte Einheiten vermeiden den häufigsten Fehler bei Zwillingen: ein Manometer, das Kilopascal als bar anzeigt.
Im Werk ersetzen Sie das Demo-Präfix durch einen Unified Namespace (UNS). Dort wird die ISA-95-Anlagenhierarchie (Enterprise → Site → Area → Line → Cell) zur Topic-Struktur auf dem Broker, und das Kühlmodul erscheint etwa unter acme/werk-stuttgart/lackiererei/linie-1/kuehlmodul-1/telemetry. Der Babylon.js-Zwilling ist dann nur ein weiterer Konsument dieses Namespace, neben SCADA, MES und Historian, und kann per Wildcard eine ganze Linie oder Halle abonnieren. Den Aufbau erklärt der Beitrag zu Sparkplug B und Unified Namespace.
Schritt 1: Der Anlagensimulator
Installieren Sie paho-mqtt 2.x (pip install "paho-mqtt>=2.0") und starten Sie dieses Skript. Es simuliert einen langsam steigenden Druck, solange die Pumpe läuft, mit gelegentlichen Spitzen über die Alarmgrenze von 6 bar, und befolgt Befehle des Zwillings.
import json
import math
import random
import time
import paho.mqtt.client as mqtt
P = "connectmq/demo/bjs-7f3a/skid1"
state = {"running": True}
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.publish(f"{P}/status", "online", qos=1, retain=True)
client.subscribe(f"{P}/cmd", qos=1)
def on_message(client, userdata, msg):
cmd = msg.payload.decode().strip().upper()
if cmd in ("START", "STOP"):
state["running"] = cmd == "START"
print("command", cmd)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="bjs-sim-7f3a")
client.will_set(f"{P}/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()
t, level = 0, 0.5
while True:
t += 1
if state["running"]:
pressure = 4.0 + 1.2 * math.sin(t / 10) + (2.5 if random.random() < 0.05 else 0)
level = max(0.1, level - 0.01)
else:
pressure = max(0.2, 1.0 + random.uniform(-0.1, 0.1))
level = min(0.95, level + 0.02)
payload = {"pressure": round(pressure, 2), "level": round(level, 2),
"running": state["running"]}
client.publish(f"{P}/telemetry", json.dumps(payload), qos=0, retain=True)
time.sleep(1)
Die Telemetrie wird retained veröffentlicht, sodass der Zwilling die neuesten Werte im Moment der Verbindung anzeigt, statt eine Sekunde zu warten. Prüfen Sie den Datenstrom mit mosquitto_sub -h broker.connectmq.com -p 1883 -i chk-7f3a -v -t "connectmq/demo/bjs-7f3a/#".
Schritt 2: Den Babylon.js Digital Twin bauen
Speichern Sie dies als index.html und stellen Sie es mit einem beliebigen statischen Server bereit (npx serve .). Die Seite lädt Babylon.js vom offiziellen CDN und MQTT.js als Browser-Bundle. Für die Produktion pinnen Sie beide auf exakte, von Ihnen getestete Versionen.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Babylon.js Digital Twin</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #111827; }
#c { width: 100%; height: 100%; touch-action: none; }
#info { position: absolute; top: 10px; left: 10px; color: #fff; font: 14px system-ui;
background: rgba(0,0,0,.6); padding: 8px 12px; border-radius: 6px; }
</style>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
</head>
<body>
<canvas id="c"></canvas>
<div id="info">Connecting...</div>
<script>
const PREFIX = "connectmq/demo/bjs-7f3a/skid1";
const ALARM_BAR = 6.0;
const info = document.getElementById("info");
const canvas = document.getElementById("c");
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.07, 0.09, 0.15, 1);
const camera = new BABYLON.ArcRotateCamera("cam", -Math.PI / 3, Math.PI / 3, 14, new BABYLON.Vector3(0, 1, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0.3, 1, 0.2), scene);
BABYLON.MeshBuilder.CreateGround("ground", { width: 16, height: 10 }, scene);
const mat = (name, hex) => {
const m = new BABYLON.StandardMaterial(name, scene);
m.diffuseColor = BABYLON.Color3.FromHexString(hex);
return m;
};
// Pumpe: Gehäuse + Rotor
const pump = BABYLON.MeshBuilder.CreateCylinder("pump", { diameter: 1.6, height: 1.2 }, scene);
pump.rotation.z = Math.PI / 2;
pump.position.set(-3, 0.8, 0);
pump.material = mat("pumpMat", "#3b82f6");
const rotor = BABYLON.MeshBuilder.CreateBox("rotor", { width: 0.1, height: 1.4, depth: 0.2 }, scene);
rotor.position.set(-2.3, 0.8, 0);
rotor.material = mat("rotorMat", "#e5e7eb");
// Tank mit Flüssigkeit, die auf Y skaliert (Ursprung an den Boden verschoben)
const shell = BABYLON.MeshBuilder.CreateCylinder("shell", { diameter: 2.2, height: 3 }, scene);
shell.position.set(1, 1.5, 0);
shell.material = mat("shellMat", "#9ca3af");
shell.material.alpha = 0.25;
const liquid = BABYLON.MeshBuilder.CreateCylinder("liquid", { diameter: 2, height: 1 }, scene);
liquid.setPivotPoint(new BABYLON.Vector3(0, -0.5, 0));
liquid.position.set(1, 0.5, 0);
liquid.material = mat("liquidMat", "#0ea5e9");
// Manometer: Skala + Zeiger mit Drehpunkt am Fuß
const dial = BABYLON.MeshBuilder.CreateDisc("dial", { radius: 0.9, tessellation: 48 }, scene);
dial.position.set(4.5, 2, -0.01);
dial.material = mat("dialMat", "#f9fafb");
const needle = BABYLON.MeshBuilder.CreateBox("needle", { width: 0.06, height: 0.8, depth: 0.02 }, scene);
needle.setPivotPoint(new BABYLON.Vector3(0, -0.4, 0));
needle.position.set(4.5, 2.4, -0.05);
needle.material = mat("needleMat", "#dc2626");
const highlight = new BABYLON.HighlightLayer("hl", scene);
// ---------- Zustand ----------
const target = { pressure: 0, level: 0, running: false, online: false };
const cur = { pressure: 0, level: 0, rotorSpeed: 0 };
let alarmOn = false;
// ---------- MQTT ----------
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "bjs-twin-" + Math.random().toString(16).slice(2, 10),
protocolVersion: 5,
reconnectPeriod: 2000,
});
const dec = new TextDecoder();
client.on("connect", () => client.subscribe(`${PREFIX}/#`, { qos: 1 }));
client.on("message", (topic, payload) => {
const text = dec.decode(payload);
if (topic === `${PREFIX}/status`) { target.online = text === "online"; return; }
if (topic !== `${PREFIX}/telemetry`) return;
try {
const d = JSON.parse(text);
if (typeof d.pressure === "number") target.pressure = d.pressure;
if (typeof d.level === "number") target.level = Math.min(Math.max(d.level, 0), 1);
if (typeof d.running === "boolean") target.running = d.running;
} catch { /* fehlerhafte Payloads ignorieren */ }
});
// ---------- Pumpe per Klick umschalten ----------
pump.actionManager = new BABYLON.ActionManager(scene);
pump.actionManager.registerAction(new BABYLON.ExecuteCodeAction(
BABYLON.ActionManager.OnPickTrigger, () => {
const cmd = target.running ? "STOP" : "START";
if (confirm(`Send ${cmd} to pump?`)) client.publish(`${PREFIX}/cmd`, cmd, { qos: 1 });
}));
pump.actionManager.registerAction(new BABYLON.SetValueAction(
BABYLON.ActionManager.OnPointerOverTrigger, pump, "scaling", new BABYLON.Vector3(1.05, 1.05, 1.05)));
pump.actionManager.registerAction(new BABYLON.SetValueAction(
BABYLON.ActionManager.OnPointerOutTrigger, pump, "scaling", new BABYLON.Vector3(1, 1, 1)));
// ---------- Aktualisierung pro Frame ----------
scene.onBeforeRenderObservable.add(() => {
const dt = engine.getDeltaTime() / 1000;
const k = 1 - Math.exp(-dt * 4);
cur.pressure += (target.pressure - cur.pressure) * k;
cur.level += (target.level - cur.level) * k;
cur.rotorSpeed += ((target.running ? 12 : 0) - cur.rotorSpeed) * k;
liquid.scaling.y = Math.max(cur.level * 2.9, 0.01);
rotor.rotation.x += cur.rotorSpeed * dt;
// 0 bar = -120 Grad, 10 bar = +120 Grad
needle.rotation.z = -BABYLON.Tools.ToRadians(-120 + (Math.min(cur.pressure, 10) / 10) * 240);
pump.material.diffuseColor = BABYLON.Color3.FromHexString(
!target.online ? "#6b7280" : target.running ? "#22c55e" : "#3b82f6");
const shouldAlarm = target.online && target.pressure > ALARM_BAR;
if (shouldAlarm !== alarmOn) {
alarmOn = shouldAlarm;
if (alarmOn) highlight.addMesh(pump, BABYLON.Color3.Red());
else highlight.removeMesh(pump);
}
info.textContent = `${client.connected ? "Live" : "Disconnected"} | ` +
`${cur.pressure.toFixed(2)} bar | level ${(cur.level * 100).toFixed(0)} % | ` +
`pump ${target.running ? "running" : "stopped"}${alarmOn ? " | HIGH PRESSURE" : ""}`;
});
engine.runRenderLoop(() => scene.render());
addEventListener("resize", () => engine.resize());
</script>
</body>
</html>
Öffnen Sie die Seite bei laufendem Simulator. Der Tank leert sich langsam, solange die Pumpe läuft, der Zeiger schwingt mit dem Druck, die Pumpe leuchtet rot, sobald der Druck 6 bar übersteigt, und ein Klick auf die Pumpe sendet einen bestätigten Befehl. Beenden Sie den Simulator mit Strg+C; sobald der Broker den Last Will veröffentlicht, wird die Pumpe grau.
So funktionieren die Babylon.js-Bindings
Pivot-Punkte statt Geometrieänderungen. setPivotPoint verschiebt den Ursprung für Skalierung und Rotation an den Boden der Flüssigkeit und an den Fuß des Zeigers, sodass die Skalierung vom Boden aus füllt und sich der Zeiger um seine Nabe dreht. Das ist das Babylon.js-Gegenstück zum Setzen des Ursprungs in Blender.
onBeforeRenderObservable als einziger Aktualisierungspunkt. Der MQTT-Handler schreibt nur in target. Alles Visuelle passiert einmal pro Frame, geglättet mit einem bildratenunabhängigen Faktor. So bleibt die Animation flüssig, auch wenn Nachrichten unregelmäßig eintreffen.
Zustandswechsel statt Arbeit in jedem Frame. Der Highlight Layer wird nur geändert, wenn der Alarmzustand umschlägt. Meshes in jedem Frame hinzuzufügen und zu entfernen, wäre verschwendete Arbeit und kann flackern.
Befehle mit Bestätigung. Ein Klick auf ein 3D-Objekt passiert leicht versehentlich, besonders beim Drehen der Kamera. Der confirm()-Dialog ist die minimale Absicherung; reale Installationen verlangen oft zusätzlich eine Rollenprüfung. Befehle verwenden QoS 1, damit sie einen kurzen Netzwerkaussetzer überstehen; warum, zeigt MQTT-QoS-Stufen erklärt.
Das Gerät bestätigt das Ergebnis. Die Pumpenfarbe spiegelt running aus der Telemetrie wider, nicht den Button, den Sie gedrückt haben. Lehnt die Anlage den Befehl ab oder verpasst sie ihn, zeigt der Zwilling die Wahrheit.
Performance-Tipps für größere Zwillinge
Ein Modul mit drei Meshes läuft mühelos, eine ganze Linie mit Hunderten von Teilen erfordert aber etwas Sorgfalt. Frieren Sie Meshes, die sich nie bewegen, mit mesh.freezeWorldMatrix() ein, damit Babylon.js ihre Matrix-Updates überspringt. Teilen Sie Materialien zwischen statischen Teilen und klonen Sie nur diejenigen, deren Farbe an Daten gebunden ist. Verwenden Sie Instanzen (mesh.createInstance) für wiederkehrende Elemente wie Rollen oder Paletten. Halten Sie pro Seite eine MQTT-Verbindung mit einem Wildcard-Abonnement und verteilen Sie Nachrichten lokal, statt pro Asset eine Verbindung zu öffnen. Drosseln Sie schließlich Text-Overlays: DOM-Elemente mit 60 Bildern pro Sekunde neu zu schreiben, ist oft teurer als das 3D-Rendering selbst, aktualisieren Sie sie also nur ein paar Mal pro Sekunde.
Checkliste zur Fehlerbehebung
- Hängt bei „Connecting...“: Prüfen Sie, ob die URL
wss://broker.connectmq.com:8084/mqttlautet, einschließlich des Pfads/mqtt, und suchen Sie in der Browserkonsole nach WebSocket-Fehlern. Unternehmens-Proxys blockieren manchmal WebSocket-Upgrades. BABYLON is not defined: Das Babylon.js-Script-Tag konnte nicht geladen werden; prüfen Sie den Netzwerk-Tab und eine eventuelle Content Security Policy.- Werte erscheinen nie: Ihr Topic-Präfix in der Seite weicht von dem des Simulators ab. Vergleichen Sie sie Zeichen für Zeichen.
- Verbindung bricht bei zwei offenen Tabs wiederholt ab: Jeder Tab muss eine eindeutige Client ID verwenden, wofür das zufällige Suffix sorgt.
- Nutzen Sie den Inspector: Rufen Sie
scene.debugLayer.show()in der Konsole auf, um Meshes, Materialien und Pivots live zu untersuchen.
Hintergründe zu WebSocket-Ports und -Pfaden finden Sie unter MQTT über WebSocket.
Von Primitiven zum echten Anlagenmodell
Ersetzen Sie die Primitive durch ein glTF-Modell, indem Sie das Babylon.js-Loaders-Skript einbinden und BABYLON.SceneLoader.ImportMeshAsync("", "/models/", "skid.glb", scene) aufrufen. Anschließend finden Sie Teile mit scene.getMeshByName("Pump_Body"). Namenskonventionen und Mapping-Muster sind in jeder Engine gleich; IoT-Daten an ein 3D-Modell binden erklärt sie, und einen webbasierten 3D Digital Twin bauen zeigt eine konfigurationsgesteuerte Struktur, die bis zu einer ganzen Fabrik skaliert.
Produktion: Ihr eigener Broker
Der öffentliche MQTT-Broker ist anonym und für jeden sichtbar – in Ordnung für dieses Tutorial, falsch für ein Werk. Ein verwalteter MQTT-Broker bietet Ihnen Zugangsdaten, TLS und Isolation. Mit ConnectMQ erstellen Sie im Portal einen Broker und erhalten einen generierten Benutzernamen, ein Passwort und eine Client ID; fügen Sie diese als username, password und clientId zu den mqtt.connect-Optionen hinzu (YOUR_USERNAME, YOUR_PASSWORD, YOUR_CLIENT_ID). Secure WebSocket auf Port 8084 nutzt ein Let's-Encrypt-Zertifikat, dem Browser ohne Konfiguration vertrauen. Stellen Sie die Zwillingsseite hinter Ihrem eigenen Login bereit, denn alles, was an den Browser ausgeliefert wird, können dessen Nutzer lesen.
Starten Sie mit ConnectMQ
Die 3D-Digital-Twin-Webdemo von ConnectMQ zeigt eine vollständige Fabrikszene, die über Secure WebSocket streamt, und derselbe Broker steht für Ihren eigenen Babylon.js Digital Twin bereit. Erstellen Sie einen kostenlosen Broker unter app.connectmq.com: Der Serverless-Plan benötigt keine Kreditkarte, unterstützt MQTT 5 und bis zu 25 Verbindungen und enthält einen Live-Topic-Explorer, mit dem Sie Ihre Telemetrie prüfen, bevor Sie sie binden. Bis dahin steht der öffentliche MQTT-Broker für Experimente und Prototypen bereit.
Häufig gestellte Fragen
Ist Babylon.js oder Three.js besser für Digital Twins?
Beide funktionieren gut. Babylon.js bringt mehr integrierte Funktionen wie Picking-Aktionen, Highlight Layer und einen Inspector mit, während Three.js kleiner und modularer ist; wählen Sie die Bibliothek, mit der Ihr Team besser vertraut ist.
Wie verbindet sich eine Babylon.js-Seite mit MQTT?
Über MQTT.js, das in derselben Seite per WebSocket läuft, weil Browser keine rohen TCP-Verbindungen öffnen können. Der Broker muss einen MQTT-over-WebSocket-Endpunkt anbieten, idealerweise Secure WebSocket.
Kann ich Objekte im Zwilling anklicken, um Anlagen zu steuern?
Ja. Hängen Sie einen ActionManager an ein Mesh und veröffentlichen Sie bei OnPickTrigger einen Befehl. Ergänzen Sie eine Bestätigung und lassen Sie das Gerät seinen tatsächlichen Zustand zurückmelden.
Wie lade ich mein eigenes CAD-Modell in Babylon.js?
Konvertieren Sie es nach glTF (.glb), reduzieren Sie die Polygonzahl, geben Sie gebundenen Teilen eindeutige Namen und laden Sie es mit SceneLoader.ImportMeshAsync.



