Web tabanlı 3D dijital ikiz, tarayıcısı olan herkesin hiçbir yazılım kurmadan bir makinenin, bir hattın ya da bütün bir tesisin canlı durumunu görmesini sağlar. Bu rehber, böyle bir ikizi doğru şekilde kurmayı adım adım anlatıyor: bir glTF modelini hazırlamak, bir Vite projesi oluşturmak, MQTT topic'lerini bir konfigürasyon dosyası üzerinden modelin parçalarına bağlamak, tıklayıp inceleme detayları eklemek ve sonucu statik bir site olarak yayınlamak. Veriler güvenli WebSocket üzerinden broker.connectmq.com adresinden gelir; bu, ConnectMQ'nun kendi 3D fabrika demosunda kullanılan yaklaşımın aynısıdır.
Web Tabanlı 3D Dijital İkizin Mimarisi
Kod yazmadan önce sistemin bütününü görmek faydalıdır. Tarayıcıdaki bir ikiz büyük ölçüde bir front-end uygulamasıdır; veriyi taşıma yükünü broker üstlenir.
| Katman | Sorumluluk | Bu rehberdeki teknoloji |
|---|---|---|
| Cihazlar ve gateway'ler | Telemetri ve durum yayınlamak | Herhangi bir MQTT client (PLC gateway, ESP32, Python) |
| Broker | Mesajları yönlendirmek, retained durumu tutmak | Yönetilen MQTT broker, 8084 portunda WSS |
| Model | Geometri, adlandırılmış parçalar, materyaller | glTF binary (.glb) |
| Bağlama konfigürasyonu | Hangi topic alanının hangi parçayı nasıl yönettiği | Bir JavaScript config modülü |
| Çalışma zamanı | Modeli yüklemek, abone olmak, animasyon, etkileşim | Bir Vite uygulamasında Three.js ve MQTT.js |
Temel fikir bağlama konfigürasyonudur. Her makine için ayrı kod yazmak yerine birkaç genel davranış (ısı parlaması, doluluk seviyesi, dönme, durum rengi) ve topic'leri model parçalarına bağlayan bir liste yazarsınız. Böylece yeni bir pompa eklemek, çalışma zamanına dokunmadan tek satırlık config ve adlandırılmış bir mesh eklemek anlamına gelir. Kavramla yeni tanışıyorsanız, dijital ikiz teknolojisine genel bakış yazımız görsel ikizlerin simülasyon ve analitik ikizlerle ilişkisini açıklıyor.
Adım 1: Varlıkları ve Topic'leri Planlayın
Yukarıda bahsedilen demo gibi tipik bir fabrika sahnesinde bir CNC tezgâhı, robot kol, fırın, tank, konveyör ve pompa bulunur. Örnek olarak bunlardan dördünü kullanacağız. Önce planı yazıya dökün; bu plan config dosyanız olacak.
| Varlık | Topic (prefix'inizin altında) | Alan | Görsel davranış |
|---|---|---|---|
| Fırın | furnace1/telemetry |
temperature (20 ile 900 arası) |
Koyudan turuncuya emissive parlama |
| Tank | tank1/telemetry |
level (0 ile 1 arası) |
Sıvı mesh'i Y ekseninde ölçeklenir |
| Pompa | pump1/telemetry |
rpm (0 ile 3000 arası) |
Pervane döner |
| CNC tezgâhı | cnc1/state |
state |
İkaz lambası rengi: çalışıyor, boşta, alarm |
Demo için connectmq/demo/factory-7f3a prefix'ini kullanıyoruz; son eki kendi rastgele dizenizle değiştirin. Üretimde acme/plant1/area2/line1/furnace1 gibi hiyerarşik bir namespace kullanırsınız; dijital ikizler için MQTT yazımız bunun nasıl tasarlanacağını anlatıyor.
Adım 2: 3D Modeli Hazırlayın
Modeliniz hangi araçtan gelirse gelsin (Blender, bir CAD dışa aktarımı, satın alınmış bir varlık), dışa aktarmadan önce onu ikize hazır hâle getirin:
- Bağlanacak her parçaya benzersiz ve anlamlı bir ad verin; örneğin
Furnace,Tank_Liquid,Pump_Impeller,CNC_Beacon. Adlar glTF dışa aktarımında korunur ve çalışma zamanı parçaları bu adlarla bulur. - Hareketli parçaları ayrı nesnelere ayırın ve pivot noktasını dönme merkezine yerleştirin. Pompa gövdesiyle birleştirilmiş bir pervane kendi başına dönemez.
- Sıvıya birim yükseklik verin ve orijinini tabana koyun; böylece Y ekseninde ölçekleme tabandan yukarı doğru dolum etkisi yaratır.
- Görsel olarak önemli olmayan her şeyde poligon sayısını azaltın. CAD dışa aktarımları çoğu zaman milyonlarca üçgen içerir; bir tarayıcı ikizi genellikle bunun çok altında kalmalıdır.
- "Apply modifiers" seçeneği etkin olarak glTF binary (
.glb) formatında dışa aktarın. Daha hızlı yükleme için isteğe bağlı olarak Draco veya Meshopt ile sıkıştırın.
Aşağıdaki kod, model bulunamazsa aynı adlara sahip basit kutular oluşturan bir yedek mekanizma içerir; böylece elinizde model olmadan da adımları takip edebilirsiniz.
Adım 3: Projenin İskeletini Oluşturun
npm create vite@latest web-twin -- --template vanilla
cd web-twin
npm install three mqtt
mkdir -p public/models # put factory.glb here
Aşağıdaki çalışma zamanı, modeli bir modülün en üst seviyesinde await ile bekler; bu nedenle vite.config.js dosyasını oluşturarak Vite'a modern tarayıcıları hedeflemesini söyleyin:
export default { build: { target: "es2022" } };
Ortamlar arasında değişen ayarlar için bir .env dosyası oluşturun. Vite, VITE_ ön ekiyle başlayan değişkenleri tarayıcıya açar:
VITE_MQTT_URL=wss://broker.connectmq.com:8084/mqtt
VITE_TOPIC_PREFIX=connectmq/demo/factory-7f3a
Adım 4: Bağlama Konfigürasyonunu Yazın
src/bindings.js dosyasını oluşturun. Her girdi hangi topic ve alanın okunacağını, modelin hangi parçasının değişeceğini ve hangi davranışın uygulanacağını belirtir.
export const PREFIX = import.meta.env.VITE_TOPIC_PREFIX;
export const bindings = [
{ topic: "furnace1/telemetry", field: "temperature", node: "Furnace", kind: "heat", min: 20, max: 900 },
{ topic: "tank1/telemetry", field: "level", node: "Tank_Liquid", kind: "fill", min: 0, max: 1 },
{ topic: "pump1/telemetry", field: "rpm", node: "Pump_Impeller", kind: "spin", axis: "z" },
{ topic: "cnc1/state", field: "state", node: "CNC_Beacon", kind: "status" },
];
export const statusColors = {
RUNNING: 0x2ecc71,
IDLE: 0xf1c40f,
ALARM: 0xe74c3c,
OFFLINE: 0x7f8c8d,
};
Adım 5: İkizin Çalışma Zamanı
src/main.js dosyasının içeriğini aşağıdaki çalışma zamanıyla değiştirin. Bu kod modeli yükler (ya da yer tutucular oluşturur), parçaların renklerini bağımsız olarak değiştirebilmesi için materyalleri klonlar, MQTT'ye bağlanır, mesajları bağlamalar üzerinden yönlendirir ve her şeyi tek bir render döngüsünde canlandırır.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import mqtt from "mqtt";
import { PREFIX, bindings, statusColors } from "./bindings.js";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x182030);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 200);
camera.position.set(10, 8, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.style.margin = "0";
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 1.3));
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
sun.position.set(8, 15, 6);
scene.add(sun, new THREE.GridHelper(30, 30, 0x4a5568, 0x2d3748));
const panel = Object.assign(document.createElement("pre"), {
style: "position:absolute;top:10px;left:10px;margin:0;padding:10px;color:#fff;" +
"background:rgba(0,0,0,.6);border-radius:6px;font:13px monospace;max-width:320px",
textContent: "Loading model...",
});
document.body.appendChild(panel);
// ----- Model loading with placeholder fallback -----
function buildPlaceholders() {
const group = new THREE.Group();
const add = (name, geo, color, pos) => {
const m = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color }));
m.name = name; m.position.set(...pos); group.add(m); return m;
};
add("Furnace", new THREE.BoxGeometry(2, 2, 2), 0x553322, [-4, 1, 0]);
const liquid = add("Tank_Liquid", new THREE.CylinderGeometry(0.9, 0.9, 1, 32), 0x2f7fd8, [0, 0, 0]);
liquid.geometry.translate(0, 0.5, 0); // origin at the bottom
add("Pump_Impeller", new THREE.BoxGeometry(0.2, 1.4, 0.2), 0xcccccc, [3, 1, 0]);
add("CNC_Beacon", new THREE.SphereGeometry(0.3, 24, 16), 0x7f8c8d, [6, 2.4, 0]);
add("CNC_Body", new THREE.BoxGeometry(2, 2, 1.5), 0x8899aa, [6, 1, 0]);
return group;
}
async function loadModel() {
try {
const gltf = await new GLTFLoader().loadAsync("/models/factory.glb");
return gltf.scene;
} catch {
console.warn("factory.glb not found, using placeholders");
return buildPlaceholders();
}
}
const root = await loadModel();
scene.add(root);
// ----- Resolve bindings to objects and give them private materials -----
const runtime = bindings.map((b) => {
const obj = root.getObjectByName(b.node);
if (!obj) console.warn(`Binding target "${b.node}" not found in model`);
obj?.traverse((o) => { if (o.isMesh) o.material = o.material.clone(); });
return { ...b, obj, target: b.min ?? 0, current: b.min ?? 0, last: null, at: 0 };
});
const byTopic = new Map();
for (const r of runtime) {
const full = `${PREFIX}/${r.topic}`;
if (!byTopic.has(full)) byTopic.set(full, []);
byTopic.get(full).push(r);
}
// ----- MQTT -----
const client = mqtt.connect(import.meta.env.VITE_MQTT_URL, {
clientId: import.meta.env.VITE_MQTT_CLIENT_ID || "web-twin-" + crypto.randomUUID().slice(0, 8),
username: import.meta.env.VITE_MQTT_USERNAME || undefined,
password: import.meta.env.VITE_MQTT_PASSWORD || undefined,
protocolVersion: 5,
reconnectPeriod: 2000,
});
let connected = false;
client.on("connect", () => { connected = true; client.subscribe(`${PREFIX}/#`, { qos: 0 }); });
client.on("close", () => (connected = false));
client.on("message", (topic, payload) => {
const targets = byTopic.get(topic);
if (!targets) return;
let data;
try { data = JSON.parse(new TextDecoder().decode(payload)); } catch { return; }
for (const r of targets) {
if (!(r.field in data)) continue;
r.target = data[r.field];
r.last = data;
r.at = Date.now();
}
});
// ----- Behaviours -----
const tmp = new THREE.Color();
const behaviours = {
heat(r, k) {
r.current += (r.target - r.current) * k;
const t = THREE.MathUtils.clamp((r.current - r.min) / (r.max - r.min), 0, 1);
r.obj.traverse((o) => {
if (!o.isMesh) return;
o.material.emissive.setRGB(1, 0.35, 0.05);
o.material.emissiveIntensity = t * 1.5;
});
},
fill(r, k) {
r.current += (r.target - r.current) * k;
r.obj.scale.y = Math.max(THREE.MathUtils.clamp(r.current, 0, 1) * 2.8, 0.001);
},
spin(r, k, dt) {
r.current += (r.target - r.current) * k;
r.obj.rotation[r.axis] += (r.current / 60) * Math.PI * 2 * dt * 0.05;
},
status(r) {
const stale = Date.now() - r.at > 10000;
tmp.setHex(stale ? statusColors.OFFLINE : statusColors[r.target] ?? statusColors.OFFLINE);
r.obj.traverse((o) => {
if (!o.isMesh) return;
o.material.color.copy(tmp);
o.material.emissive.copy(tmp);
o.material.emissiveIntensity = 0.6;
});
},
};
// ----- Click to inspect -----
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let selected = null;
renderer.domElement.addEventListener("pointerdown", (e) => {
pointer.set((e.clientX / innerWidth) * 2 - 1, -(e.clientY / innerHeight) * 2 + 1);
raycaster.setFromCamera(pointer, camera);
const hit = raycaster.intersectObjects(root.children, true)[0];
selected = hit ? runtime.find((r) => r.obj && (r.obj === hit.object || r.obj.getObjectById(hit.object.id))) : null;
});
// ----- Render loop -----
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
const k = 1 - Math.exp(-dt * 4);
for (const r of runtime) if (r.obj) behaviours[r.kind](r, k, dt);
const head = connected ? "MQTT connected" : "MQTT disconnected";
panel.textContent = selected
? `${head}\n\n${selected.node}\n${JSON.stringify(selected.last, null, 2)}`
: `${head}\nClick a machine for details`;
controls.update();
renderer.render(scene, camera);
});
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
npm run dev komutunu çalıştırıp yerel URL'yi açın. Ardından bir terminalden veri gönderin:
P=connectmq/demo/factory-7f3a
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/furnace1/telemetry" -m '{"temperature":640}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/tank1/telemetry" -m '{"level":0.7}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/pump1/telemetry" -m '{"rpm":1800}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/cnc1/state" -m '{"state":"RUNNING"}'
-r bayrağı retained mesaj yayınlar; böylece sayfayı daha sonra yeniden yükleseniz bile ikiz boş bir fabrika yerine bilinen son değerleri hemen gösterir. Bu tek bayrak, "ısınmayı bekleyen" bir ikiz ile ilk çizimde doğru olan bir ikiz arasındaki farktır; artıları ve eksileri dijital ikizler için retained MQTT mesajları yazımızda ele alınıyor.
Karşılığını Veren Tasarım Kararları
Kod yerine konfigürasyon. Tesise ikinci bir fırın geldiğinde bindings.js dosyasına bir girdi ve Furnace_2 adında bir mesh eklersiniz. Çalışma zamanı değişmez; bu da ileride config'i bir varlık veritabanından üretmeyi kolaylaştırır.
Bağlanan her parçaya özel materyal. glTF dışa aktarıcıları çoğu zaman tek bir materyali birçok mesh arasında paylaştırır. clone() adımı olmadan bir fırını parlatmak, o materyali kullanan tüm nesneleri parlatır.
Bayatlayabilen durum. status davranışı, on saniye boyunca durum mesajı gelmezse griye döner. Bir ikiz, gateway'i bir saat önce çökmüş bir makineyi asla "çalışıyor" olarak göstermemelidir.
Tek abonelik. Prefix altındaki tek bir wildcard abonelik, client'ı public broker'ın client başına 10 abonelik sınırının rahatça altında tutar; topic bazlı dağıtımı ise yönlendirici map yerel olarak yapar.
Morph target'lar ve animasyon klipleri dahil olmak üzere eşleme katmanına daha ayrıntılı bakmak için IoT verisini bir 3D modele bağlama yazımıza göz atın. 3D görünümün yanına KPI kartları ve grafikler eklemek için dijital ikiz paneli eğitimiyle devam edin.
Adım 6: Derleyin ve Yayınlayın
İkiz statik bir sitedir, bu yüzden barındırması basittir:
npm run build # outputs dist/
npx serve dist # local check of the production build
dist/ klasörünü herhangi bir statik hosting hizmetine veya CDN'li bir nesne depolama alanına yükleyin. Bunu yapmadan önce şu kontrol listesinden geçin:
VITE_MQTT_URLdeğerini kendi özel broker'ınıza yönlendirin ve üretim için bir topic prefix'i belirleyin.- Cihaz kimlik bilgilerini asla bundle içinde göndermeyin. Tarayıcının kendi kimlik bilgileri olur ve sayfa sizin oturum açma ekranınızın arkasında durur.
- Siteyi HTTPS üzerinden sunun; tarayıcılar HTTPS sayfalarından yapılan
ws://bağlantılarını engeller, ikizin 8084 portundawss://kullanmasının nedeni budur. - Modeli sıkıştırın ve yavaş bir bağlantıda yükleme süresini kontrol edin.
- Broker'a ulaşılamadığında ne olduğunu test edin: panelde "MQTT disconnected" yazmalı ve durumlar griye dönmelidir.
İkizi Kendi Broker'ınıza Taşıma
Public broker geliştirme ve demolar için idealdir, ancak üzerindeki her şey herkes tarafından görülebilir. Gerçek bir tesis için barındırılan bir MQTT broker, sunucu işletmeden kimlik bilgileri, TLS ve izolasyon sağlar. ConnectMQ ile portalda bir broker oluşturur, üretilen kullanıcı adını, şifreyi ve client ID'yi kopyalar; VITE_MQTT_USERNAME, VITE_MQTT_PASSWORD, VITE_MQTT_CLIENT_ID ve URL'yi üretim ortamınıza eklersiniz. Broker ayrıca ISA-95 tarzı topic hiyerarşilerini algılar ve Sparkplug B verisini okunabilir ISA-95 topic'lerine yeniden yayınlayabilir; bu da fabrika sahası Sparkplug konuşsa bile web ikizine bağlanabileceği temiz, JSON dostu topic'ler sunar.
ConnectMQ ile Başlayın
Bitmiş bir sonucu görmenin en hızlı yolu, güvenli WebSocket üzerinden bir fabrika sahnesine canlı veri aktaran ConnectMQ'nun 3D dijital ikiz web demosudur. Kendi web tabanlı 3D dijital ikizinizi çalıştırmaya hazır olduğunuzda, ücretsiz Serverless planı için kredi kartı gerekmeden app.connectmq.com adresinden kaydolun ve VITE_MQTT_URL değerini yeni broker'ınıza yönlendirin. Önce barındırma modelleri arasında mı seçim yapıyorsunuz? Bulut MQTT broker rehberimiz seçenekleri karşılaştırıyor.
Sık Sorulan Sorular
Web dijital ikizi için en iyi 3D format hangisidir?
glTF binary (.glb) web için standarttır: kompakttır, hızlı yüklenir ve Three.js ile Babylon.js tarafından yerel olarak desteklenir. STEP veya FBX gibi CAD formatlarını kullanmadan önce glTF'ye dönüştürüp sadeleştirin.
İkiz, sayfa yüklendikten hemen sonra doğru değerleri nasıl gösterir?
Cihazlar en son durumlarını retained mesaj olarak yayınlar; böylece broker, tarayıcı abone olur olmaz her topic'in son değerini iletir. Retained mesajlar olmadan ikiz, bir sonraki güncelleme gelene kadar boş kalır.
Web tabanlı bir 3D dijital ikizden makineleri kontrol edebilir miyim?
Evet; örneğin kullanıcı bir makineye tıkladığında sayfadan komut topic'lerine yayın yaparak. Komutlara dikkatle yaklaşın: onay isteyin, QoS 1 kullanın ve cihazın gerçek durumunu geri yayınlamasına izin verin.
Tarayıcı ikizi tabletlerde çalışır mı?
Evet. WebGL modern mobil tarayıcılarda çalışır. Kare hızlarını akıcı tutmak için poligon sayılarını ve doku boyutlarını makul düzeyde tutun ve örnekte olduğu gibi piksel oranını sınırlayın.



