Overview
This example demonstrates how to build and manipulate a scene graph, the fundamental data structure that represents the kinematic relationships between robot links and joints. A scene graph encodes how a robot is constructed: which links are connected, what constraints govern their motion, and how they relate spatially. Understanding scene graphs is essential for motion planning, collision checking, and robot simulation.
The example builds a kinematic structure, queries adjacency relationships, validates graph topology, and demonstrates how to find paths between links.
Key Concepts
- Links: Rigid bodies in the robot. Each link has a name and can have visual/collision geometry associated with it. Think of a link as a solid piece of the robot (arm segment, gripper jaw, end effector, etc.).
- Joints: Connections between links that define the kinematic relationships. Each joint has a parent link, child link, type (fixed, revolute, prismatic, etc.), and a transform from parent to child.
- Joint Types:
FIXED: Rigid connection, no motion
REVOLUTE: Rotational motion around one axis
CONTINUOUS: Revolute without limits (can spin indefinitely)
PRISMATIC: Linear motion along one axis
PLANAR: 2D motion (x, y, rotation)
FLOATING: 6DOF unconstrained motion
- Scene Graph: A directed graph representing the kinematic tree/chain. Nodes are links, edges are joints. A valid graph must be acyclic (no loops) and connected (all links reachable).
- Acyclic Graph: Contains no cycles (loops). All kinematic chains should be acyclic.
- Tree: An acyclic connected graph. Valid robot kinematic structures form trees where every link (except the root) has exactly one parent.
Typical Workflow
- Create a scene graph - Initialize the kinematic structure container
- Define links - Create Link objects representing rigid bodies
- Add links to graph - Register links in the scene graph
- Define joints - Create Joint objects with parent/child relationships and transforms
- Add joints to graph - Connect links via joints
- Validate topology - Check for cycles and connectivity
- Query relationships - Find adjacent links, children, paths
- Visualize (optional) - Save as DOT format for visualization
Creating and Populating the Graph
Start by creating an empty scene graph:
Create links (rigid bodies). Each link is identified by a unique name:
Link link_1("link_1");
Link link_2("link_2");
Link link_3("link_3");
Link link_4("link_4");
Link link_5("link_5");
Add links to the graph:
g.addLink(link_1);
g.addLink(link_2);
g.addLink(link_3);
g.addLink(link_4);
g.addLink(link_5);
Create joints that connect links. Each joint specifies:
- Name: Unique identifier
- Parent link: Which link this joint connects from
- Child link: Which link this joint connects to
- Type: How the child can move relative to parent
- Transform: Spatial relationship from parent to child
Joint joint_1("joint_1");
joint_1.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_1.parent_link_name = "link_1";
joint_1.child_link_name = "link_2";
joint_1.type = JointType::FIXED;
Joint joint_2("joint_2");
joint_2.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_2.parent_link_name = "link_2";
joint_2.child_link_name = "link_3";
joint_2.type = JointType::PLANAR;
Joint joint_3("joint_3");
joint_3.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_3.parent_link_name = "link_3";
joint_3.child_link_name = "link_4";
joint_3.type = JointType::FLOATING;
Joint joint_4("joint_4");
joint_4.parent_to_joint_origin_transform.translation()(1) = 1.25;
joint_4.parent_link_name = "link_2";
joint_4.child_link_name = "link_5";
joint_4.type = JointType::REVOLUTE;
Add joints to establish kinematic relationships:
g.addJoint(joint_1);
g.addJoint(joint_2);
g.addJoint(joint_3);
g.addJoint(joint_4);
Querying Graph Structure
Find links directly connected to a given link:
std::vector<std::string> adjacent_links = g.getAdjacentLinkNames("link_3");
for (const auto& adj : adjacent_links)
CONSOLE_BRIDGE_logInform(adj.c_str());
Find parent links (inverse adjacent):
std::vector<std::string> inv_adjacent_links = g.getInvAdjacentLinkNames("link_3");
for (const auto& inv_adj : inv_adjacent_links)
CONSOLE_BRIDGE_logInform(inv_adj.c_str());
Find all child links of a link:
std::vector<std::string> child_link_names = g.getLinkChildrenNames("link_2");
for (const auto& child_link : child_link_names)
CONSOLE_BRIDGE_logInform(child_link.c_str());
Find all child links branching from a joint:
child_link_names = g.getJointChildrenNames("joint_1");
for (const auto& child_link : child_link_names)
CONSOLE_BRIDGE_logInform(child_link.c_str());
Validating Graph Topology
Save the graph structure as a DOT file for visualization:
g.saveDOT(tesseract::common::getTempPath() + "graph_acyclic_tree_example.dot");
Test if the graph is acyclic (no loops):
bool is_acyclic = g.isAcyclic();
CONSOLE_BRIDGE_logInform(toString(is_acyclic).c_str());
Test if the graph is a valid tree (acyclic + connected):
bool is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
Handling Disconnected Components
If you add a link without connecting it via a joint, the graph is no longer a tree:
Link link_6("link_6");
g.addLink(link_6);
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
Remove disconnected links to restore tree structure:
g.removeLink("link_6");
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
Creating Cycles
Add a joint that creates a cycle (kinematic loop). This is valid for some mechanisms (like parallel robots) but creates a more complex graph:
Joint joint_5("joint_5");
joint_5.parent_to_joint_origin_transform.translation()(1) = 1.25;
joint_5.parent_link_name = "link_5";
joint_5.child_link_name = "link_4";
joint_5.type = JointType::CONTINUOUS;
g.addJoint(joint_5);
Save the modified graph:
g.saveDOT(tesseract::common::getTempPath() + "graph_acyclic_not_tree_example.dot");
Verify the graph is still acyclic (it is—cycles are loops, not detected here):
is_acyclic = g.isAcyclic();
CONSOLE_BRIDGE_logInform(toString(is_acyclic).c_str());
But it is no longer a tree:
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
Finding Paths
Find the shortest kinematic path between two links:
ShortestPath path = g.getShortestPath("link_1", "link_4");
CONSOLE_BRIDGE_logInform(toString(path).c_str());
This is useful for finding which joints affect the relative motion between two links.
Common Patterns
Building a robot model: Load links from a URDF file and extract the scene graph, or programmatically construct it as shown here.
Validating a model: Use isTree() to ensure your kinematic structure is valid.
Collision checking setup: Use query methods to determine which links can collide and which are always adjacent (connected by a joint).
Trajectory planning: Use path queries to understand kinematic dependencies when planning joint-space trajectories.
Kinematics chains: Use adjacency queries to traverse chains from based to end-effector.
Tips and Best Practices
- Root link: Typically a fixed base or world frame. Other links connect downstream from it.
- Unique names: All link and joint names must be unique within the graph.
- Validate early: Check
isTree() after building to catch structural errors.
- Understand joint types: Choose appropriate types (FIXED for rigid, REVOLUTE for movable joints).
- Transforms matter: The
parent_to_joint_origin_transform defines spatial relationships.
- DOT visualization: Save graphs as DOT files and view with Graphviz to debug structure.
- Kinematic loops: Some robots (parallel manipulators) intentionally have loops. Know your robot!
Integration with Other Systems
Scene graphs are used throughout Tesseract:
- Environment: Holds the scene graph and manages collision objects
- Kinematics: Uses graph traversal to compute forward kinematics
- Planning: Queries relationships to determine which joints affect which links
- Collision: Uses adjacency info to skip unnecessary checks between connected links
Summary
This example demonstrates the complete workflow for building a kinematic scene graph:
- Create empty graph
- Add links and joints
- Validate graph topology (tree vs acyclic vs cyclic)
- Query relationships (adjacent, children, paths)
- Handle invalid configurations and fix them
- Visualize structure for debugging
A valid scene graph is the foundation for all downstream motion planning and simulation operations in Tesseract.
Full source code
For reference, here is the complete source of this example:
TESSERACT_COMMON_IGNORE_WARNINGS_PUSH
#include <console_bridge/console.h>
TESSERACT_COMMON_IGNORE_WARNINGS_POP
#include <tesseract/scene_graph/link.h>
#include <tesseract/scene_graph/joint.h>
using namespace tesseract::scene_graph;
{
std::stringstream ss;
ss << path;
return ss.str();
}
std::string toString(bool b) { return b ? "true" : "false"; }
{
console_bridge::setLogLevel(console_bridge::LogLevel::CONSOLE_BRIDGE_LOG_INFO);
g.addLink(link_1);
g.addLink(link_2);
g.addLink(link_3);
g.addLink(link_4);
g.addLink(link_5);
Joint joint_1(
"joint_1");
joint_1.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_1.parent_link_name = "link_1";
joint_1.child_link_name = "link_2";
joint_1.type = JointType::FIXED;
Joint joint_2(
"joint_2");
joint_2.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_2.parent_link_name = "link_2";
joint_2.child_link_name = "link_3";
joint_2.type = JointType::PLANAR;
Joint joint_3(
"joint_3");
joint_3.parent_to_joint_origin_transform.translation()(0) = 1.25;
joint_3.parent_link_name = "link_3";
joint_3.child_link_name = "link_4";
joint_3.type = JointType::FLOATING;
Joint joint_4(
"joint_4");
joint_4.parent_to_joint_origin_transform.translation()(1) = 1.25;
joint_4.parent_link_name = "link_2";
joint_4.child_link_name = "link_5";
joint_4.type = JointType::REVOLUTE;
g.addJoint(joint_1);
g.addJoint(joint_2);
g.addJoint(joint_3);
g.addJoint(joint_4);
std::vector<std::string> adjacent_links = g.getAdjacentLinkNames("link_3");
for (const auto& adj : adjacent_links)
CONSOLE_BRIDGE_logInform(adj.c_str());
std::vector<std::string> inv_adjacent_links = g.getInvAdjacentLinkNames("link_3");
for (const auto& inv_adj : inv_adjacent_links)
CONSOLE_BRIDGE_logInform(inv_adj.c_str());
std::vector<std::string> child_link_names = g.getLinkChildrenNames("link_2");
for (const auto& child_link : child_link_names)
CONSOLE_BRIDGE_logInform(child_link.c_str());
child_link_names = g.getJointChildrenNames("joint_1");
for (const auto& child_link : child_link_names)
CONSOLE_BRIDGE_logInform(child_link.c_str());
g.saveDOT(tesseract::common::getTempPath() + "graph_acyclic_tree_example.dot");
bool is_acyclic = g.isAcyclic();
CONSOLE_BRIDGE_logInform(toString(is_acyclic).c_str());
bool is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
g.addLink(link_6);
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
g.removeLink("link_6");
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
Joint joint_5(
"joint_5");
joint_5.parent_to_joint_origin_transform.translation()(1) = 1.25;
joint_5.parent_link_name = "link_5";
joint_5.child_link_name = "link_4";
joint_5.type = JointType::CONTINUOUS;
g.addJoint(joint_5);
g.saveDOT(tesseract::common::getTempPath() + "graph_acyclic_not_tree_example.dot");
is_acyclic = g.isAcyclic();
CONSOLE_BRIDGE_logInform(toString(is_acyclic).c_str());
is_tree = g.isTree();
CONSOLE_BRIDGE_logInform(toString(is_tree).c_str());
CONSOLE_BRIDGE_logInform(toString(path).c_str());
}
Common Tesseract Utility Functions.
int main(int argc, char **argv)
Definition create_convex_hull.cpp:43
A basic scene graph using boost.
Holds the shortest path information.
Definition graph.h:93