Prototyping Maya Physics Move Tool with C++

Introduction

I've recently embarked on a journey into C++ development with an initial idea in mind: to create a move tool that offers real-time collision detection for artists working in Autodesk Maya. This concept aims to enhance scene and environment building, allowing for efficient and streamlined workflows. Users can place objects and assemble large locations without any pre-setup or physical simulation, as everything operates seamlessly in the background, much like the standard move tool, but with added collision detection for any non-selected mesh in the scene. Below, you'll find a GIF showcasing a working example of my plugin in action. It roughly illustrates the real-time collision detection capabilities as simple objects interact within the Maya environment:


In this article, I'll dive deeper into the implementation details of my plugin. It's important to note that this project is a work in progress, not yet a finished product. The insights shared here reflect its current state and the ongoing process of development and refinement.

Initial Idea and Overall Architecture

My initial goal was ambitious: enabling the tool to handle collisions with scenes of any size, regardless of the amount of geometry. Before delving into the complexities, let's first explore how the plugin operates at a high level. Here's a schema representing the architecture of the plugin's workflow:

1) Tool Activation: When the tool is activated within Autodesk Maya, it sets up an empty physics world in the background. This serves as the stage for all the subsequent real-time physics calculations.

2) Spatial Indexing with R-tree: To manage the scene efficiently, an R-tree is generated for the scene meshes. Each mesh's bounding box is used as a key for this spatial index, which is crucial for performing fast and efficient queries to determine potential collisions.

3) Object Selection: As a user selects an object within the Maya viewport, the plugin springs into action. The selected objects are then converted into active rigid bodies, which are subsequently added to the physics world. 

4) Collision Detection during Manipulation: Once objects are active in the physics world, the user can manipulate them using the standard Maya tools. During this manipulation, my plugin's 'doDrag' function is called. Within this function, collision detection is performed with the potential collision candidates identified by the R-tree.


The tool's workflow demonstrates a logical progression from initialization to real-time interaction, with each step building upon the previous to ensure seamless operation. This workflow not only showcases the complexity involved in developing such a tool but also my commitment to creating a robust and user-friendly experience within Maya.

R-trees and spatial data structures

Spatial data structures are essential in computer graphics for organizing spatial information—data representing objects in a multi-dimensional space. In 3D environments, where objects are distributed across vast spaces, these structures allow for efficient operations like searching and possible collision detection.

For my tool, I employed bounding boxes, which every Mesh in Maya has. These bounding boxes serve as a quick way to identify potential collisions. By checking if bounding boxes intersect, my tool can swiftly rule out objects that are too far apart to interact, reducing unnecessary calculations. 

I chose C++ Boost Library R-trees, a type of spatial data structure akin to B-trees but for spatial information. I used the bounding boxes of Maya meshes as keys in the R-tree. This method enables rapid querying of objects that might collide when a user moves an object in the scene. By focusing only on objects with intersecting bounding boxes, the tool efficiently narrows down potential collisions.
Here is a more detailed look into the doDrag schema, a function which launches each time user moves the manipulator:


This approach was pivotal in maintaining the responsiveness of my tool, especially in dense scenes. It allowed me to optimize performance by focusing collision detection calculations only on objects likely to collide, a critical factor for real-time applications like mine. R-tree was working perfectly - fast and very responsive. Here's a snippet of the code where I initialize R-tree:

MStatus CollisionCandidatesFinder::initializeRTree()
{
    if (this->allSceneMFnMeshes.empty()) {
        MGlobal::displayError("MFnMeshes vector is empty");
        return MS::kFailure;
    }

    MStatus status;
    for (size_t i = 0; i < this->allSceneMFnMeshes.size(); ++i) {
        MDagPath dagPath;
        status = this->allSceneMFnMeshes[i]->getPath(dagPath);
        if (status != MS::kSuccess) {
            MGlobal::displayError("Failed to get MDagPath from MFnMesh");
            return status;
        }

        MBoundingBox mbbox = this->allSceneMFnMeshes[i]->boundingBox();
        MMatrix worldMatrix = dagPath.inclusiveMatrix();

        MPoint minPoint = mbbox.min() * worldMatrix;
        MPoint maxPoint = mbbox.max() * worldMatrix;

        box bbox(point(minPoint.x, minPoint.y, minPoint.z), point(maxPoint.x, maxPoint.y, maxPoint.z));
        this->rTree.insert(std::make_pair(bbox, i));
    }

    return MS::kSuccess;
}

After that, we can use this R-tree in order to query if the specified bounding box intersects with any objects in our scene:

box queryBox(
point(worldMinPoint.x, worldMinPoint.y, worldMinPoint.z),
point(worldMaxPoint.x, worldMaxPoint.y, worldMaxPoint.z)
);

std::vector<value> result;
this->rtree.query(bgi::intersects(queryBox), std::back_inserter(result));


First problems

I encountered unexpected challenges. Specifically, the conversion of Maya mesh to Bullet3's btRigidBody for collision candidates proved resource-intensive. This conversion, involving iteration over each triangle of an MFnMesh, was too costly for real-time applications.

Recognizing the need for efficiency and responsiveness, I made a strategic pivot in the development process. The initial approach involving R-tree calculations for every interaction proved to be resource-intensive because of the collision conversion. In pursuit of a more streamlined process, I decided to simplify the workflow.

As a result, I removed the parts of the code responsible for dynamic R-tree calculation. Instead, I chose to initialize all static meshes for the Bullet3 physics world right at the tool's startup. Now, during the tool initialization, I iterate over each mesh in the scene and create bulletShapes. These shapes are later used for the initialization of both active and static rigid bodies, establishing the collision environment from the get-go. However, it's important to note that, as it currently stands, this method will restrict the tool's application to primarily small to medium-sized scenes. Here is the final schema reflecting the current state of the plugin:

Coordinate System Differences: Bullet3 and Maya

A notable aspect of this project was reconciling the coordinate systems of Bullet3 and Maya. Even though inside bullet3 documentation, it is stated that its coordinate system is right-handed with Y as Up, it was not working out of the box in my case. Only after I converted the coordinate system did it start to behave correctly. In this image, you can see the resulting difference:


This discrepancy necessitates careful data conversion between the two systems. Here's how I handled the coordinate system conversion:

btTransform BulletCollisionHandler::convertMayaToBulletMatrix(const MMatrix& mayaMatrix) {
    MTransformationMatrix mayaTransMatrix(mayaMatrix);
    MQuaternion mayaQuat = mayaTransMatrix.rotation();

    // Convert Maya quaternion to Bullet quaternion, adjusting for coordinate system differences
    btQuaternion bulletQuat(mayaQuat.x, mayaQuat.z, -mayaQuat.y, mayaQuat.w);

    MVector mayaTranslation = mayaTransMatrix.getTranslation(MSpace::kWorld);
    // Adjust the translation for Bullet's coordinate system (Z-up)
    btVector3 bulletTranslation(mayaTranslation.x, mayaTranslation.z, -mayaTranslation.y);

    btTransform bulletTransform;
    bulletTransform.setRotation(bulletQuat);
    bulletTransform.setOrigin(bulletTranslation);

    return bulletTransform;
}

MMatrix BulletCollisionHandler::convertBulletToMayaMatrix(const btTransform& bulletTransform) {
    btQuaternion bulletQuat = bulletTransform.getRotation();
    // Convert Bullet quaternion to Maya quaternion
    MQuaternion mayaQuat(bulletQuat.getX(), -bulletQuat.getZ(), bulletQuat.getY(), bulletQuat.getW());
    MMatrix mayaMatrix = mayaQuat.asMatrix();

    btVector3 bulletTranslation = bulletTransform.getOrigin();
    // Adjust the translation for Maya's coordinate system (Y-up)
    mayaMatrix[3][0] = bulletTranslation.getX();
    mayaMatrix[3][1] = -bulletTranslation.getZ();
    mayaMatrix[3][2] = bulletTranslation.getY();

    // Reset the scale component of the matrix to 1
    for (int i = 0; i < 3; ++i) {
        double length = sqrt(mayaMatrix[i][0] * mayaMatrix[i][0] +
            mayaMatrix[i][1] * mayaMatrix[i][1] +
            mayaMatrix[i][2] * mayaMatrix[i][2]);
        if (length != 0) {
            for (int j = 0; j < 3; ++j) {
                mayaMatrix[i][j] /= length;
            }
        }
    }
    return mayaMatrix;
}

Converting Maya Meshes into Rigid Bodies for Physics World

To integrate Bullet3 physics, I set up the dynamics world and converted Maya all scene meshes into Bullet3 rigid bodies. Here are some function examples which convert Maya MFnMeshes to static or active collision rigid body:
btCollisionShape* BulletCollisionHandler::convertMFnMeshToStaticCollisionShape(MFnMesh* mfnMesh) {
    // Get the points in world space
    MPointArray mayaVertices;
    mfnMesh->getPoints(mayaVertices, MSpace::kWorld);

    // Create the Bullet triangle mesh
    btTriangleMesh* triMesh = new btTriangleMesh();

    // Get triangles from the mesh
    MIntArray triangleCounts, triangleVertices;
    mfnMesh->getTriangles(triangleCounts, triangleVertices);

    // Index variables for triangleVertices
    int triangleIndex = 0;
    for (unsigned int i = 0; i < triangleCounts.length(); ++i) {
        for (int j = 0; j < triangleCounts[i]; ++j) {
            btVector3 vertices[3];
            for (int k = 0; k < 3; ++k) {
                // Get the vertex index
                int vertexIndex = triangleVertices[triangleIndex + k];
                // Transform the vertex position to world space
                MPoint worldSpaceVertex = mayaVertices[vertexIndex];
                // Add vertex to Bullet triangle mesh
                // Convert from Maya's right-handed Y-up to Bullet's left-handed Z-up system
                vertices[k] = btVector3(
                    static_cast<btScalar>(worldSpaceVertex.x),
                    static_cast<btScalar>(worldSpaceVertex.z), // Swap Y and Z
                    static_cast<btScalar>(-worldSpaceVertex.y)
                ); // Invert Z for left-handed system
            }
            // Add the triangle to the mesh
            triMesh->addTriangle(vertices[0], vertices[1], vertices[2]);
            // Move to the next set of vertices
            triangleIndex += 3;
        }
    }

    // Create the mesh shape
    bool useQuantizedAABBCompression = true;
    btBvhTriangleMeshShape* meshShape = new btBvhTriangleMeshShape(triMesh, useQuantizedAABBCompression);

    return meshShape;
}

btCollisionShape* BulletCollisionHandler::convertMFnMeshToActiveCollisionShape(MFnMesh* mfnMesh) {
    // Get the points in local space
    MPointArray mayaVertices;
    mfnMesh->getPoints(mayaVertices, MSpace::kObject);

    // Create the Bullet convex hull shape
    btConvexHullShape* convexHull = new btConvexHullShape();
    // Loop through the vertices and add them to the convex hull
    for (unsigned int i = 0; i < mayaVertices.length(); ++i) {
        MPoint vertex = mayaVertices[i];
        // Convert from Maya's right-handed Y-up to Bullet's left-handed Z-up system
        btVector3 bulletVertex(
            static_cast<btScalar>(vertex.x),
            static_cast<btScalar>(vertex.z), // Swap Y and Z
            static_cast<btScalar>(-vertex.y) // Invert Z for left-handed system
        );
        // Add the vertex to the convex hull shape
        convexHull->addPoint(bulletVertex);
    }
    convexHull->optimizeConvexHull();
    convexHull->initializePolyhedralFeatures();
    return convexHull;
}

Reselection, Dynamics Object Setup, and Collider Updates

Handling reselection and updating colliders efficiently was crucial. Instead of recreating colliders with each reselection, I utilized singleton patterns in my CollisionCandidatesFinder.cpp and BulletCollisionHandler.cpp classes. This approach, integrated into the ManipulationContext, allows for efficient manipulator recreation without redundant collider initialization. Singleton example from header:
class CollisionCandidatesFinder {
    public:
        /**
         * @brief Retrieves the singleton instance of the class.
         * @return Reference to the singleton instance.
         */
        static CollisionCandidatesFinder& getInstance();

        // Prevent copying and assignment.
        CollisionCandidatesFinder(const CollisionCandidatesFinder&) = delete;
        CollisionCandidatesFinder& operator=(const CollisionCandidatesFinder&) = delete;

    private:
        CollisionCandidatesFinder();  // Constructor is private for singleton
        ~CollisionCandidatesFinder();  // Destructor

Dragging and Physics World Update

All drag operations are performed within the doDrag function. The aspect I find interesting is how I update the position of our dynamic object by applying linear velocity, a technique widely used in physics engines. However, I've put a cap on the velocity values to prevent excessively large numbers during substantial drag movements by the user. Essentially, I compute the difference between the locator's current position and that of the object. The resulting vector is then confined within a certain range. This step is crucial because if the user moves our manipulator too far in the viewport, it could generate an excessively high-velocity vector, leading to poor results in the collision calculations for Bullet3. Below is a snippet of the code that accomplishes this:
    // object pos of locator
    MPoint currentPosition;
    this->getConverterManipValue(0, currentPosition);

    btVector3 currentPos(
        this->currentManipPosition.x,
        this->currentManipPosition.z,
        -(this->currentManipPosition.y)
    );

    btVector3 targetPos(
        currentPosition.x,
        currentPosition.z,
        -currentPosition.y
    );

    // Here we calculate velocity vector by clamping in specified range
    // it is nessesary to do in order to avoid too huge or too small velocity values
    float timeStep = 1.0f / 60.0f;
    btVector3 requiredVelocity = (targetPos - currentPos) / timeStep;
    requiredVelocity *= 0.04*2;
    float threshold = 0.05f;

    if (std::abs(requiredVelocity.x()) < threshold) requiredVelocity.setX(0);
    if (std::abs(requiredVelocity.y()) < threshold) requiredVelocity.setY(0);
    if (std::abs(requiredVelocity.z()) < threshold) requiredVelocity.setZ(0);

    // Define range for clamping
    float minValue = -0.4f*4;
    float maxValue = 0.4f*4;
    // Clamp values within the range
    requiredVelocity.setX(std::min(std::max(requiredVelocity.x(), minValue), maxValue));
    requiredVelocity.setY(std::min(std::max(requiredVelocity.y(), minValue), maxValue));
    requiredVelocity.setZ(std::min(std::max(requiredVelocity.z(), minValue), maxValue));

    MVector avgPosition(0.000, 0.000, 0.000);
    unsigned int count = 0;

    for (auto& pair : this->bulletCollisionHandler.activeRigidBodies) {
        std::string name = pair.first;
        btRigidBody* body = pair.second;
        body->setLinearVelocity(requiredVelocity);
    }

Limitations and possible improvements

This plugin is still in its early stages, with several key features yet to be implemented. Notably, it lacks support for pivot positions on selected meshes and does not yet offer rotate/scale capabilities, which are integral for a seamless user experience aligned with Maya's default hotkeys. Stability enhancements are on the horizon, as well as extending support for various mesh types, including compatibility with Universal Scene Description (USD).

A critical next step is thorough testing across different scene sizes, coupled with detailed profiling, to ensure efficiency and prevent memory leaks. The addition of unit testing will further solidify the plugin's reliability.

Despite these challenges, the plugin has been an invaluable starting point. It signifies a meaningful advancement in my tool development journey and a rich opportunity to further my expertise in C++ coding. With an eye towards future updates, there's also a vast potential for optimizing the integration and performance of the OpenMaya and Bullet APIs. These improvements aim not just to enhance the tool's functionality but also to refine the C++ development workflow, making it as efficient and effective as possible.

The journey through C++ has been as rewarding as it is complex, and the excitement for what lies ahead is a driving force in the continued evolution of this project. The source code for this project as usual, is available on my GitHub repository.
15 Jan 2024
Switch Theme