Dijital İkiz

Three.js ile Dijital İkiz: MQTT ile Canlı 3D Görselleştirme

Güvenli WebSocket üzerinden canlı MQTT telemetrisine tepki veren bir tank ve motorun tarayıcı tabanlı Three.js dijital ikizini oluşturun; bir Node.js simülatörü, yumuşak interpolasyon ve production ipuçlarıyla.

Tarayıcıda canlı MQTT sensör verileriyle renk değiştiren bir tank ve motorun Three.js dijital ikizi
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

Bir Three.js dijital ikizi, sensör mesajlarından oluşan bir akışı etrafında dönebileceğiniz, yakınlaştırabileceğiniz ve bir bakışta anlayabileceğiniz bir 3D sahneye dönüştürür. Bu eğitimde tek bir HTML dosyasında bir tane oluşturacaksınız: sıvı seviyesi yükselip alçalan bir tank ve sıcaklıkla renk değiştiren, bildirilen hızında dönen bir motor; hepsi broker.connectmq.com üzerinden güvenli WebSocket ile MQTT tarafından yönlendirilir. Ayrıca, ortada henüz gerçek bir donanım yokken ikizi test edebilmeniz için küçük bir simülatör de yazacaksınız.

Bir Three.js Dijital İkizinin İhtiyaç Duydukları

Bir web dijital ikizinin üç hareketli parçası vardır ve bunları ayrı tutmak onu sürdürülebilir kılan şeydir:

  1. Bir veri akışı. Cihazlar telemetriyi MQTT topic'lerine yayımlar. Tarayıcı, ham TCP soketleri açamadığı için WebSocket üzerinden MQTT.js ile abone olur.
  2. Bir durum deposu. Gelen mesajlar, hedef değerlerden oluşan düz bir JavaScript nesnesini günceller. Mesaj işleyicisindeki hiçbir şey 3D sahneye doğrudan dokunmaz.
  3. Bir render döngüsü. Her animasyon karesinde sahne mevcut değerlerini hedeflere doğru yumuşakça yaklaştırır ve yeniden çizer.

Bu ayrım göründüğünden daha önemlidir. Mesajlar, tarayıcının çizdiği saniyede 60 karelik ritimden bağımsız olarak toplu hâlde gelebilir. Mesh'leri MQTT callback'i içinde değiştirirseniz sıçramalı hareket ve bulması zor yarış koşulları (race condition) elde edersiniz. Arada bir durum deposu olduğunda render her zaman tutarlı bir anlık görüntüden çalışır ve yumuşak interpolasyon yapabilirsiniz.

Bunun büyük resimde nereye oturduğunu görmek için dijital ikiz teknolojisi rehberimiz, bunun gibi görsel bir ikiz ile simülasyon veya tahmin de yapan ikizler arasındaki farkı açıklar.

İkiz İçin Topic Tasarımı

Varlık başına bir topic ve mesaj başına bir JSON payload kullanın. Public broker'da test yapan diğer kişilerle çakışmamak için önek için rastgele bir son ek seçin:

Topic Payload örneği Sahnede neyi yönlendirir
connectmq/demo/twin-7f3a/tank1/telemetry {"level":0.62,"ts":1760600000000} Sıvı yüksekliği (0 ile 1 arası)
connectmq/demo/twin-7f3a/motor1/telemetry {"temperature":58.4,"rpm":1320,"ts":...} Gövde rengi ve fan dönüşü
connectmq/demo/twin-7f3a/motor1/status online / offline (retained) Çevrimdışıyken griye döner

