Um IoT-Daten an ein 3D-Modell zu binden, brauchen Sie eine kleine, klar definierte Schicht, die eine Nachricht von einem Topic entgegennimmt, einen Wert daraus herausgreift, ihn transformiert und auf ein bestimmtes Teil des Modells anwendet. Dieser Leitfaden baut diese Schicht für glTF-Modelle in Three.js: Sie bilden MQTT-Werte auf Transformationen, Sichtbarkeit, Materialfarben, Morph Targets und Animationsclips ab und speichern die Binding-Definitionen mithilfe von glTF-Extras im Modell selbst. Die Daten kommen von broker.connectmq.com, sodass Sie jedes Binding aus einem Terminal testen können.
Die Anatomie eines Bindings
Jedes Binding beantwortet unabhängig von der Engine fünf Fragen:
| Frage | Beispiel | Feld in unserem Binding |
|---|---|---|
| Welches Topic? | connectmq/demo/bind-7f3a/press1/telemetry |
topic (unterstützt + und #) |
| Welcher Wert im Payload? | hydraulics.pressure |
path |
| Wie wird er transformiert? | 0 bis 250 bar wird zu 0 bis 1 | map: [inMin, inMax, outMin, outMax], clamp |
| Welches Teil des Modells? | Knoten namens Press_Ram |
node |
| Welche Eigenschaft ändert sich? | Position auf Y, emissive Farbe, Animationsgeschwindigkeit | kind und seine Optionen |
Diese Angaben als Daten statt als Code festzuhalten, ist die wichtigste Entscheidung bei einem Zwilling. Der Code enthält ein paar generische „Kinds“; die Daten sagen, welches Topic welches Teil steuert. Fügt ein Ingenieur einen Sensor hinzu, muss niemand JavaScript bearbeiten. Wenn Sie noch keinen Zwilling gebaut haben, beginnen Sie mit dem Tutorial zum Three.js Digital Twin, kommen Sie dann für die Mapping-Schicht hierher zurück und lesen Sie für den größeren Zusammenhang den Leitfaden zur Digital-Twin-Technologie.
Das glTF-Modell für Data Binding vorbereiten
Ein Modell, das für Marketing-Renderings exportiert wurde, ist selten bereit, an Daten gebunden zu werden. Beheben Sie vor dem Export Folgendes in Blender (oder Ihrem DCC-Tool):
- Eindeutige, stabile Knotennamen.
Press_Ram,Press_Beacon,Tank_Liquid. Vermeiden Sie Namen wieCylinder.014, die sich bei einem erneuten Export ändern. - Korrekte Pivots. Ein drehender Arm braucht seinen Ursprung im Gelenk, ein Füllstand seinen Ursprung am Boden.
- Separate Objekte für gebundene Teile. Ist die Signalleuchte mit dem Maschinenkörper verbunden, färbt eine Farbänderung die ganze Maschine ein.
- Morph Targets (Shape Keys) für kontinuierliche Verformungen wie einen sich dehnenden Schlauch oder ein Zifferblatt und Animationsclips (Actions) für zyklische Bewegungen wie ein Förderband oder einen Lüfter.
- Custom Properties für Bindings. Blender exportiert Custom Properties von Objekten als glTF-
extras, wenn „Include > Custom Properties“ aktiviert ist, und Three.js legt sie inobject.userDataab.
Der letzte Punkt ermöglicht es 3D-Artist und Automatisierungsingenieur, in einer Datei zu arbeiten. Fügen Sie Press_Ram eine Custom Property namens binding mit einem JSON-String als Wert hinzu:
{
"topic": "connectmq/demo/bind-7f3a/press1/telemetry",
"path": "ram.position",
"map": [0, 400, 0, -1.2],
"clamp": true,
"kind": "translate",
"axis": "y"
}
Nach Export und Laden enthält object.userData.binding diesen String.
Binding-Arten, die die meisten Zwillinge abdecken
Sie brauchen überraschend wenige Arten. Diese sechs decken den Großteil industrieller Visualisierungen ab:
| Kind | Wirkt auf | Typische Daten | Umsetzung |
|---|---|---|---|
translate |
Knotenposition auf einer Achse | Zylinderhub, Hubhöhe | Versatz zur Ruheposition |
rotate |
Knotenrotation um eine Achse | Ventilöffnung, Armwinkel | Winkel zur Ruherotation |
spin |
Kontinuierliche Rotation | Motordrehzahl, Lüftergeschwindigkeit | value * dt pro Frame addieren |
color |
Material- oder emissive Farbe | Temperatur, Status-Enum | Farbverlauf oder Lookup-Tabelle |
morph |
Einfluss eines Morph Targets | Füllstand, Verformung | morphTargetInfluences[i] |
clip |
Geschwindigkeit oder Gewicht eines Animationsclips | Bandgeschwindigkeit, laufender Zyklus | AnimationAction.timeScale |
Sichtbarkeit ist ein Sonderfall von color-ähnlichen Lookups (visible: true/false aus einem Boolean), und Textbeschriftungen sind meist als HTML-Overlays besser aufgehoben als als 3D-Text.
Die Binding-Engine
Das folgende Modul lädt ein glTF, sammelt Bindings aus userData, gleicht eingehende MQTT-Topics ab (einschließlich Wildcards), extrahiert und transformiert Werte und wendet sie in jedem Frame an. Speichern Sie es als binding-engine.js; es setzt voraus, dass Three.js und MQTT.js wie in den vorherigen Tutorials über eine Import Map oder einen Bundler verfügbar sind.
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// ---------- Hilfsfunktionen ----------
export function topicMatches(filter, topic) {
const f = filter.split("/"), t = topic.split("/");
for (let i = 0; i < f.length; i++) {
if (f[i] === "#") return true;
if (i >= t.length) return false;
if (f[i] !== "+" && f[i] !== t[i]) return false;
}
return f.length === t.length;
}
const getPath = (obj, path) =>
path.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
function transform(value, b) {
if (b.lookup) return b.lookup[value] ?? b.lookup.default;
let v = Number(value);
if (Number.isNaN(v)) return undefined;
if (b.map) {
const [a, c, d, e] = b.map;
v = d + ((v - a) / (c - a)) * (e - d);
if (b.clamp) v = THREE.MathUtils.clamp(v, Math.min(d, e), Math.max(d, e));
}
return v;
}
// ---------- Engine ----------
export async function createTwin(scene, url) {
const gltf = await new GLTFLoader().loadAsync(url);
scene.add(gltf.scene);
const mixer = new THREE.AnimationMixer(gltf.scene);
const bindings = [];
gltf.scene.traverse((obj) => {
if (!obj.userData.binding) return;
let b;
try { b = JSON.parse(obj.userData.binding); } catch { console.warn("Bad binding on", obj.name); return; }
b.obj = obj;
b.rest = { position: obj.position.clone(), rotation: obj.rotation.clone() };
b.target = b.current = undefined;
if (b.kind === "color") {
obj.traverse((o) => { if (o.isMesh) o.material = o.material.clone(); });
}
if (b.kind === "clip") {
const clip = THREE.AnimationClip.findByName(gltf.animations, b.clip);
if (clip) { b.action = mixer.clipAction(clip); b.action.play(); b.action.timeScale = 0; }
else console.warn(`Clip ${b.clip} not found`);
}
bindings.push(b);
});
function onMessage(topic, payload) {
let data;
const text = new TextDecoder().decode(payload);
try { data = JSON.parse(text); } catch { data = text; }
for (const b of bindings) {
if (!topicMatches(b.topic, topic)) continue;
const raw = b.path ? getPath(data, b.path) : data;
const v = transform(raw, b);
if (v !== undefined) b.target = v;
}
}
const color = new THREE.Color();
function update(dt) {
const k = 1 - Math.exp(-dt * 5);
for (const b of bindings) {
if (b.target === undefined) continue;
if (typeof b.target === "number") {
b.current = b.current === undefined ? b.target : b.current + (b.target - b.current) * k;
} else {
b.current = b.target; // Strings, Farben, Booleans: keine Glättung
}
const o = b.obj, v = b.current;
switch (b.kind) {
case "translate": o.position[b.axis] = b.rest.position[b.axis] + v; break;
case "rotate": o.rotation[b.axis] = b.rest.rotation[b.axis] + THREE.MathUtils.degToRad(v); break;
case "spin": o.rotation[b.axis] += v * dt; break;
case "visible": o.visible = Boolean(v); break;
case "color":
color.set(typeof v === "number"
? new THREE.Color(b.from).lerp(new THREE.Color(b.to), v)
: v);
o.traverse((m) => {
if (!m.isMesh) return;
m.material[b.channel ?? "color"].copy(color);
});
break;
case "morph":
o.traverse((m) => {
if (m.morphTargetInfluences && m.morphTargetDictionary?.[b.morph] !== undefined) {
m.morphTargetInfluences[m.morphTargetDictionary[b.morph]] = v;
}
});
break;
case "clip": if (b.action) b.action.timeScale = v; break;
}
}
mixer.update(dt);
}
const filters = [...new Set(bindings.map((b) => b.topic))];
return { bindings, filters, onMessage, update };
}
So binden Sie es in eine Seite ein:
import * as THREE from "three";
import mqtt from "mqtt";
import { createTwin } from "./binding-engine.js";
// Szene, Kamera und Renderer wie gewohnt einrichten ...
const twin = await createTwin(scene, "/models/press.glb");
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "bind-viewer-" + crypto.randomUUID().slice(0, 8),
protocolVersion: 5,
});
client.on("connect", () => client.subscribe(twin.filters, { qos: 0 }));
client.on("message", twin.onMessage);
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
twin.update(clock.getDelta());
renderer.render(scene, camera);
});
Wenn Sie genau die Filter abonnieren, die das Modell deklariert, statt #, bleibt der Datenverkehr gering. Beachten Sie auf dem öffentlichen Broker das Limit von 10 Abonnements pro Client; deklariert ein Modell mehr Filter, abonnieren Sie stattdessen einen gemeinsamen übergeordneten Pfad mit einer Wildcard.
Weitere Binding-Beispiele
Dieselbe Engine verarbeitet ganz unterschiedliche Teile allein über Daten. Eine Signalleuchte, die von einem Enum gesteuert wird, mit Lookup-Tabelle und dem emissiven Kanal:
{
"topic": "connectmq/demo/bind-7f3a/press1/state",
"path": "state",
"lookup": { "RUNNING": "#22c55e", "IDLE": "#eab308", "FAULT": "#ef4444", "default": "#6b7280" },
"kind": "color",
"channel": "emissive"
}
Ein Tank, dessen Füllstand ein Morph Target namens Fill ist, gespeist von einem Prozentwert:
{ "topic": "connectmq/demo/bind-7f3a/tank1/telemetry", "path": "level_pct",
"map": [0, 100, 0, 1], "clamp": true, "kind": "morph", "morph": "Fill" }
Eine Förderbandanimation, deren Geschwindigkeit der Antriebsfrequenz folgt (bei 50 Hz läuft der Clip mit normaler Geschwindigkeit):
{ "topic": "connectmq/demo/bind-7f3a/conv1/telemetry", "path": "drive.hz",
"map": [0, 50, 0, 1], "clamp": true, "kind": "clip", "clip": "BeltLoop" }
Testen Sie jedes Binding direkt aus einem Terminal:
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-bind-7f3a -r \
-t "connectmq/demo/bind-7f3a/press1/telemetry" -m '{"ram":{"position":220}}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-bind-7f3a -r \
-t "connectmq/demo/bind-7f3a/press1/state" -m '{"state":"FAULT"}'
Das Flag -r hält die Nachricht als retained vor, sodass das Modell den Wert sofort nach dem Laden anzeigt. Retained-Zustand ist das Rückgrat eines Zwillings, der schon beim ersten Rendern korrekt ist; wann Sie ihn einsetzen sollten, erläutert Retained MQTT Messages für Digital Twins.
Stolperfallen beim Binden von IoT-Daten an ein 3D-Modell
- Geteilte Materialien. glTF-Exporter verwenden Materialien wieder. Klonen Sie sie für jedes Teil mit gebundener Farbe, sonst färbt ein einziger Alarm die ganze Fabrik rot.
- Absolute versus relative Transformationen. Wenden Sie Werte immer relativ zur beim Laden erfassten Ruhepose an. Andernfalls springt das Teil zum Weltursprung.
- Grad versus Bogenmaß. Sensoren melden Grad, Three.js erwartet Bogenmaß. Rechnen Sie an einer zentralen Stelle um, wie es
rotatetut. - Fehlende Werte. Payloads entwickeln sich weiter. Löst
pathzuundefinedauf, behalten Sie den vorherigen Wert, statt auf null zu springen. - Veraltete Daten. Halten Sie fest, wann jedes Binding zuletzt aktualisiert wurde, und dimmen Sie Teile, die eine Weile nichts von ihrem Gerät gehört haben.
- Umbenannter Knoten im Modell. Protokollieren Sie beim Laden eine Warnung für jedes Binding, dessen Knoten oder Clip nicht gefunden wird; stille Fehler sind am schwersten zu debuggen.
- Zu viel Glättung. Glättung passt zu kontinuierlichen Werten, verschleiert aber diskrete Ereignisse. Glätten Sie keine Enums, Booleans oder Alarmzustände.
Latenzbudgets für jede dieser Stufen behandelt Latenz von Echtzeit-Digital-Twins.
Woher die Daten kommen
Bindings sind nur so gut wie die Topics dahinter. Eine konsistente Hierarchie wie site/area/line/cell/asset/metric ermöglicht Bindings mit Wildcards und die Wiederverwendung von Modellen über Linien hinweg. In der Fabrik ist das der Unified Namespace (UNS): Die ISA-95-Ebenen Unternehmen → Standort → Bereich → Linie → Zelle bilden die Topic-Struktur auf dem MQTT-Broker, etwa acme/werk-stuttgart/montage/linie-1/zelle-3/temperatur. Ein Zwilling, der gegen den UNS gebunden ist, lässt sich auf eine zweite Linie übertragen, indem Sie nur das Linien-Segment im Topic-Filter austauschen, und er liest dieselben Daten wie MES, SCADA und OEE-Dashboards. Wenn Ihr Werk Sparkplug B veröffentlicht, kann ConnectMQ es dekodieren und in lesbare Topics im ISA-95-Stil republizieren, etwa connectmq/factory1/production/line1/cnc1/telemetry, die sich weit einfacher binden lassen als Protobuf-Payloads. Der Leitfaden zu Sparkplug B und Unified Namespace erklärt diese Struktur im Detail. Eine vollständige Anwendung, die diese Engine mit Konfiguration und Deployment nutzt, finden Sie unter einen webbasierten 3D Digital Twin bauen; ein fokussiertes Beispiel bietet das Tutorial zum Förderband-Digital-Twin.
Starten Sie mit ConnectMQ
Sobald Ihre Bindings auf dem öffentlichen Broker funktionieren, ziehen Sie sie auf einen privaten um. Ein verwalteter MQTT-Broker von ConnectMQ bietet Secure WebSocket auf Port 8084 mit vertrauenswürdigem Zertifikat sowie einen Live-Topic-Explorer, der genau zeigt, welche Pfade in Ihren Payloads existieren – beim Schreiben von Bindings unschätzbar wertvoll. Erstellen Sie einen kostenlosen Broker unter app.connectmq.com; der Serverless-Plan benötigt keine Kreditkarte. Für schnelle Experimente mit unkritischen Daten können Sie weiterhin den kostenlosen öffentlichen MQTT-Broker nutzen; für Produktionsdaten ist er nicht gedacht.
Häufig gestellte Fragen
Wo sollten Binding-Definitionen liegen: im Modell oder im Code?
Beides funktioniert. Als glTF-Extras gespeichert, bleiben Modell und Datenmapping für kleine Teams zusammen, während eine separate JSON-Konfiguration sich leichter aus einer Asset-Datenbank generieren und in der Versionsverwaltung prüfen lässt.
Kann ich Daten an Animationen binden, die aus Blender exportiert wurden?
Ja. Exportierte Actions werden zu glTF-Animationsclips. Spielen Sie sie über einen AnimationMixer ab und binden Sie die timeScale der Action für die Geschwindigkeit oder ihr Gewicht für das Überblenden zwischen Zuständen.
Wie binde ich Daten an ein Teil, das kein eigenes Mesh hat?
Einen Teil eines zusammengeführten Meshes können Sie nicht unabhängig bewegen oder umfärben. Trennen Sie das Teil in Ihrem Modellierungswerkzeug in ein eigenes Objekt und exportieren Sie erneut, oder verwenden Sie ein Morph Target, wenn die Änderung eine Verformung ist.
Funktioniert dieser Ansatz in Babylon.js oder Unity?
Das Binding-Modell aus Topic, Pfad, Transformation, Knoten und Kind ist engine-unabhängig. Nur der Anwendungsschritt ändert sich und nutzt die jeweiligen APIs der Engine für Transformationen, Materialien und Animationen.



