Fx2D joints connect two entities and expose a motor API built on a shared PID controller. Both joint types (FxRevoluteJoint, FxPrismaticJoint) inherit the same base interface and differ only in the physical quantity they control (angle vs. translation).
Include via:
#include "Fx2D/Core.h"
Joints are looked up by name from a scene:
auto joint = scene.get_joint("wheel_hinge"); // returns shared_ptr<FxJoint>, nullptr if missing
auto rev = std::dynamic_pointer_cast<FxRevoluteJoint>(joint);
auto pri = std::dynamic_pointer_cast<FxPrismaticJoint>(joint);
Other scene-level helpers:
scene.joint_exists("wheel_hinge"); // bool
scene.joint_count(); // size_t
scene.delete_joint("wheel_hinge"); // bool – removes joint and its constraints
Every joint has one active ControlMode:
| Mode | Enum | FxRevoluteJoint target unit |
FxPrismaticJoint target unit |
|---|---|---|---|
POSITION |
ControlMode::POSITION |
angle (radians) | translation along axis |
VELOCITY |
ControlMode::VELOCITY |
angular velocity (rad/s) | linear velocity |
EFFORT |
ControlMode::EFFORT |
torque | force |
Switching mode resets the PID integral and derivative accumulators automatically.
joint->set_control_mode(ControlMode::VELOCITY);
joint->get_control_mode(); // ControlMode
FxRevoluteJoint)Controls the relative angle between two bodies around a shared anchor point.
// Position mode — drive to an angle
rev->set_control_mode(ControlMode::POSITION);
rev->set_theta(0.5f); // target 0.5 rad; PID drives there over time
rev->set_theta(0.5f, true); // also snaps entities immediately (instant correction)
// Velocity mode — spin at a constant rate
rev->set_control_mode(ControlMode::VELOCITY);
rev->set_omega(3.14f); // target ~180°/s
// Effort mode — apply a fixed torque each step
rev->set_control_mode(ControlMode::EFFORT);
rev->set_torque(15.0f); // 15 N·m
float angle = rev->get_theta(); // current relative angle in radians
float omega = rev->get_omega(); // current relative angular velocity in rad/s
rev->set_max_torque(20.0f); // clamps motor output; alias for set_max_effort()
float limit = rev->get_max_torque();
FxPrismaticJoint)Controls the relative translation of two bodies along a locked axis.
// Position mode — move to a point along the axis
pri->set_control_mode(ControlMode::POSITION);
pri->set_position(1.5f); // target offset 1.5 units from initial distance
pri->set_position(1.5f, true); // also snaps entities immediately
// Velocity mode — slide at a constant speed
pri->set_control_mode(ControlMode::VELOCITY);
pri->set_velocity(2.0f); // target 2 units/s along axis
// Effort mode — apply a fixed force each step
pri->set_control_mode(ControlMode::EFFORT);
pri->set_force(8.0f); // 8 N along axis
float pos = pri->get_position(); // current translation relative to initial distance
float vel = pri->get_velocity(); // current velocity along axis
pri->set_max_force(10.0f); // alias for set_max_effort()
float limit = pri->get_max_force();
All motor modes except EFFORT route their error signal through a PID controller before applying the result as effort.
joint->set_pid({5.0f, 0.1f, 0.2f}); // set P, I, D gains at once (resets state)
joint->set_p(5.0f);
joint->set_i(0.1f);
joint->set_d(0.2f);
FxVec3f gains = joint->get_pid(); // {p, i, d}
set_pid() always resets the integral and previous-error accumulators. Calling set_p/i/d individually does not reset state — useful for live tuning.
instant modeBy default (m_instant = true), set_theta, set_omega, set_position, and set_velocity also apply an immediate pose/velocity correction on top of the PID output. Disable this for smoother, purely PID-driven motion:
joint->set_instant(false);
Or override per call:
rev->set_theta(0.5f, /*instant=*/false);
These apply to both joint types:
joint->enabled = false; // disable motor (constraints still active)
joint->entities_collide = true; // allow parent/child collision
joint->set_max_effort(20.0f); // universal effort cap
float cap = joint->get_max_effort();
joint->get_name(); // const string&
joint->get_entity1(); // shared_ptr<FxEntity> — parent
joint->get_entity2(); // shared_ptr<FxEntity> — child
joint->is_revolute(); // bool
joint->is_prismatic(); // bool
Joints can be created directly without YAML:
auto chassis = scene.get_entity("chassis");
auto wheel = scene.get_entity("wheel");
// Revolute: anchor in chassis local frame, limits ±0.6 rad
auto hinge = std::make_shared<FxRevoluteJoint>(
"wheel_hinge", chassis, wheel,
FxVec2f{0.0f, -0.5f}, // anchor point
-0.6f, 0.6f // angle_min, angle_max
);
hinge->set_pid({5.0f, 0.2f, 0.1f});
hinge->set_max_torque(20.0f);
hinge->set_control_mode(ControlMode::EFFORT);
hinge->set_torque(12.0f);
scene.add_joint(hinge);
// Prismatic: slide along X axis, limits −2 to +2
auto rail = scene.get_entity("rail");
auto carriage = scene.get_entity("carriage");
auto slider = std::make_shared<FxPrismaticJoint>(
"slider", rail, carriage,
FxVec2f{1.0f, 0.0f}, // axis
-2.0f, 2.0f // position_min, position_max
);
slider->set_pid({4.0f, 0.0f, 0.2f});
slider->set_max_force(8.0f);
slider->set_control_mode(ControlMode::VELOCITY);
slider->set_velocity(1.5f);
scene.add_joint(slider);
For YAML-based joint configuration see scene_yml.md.
../examples/joint_control_demo/ is a complete headless demo. Its Scene.yml declares an arm_motor (revolute) and a slider_motor (prismatic), and main.cpp drives both through POSITION, VELOCITY, and EFFORT phases, printing angle, angular velocity, slider offset, and slider velocity as they track. Swap the step loops for FxRylbRenderer(scene, 60).run() to watch it instead.
Two things the example demonstrates that are easy to get wrong:
Position and velocity loops need different gains. A position loop wants strong damping — roughly D = 2*sqrt(P * inertia) for a revolute joint, or 2*sqrt(P * mass) for a prismatic one — or the joint oscillates around the target. In velocity mode the error is already a rate, so reusing that D differentiates acceleration and fights the motor hard enough to stall it. The example calls set_pid() at the start of each phase.
Very small timesteps weaken offset-pivot motors. Per-substep rotations shrink quadratically with the timestep, and constraint corrections are computed on float world coordinates. Once a correction falls below one ulp of the coordinate magnitude — about 4.8e-7 at x = 7 — it is lost to rounding. A revolute joint whose anchor is offset from the child’s centre of mass depends on exactly those small displacements, so at dt = 1e-3 such a motor loses most of its authority, and the effect worsens the further the scene sits from the origin. The same joint placed near the origin rotates roughly five orders of magnitude further per unit time.
The example runs at dt = 0.01, where the effect is negligible. If a motor seems weak or dead, try a larger timestep, or move the scene closer to the origin, before re-tuning gains. Anchoring the joint at the child’s centre of mass also avoids it, since rotation about the centroid does not displace the anchor.
Note that a related but distinct bug — FxAngleWrap rounding away any rotation under 1.2e-7 rad, which froze all angular motion at small timesteps — has been fixed. Free bodies and centre-anchored joints now rotate correctly at dt = 1e-3.
A joint’s constraints are registered as <jointname>_<Type> — lift_Anchor,
bridge_j3_AngleLmt — with _N appended only when one joint owns several constraints of the
same type. Joint names are unique in the registry, so constraint names are unique by
construction, and they survive entity renames because the entity pair is not part of the name.
The YAML joints: section is name-keyed, so authored joint names flow straight through.
Standalone constraints added directly with add_constraint keep their e1_e2_Type names.