Varlık başına bir topic, tarayıcı aboneliğini basit tutar (önekinizin altında tek bir # wildcard'ı) ve düzinelerce varlığa temiz bir şekilde ölçeklenir. 0 ile 1 arasındaki bir seviye gibi normalleştirilmiş değerler de sahne kodunu birim dönüşümlerinden arındırır. Daha büyük bir ikiz için topic tasarlıyorsanız dijital ikizler için MQTT hiyerarşi ve payload kurallarını daha derinlemesine ele alır.

Adım 1: Ekipmanı Simüle Edin

Başlamak için donanıma ihtiyacınız yok. Bu Node.js betiği, tank ve motor için saniyede bir gerçekçi görünen veriler yayımlar. simulator.mjs olarak kaydedin, npm install mqtt çalıştırın, ardından node simulator.mjs ile başlatın.

import mqtt from "mqtt";

const PREFIX = "connectmq/demo/twin-7f3a";
const client = mqtt.connect("mqtts://broker.connectmq.com:8883", {
  clientId: "twin-sim-7f3a",
  protocolVersion: 5,
  will: { topic: `${PREFIX}/motor1/status`, payload: "offline", qos: 1, retain: true },
});

let t = 0;

client.on("connect", () => {
  console.log("Simulator connected");
  client.publish(`${PREFIX}/motor1/status`, "online", { qos: 1, retain: true });

  setInterval(() => {
    t += 1;
    const level = 0.5 + 0.4 * Math.sin(t / 20);
    const temperature = 50 + 30 * Math.sin(t / 15);
    const rpm = temperature > 75 ? 0 : Math.round(1200 + 300 * Math.sin(t / 7));

    client.publish(`${PREFIX}/tank1/telemetry`,
      JSON.stringify({ level: Number(level.toFixed(3)), ts: Date.now() }));
    client.publish(`${PREFIX}/motor1/telemetry`,
      JSON.stringify({ temperature: Number(temperature.toFixed(1)), rpm, ts: Date.now() }));
  }, 1000);
});

client.on("error", (err) => console.error("MQTT error:", err.message));

Simüle edilen sıcaklık 75 dereceyi her geçtiğinde motor "atar" ve durur; bu da ikize gösterecek ilginç bir şey verir. Saniyede iki mesaj, public broker'ın client başına saniyede 10 mesajlık limitinin rahatça altındadır. Herhangi bir 3D koda dokunmadan önce verinin aktığını doğrulayın:

mosquitto_sub -h broker.connectmq.com -p 1883 -i check-7f3a -v -t "connectmq/demo/twin-7f3a/#"

Adım 2: Three.js Dijital İkiz Sahnesini Oluşturun

Aşağıdakini index.html olarak kaydedin. Three.js ve MQTT.js'i bir CDN'den ES modülleri olarak yüklemek için bir import map kullanır; bu yüzden derleme adımı yoktur. Herhangi bir statik sunucuyla, örneğin npx serve . ile sunun ve yazdırılan URL'yi açın. Dosyayı doğrudan diskten açmak da çoğu tarayıcıda çalışır, ancak yerel bir sunucu modül yükleme kısıtlamalarından kaçınır.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Three.js Digital Twin</title>
  <style>
    body { margin: 0; overflow: hidden; font-family: system-ui, sans-serif; background: #1d2330; }
    #hud { position: absolute; top: 12px; left: 12px; color: #fff; background: rgba(0,0,0,.55);
           padding: 8px 12px; border-radius: 6px; font-size: 14px; line-height: 1.5; }
  </style>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
      "mqtt": "https://unpkg.com/mqtt@5.10.1/dist/mqtt.esm.js"
    }
  }
  </script>
