Dijital İkiz

IoT Verisini 3D Modele Bağlama: glTF Mesh'leri, Materyaller ve Animasyonlar

Dijital ikizin eşleme katmanına pratik bir rehber: MQTT topic değerlerini glTF node'larına, materyallere, morph target'lara ve animasyon kliplerine eşleyerek IoT verisini bir 3D modele bağlama ve bağlamaları modelin içinde saklama.

MQTT topic'lerinin glTF mesh'lerine ve materyallerine eşlendiği, IoT verisinin bir 3D modele nasıl bağlandığını gösteren diyagram
Bunu kendi MQTT broker’ınızda çalıştırınFree planla özel, yönetilen bir MQTT broker alın ve hazır kod örnekleriyle başlayın.
Ücretsiz deneyin Kod örnekleri Kart gerekmez

IoT verisini bir 3D modele bağlamak için bir topic'ten gelen mesajı alan, içinden bir değer seçen, onu dönüştüren ve modelin belirli bir parçasına uygulayan küçük ve iyi tanımlanmış bir katmana ihtiyacınız vardır. Bu rehber, Three.js'teki glTF modelleri için bu katmanı kuruyor: MQTT değerlerini transform'lara, görünürlüğe, materyal renklerine, morph target'lara ve animasyon kliplerine eşleyecek, bağlama tanımlarını glTF extras kullanarak modelin içinde saklayacaksınız. Veriler broker.connectmq.com adresinden akar; böylece her bağlamayı bir terminalden test edebilirsiniz.

Bir Bağlamanın Anatomisi

Motordan bağımsız olarak her bağlama beş soruyu yanıtlar:

Soru Örnek Bağlamamızdaki alan
Hangi topic? connectmq/demo/bind-7f3a/press1/telemetry topic (+ ve # desteklenir)
Payload'daki hangi değer? hydraulics.pressure path
Nasıl dönüştürülür? 0 ile 250 bar arası, 0 ile 1 arasına dönüşür map: [inMin, inMax, outMin, outMax], clamp
Modelin hangi parçası? Press_Ram adlı node node
Hangi özellik değişir? Y eksenindeki konum, emissive renk, animasyon hızı kind ve seçenekleri

Bunları kod yerine veri olarak yazmak, bir ikizdeki en önemli karardır. Kod birkaç genel "tür" barındırır; veri ise hangi topic'in hangi parçayı yönettiğini söyler. Bir mühendis yeni bir sensör eklediğinde kimse JavaScript düzenlemez. Daha önce bir ikiz oluşturmadıysanız Three.js dijital ikiz eğitimiyle başlayın, ardından eşleme katmanı için buraya dönün; daha geniş bağlam için dijital ikiz teknolojisi rehberine bakın.

glTF Modelini Veri Bağlamaya Hazırlama

Pazarlama render'ları için dışa aktarılmış bir model nadiren veriye bağlanmaya hazırdır. Dışa aktarmadan önce Blender'da (veya kullandığınız DCC aracında) şunları düzeltin:

  1. Benzersiz ve kalıcı node adları. Press_Ram, Press_Beacon, Tank_Liquid. Yeniden dışa aktarımda değişen Cylinder.014 gibi adlardan kaçının.
  2. Doğru pivot noktaları. Dönen bir kolun orijini eklem noktasında, bir doluluk seviyesinin orijini ise tabanda olmalıdır.
  3. Bağlanan parçalar için ayrı nesneler. İkaz lambası makine gövdesiyle birleştirilmişse, rengini değiştirmek bütün makineyi boyar.
  4. Morph target'lar (shape key'ler) bir hortumun uzaması veya bir gösterge yüzü gibi sürekli deformasyonlar için; animasyon klipleri (action'lar) ise konveyör veya fan gibi döngüsel hareketler için.
  5. Bağlamalar için özel özellikler. Blender, "Include > Custom Properties" etkinleştirildiğinde nesnelerin özel özelliklerini glTF extras olarak dışa aktarır ve Three.js bunları object.userData içine yerleştirir.

Bu son madde, 3D sanatçısının ve otomasyon mühendisinin tek bir dosyada çalışmasına olanak tanır. Press_Ram nesnesine, değeri bir JSON dizesi olan binding adlı özel bir özellik ekleyin:

{
  "topic": "connectmq/demo/bind-7f3a/press1/telemetry",
  "path": "ram.position",
  "map": [0, 400, 0, -1.2],
  "clamp": true,
  "kind": "translate",
  "axis": "y"
}

Dışa aktarma ve yükleme sonrasında object.userData.binding bu dizeyi içerir.

İkizlerin Çoğunu Kapsayan Bağlama Türleri

Şaşırtıcı derecede az türe ihtiyacınız vardır. Bu altı tür, endüstriyel görselleştirmelerin büyük bölümünü kapsar:

Tür Uygulandığı yer Tipik veri Uygulama
translate Bir eksendeki node konumu Silindir stroku, kaldırma yüksekliği Dinlenme konumundan ofset
rotate Bir eksendeki node dönüşü Vana açıklığı, kol açısı Dinlenme dönüşünden açı
spin Sürekli dönüş Motor devri, fan hızı Her karede value * dt ekleme
color Materyal rengi veya emissive Sıcaklık, durum enum'u Gradyan veya arama tablosu
morph Morph target etkisi Doluluk seviyesi, deformasyon morphTargetInfluences[i]
clip Animasyon klibi hızı veya ağırlığı Konveyör hızı, çalışan çevrim AnimationAction.timeScale

Görünürlük, color benzeri aramaların özel bir durumudur (bir boolean'dan visible: true/false) ve metin etiketleri genellikle 3D metin yerine HTML katmanları olarak daha iyi sonuç verir.

Bağlama Motoru

Aşağıdaki modül bir glTF yükler, bağlamaları userData içinden toplar, gelen MQTT topic'lerini (wildcard'lar dahil) eşleştirir, değerleri çıkarıp dönüştürür ve her karede uygular. Bunu binding-engine.js olarak kaydedin; önceki eğitimlerde olduğu gibi Three.js ve MQTT.js'in bir import map veya bundler aracılığıyla kullanılabilir olduğunu varsayar.

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

// ---------- helpers ----------
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, colors, booleans: no easing
      }
      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 };
}

