Bir Babylon.js dijital ikiz, tarayıcıda yerleşik seçim (picking), vurgulama efektleri ve inspector içeren tam özellikli bir 3D motor sunar; bu da etkileşimli ikizleri hızla geliştirmeyi mümkün kılar. Bu eğitimde, güvenli WebSocket üzerinden canlı MQTT telemetrisine tepki veren, yüksek basınçta kırmızı parlayan ve üzerine tıklayarak pompayı başlatıp durdurmanıza olanak tanıyan küçük bir soğutma ünitesi (bir pompa, bir tank ve bir basınç göstergesi) oluşturacaksınız. Tüm veriler broker.connectmq.com adresindeki public broker üzerinden akar ve gerçek ekipmanın rolünü bir Python simülatörü üstlenir.
Dijital İkiz İçin Neden Babylon.js?
Babylon.js ve Three.js'in ikisi de tarayıcıda WebGL sahneleri render eder ve ikisi de MQTT ile iyi çalışır. Fark, kutudan neyin çıktığındadır:
| Özellik | Babylon.js | Three.js |
|---|---|---|
| Seçim ve işaretçi eylemleri | Yerleşik (ActionManager, scene.pick) |
Raycaster, olayları kendiniz bağlarsınız |
| Parlama ve vurgulama efektleri | HighlightLayer, GlowLayer yerleşik |
Bir araya getirilmesi gereken post-processing adımları |
| Hata ayıklama inspector'ı | scene.debugLayer.show() |
Üçüncü taraf araçlar |
| Bundle boyutu ve API tarzı | Daha büyük, daha fazla hazır özellik | Daha küçük, daha modüler |
| glTF yükleme | Loaders paketi | GLTFLoader eklentisi |
Daha hafif bir kütüphane tercih ediyorsanız Three.js dijital ikiz eğitimimiz benzer bir sahne kuruyor. İkiz esas olarak etkileşimle ilgiliyse (bir makineye tıklamak, ayrıntıları görmek, komut göndermek) Babylon.js sizi çok fazla altyapı kodundan kurtarır. Daha geniş kavramlar dijital ikiz teknolojisi rehberimizde ele alınıyor.
Topic'ler ve Payload'lar
İkiz, benzersiz bir prefix altında üç topic kullanır. bjs-7f3a yerine kendi rastgele son ekinizi kullanın:
| Topic | Yayıncı | Payload | İkiz davranışı |
|---|---|---|---|
connectmq/demo/bjs-7f3a/skid1/telemetry |
Ekipman | {"pressure":3.2,"level":0.55,"running":true} |
Gösterge ibresi, tank seviyesi, pompa dönüşü |
connectmq/demo/bjs-7f3a/skid1/cmd |
İkiz (tıklandığında) | START veya STOP |
Simülatör pompa durumunu değiştirir |
connectmq/demo/bjs-7f3a/skid1/status |
Ekipman, retained, last will | online / offline |
Çevrimdışıyken grileşme |
Basınç bar cinsinden, seviye ise 0 ile 1 arasında bir oran olarak verilir. Birimleri sabit ve belgelenmiş tutmak, en yaygın ikiz hatasını önler: kilopaskal değerini bar olarak gösteren bir gösterge.
Adım 1: Ekipman Simülatörü
paho-mqtt 2.x'i kurun (pip install "paho-mqtt>=2.0") ve bu script'i çalıştırın. Script, pompa çalışırken basıncın yavaşça yükselmesini, zaman zaman 6 bar alarm sınırının üzerine sıçramasını simüle eder ve ikizden gelen komutlara uyar.
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)
Telemetri retained olarak yayınlanır; böylece ikiz bir saniye beklemek yerine bağlandığı anda en son değerleri gösterir. Akışı mosquitto_sub -h broker.connectmq.com -p 1883 -i chk-7f3a -v -t "connectmq/demo/bjs-7f3a/#" ile kontrol edin.
Adım 2: Babylon.js Dijital İkizi Oluşturun
Bunu index.html olarak kaydedin ve herhangi bir statik sunucuyla (npx serve .) sunun. Sayfa Babylon.js'i resmi CDN'den, MQTT.js'i ise tarayıcı bundle'ı olarak yükler. Üretimde ikisini de test ettiğiniz tam sürümlere sabitleyin.
<!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;
};
// Pump: body + 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 with liquid that scales on Y (origin moved to the bottom)
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");
// Pressure gauge: dial + needle pivoting at its base
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);
// ---------- State ----------
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 { /* ignore malformed payloads */ }
});
// ---------- Click the pump to toggle it ----------
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)));
// ---------- Per-frame update ----------
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 degrees, 10 bar = +120 degrees
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>
Simülatör çalışırken sayfayı açın. Pompa çalışırken tank yavaşça boşalır, ibre basınçla birlikte salınır, basınç 6 barı aştığında pompa kırmızı parlar ve pompaya tıklamak onaylı bir komut gönderir. Simülatörü Ctrl+C ile durdurun; broker last will mesajını yayınladığında pompa griye döner.
Babylon.js Bağlamaları Nasıl Çalışır?
Geometri düzenlemesi yerine pivot noktaları. setPivotPoint, ölçekleme ve dönme orijinini sıvının tabanına ve ibrenin dibine taşır; böylece ölçekleme tabandan dolar ve ibre kendi göbeği etrafında döner. Bu, Blender'da orijin ayarlamanın Babylon.js'teki karşılığıdır.
Tek güncelleme noktası olarak onBeforeRenderObservable. MQTT handler yalnızca target nesnesine yazar. Görsel olan her şey, kare hızından bağımsız bir faktörle yumuşatılarak karede bir kez gerçekleşir. Bu, mesajlar düzensiz geldiğinde bile animasyonu akıcı tutar.
Kare başına çalkantı değil, durum değişiklikleri. Vurgulama katmanı yalnızca alarm durumu değiştiğinde güncellenir. Her karede mesh ekleyip çıkarmak işlem gücünü boşa harcar ve titremeye yol açabilir.
Onaylı komutlar. Bir 3D nesneye yanlışlıkla tıklamak kolaydır, özellikle kamerayı döndürürken. confirm() diyaloğu asgari bir güvencedir; gerçek kurulumlar çoğu zaman bir rol kontrolü de gerektirir. Komutlar kısa bir ağ aksaklığından sağ çıkmaları için QoS 1 kullanır; nedenini MQTT QoS seviyeleri yazımız açıklıyor.
Sonucu cihaz onaylar. Pompanın rengi bastığınız butonu değil, telemetrideki running değerini yansıtır. Ekipman komutu reddeder veya kaçırırsa ikiz gerçeği gösterir.
Daha Büyük İkizler İçin Performans İpuçları
Üç mesh'ten oluşan bir ünite zahmetsizce çalışır, ancak yüzlerce parçadan oluşan bütün bir hat biraz özen ister. Hiç hareket etmeyen mesh'leri mesh.freezeWorldMatrix() ile dondurun; böylece Babylon.js onların matris güncellemelerini atlar. Statik parçalar arasında materyalleri paylaştırın ve yalnızca rengi veriye bağlı olanları klonlayın. Silindirler veya paletler gibi tekrarlanan öğeler için instance'lar (mesh.createInstance) kullanın. Her varlık için ayrı bağlantı açmak yerine sayfa başına bir MQTT bağlantısı ve wildcard abonelik kullanıp mesajları yerel olarak dağıtın. Son olarak metin katmanlarını yavaşlatın: DOM öğelerini saniyede 60 kez yeniden yazmak çoğu zaman 3D render'ın kendisinden daha pahalıdır, bu yüzden onları saniyede birkaç kez güncelleyin.
Sorun Giderme Kontrol Listesi
- "Connecting..." ekranında takılı kalma: URL'nin
/mqttyolu dahilwss://broker.connectmq.com:8084/mqttolduğunu doğrulayın ve tarayıcı konsolunda WebSocket hatalarını kontrol edin. Kurumsal proxy'ler bazen WebSocket yükseltmelerini engeller. BABYLON is not defined: Babylon.js script etiketi yüklenemedi; ağ sekmesini ve varsa content security policy'yi kontrol edin.- Değerler hiç görünmüyor: sayfadaki topic prefix'iniz simülatördekinden farklıdır. İkisini karakter karakter karşılaştırın.
- İki sekme açıkken bağlantı sürekli kopuyor: her sekme benzersiz bir client ID kullanmalıdır; rastgele son ek bunu sağlar.
- Inspector'ı kullanın: mesh'leri, materyalleri ve pivotları canlı incelemek için konsolda
scene.debugLayer.show()çağırın.
WebSocket portları ve yolları hakkında arka plan bilgisi için WebSocket üzerinden MQTT yazımıza bakın.
Basit Şekillerden Gerçek Bir Tesis Modeline
Basit şekilleri bir glTF modeliyle değiştirmek için Babylon.js loaders script'ini ekleyip BABYLON.SceneLoader.ImportMeshAsync("", "/models/", "skid.glb", scene) çağırın, ardından parçaları scene.getMeshByName("Pump_Body") ile bulun. Adlandırma kuralları ve eşleme kalıpları her motorda aynıdır; IoT verisini bir 3D modele bağlama yazımız bunları açıklıyor, web tabanlı 3D dijital ikiz oluşturma yazımız ise bütün bir fabrikaya ölçeklenen, konfigürasyon odaklı bir yapı gösteriyor.
Üretim: Kendi Broker'ınız
Public broker anonimdir ve herkes tarafından görülebilir; bu eğitim için uygundur, bir tesis için yanlıştır. Barındırılan bir MQTT broker size kimlik bilgileri, TLS ve izolasyon sağlar. ConnectMQ ile portalda bir broker oluşturur ve üretilmiş bir kullanıcı adı, şifre ve client ID alırsınız; bunları mqtt.connect seçeneklerine username, password ve clientId olarak ekleyin (YOUR_USERNAME, YOUR_PASSWORD, YOUR_CLIENT_ID). 8084 portundaki güvenli WebSocket bir Let's Encrypt sertifikası kullanır, bu yüzden tarayıcılar ek yapılandırma olmadan ona güvenir. Tarayıcıya iletilen her şey kullanıcıları tarafından okunabileceği için ikiz sayfasını kendi oturum açma ekranınızın arkasında sunun.
ConnectMQ ile Başlayın
ConnectMQ'nun 3D dijital ikiz web demosu, güvenli WebSocket üzerinden veri akışı alan eksiksiz bir fabrika sahnesi gösterir ve aynı broker kendi Babylon.js dijital ikiziniz için de kullanılabilir. app.connectmq.com adresinden ücretsiz bir broker oluşturun: Serverless plan kredi kartı gerektirmez, MQTT 5'i ve 25 bağlantıya kadar desteği sunar ve telemetrinizi bağlamadan önce kontrol etmeniz için canlı bir topic gezgini içerir. O zamana kadar public MQTT test broker'ı denemeleriniz için hazır.
Sık Sorulan Sorular
Dijital ikizler için Babylon.js mi yoksa Three.js mi daha iyi?
İkisi de iyi çalışır. Babylon.js seçim eylemleri, vurgulama katmanları ve inspector gibi daha fazla yerleşik özellik içerirken Three.js daha küçük ve daha modülerdir; ekibinizin daha rahat olduğu kütüphaneyi seçin.
Bir Babylon.js sayfası MQTT'ye nasıl bağlanır?
Tarayıcılar ham TCP bağlantısı açamadığı için aynı sayfada WebSocket üzerinden çalışan MQTT.js aracılığıyla. Broker'ın bir MQTT over WebSocket uç noktası, tercihen güvenli WebSocket sunması gerekir.
Ekipmanı kontrol etmek için ikizdeki nesnelere tıklayabilir miyim?
Evet. Bir mesh'e ActionManager ekleyin ve OnPickTrigger üzerinde bir komut yayınlayın. Onay adımı ekleyin ve cihazın gerçek durumunu geri bildirmesini sağlayın.
Kendi CAD modelimi Babylon.js'e nasıl yüklerim?
Modeli glTF'ye (.glb) dönüştürün, poligon sayısını azaltın, bağlanacak parçalara benzersiz adlar verin ve SceneLoader.ImportMeshAsync ile yükleyin.