</head>
<body>
  <div id="hud">Connecting...</div>
  <script type="module">
    import * as THREE from "three";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";
    import mqtt from "mqtt";

    const PREFIX = "connectmq/demo/twin-7f3a";
    const hud = document.getElementById("hud");

    // ---------- Sahne ----------
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x1d2330);
    const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
    camera.position.set(6, 5, 8);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 1, 0);
    controls.enableDamping = true;

    scene.add(new THREE.HemisphereLight(0xffffff, 0x444466, 1.2));
    const sun = new THREE.DirectionalLight(0xffffff, 1.5);
    sun.position.set(5, 10, 5);
    scene.add(sun, new THREE.GridHelper(20, 20, 0x556070, 0x333a48));

    // Tank: saydam gövde + sıvı sütunu (geometri yüksekliği 1, Y ekseninde ölçeklenir)
    const TANK_H = 3;
    const shell = new THREE.Mesh(
      new THREE.CylinderGeometry(1, 1, TANK_H, 40, 1, true),
      new THREE.MeshStandardMaterial({ color: 0x99aabb, transparent: true, opacity: 0.25, side: THREE.DoubleSide })
    );
    shell.position.set(-2, TANK_H / 2, 0);
    const liquid = new THREE.Mesh(
      new THREE.CylinderGeometry(0.95, 0.95, 1, 40),
      new THREE.MeshStandardMaterial({ color: 0x2f7fd8 })
    );
    liquid.position.x = -2;
    scene.add(shell, liquid);

    // Motor: gövde + rpm ile dönen fan
    const motorMat = new THREE.MeshStandardMaterial({ color: 0x44aa66 });
    const motor = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1, 1), motorMat);
    motor.position.set(2, 0.5, 0);
    const fan = new THREE.Mesh(
      new THREE.BoxGeometry(0.08, 0.9, 0.15),
      new THREE.MeshStandardMaterial({ color: 0xdddddd })
    );
    fan.position.set(2.75, 0.5, 0);
    scene.add(motor, fan);

    // ---------- Durum deposu ----------
    const target = { level: 0, temperature: 20, rpm: 0, online: false };
    const current = { level: 0, temperature: 20, rpm: 0 };
    let lastMessageAt = 0;

    // ---------- MQTT ----------
    const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
      clientId: "twin-viewer-" + Math.random().toString(16).slice(2, 10),
      protocolVersion: 5,
      reconnectPeriod: 2000,
    });
    const decoder = new TextDecoder();

    client.on("connect", () => {
      hud.textContent = "Connected";
      client.subscribe(`${PREFIX}/#`, { qos: 0 });
    });
    client.on("reconnect", () => (hud.textContent = "Reconnecting..."));
    client.on("message", (topic, payload) => {
      const text = decoder.decode(payload);
      lastMessageAt = performance.now();
      if (topic.endsWith("/motor1/status")) { target.online = text === "online"; return; }
      let data;
      try { data = JSON.parse(text); } catch { return; }
      if (topic.endsWith("/tank1/telemetry") && typeof data.level === "number") {
        target.level = THREE.MathUtils.clamp(data.level, 0, 1);
      } else if (topic.endsWith("/motor1/telemetry")) {
        if (typeof data.temperature === "number") target.temperature = data.temperature;
        if (typeof data.rpm === "number") target.rpm = data.rpm;
      }
    });

    // ---------- Render döngüsü ----------
    const clock = new THREE.Clock();
    const cold = new THREE.Color(0x2e86de), hot = new THREE.Color(0xe74c3c), grey = new THREE.Color(0x666666);

    function animate() {
      requestAnimationFrame(animate);
      const dt = clock.getDelta();
      const k = 1 - Math.exp(-dt * 4); // kare hızından bağımsız yumuşatma
      current.level += (target.level - current.level) * k;
      current.temperature += (target.temperature - current.temperature) * k;
      current.rpm += (target.rpm - current.rpm) * k;

      const h = Math.max(current.level * (TANK_H - 0.1), 0.001);
      liquid.scale.y = h;
      liquid.position.y = h / 2 + 0.05;

      const t01 = THREE.MathUtils.clamp((current.temperature - 20) / 70, 0, 1);
      if (target.online) motorMat.color.copy(cold).lerp(hot, t01);
      else motorMat.color.copy(grey);

      fan.rotation.x += (current.rpm / 60) * Math.PI * 2 * dt * 0.05; // görünürlük için yavaşlatıldı

      const stale = performance.now() - lastMessageAt > 5000;
      hud.innerHTML = `Level ${(current.level * 100).toFixed(0)} %<br>` +
        `Motor ${current.temperature.toFixed(1)} C, ${Math.round(current.rpm)} rpm` +
        (stale ? "<br>No data for 5 s" : "");

      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    addEventListener("resize", () => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    });
  </script>
</body>
</html>

Simülatör çalışırken sıvı sütunu yükselip alçalır, motor ısındıkça maviden kırmızıya döner, motor attığında fan yavaşlayarak durur ve simülatörü durdurup last will tetiklendiğinde motor griye döner.

Canlı Bağlamalar Nasıl Çalışır?

Bu dosyadaki birkaç ayrıntı, geliştireceğiniz her Three.js dijital ikizine kopyalanmaya değer.

Birim yükseklikte bir geometriyi ölçekleyin. Sıvı silindiri 1 yüksekliğinde oluşturulur; böylece scale.y sahne birimlerindeki yüksekliğine eşittir. position.y'yi yüksekliğin yarısına taşımak alt kısmı tank tabanına sabitler. Bu, her mesajda geometriyi yeniden oluşturmaktan daha ucuz ve basittir.

Materyalleri değiştirin, yenilemeyin. material.color.copy(...) çağırmak mevcut materyali günceller. Her mesaj için yeni bir materyal oluşturmak GPU kaynaklarını sızdırır ve shader'ların yeniden derlenmesine neden olur.

Hedeflere doğru yumuşak geçiş yapın. 1 - Math.exp(-dt * 4) ifadesi kare hızından bağımsız bir yumuşatma sağlar: hareket saniyede 30 karede de 144 karede de aynı görünür. Saniyede bir mesajla ham değerler sahnenin saniyede bir sıçramasına neden olurdu.

Eskimişliği gösterin. Sessizce donan bir ikiz, hiç ikiz olmamasından daha kötüdür; çünkü insanlar ona güvenir. HUD beş saniye boyunca mesaj gelmediğinde bunu işaretler; retained status topic'i ve simülatörün last will'i ise cihazın kendisinin ortadan kaybolup kaybolmadığını söyler. Retained message'lar ayrıca yeni açılan bir sayfanın bilinen en son durumu hemen göstermesini sağlar; ne zaman retain edip ne zaman etmeyeceğiniz için dijital ikizler için retained message'lar yazısına bakın.

Yaygın Tuzaklar

Belirti Olası neden Çözüm
Sayfa yükleniyor, HUD "Connecting..." durumunda kalıyor Yanlış URL veya eksik /mqtt yolu wss://broker.connectmq.com:8084/mqtt kullanın
Bağlanıyor, sonra birkaç saniyede bir kopuyor Sekmeler arasında yinelenen client ID Client ID'yi her sayfa yüklemesinde rastgele üretin
Siyah ekran, modüllerle ilgili hatalar Dosya modül içe aktarımlarını engelleyen bir şekilde açılmış npx serve . veya başka bir statik sunucuyla sunun
Değerler güncelleniyor ama hareket kesik kesik Mesh'ler MQTT callback'i içinde güncelleniyor Durum deposuna yazın, render döngüsünde canlandırın
HTTPS sitesinde mixed content hatası https:// sayfasından ws:// kullanımı 8084 portunda her zaman wss:// kullanın

Mesajlar hiç gelmiyorsa tarayıcı geliştirici araçlarının Network sekmesinde WebSocket bağlantısını kontrol edin ve portların, yolların ve proxy'lerin tam açıklaması için WebSocket üzerinden MQTT yazısını okuyun.

İlkel Şekillerden Gerçek Modellere

Kutular ve silindirler veri yolunu kanıtlamak için mükemmeldir. Bir sonraki adım genellikle Blender'dan veya bir CAD aracından dışa aktarılan bir glTF modelidir; burada mesh'leri adlarına göre bulup topic'leri onlara bağlarsınız. IoT verilerini bir 3D modele bağlama yazısı materyaller, morph target'lar ve animasyon klipleri dahil tam olarak bunu ele alır. Yapılandırmaya dayalı bağlamalarla production'a hazır bir web ikizinin uçtan uca yapısını istiyorsanız web tabanlı 3D dijital ikiz geliştirme yazısını izleyin. Farklı bir motor mu tercih ediyorsunuz? Aynı MQTT kalıbı Babylon.js ile de çalışır.