Modülü bir sayfaya bağlayın:

import * as THREE from "three";
import mqtt from "mqtt";
import { createTwin } from "./binding-engine.js";

// scene, camera and renderer set up as usual ...
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);
});

# yerine tam olarak modelin bildirdiği filtrelere abone olmak trafiği düşük tutar. Public broker'da client başına 10 abonelik sınırına dikkat edin; bir model bundan fazla filtre bildiriyorsa bunun yerine ortak bir üst seviyeye wildcard ile abone olun.

Daha Fazla Bağlama Örneği

Aynı motor, yalnızca veriyle birbirinden çok farklı parçaları yönetir. Arama tablosu ve emissive kanalı kullanan, bir enum ile yönetilen durum lambası:

{
  "topic": "connectmq/demo/bind-7f3a/press1/state",
  "path": "state",
  "lookup": { "RUNNING": "#22c55e", "IDLE": "#eab308", "FAULT": "#ef4444", "default": "#6b7280" },
  "kind": "color",
  "channel": "emissive"
}

Seviyesi Fill adlı bir morph target olan ve bir yüzde değeriyle beslenen bir tank:

{ "topic": "connectmq/demo/bind-7f3a/tank1/telemetry", "path": "level_pct",
  "map": [0, 100, 0, 1], "clamp": true, "kind": "morph", "morph": "Fill" }

Hızı sürücü frekansını takip eden bir konveyör bandı animasyonu (50 Hz klibi normal hızda oynatır):

{ "topic": "connectmq/demo/bind-7f3a/conv1/telemetry", "path": "drive.hz",
  "map": [0, 50, 0, 1], "clamp": true, "kind": "clip", "clip": "BeltLoop" }

Herhangi bir bağlamayı doğrudan bir terminalden test edin:

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"}'

-r bayrağı mesajı retained olarak tutar; böylece model yüklenir yüklenmez değeri gösterir. Retained durum, ilk çizimde doğru olan bir ikizin bel kemiğidir; ne zaman kullanılacağını dijital ikizler için retained MQTT mesajları yazımız ele alıyor.

