How to Create Custom Shapes and Collision Geometries in DART: A Complete Guide

To create custom shapes and collision geometries in DART, derive a class from dart::dynamics::Shape, implement the pure virtual interface including computeInertia(), clone(), and bounding box updates, then attach it to a BodyNode via a ShapeNode with CollisionAspect for physics simulation.

DART (Dynamic Animation and Robotics Toolkit) provides a flexible geometry system through the dart::dynamics namespace that allows developers to extend the simulator with arbitrary collision geometries. Whether you need a custom convex hull for collision detection or a novel visual primitive, understanding how to create custom shapes and collision geometries in DART is essential for advanced robotics simulation. The architecture separates visual representation from collision representation, enabling you to mix and match aspects as needed.

Understanding DART's Shape Architecture

DART represents all geometry through the dart::dynamics::Shape class hierarchy defined in dart/dynamics/shape.hpp. This abstract base class declares the contract that every geometry must satisfy, including type identification, inertia calculation, and spatial bounds.

The key components work together as follows:

  • dart::dynamics::Shape – The abstract base in shape.hpp that defines pure virtual methods for getType(), computeInertia(), clone(), updateBoundingBox(), and updateVolume().
  • Concrete implementations – Classes like SphereShape and BoxShape in dart/dynamics/ provide reference implementations of the interface.
  • dart::dynamics::ShapeNode – Defined in shape_node.cpp, this class binds a Shape to a BodyNode and manages aspects: VisualAspect for rendering, CollisionAspect for physics, and DynamicsAspect for mass properties.
  • FCL integration – The FCL collision detector converts shapes into BVH models using utilities in dart/collision/fcl/collision_shapes.hpp.

Implementing a Custom Shape Class

To create custom shapes and collision geometries in DART, you must subclass Shape and implement five critical virtual methods. These methods enable DART to compute physical properties, perform broad-phase collision culling, and clone skeletons.

Required Virtual Methods

Every custom shape must override:

  • getType() – Returns a std::string_view identifier for debugging and serialization.
  • computeInertia(double mass) – Returns an Eigen::Matrix3d representing the inertia tensor for the given mass.
  • clone() – Returns a std::shared_ptr<Shape> deep copy for DART's skeleton cloning system.
  • updateBoundingBox() – Populates mBoundingBox with axis-aligned bounds in the local frame.
  • updateVolume() – Computes and caches the volume in mVolume.

Complete Capped Cylinder Example

Below is a minimal implementation of a capped cylinder, demonstrating how to satisfy the Shape contract:

// MyCappedCylinder.hpp
#pragma once

#include <dart/dynamics/Shape.hpp>
#include <Eigen/Dense>

class MyCappedCylinder : public dart::dynamics::Shape
{
public:
  MyCappedCylinder(double radius, double height)
    : Shape(SHAPE_TYPE::CYLINDER),  // reuse existing type enum
      mRadius(radius), mHeight(height) {}

  std::string_view getType() const override { 
    return "MyCappedCylinder"; 
  }

  Eigen::Matrix3d computeInertia(double mass) const override
  {
    // Solid cylinder inertia (simplified, caps omitted)
    double Ixx = 0.25 * mass * mRadius * mRadius
               + (1.0 / 12.0) * mass * mHeight * mHeight;
    double Iyy = Ixx;
    double Izz = 0.5 * mass * mRadius * mRadius;
    Eigen::Matrix3d I = Eigen::Matrix3d::Zero();
    I(0,0) = Ixx; I(1,1) = Iyy; I(2,2) = Izz;
    return I;
  }

  dart::dynamics::ShapePtr clone() const override
  {
    return std::make_shared<MyCappedCylinder>(mRadius, mHeight);
  }

protected:
  void updateBoundingBox() const override
  {
    mBoundingBox.setMin(Eigen::Vector3d(-mRadius, -mRadius, -mHeight/2));
    mBoundingBox.setMax(Eigen::Vector3d( mRadius,  mRadius,  mHeight/2));
    mIsBoundingBoxDirty = false;
  }

  void updateVolume() const override
  {
    mVolume = M_PI * mRadius * mRadius * mHeight;
    mIsVolumeDirty = false;
  }

private:
  double mRadius, mHeight;
};

The constructor initializes the base class with a ShapeType enum value. You can reuse an existing type like CYLINDER or add a new entry to shape.hpp if your geometry requires distinct handling.

Attaching Custom Shapes to Body Nodes

Once implemented, attach your custom shape to a skeleton using BodyNode::createShapeNodeWith<Aspects...>. This template method instantiates a ShapeNode and attaches the specified aspects in a single call.

#include <dart/dart.hpp>
#include "MyCappedCylinder.hpp"

