Driving Mechanisms
Not every mechanism is driven the same way. Some machines can be animated by simply updating joint transforms. Others require solving a kinematic system, or handing the problem to the physics engine. And some are not rigid-body mechanisms at all, and are better represented with rendering techniques. This page gives the recommended approach for the most common industrial mechanisms.
Choosing the Right Approach
| Mechanism | Recommended approach |
|---|---|
| Industrial robots, articulated arms, gantries | Update the transform of each joint |
| Delta robots, four-bar linkages, scissor lifts | Compute the kinematics, or let the physics engine solve them |
| Conveyors and belts | Move the carried bodies with physics, scroll the belt surface's texture |
| Gear trains and transmissions | Rotate each gear from the driving gear ratio |
| Tanks, silos and hoppers | Scale the visible level from the measured value |
| Pipes and process flow | Animate the material, not the geometry |
| Discrete items on a line | Spawn one entity per item |
Serial Kinematic Chains
The easy case, and the one most industrial machines fall into. Each part is carried by the previous one, exactly like the real mechanism:
In the scene graph that is simply a nesting:
Base
└── Arm 1
└── Arm 2
└── End-Effector
Each entity's transform is relative to its parent, so motion propagates for free:
- Move or rotate the Base and everything below it follows.
- Rotate Arm 1 and Arm 2 and the End-Effector follow.
- Rotate Arm 2 and only the End-Effector follows.
Nothing computes this — it falls out of the hierarchy. Which means driving a six-axis robot from live data is six component writes and no mathematics at all.
Updating the mechanism is simply a matter of updating the transform of each joint. A controller publishing all its
joint angles in one message is the norm, and it maps directly onto the array form of updates:
const robotMapping: EventMapping = {
channel: "plant/robot-01/state",
// Entities named after the axes the controller reports.
entities: { byName: "axis-{id}" },
// One event, six entities.
updates: event => {
const { joints } = event.payload as { joints: Array<number> };
return joints.map((angle, index) => ({
id: String(index + 1),
update: { local_transform: { eulerOrientation: [0, radiansToDegrees(angle), 0] } },
}));
},
};
That is the whole integration. See mapping events to entities for the general shape.
Parallel and Closed-Loop Mechanisms
Some mechanisms cannot be represented by a parent-child hierarchy, because several links influence the same component at once. Typical examples include delta robots, four-bar linkages, scissor lifts and Stewart platforms.
The hierarchy cannot express this: the end-effector has four parents, and each forearm depends on the position of all the upper arms at once, so there is no propagation order to follow. And your data source will typically give you only the actuated values — the three upper-arm angles — leaving everything else to be derived.
There are two recommended approaches, and they are mutually exclusive per part.
Compute the Kinematics
If the forward kinematics are known, compute the dependent transforms from the actuated joints and update every moving
part directly. The agent's updates function is the natural home for it: it receives the event, and it can return as
many entity updates as the mechanism has moving parts.
const deltaMapping: EventMapping = {
channel: "plant/delta-01/state",
entities: { byName: "{id}" },
updates: event => {
const { theta1, theta2, theta3 } = event.payload as Record<string, number>;
// Your forward-kinematics solver: actuated angles in, every dependent
// transform out.
const pose = solveDeltaForwardKinematics(theta1, theta2, theta3);
return [
{ id: "upper-arm-1", update: { local_transform: { eulerOrientation: [0, 0, theta1] } } },
{ id: "upper-arm-2", update: { local_transform: { eulerOrientation: [0, 0, theta2] } } },
{ id: "upper-arm-3", update: { local_transform: { eulerOrientation: [0, 0, theta3] } } },
{ id: "forearm-1", update: { local_transform: pose.forearms[0] } },
{ id: "forearm-2", update: { local_transform: pose.forearms[1] } },
{ id: "forearm-3", update: { local_transform: pose.forearms[2] } },
{ id: "platform", update: { local_transform: pose.platform } },
];
},
};
When to choose it: the solver is known — most parallel machines have a closed-form solution published — and you want the scene to be a faithful mirror of the real machine rather than a plausible one. This produces the most accurate representation.
Use the Physics Engine
When analytical kinematics are difficult or unnecessary, build the mechanism out of physics constraints and drive only the actuated joints. The physics solver computes the remaining motion.
Each link is connected by a
updates: event => {
const { theta1, theta2, theta3 } = event.payload as Record<string, number>;
return [
{ id: "upper-arm-1", update: { constraint_actuator: { goalOrientation: yawQuaternion(theta1) } } },
{ id: "upper-arm-2", update: { constraint_actuator: { goalOrientation: yawQuaternion(theta2) } } },
{ id: "upper-arm-3", update: { constraint_actuator: { goalOrientation: yawQuaternion(theta3) } } },
];
},
See joints for how to build the constraint itself: the unlockedMotion bitfield, limits and
springs, breaking forces, and collision behaviour between the constrained bodies.
When to choose it: the mechanism is hard to solve analytically, or approximately right is good enough and you would rather not own a solver. The cost is that the result is a simulation — it converges, it can overshoot, and it will not match the real machine to the millimeter.
Mechanisms That Are Not Rigid Chains
Rigid bodies connected by joints do not cover everything a plant contains. The rest is approximated — and knowing exactly what is being approximated is what keeps the twin honest.
Conveyors
Moving what sits on the belt is a first-class physics feature. Make the belt a static body, set modifyContact and
contactVelocity on its physics_material component, and every rigid body touching it is given that relative
velocity — see contact velocity, which has a video walkthrough.
Making the belt surface look like it is moving is a rendering problem, solved by scrolling its texture — below.
Belts, Chains and Gear Trains
Modelling every link of a roller chain and simulating it is not viable, and it is not what anyone does. The standard approach is a static mesh with a scrolling texture. The PBR shader exposes a UV offset as a material parameter:
finalUV = originalUV * scale + offset
offset is documented in UVs and reachable per entity through the
dataJSON of the material component.
Meshing gears are the same idea with a different target — write local_transform.orientation per gear, scaled by the
gear ratio, from one reported input speed. Neither needs the physics engine, and neither should use it: a simulated gear
train is no more accurate than multiplying by a ratio you already know.
Levels, Flow and Deformation
3dverse has no fluid simulation. Physics bodies are rigid and do not deform. Say what you are showing accordingly — these are indications, not simulations:
| To show | Do this |
|---|---|
| Flow through a pipe or channel | A static mesh with a scrolling material, driven by the reported flow rate |
| A tank or silo level | Scale a mesh along one axis from the reported level, offsetting position by half its height |
| Discrete items on a line | Spawn one entity per item and move it — the stream defines the population |
| A surface that changes shape | Weights on a morph_targets component, authored in the DCC tool |
The tank case is worth calling out because it is so common and so cheap: a level reading between 0 and 100 becomes a scale and a position, and reads correctly at a glance. Scaling happens about the entity's origin, so lift the entity by half its height to keep its base on the floor.
Which Technique Should I Use?
| If your mechanism is... | Use... | What the data provides |
|---|---|---|
| Robot arm, gantry, articulated machine | Scene hierarchy, one write per joint | Joint angles or positions |
| Delta robot, linkage, scissor lift | Solve in the mapping, write every dependent transform | The actuated axes only |
| Same, when approximate is enough | joint + constraint + constraint_actuator | Targets for the actuated axes |
| Conveyor, belt, chain | Scrolling UV offset (+ contactVelocity for carried bodies) | A speed, integrated to an offset |
| Gear train, transmission | local_transform.orientation × gear ratio | One input speed |
| Tank, silo, hopper level | Scale a mesh on one axis | A level reading |
| Pipe or channel flow | Scrolling material on a static mesh | A flow rate |
| Discrete items on a line | Spawn an entity per item | Item identity and position |
Next steps
- Quickstart — get the first values moving.
- Mapping events to entities — the full mapping contract.
- Real-Time Robot Digital Twin — a devlog walking through a robot arm driven over OPC-UA, including the update-rate tuning.