IoT Verisini 3D Modele Bağlarken Sık Yapılan Hatalar

  1. Paylaşılan materyaller. glTF dışa aktarıcıları materyalleri yeniden kullanır. Rengi bağlanan her parça için materyali klonlayın; aksi hâlde tek bir alarm bütün fabrikayı kırmızıya boyar.
  2. Mutlak ve göreli transform'lar. Değerleri her zaman yükleme anında yakalanan dinlenme pozuna göre uygulayın. Aksi hâlde parça dünya orijinine sıçrar.
  3. Derece ve radyan. Sensörler derece bildirir; Three.js radyan ister. rotate türünde olduğu gibi dönüşümü tek bir yerde yapın.
  4. Eksik değerler. Payload'lar zamanla değişir. path sonucu undefined olursa sıfıra atlamak yerine önceki değeri koruyun.
  5. Bayat veri. Her bağlamanın en son ne zaman güncellendiğini kaydedin ve bir süredir cihazından haber alamayan parçaları soluklaştırın.
  6. Modelde yeniden adlandırılmış node. Yükleme anında node'u veya klibi bulunamayan her bağlama için bir uyarı loglayın; sessiz hatalar ayıklaması en zor olanlardır.
  7. Aşırı yumuşatma. Yumuşatma sürekli değerlere uygundur ancak ayrık olayları gizler. Enum'ları, boolean'ları veya alarm durumlarını yumuşatmayın.

Bu aşamaların her biri için gecikme bütçeleri gerçek zamanlı dijital ikiz gecikmesi yazımızda ele alınıyor.

Veri Nereden Geliyor?

Bağlamalar, arkalarındaki topic'ler kadar iyidir. site/area/line/cell/asset/metric gibi tutarlı bir hiyerarşi, wildcard'larla bağlama yapmanızı ve modelleri hatlar arasında yeniden kullanmanızı sağlar. Tesisiniz Sparkplug B yayınlıyorsa ConnectMQ bu veriyi çözüp connectmq/factory1/production/line1/cnc1/telemetry gibi okunabilir ISA-95 tarzı topic'lere yeniden yayınlayabilir; bunlara bağlama yapmak Protobuf payload'larına göre çok daha kolaydır. Sparkplug B ve Unified Namespace rehberi bu yapıyı açıklıyor. Bu motoru konfigürasyon ve yayınlamayla birlikte kullanan eksiksiz bir uygulama için web tabanlı 3D dijital ikiz oluşturma yazımıza bakın veya odaklı bir örnek için konveyör dijital ikizi eğitimini takip edin.

ConnectMQ ile Başlayın

Bağlamalarınız public broker'da çalıştığında onları özel bir broker'a taşıyın. ConnectMQ'nun yönetilen MQTT broker hizmeti, güvenilir bir sertifikayla 8084 portunda güvenli WebSocket ve payload'larınızda tam olarak hangi yolların bulunduğunu gösteren canlı bir topic gezgini sunar; bu, bağlama yazarken paha biçilmezdir. app.connectmq.com adresinden ücretsiz bir broker oluşturun; Serverless plan kredi kartı gerektirmez. Hassas olmayan verilerle hızlı denemeler için ücretsiz public MQTT broker kullanmaya devam edin.

Sık Sorulan Sorular

Bağlama tanımları nerede durmalı: modelde mi, kodda mı?

İkisi de olur. Bunları glTF extras olarak saklamak küçük ekipler için modeli ve veri eşlemesini bir arada tutar; ayrı bir JSON config ise bir varlık veritabanından üretmesi ve sürüm kontrolünde incelemesi daha kolay bir seçenektir.

Blender'dan dışa aktarılan animasyonlara veri bağlayabilir miyim?

Evet. Dışa aktarılan action'lar glTF animasyon kliplerine dönüşür. Bunları bir AnimationMixer üzerinden oynatın ve hız için action'ın timeScale değerini, durumlar arası geçiş için ise ağırlığını bağlayın.

Ayrı bir mesh'i olmayan bir parçaya nasıl veri bağlarım?

Birleştirilmiş bir mesh'in bir kısmını bağımsız olarak hareket ettiremez veya yeniden renklendiremezsiniz. Modelleme aracınızda parçayı kendi nesnesine ayırıp yeniden dışa aktarın ya da değişiklik bir deformasyonsa bir morph target kullanın.

Bu yaklaşım Babylon.js veya Unity'de çalışır mı?

Topic, path, dönüşüm, node ve türden oluşan bağlama modeli motordan bağımsızdır. Yalnızca uygulama adımı değişir; her motorun transform, materyal ve animasyon için kendi API'leri kullanılır.

İlgili yazılar

Tüm yazılar →

Özel MQTT broker’ınız birkaç tık uzağınızda

Hesabınızı oluşturun, broker kimlik bilgilerinizi alın ve ilk cihazınızı bağlayın — kurulacak, yamalanacak ya da ölçeklendirilecek sunucu yok.