int main()
{
  // Create skeleton and body
  auto skel = dart::dynamics::Skeleton::create("robot");
  auto body = skel->createBodyNode();

  // Instantiate custom shape
  auto myShape = std::make_shared<MyCappedCylinder>(0.3, 1.2);

  // Create ShapeNode with visual, collision, and dynamics aspects
  auto shapeNode = body->createShapeNodeWith<
      dart::dynamics::VisualAspect,
      dart::dynamics::CollisionAspect,
      dart::dynamics::DynamicsAspect>(myShape);

  // Configure visual properties
  shapeNode->getVisualAspect()->setColor({0.8, 0.2, 0.2, 1.0});
  
  return 0;
}

By including CollisionAspect, you enable physics simulation. DART automatically converts the shape for the FCL collision detector using the generic mesh pathway defined in dart/collision/fcl/collision_shapes.hpp.

Providing Custom Collision Geometries for FCL

When your visual geometry does not map cleanly to primitives, you can supply a custom FCL BVH model directly. This is necessary for complex convex hulls or procedurally generated collision meshes.

Building FCL BVH Models

The utilities in collision_shapes.hpp demonstrate how to construct fcl::BVHModel instances programmatically. Adapt this pattern for custom convex hulls:

#include <dart/collision/fcl/collision_shapes.hpp>
#include <fcl/geometry/bvh/bvh_model.h>

std::shared_ptr<::fcl::CollisionGeometryd> createCustomConvexHull(
    const std::vector<Eigen::Vector3d>& points,
    const dart::math::Isometry3d& transform)
{
  using BV = ::fcl::OBBRSSd;  // Bounding volume type
  auto model = std::make_shared<::fcl::BVHModel<BV>>();
  model->beginModel();

  // Convert points and add triangles
  for (size_t i = 0; i + 2 < points.size(); i += 3)
  {
    auto fclTransform = dart::collision::fcl::toFclTransform(transform);
    ::fcl::Vector3d p1 = fclTransform * ::fcl::Vector3d(points[i][0], points[i][1], points[i][2]);
    ::fcl::Vector3d p2 = fclTransform * ::fcl::Vector3d(points[i+1][0], points[i+1][1], points[i+1][2]);
    ::fcl::Vector3d p3 = fclTransform * ::fcl::Vector3d(points[i+2][0], points[i+2][1], points[i+2][2]);
    
    model->addTriangle(p1, p2, p3);
  }

  model->endModel();
  return model;
}

This follows the same implementation pattern as createCube() and createEllipsoid() in the DART source.

Integrating with CollisionAspect

To use your custom FCL geometry, wrap it in a collision object and attach it to a body:

auto customCollision = std::make_shared<dart::collision::FCLCollisionObject>(
    createCustomConvexHull(myPoints, dart::math::Isometry3d::Identity()));

body->createShapeNodeWith<dart::dynamics::CollisionAspect>(customCollision);

This approach bypasses the automatic shape-to-mesh conversion, giving you full control over the collision representation while maintaining compatibility with DART's simulation pipeline.

Summary

  • Subclass dart::dynamics::Shape and implement getType(), computeInertia(), clone(), updateBoundingBox(), and updateVolume() to define custom geometry.
  • Use BodyNode::createShapeNodeWith<...> to attach shapes with VisualAspect, CollisionAspect, and DynamicsAspect as needed.
  • Leverage dart/collision/fcl/collision_shapes.hpp for reference implementations of FCL BVH model creation when building custom collision geometries.
  • Provide clone() implementations that return std::shared_ptr<Shape> to ensure skeleton copying works correctly throughout the framework.

Frequently Asked Questions

Do I need to modify DART's source code to add a custom shape?

No. You can subclass dart::dynamics::Shape in your own project headers without modifying dartsim/dart. Simply include <dart/dynamics/Shape.hpp> and link against DART. The dynamic nature of the Shape hierarchy allows runtime registration of your custom types.

How does DART convert shapes for collision detection?

By default, DART uses the FCL collision detector. When you add a CollisionAspect to a ShapeNode, DART attempts to convert the shape into an FCL BVH model using converters in dart/collision/fcl/collision_shapes.hpp. For standard types like spheres and boxes, it uses optimized FCL primitives. For meshes, it builds a BVH tree from the triangle data.

Can I use a custom collision geometry without a visual representation?

Yes. Create a ShapeNode with only CollisionAspect specified in the template parameters: body->createShapeNodeWith<dart::dynamics::CollisionAspect>(myCollisionShape). This creates an invisible collision geometry that participates in physics simulation but renders nothing, useful for simplified collision proxies.

What is the difference between Shape and ShapeNode in DART?

A Shape represents the geometric data itself—vertices, inertia, and bounding volume. A ShapeNode is the scene graph attachment point that associates a Shape with a specific BodyNode and manages aspects (visual, collision, dynamics). Multiple ShapeNode instances can share the same Shape instance, allowing efficient memory use when identical geometries appear multiple times in a skeleton.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →