An AWS IoT TwinMaker alternative makes sense when you want interactive 3D digital twins but prefer open formats, standard protocols and the freedom to run anywhere. This guide compares the realistic alternatives, from other platforms to fully open web stacks, and then shows the core technique: binding live MQTT data to nodes of a glTF model in the browser. You will finish with a working pattern you can extend into a full 3D twin.
For background on twins in general, see the digital twin technology guide.
What TwinMaker Bundles Together
AWS IoT TwinMaker combines several pieces that a 3D twin needs:
- Entities and components that model your assets and link them to data sources.
- Connectors to other AWS data services for time-series data, video and documents.
- A scene composer for placing 3D models and attaching data-driven tags and color rules.
- Visualization through a Grafana plugin or an application kit for custom web apps.
When your data already lives in those AWS services, this is convenient. The trade-off is that the twin, its scenes and its data bindings are expressed in service-specific concepts. Moving to another cloud, or to an on-premises plant network, means rebuilding them. Platform roadmaps also change over time, so check the current status of any managed twin service before committing a long-lived project to it.
What a Portable 3D Twin Needs
Strip away the branding and every 3D twin has the same five layers:
| Layer | Job | Portable options |
|---|---|---|
| Geometry | The 3D model of the plant or machine | glTF/GLB exported from CAD or Blender |
| Rendering | Draw and interact with the scene in a browser | Three.js, Babylon.js, CesiumJS for geospatial |
| Live data | Deliver current values to the scene | MQTT over Secure WebSocket |
| Binding | Map data fields to nodes, colors, labels, animations | A small JSON mapping file plus your code |
| History | Trends, playback, analytics | Time-series database plus Grafana or your own charts |
Each layer has mature open choices, and each can be replaced without touching the others. That independence is the real answer to lock-in.
The Alternatives Compared
| Approach | 3D authoring | Data path | Lock-in | Effort |
|---|---|---|---|---|
| Other cloud twin services | Vendor scene editor | Vendor IoT and data services | High | Low to medium |
| Game engines (Unity, Unreal) | Full engine editor | Plugins or custom MQTT clients | Medium (engine licensing, runtimes) | Medium to high |
| Three.js + MQTT | Code, glTF from any tool | MQTT over WSS directly in the browser | Low | Medium |
| Babylon.js + MQTT | Code plus Babylon tooling | MQTT over WSS | Low | Medium |
| Grafana + panels + MQTT-fed database | Limited 3D, strong charts | Telegraf or a subscriber writes to a database | Low | Low |
Game engines shine for photorealistic training and VR; see the Unity digital twin guide. For operations dashboards that anyone can open in a browser, web libraries are usually the better fit. The web-based 3D digital twin guide compares Three.js and Babylon.js in more depth.
Binding MQTT Data to a glTF Model
The heart of any TwinMaker replacement is the binding layer: "when this topic changes, update that 3D node". Keep the binding in data, not code, so non-developers can maintain it.
Here is a mapping file. Each entry links a topic and payload field to a named node in the glTF scene and says what to do with the value:
[
{ "topic": "furnace1/state", "field": "temp_c", "node": "Furnace_Body",
"action": "color", "ranges": [[0, 600, "#4caf50"], [600, 900, "#ff9800"], [900, 2000, "#f44336"]] },
{ "topic": "conveyor1/state", "field": "running", "node": "Conveyor_Belt",
"action": "visible-flag" },
{ "topic": "tank3/state", "field": "level_pct", "node": "Tank3_Liquid",
"action": "scale-y", "max": 100 }
]
And the browser module that applies it. Install three and mqtt with npm and bundle with Vite or similar. The model plant.glb must contain nodes with the names used in the mapping.
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import mqtt from "mqtt";
import bindings from "./bindings.json";
const PREFIX = "connectmq/demo/tm-alt-2h7/site1";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 500);
camera.position.set(20, 15, 20);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x404040, 3));
const nodes = {};
new GLTFLoader().load("/plant.glb", (gltf) => {
scene.add(gltf.scene);
for (const b of bindings) {
const obj = gltf.scene.getObjectByName(b.node);
if (!obj) { console.warn("Missing node", b.node); continue; }
obj.traverse((o) => { if (o.isMesh) o.material = o.material.clone(); });
nodes[b.node] = obj;
}
});
function apply(b, value) {
const obj = nodes[b.node];
if (!obj) return;
if (b.action === "color") {
const hit = b.ranges.find(([lo, hi]) => value >= lo && value < hi);
if (hit) obj.traverse((o) => o.isMesh && o.material.color.set(hit[2]));
} else if (b.action === "visible-flag") {
obj.traverse((o) => o.isMesh && o.material.emissive?.set(value ? 0x224422 : 0x000000));
} else if (b.action === "scale-y") {
obj.scale.y = Math.max(0.01, value / b.max);
}
}
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "tm-alt-2h7-view-" + Math.random().toString(16).slice(2, 8),
protocolVersion: 5,
});
client.on("connect", () => client.subscribe(`${PREFIX}/+/state`));
client.on("message", (topic, payload) => {
const rel = topic.slice(PREFIX.length + 1);
const data = JSON.parse(payload.toString());
for (const b of bindings) if (b.topic === rel && b.field in data) apply(b, data[b.field]);
});
renderer.setAnimationLoop(() => renderer.render(scene, camera));
Test it by publishing retained states to the public MQTT test broker:
mosquitto_pub -h broker.connectmq.com -p 1883 -i tm-alt-2h7-pub -r \
-t "connectmq/demo/tm-alt-2h7/site1/furnace1/state" -m '{"temp_c":935}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i tm-alt-2h7-pub2 -r \
-t "connectmq/demo/tm-alt-2h7/site1/tank3/state" -m '{"level_pct":62}'
Because the states are retained, the scene shows correct colors and levels the moment it loads, then updates live. That is the same experience a managed scene composer offers, built from open parts. The guide to binding IoT data to a 3D model covers more actions such as animations, labels and alarm blinking, and the Three.js documentation is the reference for the rendering side.
Pitfalls When Leaving a Managed Twin Service
- Node names drift. Re-exporting a CAD model can rename nodes and silently break bindings. Log missing nodes at load time, as the code above does, and keep names stable in your export pipeline.
- Shared materials. In glTF, many meshes often share one material, so coloring one node colors them all. Clone materials for bound nodes.
- Heavy models. CAD exports can be enormous. Decimate geometry, merge static meshes and use compressed glTF to keep load times reasonable.
- History is separate. Live MQTT data drives the scene, but trends and playback need a database. Subscribe a writer service or Telegraf to the same topics.
- Browser credentials. Anything shipped to a browser can be read by its user. Give viewers read-oriented credentials separate from those of gateways that publish.
- Missing state on first load. If gateways publish only changes, a freshly opened scene stays blank until something changes. Publish full state messages with the retain flag so every viewer starts from the current picture.
- Rendering every message. High-rate telemetry can trigger more scene updates than the screen can show. Store the latest values when messages arrive and apply them once per animation frame instead.
A Migration Path in Four Moves
If you are replacing an existing managed twin rather than starting fresh, move in small, reversible steps:
- Export your 3D assets to glTF and confirm the node names you need for bindings.
- Make gateways publish to an MQTT broker in parallel with the existing data path.
- Rebuild scene bindings as a mapping file and compare the new viewer against the old one side by side.
- Move history to a time-series database fed from the same topics, then retire the old path.
Hosting the Data Layer
The live data layer is the only piece that must be always on and reachable by both the plant and the browser. A hosted MQTT broker with Secure WebSocket support solves this without opening ports at the plant: gateways connect outbound over TLS, browsers connect over WSS.
ConnectMQ provides exactly that: TLS on 8883 for gateways, Secure WebSocket on 8084 at path /mqtt for browsers, MQTT 5 support and a portal with a live topic explorer. ConnectMQ also runs a public 3D digital twin web demo, a factory with a CNC machine, robot arm, furnace, tank, conveyor and pump updated live over WSS, built on this same browser-plus-MQTT pattern. For production, swap the public endpoint for your private broker credentials from the portal: YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID.
Build Your 3D Twin on ConnectMQ
Open web 3D libraries plus a standard MQTT broker give you an interactive twin you fully control. Start with a free ConnectMQ Serverless broker, no credit card required, and connect your first glTF scene to live data today.
Frequently Asked Questions
What is a good AWS IoT TwinMaker alternative for 3D?
For browser-based operations twins, Three.js or Babylon.js with glTF models and MQTT over Secure WebSocket is a flexible, portable choice. Game engines like Unity suit photorealistic or VR experiences.
Can I reuse my 3D models from TwinMaker?
Usually yes, if they are in glTF or GLB format. You will need to recreate data bindings and tag placements, which is easiest if you keep them in a mapping file as shown above.
Do browsers connect to MQTT directly?
Yes. MQTT.js connects over WebSocket or Secure WebSocket, so a web page can subscribe to topics without a backend relay. Use TLS and separate viewer credentials in production.
Where does historical data go without TwinMaker?
Use a time-series database fed by an MQTT subscriber such as Telegraf or a small script, and visualize it in Grafana or charts inside your twin page.