Production'a Geçiş

Public broker bu eğitim için harikadır, ancak anonimdir ve herkes tarafından okunabilir; bu yüzden gerçek fabrika verilerini asla onun üzerinden aktarmayın. Production için kimlik bilgileri ve TLS içeren kendi broker'ınızı istersiniz. ConnectMQ, 8084 portunda güvenli WebSocket ve güvenilir bir Let's Encrypt sertifikası sunan yönetilen bir MQTT broker sağlar; böylece tarayıcı bağlantısı hiçbir sertifika kurulumu gerektirmeden çalışır. Kimlik bilgilerini MQTT.js seçeneklerine ekleyin:

const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
  clientId: "YOUR_CLIENT_ID",
  username: "YOUR_USERNAME",
  password: "YOUR_PASSWORD",
  protocolVersion: 5,
});

Bir tarayıcıya gönderilen her şey kullanıcıları tarafından görülebilir; bu yüzden görüntüleyici sayfaya, bağımsız olarak değiştirebilmeniz için cihaz kimlik bilgilerinden ayrı kendi kimlik bilgilerini verin ve sayfayı yalnızca kimliği doğrulanmış kullanıcılara sunun. Birçok ekip sayfanın önüne ince bir giriş katmanı koyar ve MQTT kimlik bilgilerini oturum açıldıktan sonra enjekte eder.

ConnectMQ ile Başlayın

ConnectMQ, tam olarak bu yazıdaki kalıp üzerine kurulmuş, CNC tezgâhı, robot kol, fırın, tank, konveyör ve pompa içeren ve güvenli WebSocket üzerinden canlı güncellenen bir fabrikadan oluşan kendi 3D dijital ikiz web demosunu sunar. Kendi Three.js dijital ikizinizi özel bir broker'da çalıştırmak için app.connectmq.com adresinde ücretsiz bir broker oluşturun. Ücretsiz Serverless plan kredi kartı gerektirmez ve 25 bağlantıya kadar destekler; bu, birkaç cihaz ve görüntüleyici için yeterlidir. Portalın canlı topic gezgini de her bağlamayı kontrol etmenize yardımcı olur. Oraya geçmeden önce public MQTT test broker'ında denemeye devam edebilirsiniz.

Sık Sorulan Sorular

Three.js doğrudan MQTT'ye bağlanabilir mi?

Three.js yalnızca render yapar; bağlantıyı aynı sayfadaki MQTT.js yönetir. MQTT.js tarayıcıda WebSocket kullanır; bu yüzden broker'ın /mqtt yoluyla 8084 portu gibi bir WebSocket üzerinden MQTT uç noktası sunması gerekir.

Bir tarayıcı dijital ikizi kaç nesneyi gerçek zamanlı güncelleyebilir?

Dönüşümleri ve materyal renklerini güncellemek ucuzdur; bu yüzden sıradan donanımda yüzlerce bağlı nesne gerçekçidir. Olağan darboğaz MQTT mesaj işleme değil, geometri karmaşıklığı ve draw call'lardır.

İkiz QoS 0 ile mi yoksa QoS 1 ile mi abone olmalı?

Görsel telemetri için genellikle QoS 0 doğrudur; çünkü daha yeni bir değer kaybolan bir değerin yerini saniyeler içinde alır. Kaçırılmaması gereken komutlar ve alarmlar için QoS 1 kullanın.

Bir Three.js dijital ikizi için backend sunucusuna ihtiyacım var mı?

Canlı görselleştirme için hayır: tarayıcı broker ile doğrudan konuşur. Backend'e yalnızca geçmiş sorguları, kullanıcı kimlik doğrulaması veya kullanıcı başına kimlik bilgisi verme gibi işler için ihtiyaç duyarsınız.

İ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.