Sample Code

I have written a great deal of code in my time. Although I do not have a computer science degree, I consider myself a programmer and software engineer. I gleaned my knowledge from course work, books, colleagues and a lot of experience. I have been programming for over 15 years and I've gotten pretty damn good at it. Below are just a few examples that demonstrate my capabilities as a CG tool and pipeline developer. All work on this page was created for personal projects and may be freely downloaded, copied and redistributed.

Weight Transfer Tool

WeightTransferTool.zip

A typical production pipeline needs to be able to make model changes to an asset after texturing and rigging has already begun on a previous version. This means that any vertex attributes that were painted by an artist have to be repainted or else programmaticaly transferred.

Most attribute transfer tools work by writing an image represention of the values using the texture coordinates of the source mesh. Then they apply those values using the texture coordinates of the destination model. This method is satisfactory in many cases; however, some model changes do not lend themselves to this process. For example, updates that involve topololgy changes, texture coordinate changes, or feature additions such as extrusion will not usually support transfer using this method.

To resolve these issues this transfer tool works by iterating through the destination model's vertices and sampling the weights at the closest position on the source model surface. This approach provides uses beyond just asset updates. For example, attributes could be painted onto a base model then transferred onto a family of similarly designed assets such as agents in a crowd simulation.

Weight Transfer Plugin - This file defines the WeightsSource, WeightsDestination and WeightTransfer classes and Maya plug-in initialization.

(show)
Download
// The main weight transfer tool plug-in code...

#include <weightTransfer.h>

namespace WeightTransferTool
{
    // Checks for and returns the next valid shape
    // node dag path in the selection list.
    MDagPath get_shape_node(MItSelectionList& iter)
    {
        MDagPath dag_path;
        MObject component;
        MObject node;
        unsigned int num_shapes;

        if(iter.isDone())
        {
            display_error("Not enough objects selected.");
            // return an empty dag  path
            return MDagPath();
        }
        // typically the transform will be selected not the shape
        // node so extend the DAG path to the first shape node.
        iter.getDagPath(dag_path, component);
        dag_path.numberOfShapesDirectlyBelow(num_shapes);
        if(num_shapes > 0)
            dag_path.extendToShapeDirectlyBelow(0);
        // get shape node from dag path
        node = dag_path.node();
        if(!node.hasFn(MFn::kMesh))
        {
            display_error(MString("Node is not a mesh object: ") + dag_path.fullPathName());
            return MDagPath();
        }
        return dag_path;
    }

    // WeightTransfer creator function required by Maya plug-in.
    void* WeightTransfer::creator()
    {
        return new WeightTransfer();
    }

    // Main entry function to execute weight transfer command.
    MStatus WeightTransfer::doIt( const MArgList& args )
    {
        MStatus stat;
        if(args.length() < 2)
        {
            display_error("The weightTransfer command requires two arguments, a source and destination attribute.");
            return MS::kFailure;
        }

        MString source_attr_name;
        MString dest_attr_name;
        args.get(0, source_attr_name);
        args.get(1, dest_attr_name);

        MSelectionList selected;
        stat = MGlobal::getActiveSelectionList(selected);
        MCHECK_ERROR(stat);
        MItSelectionList iter( selected );

        // first selection is the source mesh
        MDagPath source_dag = get_shape_node(iter);
        if(!source_dag.isValid())
            return MS::kFailure;
        WeightsSource source(source_dag, source_attr_name);
        if(!source.is_valid)
            return MS::kFailure;

        // second selection is the destination mesh
        iter.next();
        MDagPath dest_dag = get_shape_node(iter);
        if(!dest_dag.isValid())
            return MS::kFailure;

        WeightsDestination dest(dest_dag, dest_attr_name);
        if(!dest.is_valid)
            return MS::kFailure;

        stat = dest.transfer_weights(source);
        if(stat)
            display_msg("Weights transferred succesfully!");
        return stat;
    }

    // WeightsSource class constructor.
    WeightsSource::WeightsSource(MDagPath& mesh_dag, MString weight_attr_name)
    {
        MStatus mesh_status = set_mesh(mesh_dag);
        MStatus attr_status = set_weight_attribute(weight_attr_name);

        retrieve_weights();

        // validate source mesh
        char buffer[MAX_STRING_SIZE];
        MString out_msg;
        if(vertex_count == 0)
        {
            display_error("The source mesh has zero vertices!");
            return;
        }
        if(vertex_count != weight_count)
        {
            sprintf_s(buffer, MAX_STRING_SIZE, "The source mesh's vertex count %d does not match the weight count %d.",
                          vertex_count, weight_count);
            display_error(buffer);
            return;
        }
        if(!mesh_status)
        {
            display_error("The source mesh was invalid.");
            return;
        }
        if(!attr_status)
        {
            display_error("The specified weight attribute is invalid.");
            return;
        }
        is_valid = true;

        MStatus stat;
        MObject mesh_obj = mesh_dag.node();

        // Construct Maya's mesh intersector which finds the closest
        // point on source mesh to a sample position.
        xform_matrix = mesh_dag.inclusiveMatrix();
        stat = intersector.create(mesh_obj, xform_matrix);
        MCHECK_ERROR(stat);

        unsigned poly_count = fn_mesh.numPolygons();

        weighted_polys = new WeightedPolygon[poly_count];
        weighted_verts = new WeightedVertex[vertex_count];

        MItMeshVertex vtx_iter(mesh_dag, MObject::kNullObj, &stat);
        MCHECK_ERROR(stat);

        MPoint position;
        double* weights;
        int index = 0;
        MIntArray connected_polys;
        MIntArray connected_edges;

        for( ; !vtx_iter.isDone(); vtx_iter.next())
        {
            position = vtx_iter.position(MSpace::kWorld, &stat);
            MCHECK_ERROR(stat);
            weights = get_weight(index);

            WeightedVertex& cur_vert = weighted_verts[index];
            cur_vert.set_vertex(position, weights);
            index++;
        }

        // initialize triangulated mesh data
        MIntArray tri_counts;
        MIntArray tri_verts;
        fn_mesh.getTriangles(tri_counts, tri_verts);

        unsigned start_index = 0;
        for(unsigned i=0; i < poly_count; i++)
        {
            weighted_polys[i].update_triangles(i, tri_counts[i], start_index,
                                                tri_verts, weighted_verts);
            start_index += tri_counts[i] * 3;
        }
    }

    // Samples the weight source mesh at an arbitray position in space.
    void WeightsSource::sample_mesh(const MPoint& sample_point, double* out_weights)
    {
        MPointOnMesh point_info;
        MPoint point(sample_point);
        intersector.getClosestPoint(point, point_info);
        int face_index = point_info.faceIndex();
        MPoint closest_pos = MPoint(point_info.getPoint());
        // the point returned by the mesh intersector is in the mesh's
        // local space but our data structures are in world space
        closest_pos *= xform_matrix;

        WeightedPolygon& poly = weighted_polys[face_index];
        WeightedVertex* matching_vert = poly.get_matching_vertex(closest_pos);
        if(matching_vert != NULL)
        {
            matching_vert->copy_weights(out_weights);
            return;
        }

        WeightedTriangle* tri = poly.get_intersected_triangle(closest_pos);
        tri->sample_weights(closest_pos, out_weights);
    }

    // WeightsDestination class constructor.
    WeightsDestination::WeightsDestination(MDagPath& mesh_dag, MString weight_attr_name)
    {
        MStatus mesh_stat = set_mesh(mesh_dag);
        MStatus weight_attr_stat = set_weight_attribute(weight_attr_name);

        if(mesh_stat && weight_attr_stat)
            is_valid = true;
    }

    // Transfers weights from the specified source to this mesh.
    MStatus WeightsDestination::transfer_weights(WeightsSource& source)
    {
        switch(weight_attr_type)
        {
            case MFnData::kDoubleArray:
                weight_double_vals.setLength(vertex_count);
                break;
            case MFnData::kVectorArray:
                weight_vector_vals.setLength(vertex_count);
                break;
            case MFnData::kPointArray:
                weight_point_vals.setLength(vertex_count);
                break;
            default:
                return MS::kFailure;
        }

        MStatus stat;
        MItMeshVertex vtx_iter(mesh_dag, MObject::kNullObj, &stat);
        double* weights = new double[4];
        unsigned index = 0;

        for(; !vtx_iter.isDone(&stat); vtx_iter.next())
        {
            MPoint p = vtx_iter.position(MSpace::kWorld, &stat);
            // Sample the source mesh for the current
            // point position and store the result.
            source.sample_mesh(p, weights);
            set_weight(index, weights);
            index++;
        }

        // assign weight values from array to weights attribute
        assign_weights();

        return MS::kSuccess;
    }
}

Weighted Mesh - This file defines the WeightedMesh, WeightedPolygon, WeightTriangle, and WeightedVertex classes. These are the main work horse classes.

(show)
Download
// weighted mesh and supporting classes...

#include <weightedMesh.h>

namespace WeightTransferTool
{
    // Tests two 2D points to see if the line segment they define crosses the positive X-axis.
    bool edge_crosses_x_axis(const Point2d& p0, const Point2d& p1)
    {
        if(p0.y == 0.0 && p1.y == 0.0)
            // The edge is on the X-axis, count this as an intersection
            // as long as the segment is partially positive.
            return p0.x > 0 || p1.x > 0;
        if(simple_sign(p0.y) == simple_sign(p1.y))
            // Both end points are on the same side of the X-axis,
            // so the edge cannot cross it.
            return false;
        if(simple_sign(p0.x) && simple_sign(p1.x))
            // Both end points are to the right of the Y-axis but opposite
            // sides of the X-axis. A positive intersection must occur.
            return true;
        if(!simple_sign(p0.x) && !simple_sign(p1.x))
            // Both end points are to the left of the Y-axis.
            // No intersection with the positive X-axis is possible.
            return false;

        // The edge crosses the X-axis.
        // Calculate the x-intercept.
        //
        double inv_slope = (p1.x - p0.x) / (p1.y - p0.y);
        double x_int = p0.x - inv_slope * p0.y;

        // check for positive value
        return simple_sign(x_int);
    }

    // Returns true if the number is greater than or equal to zero, false otherwise.
    bool simple_sign(double number)
    {
        return number >= 0;
    }

    // Appends a value to an integer array if it is not already present.
    bool append_if_unique(MIntArray& int_array, int new_value)
    {
        for(unsigned i = 0; i < int_array.length(); i++)
            if(int_array[i] == new_value)
                return false;
        int_array.append(new_value);
        return true;
    }

    // WeightedMesh class constructor.
    WeightedMesh::WeightedMesh()
    {
        attr_name = MString("");
        is_valid = false;
        vertex_count = 0;
        weight_count = 0;
    }

    // Sets this instance's source Maya mesh node.
    MStatus WeightedMesh::set_mesh(MDagPath& new_mesh_dag)
    {
        mesh_dag = new_mesh_dag;
        MStatus stat = fn_mesh.setObject(new_mesh_dag);
        MCHECK_ERROR(stat);
        vertex_count = fn_mesh.numVertices();
        return stat;
    }

    // Sets the mesh node attribute name to find weight values in.
    MStatus WeightedMesh::set_weight_attribute(MString weight_attr_name)
    {
        MStatus stat;
        weight_plug = fn_mesh.findPlug(weight_attr_name, true, &stat);

        if(!stat)
        {
            display_error(MString("Unable to find weight plug: ") + weight_attr_name);
            return MS::kFailure;
        }

        // get weight attribute information
        MObject weight_attr = weight_plug.attribute();
        if(!weight_attr.hasFn(MFn::kTypedAttribute))
        {
            display_error(MString("Invalid weights attribute: ") + weight_attr_name);
            return MS::kFailure;
        }
        MFnTypedAttribute fn_weight_attr(weight_attr);
        weight_attr_type = fn_weight_attr.attrType(&stat);
        MCHECK_ERROR(stat);

        switch(weight_attr_type)
        {
            // This class can read and write weights from a
            // double, vector or point array attribute type.
            case MFnData::kDoubleArray:
            case MFnData::kVectorArray:
            case MFnData::kPointArray:
                break;
            default:
                display_error(MString("The weight attribute type is not supported.  ") +
                                MString("The attribute must be doubleArray, pointArray or vectorArray."));
                return MS::kFailure;
        }
        return MS::kSuccess;
    }

    // Retrieves weight values for the specified vertex index.
    double* WeightedMesh::get_weight(unsigned index)
    {
        double* weight_val = new double[4];

        double dvalue;
        MVector vvalue;
        MPoint pvalue;
        switch(weight_attr_type)
        {
            case MFnData::kDoubleArray:
                // handle double weights
                dvalue = weight_double_vals[index];
                weight_val[0] = dvalue;
                weight_val[1] = dvalue;
                weight_val[2] = dvalue;
                weight_val[3] = dvalue;
                break;
            case MFnData::kVectorArray:
                // handle vector 3 weights
                vvalue = weight_vector_vals[index];
                weight_val[0] = vvalue.x;
                weight_val[1] = vvalue.y;
                weight_val[2] = vvalue.z;
                weight_val[3] = 0.0;
                break;
            case MFnData::kPointArray:
                // handle vector 4 weights
                pvalue = weight_point_vals[index];
                weight_val[0] = pvalue.x;
                weight_val[1] = pvalue.y;
                weight_val[2] = pvalue.z;
                weight_val[3] = pvalue.w;
                break;
            default:
                break;
        }
        return weight_val;
    }

    // Sets weight values for the specified vertex index.
    void WeightedMesh::set_weight(unsigned index, const double* weights)
    {
        switch(weight_attr_type)
        {
            case MFnData::kDoubleArray:
                weight_double_vals.set(weights[0], index);
                break;
            case MFnData::kVectorArray:
                weight_vector_vals.set(MVector(weights[0], weights[1],
                                                  weights[2]), index);
                break;
            case MFnData::kPointArray:
                weight_point_vals.set(MPoint(weights[0], weights[1],
                                              weights[2], weights[3]), index);
                break;
            default:
                break;
        }
    }

    // Retrieves and stores weight information from the current weight attribute.
    void WeightedMesh::retrieve_weights()
    {
        MObject plug_mobject = weight_plug.asMObject();
        weight_double_vals.clear();
        weight_vector_vals.clear();
        weight_point_vals.clear();

        switch(weight_attr_type)
        {
            case MFnData::kDoubleArray:
                // handle double weights
                plug_ddata.setObject(plug_mobject);
                weight_double_vals = plug_ddata.array();
                weight_count = weight_double_vals.length();
                break;
            case MFnData::kVectorArray:
                // handle vector 3 weights
                plug_vdata.setObject(plug_mobject);
                weight_vector_vals = plug_vdata.array();
                weight_count = weight_vector_vals.length();
                break;
            case MFnData::kPointArray:
                // handle vector 4 weights
                plug_pdata.setObject(plug_mobject);
                weight_point_vals = plug_pdata.array();
                weight_count = weight_point_vals.length();
                break;
            default:
                break;
        }
    }

    // Assigns values stored in the internal weight arrays to the mesh's weight attribute.
    void WeightedMesh::assign_weights()
    {
        MStatus stat;
        MObject weights_mobject;

        switch(weight_attr_type)
        {
            case MFnData::kDoubleArray:
                // handle double weights
                weights_mobject = plug_ddata.create(weight_double_vals, &stat);
                break;
            case MFnData::kVectorArray:
                // handle vector 3 weights
                weights_mobject = plug_vdata.create(weight_vector_vals, &stat);
                break;
            case MFnData::kPointArray:
                // handle vector 4 weights
                weights_mobject = plug_pdata.create(weight_point_vals, &stat);
                break;
            default:
                break;
        }
        weight_plug.setMObject(weights_mobject);
    }

    // WeightedPolygon class constructor.
    WeightedPolygon::WeightedPolygon()
    {
        triangle_count = 0;
        face_index = 0;
        tris = NULL;
    }

    // WeightedPolygon class deconstructor.
    WeightedPolygon::~WeightedPolygon()
    {
        tris = NULL;
        verts = NULL;
    }

    // Updates the list of triangles that compose this weighted polygon.
    void WeightedPolygon::update_triangles(unsigned my_face_index,
                                              unsigned new_triangle_count,
                                              unsigned start_index,
                                              const MIntArray& tri_vert_indexes,
                                              WeightedVertex* all_verts)
    {
        face_index = my_face_index;
        triangle_count = new_triangle_count;
        tris = new WeightedTriangle[new_triangle_count];
        WeightedVertex *v0, *v1, *v2;
        MIntArray uniqe_verts_indexes;
        // triangle vertex indexes
        unsigned i0, i1, i2;

        for(unsigned i=0; i < new_triangle_count; i++)
        {
            i0 = tri_vert_indexes[start_index];
            i1 = tri_vert_indexes[start_index+1];
            i2 = tri_vert_indexes[start_index+2];
            // keep an array of the unique indexes which make up this polygon
            append_if_unique(uniqe_verts_indexes, i0);
            append_if_unique(uniqe_verts_indexes, i1);
            append_if_unique(uniqe_verts_indexes, i2);
            // get vertices from array of all mesh verts
            v0 = &all_verts[i0];
            v1 = &all_verts[i1];
            v2 = &all_verts[i2];
            // update triangle data
            tris[i].set_vertices(v0, v1, v2);
            start_index += 3;
        }

        vertex_count = uniqe_verts_indexes.length();
        verts = new WeightedVertex*[vertex_count];
        for(unsigned i=0; i < vertex_count; i++)
            verts[i] = &all_verts[uniqe_verts_indexes[i]];
    }

    // Tests this polygon's vertices to see if any have an equal position to the sample point.
    WeightedVertex* WeightedPolygon::get_matching_vertex(const MPoint& sample_point)
    {
        for(unsigned i = 0; i < vertex_count; i++)
            if(verts[i]->equals_position(sample_point))
                return verts[i];
        return (WeightedVertex*)NULL;
    }

    // Find this polygon's triangle that contains the sample point.
    WeightedTriangle* WeightedPolygon::get_intersected_triangle(const MPoint& sample_point)
    {
        // Perfrom a simple test to detemine what triangle
        // contains the sample point.
        for(unsigned i = 0; i < triangle_count; i++)
            if(tris[i].point_is_inside(sample_point))
                return &tris[i];

        // If not triangle could be found do a more sophisticated
        // barycentric coordinate test to determine the triangle
        // which contains the point.
        for(unsigned i = 0; i < triangle_count; i++)
            if(tris[i].point_is_inside_bary(sample_point))
                return &tris[i];

        // This should never happen.
        display_error("No intersected triangle found!");
        // Return first triangle by default.
        return &tris[0];
    }

    // WeightedTriangle class constructor.
    WeightedTriangle::WeightedTriangle()
    {
        v0 = NULL;
        v1 = NULL;
        v2 = NULL;
        area_times_2 = 1;
    }

    // WeightedTriangle class deconstructor.
    WeightedTriangle::~WeightedTriangle()
    {
        v0 = NULL;
        v1 = NULL;
        v2 = NULL;
    }

    // Set the three vertices that make up this triangle and
    // store relevant triangle information.
    void WeightedTriangle::set_vertices(WeightedVertex* new_v0,
                                        WeightedVertex* new_v1,
                                        WeightedVertex* new_v2)
    {
        // store vertices as class attributes
        v0 = new_v0;
        v1 = new_v1;
        v2 = new_v2;

        MPoint p0 = v0->position;
        MPoint p1 = v1->position;
        MPoint p2 = v2->position;

        // calculate triangle centroid
        //
        centroid = MPoint(0.0, 0.0, 0.0);
        centroid += MVector(p0);
        centroid += MVector(p1);
        centroid += MVector(p2);
        centroid = centroid / 3.0;

        // calculate triangle normal and area
        //
        MVector e0 = p1 - p0;
        MVector e1 = p2 - p0;
        normal = e0 ^ e1;
        area_times_2 = normal.length();
        normal = normal / area_times_2;

        // The major axis is the largest absolute component of the triangle's
        // normal.  The major axis is the axis along which points on this triangle
        // will be project into 2D. This ensures we get the least distorted
        // 2D approximation and never project to a line.
        MVector abs_normal = MVector(abs(normal.x),
                                      abs(normal.y),
                                      abs(normal.z));
        if(abs_normal.x > abs_normal.y)
        {
            if(abs_normal.x > abs_normal.z)
                major_axis = X_AXIS;
            else if(abs_normal.y > abs_normal.z)
                major_axis = Y_AXIS;
            else
                major_axis = Z_AXIS;
        }
        else
        {
            if(abs_normal.y > abs_normal.z)
                major_axis = Y_AXIS;
            else
                major_axis = Z_AXIS;
        }

        // Define simplified triangle projected into 2D.
        v0_2d = project_to_2d(p0);
        v1_2d = project_to_2d(p1);
        v2_2d = project_to_2d(p2);
    }

    // Calculates and returns the weights of this triangle at the specified sample position.
    void WeightedTriangle::sample_weights(const MPoint& sample_point, double* out_weights) const
    {
        MVector bary_coords = get_bary_coords(sample_point, true);
        double w0, w1, w2;
        for(unsigned i = 0; i < 4; i++)
        {
            w0 = v0->weights[i] * bary_coords.x;
            w1 = v1->weights[i] * bary_coords.y;
            w2 = v2->weights[i] * bary_coords.z;
            out_weights[i] = w0 + w1 + w2;
        }
    }

    // Performs a fast test of the sample point to see if it is inside this triangle.
    bool WeightedTriangle::point_is_inside(const MPoint& sample_point) const
    {
        if(!point_is_on_plane(sample_point))
            return false;

        Point2d sample_2d = project_to_2d(sample_point);
        // adjust the 2D triangle so that the sample point is the origin.
        Point2d adj_v0 = {v0_2d.x - sample_2d.x, v0_2d.y - sample_2d.y};
        Point2d adj_v1 = {v1_2d.x - sample_2d.x, v1_2d.y - sample_2d.y};
        Point2d adj_v2 = {v2_2d.x - sample_2d.x, v2_2d.y - sample_2d.y};

        // Test to see how many adjusted triangle edges intersect the positive X-axis.
        unsigned intersections = 0;
        if(edge_crosses_x_axis(adj_v0, adj_v1))
            intersections++;
        if(edge_crosses_x_axis(adj_v1, adj_v2))
            intersections++;
        if(edge_crosses_x_axis(adj_v0, adj_v2))
            intersections++;

        // An odd number of intersection indicates the sample point is inside the triangle.
        return (intersections % 2 == 1);
    }

    // Tests the sample point to see if it lies in the plane of the triangle.
    bool WeightedTriangle::point_is_on_plane(const MPoint& sample_point) const
    {
        // direction from point on triangle to the sample position
        MVector sample_direction = sample_point - centroid;
        sample_direction.normalize();

        // dot product of sample direction and normal direction
        double cos_theta = sample_direction * normal;
        // A result close to zero indicates the sample
        // direction is orthogonal to the triangle noraml.
        return abs(cos_theta) < EPSILON;
    }

    // Tests the sample point to see if it is inside this triangle using barycentric coordinates.
    bool WeightedTriangle::point_is_inside_bary(const MPoint& sample_point) const
    {
        MVector bary_coords = get_bary_coords(sample_point, false);
        double total_area = bary_coords.x + bary_coords.y + bary_coords.z;
        return abs(1.0 - total_area) < EPSILON;
    }

    // Calculates the barycentric coordinates of the sample point in this triangle.
    MVector WeightedTriangle::get_bary_coords(const MVector& sample_point,
                                                            bool normalized) const
    {
        MVector out_bary_coords = MVector::xAxis;
        MVector cross;

        // Calculate areas of triangle fragmenets created
        // by sample point inside of large triangle.
        MVector e0 = v0->position - sample_point;
        MVector e1 = v1->position - sample_point;
        MVector e2 = v2->position - sample_point;

        // Each bary coordinate is defined as the fraction of the
        // larger area occupied by each triangle fragment.
        cross = e2 ^ e1;
        out_bary_coords.x = cross.length() / area_times_2;
        cross = e0 ^ e2;
        out_bary_coords.y = cross.length() / area_times_2;

        if(normalized)
        {
            // Calculating the final coordinate this ways is faster
            // and guarantees normalized coordinates that sum to 1.
            out_bary_coords.z = 1 - (out_bary_coords.y + out_bary_coords.x);
        }
        else
        {
            // Calculate the true coordinates which may sum to more than 1/
            cross = e0 ^ e1;
            out_bary_coords.z = cross.length() / area_times_2;
        }
        return out_bary_coords;
    }

    // Projects a 3D point into 2D by removing a vector component.
    Point2d WeightedTriangle::project_to_2d(const MPoint& position) const
    {
        Point2d pos_2d;
        switch(major_axis)
        {
            case X_AXIS:
                pos_2d.x = position.y;
                pos_2d.y = position.z;
                break;
            case Y_AXIS:
                pos_2d.x = position.x;
                pos_2d.y = position.z;
                break;
            case Z_AXIS:
                pos_2d.x = position.x;
                pos_2d.y = position.y;
                break;
        }
        return pos_2d;
    }

    // Tests the sample point to see if it equals vertex position.
    bool WeightedVertex::equals_position(const MPoint& sample_point)
    {
        MVector delta = sample_point - position;

        // Allow for a small error tolerance.
        return (abs(delta.x) < EPSILON &&
                abs(delta.y) < EPSILON &&
                abs(delta.z) < EPSILON);
    }

    // Gets a copy of this vertex's weights.
    void WeightedVertex::copy_weights(double* out_weights)
    {
        memcpy(out_weights, weights, sizeof(double) * 4);
    }

    // Sets this vertex's position and weights.
    void WeightedVertex::set_vertex(MPoint& new_position, double* new_weights)
    {
        position = new_position;
        weights = new_weights;
    }
} // end namespace WeightTransferTool

Houdini Node Library

HouNodeLib.zip

A problem arose when I started developing tools for Houdini in Python. While the hou.Node class provided by Side Effect Software is robust and powerful, I found it beneficial to extend this class with my own custom classes to create standardized nodes that could be integrated into a studio pipeline. However, this was not possible because the hou.Node class cannot be directly instantiated because it is defined using a SWIG C++ plug-in.

My solution was to create a new class called HouNode that provides access to all hou.Node attributes vicariously through an override of the "__getattr__" method. The new class also provides access to node parameters as instance attributes allowing for more concise, pythonic code in its usage. The new custom HouNode and HouParm classes can now be freely sub-classed. In addition, the new classes can incoporate various utility methods as needed. I have also developed a similar set of classes for handling nodes in Maya.

HouNode - A class that psuedo-extends the hou.Node class in Houdini providing additional functionality.

(show)
Download
# The MetaHouNode and HouNode class..

import os
import sys
import hou

from hou_parm import MetaHouParm

def get_hou_node(node, *args, **kwargs):
    """
    Gets a HouNode instance for the specified node.
    The node parameter can be any of: str, hou.Node, HouNode
    """
    node_cls = MetaHouNode.get_node_cls(node)
    if node_cls == None:
        return None
    return node_cls(node, *args, **kwargs)

class MetaHouNode(type):
    """
    A meta class to track all HouNode subclasses.
    """
    NODE_TYPE_TO_CLASS = dict()
    DEFAULT_CLASS = None

    def __new__(cls, name, bases, class_dict):
        """
        Called when a new Base class is created using this metaclass.
        """
        # create the new class
        new_cls = type.__new__(cls, name, bases, class_dict)
        if cls.DEFAULT_CLASS == None:
            # if this is the first class declared (which will be HouNode)
            # store it as the default class
            cls.DEFAULT_CLASS = new_cls
        # associate the defined list of node types with the new class definition
        for node_type in new_cls.SUPPORTED_TYPES:
            cls.NODE_TYPE_TO_CLASS[node_type] = new_cls
        return new_cls

    @classmethod
    def get_sesi_node(cls, node):
        """
        Retrieves a Houdini node object from various inputs.
        """
        if isinstance(node, hou.Node):
            # object is already a SESI node
            return node
        if isisntance(node, str):
            # object is string (assume node path)
            return hou.node(node)
        if isinstance(node, HouNode):
            # object is another HouNode instance
            return node.get_sesi_node()
        return None

    @classmethod
    def get_node_cls(cls, node):
        """
        Retrieves a node class to use for the specified node.
        """
        node = cls.get_sesi_node(node)
        if node == None:
            return None
        node_type = node.type().name()
        return cls.NODE_TYPE_TO_CLASS.get(node_type, cls.DEFAULT_CLASS)

class HouNode(object):
    """
    The HouNode class represent a network node in Houdini.
    It extends the behavior provided by the SESI hou.Node class
    and provides various convience methods that can be used in
    scripts, tools and subclasses.
    """
    __metaclass__ = MetaHouNode

    # a list of all instances of HouNode or a sub-class
    _HOU_NODE_INSTANCES = []

    # a virtual list of node type names (e.g. ifd, geo, point, etc) that a sub-class should be instantiated to represent
    # In other words if a node is passed to the "get_node" function above whose type is in a class's "SUPPORTED_TYPES" list,
    # the returned object will be an instance of the corresponding class.
    SUPPORTED_TYPES = []

    def __new__(cls, node, *args, **kwargs):
        """
        Constructor for a new HouNode object. If a HouNode
        instance was previously constructed for the node passed
        to the constructor. The same instance is returned.
        Otherwise a new instance is constructed and returned.
        """
        if args:
            node = cls.get_sesi_node(args.pop())
        elif 'node' in kwargs:
            node = cls.get_sesi_node(kwargs.pop('node'))
        else:
            node = hou.pwd()
        if not node:
            raise Exception('No Houdini node could be identified'
                            ' in the HouNode constructor.')
        new_instance = kwargs.pop('new_instance', False)
        if not new_instance:
            # if an instance has already been created for
            # the current node return that cached instance
            for instance in cls._HOU_NODE_INSTANCES:
                if instance._sesi_node == node:
                    return instance
        new_inst = object.__new__(cls)
        # supported in Houdini 11.0 and later
        #node.addEventCallback(hou.nodeEventType.BeingDeleted,
        # new_inst.on_node_deleted)
        cls._HOU_NODE_INSTANCES.append(new_inst)
        return new_inst

    def __init__(self, node, *args, **kwargs):
        """
        HouNode initializer.
        """
        self._sesi_node = MetaHouNode.get_sesi_node(node)

        self.magic_set_parms = kwargs.pop('magic_set_parms', True)
        self.magic_get_parms = kwargs.pop('magic_get_parms', True)
        self._py_attrs_persist = kwargs.pop('py_attrs_persist', False)
        self._node_parms = dict()
        self._node_method_names = []

        if self._py_attrs_persist:
            # restore previously saved python attribute values
            self.restore_python_attriubtes()
        # update parameter dictionary from node
        parm_names = kwargs.pop('parm_names', None)
        self.update_node_parms(parm_names)
        # update the list of method names from SESI node class
        method_names = kwargs.pop('method_names', None)
        self.update_node_methods(method_names)

    @classmethod
    def on_scene_saved(cls):
        """
        Callback just before the scene is saved. This method saves persistant
        python attributes onto the node objects which are flagged to do so.
        """
        cls._remove_deleted_nodes()
        for instance in cls._HOU_NODE_INSTANCES:
            if instance._py_attrs_persist:
                # save pickled python attributes to the Node's user data
                instance.save_python_attrs()

    @classmethod
    def on_scene_load(cls, sesi_nodes=None):
        """
        Callback just after a scene is loaded or the session is cleared.
        This method clears the list of HouNode instance references.
        """
        cls.clear_node_instances()

    @classmethod
    def _remove_deleted_nodes(cls):
        """
        Remove node entries from the instance list which have been deleted.
        """
        cls._HOU_NODE_INSTANCES[:] = (node for node in cls._HOU_NODE_INSTANCES
                                      if node.node_was_deleted())

    @classmethod
    def clear_node_instances(cls):
        """
        Clears the list of HouNode instance references.
        """
        if cls._HOU_NODE_INSTANCES:
            cls._HOU_NODE_INSTANCES = []

    def update_node_parms(self, names=None):
        """
        Updates the dictionary mapping parameter names to NodeParm objects.
        """
        if names == None:
            # get the list of all parameter names from the SESI node.
            tuple_names = [tuple.name() for tuple in self._sesi_node.parmTuples()]
            parm_names = [parm.name() for parm in self._sesi_node.parms()]
            names = list(set(parm_names + tuple_names))
        self._node_parms = dict.fromkeys(names)

    def update_node_methods(self, method_names=None):
        """
        Updates the list of SESI node method names
        that can be called vicariously on this HouNode instance.
        """
        if method_names == None:
            method_names = dir(self._sesi_node)
        self._node_method_names = method_names

    def on_node_created(self):
        """
        Callback when a new node is created. This method should be
        overwritten to customize creation behavior and initialization.
        """
        pass

    def on_node_deleted(self):
        """
        Callback when this node is deleted. This method removes
        this instance from the HouNode instance list.
        """
        if hou.hipFile.isShuttingDown():
            # If the session or scene is being closed
            # clear the entire instance list
            self.clear_node_instances()
        else:
            while self in self._HOU_NODE_INSTANCES:
                self._HOU_NODE_INSTANCES.remove(self)

    def node_was_deleted(self):
        """
        Indicates if the Houdini node represented by
        this instance has been deleted.
        """
        if self._sesi_node == None:
            return True
        try:
            # call a method on the SESI node
            self._sesi_node.name()
        except hou.ObjectWasDeleted:
            # catch object deleted exception
            return True
        return False

    def __getattr__(self, key):
        """
        Overrides the attribute retrieval method for HouNode instances.
        This method is only called if a python attribute or method of the
        requested name was not found.
        """

        # check for a node parameter for the requested attribute name
        #

        # use the __dict__ object to retrieve attribute values
        magic_get_parms = self.__dict__.get('magic_get_parms', False)
        if magic_get_parms:
            node_parms = self.__dict__.get('_node_parms', None)
            if node_parms and key in node_parms:
                return self.get_node_parm(key)

        # check for a method on the SESI node for the requested attribute name
        #
        method_names = self.__dict__.get('_node_method_names', None)
        sesi_node = self.__dict__.get('_sesi_node', None)
        if sesi_node and method_names and key in method_names:
            return getattr(sesi_node, key)

        # no valid attribute could be found
        raise AttributeError('Unknown attribute name "%s"' % key)

    def __setattr__(self, key, value):
        """
        """
        # check for a parameter with the name of the attribute being set
        #
        magic_set_parms = self.__dict__.get('magic_set_parms', False)
        if magic_set_parms:
            node_parms = self.__dict__.get('_node_parms', None)
            if node_parms and key in node_parms:
                node_parm = self.get_node_parm(key)
                node_parm.set_value(value)
                return

        # by default set the python instance attribute
        self.__dict__[key] = value

    def get_node_parm(self, parm_name):
        """
        Gets the NodeParm instance representing the specified
        parameter name for this node.
        """
        if parm_name not in self._node_parms:
            return None
        node_parm = self._node_parms[parm_name]
        if node_parm == None:
            node_parm = MetaHouParm.get_node_parm(self, parm_name)
            self._node_parms[parm_name] = node_parm
        return node_parm

    def _get_python_attributes(self):
        """
        Gets the mapping of pickleable python attribute name and values.
        """
        python_attrs = dict()
        for key, value in vars(self).iteritems():
            if key.startswith('_'):
                continue
            if isinstance(value, hou.Node):
                continue
            python_attrs[key] = value
        return python_attrs

    def save_python_attrs(self):
        """
        Pickles this instance's attributes and
        saves them to the Houdini node's user data.
        """
        python_attrs = self._get_python_attributes()
        value = string_utils.obj_to_str(python_attrs)
        self._sesi_node.setUserData('hou_node_py_attrs', value)

    def restore_python_attrs(self):
        """
        Restores previously pickled instance attributes
        from the Houdini node's user data.
        """
        value = self._sesi_node.userData('hou_node_py_attrs')
        python_attrs = string_utils.str_to_obj(python_attrs)
        restore_magic_set_parm = self.magic_set_parms
        self.magic_set_parms = False
        for key, value in python_attrs.iteritems():
            setattr(self, key, value)
        self.magic_set_parms = restore_magic_set_parm

    def __repr__(self):
        return '<%s path "%s" at %d>' % (self.__class__.__name__,
                                        self.path(), id(self))

HouParm - A class that psuedo-extends the hou.Parm class in Houdini providing additional functionality.

(show)
Download
# The MetaHouParm and HouParm class..

import os
import re
import hou

class MetaHouParm(type):
    """
    The MetaHouParm class is a metaclass that keeps track of which
    classes support which parameter types.
    """
    PARM_TYPE_TO_CLASS = dict()

    def __new__(cls, class_name, bases, class_dict):
        """
        Called when a class is created that uses this metaclass
        """
        # create the new class
        new_cls = type.__new__(cls, class_name, bases, class_dict)

        supported_parm_types = class_dict.get('SUPPORTED_TYPES', [])
        for parm_type in supported_parm_types:
            cls.PARM_TYPE_TO_CLASS[parm_type] = new_cls
        return new_cls

    @classmethod
    def get_node_parm(mcls, hou_node, parm_name):
        """
        Instantiates and returns a HouParm object
        to represent the specified node paramter.
        """
        sesi_tuple = hou_node.parmTuple(parm_name)
        if sesi_tuple and len(sesi_tuple) > 1:
            parm_template = sesi_tuple.parmTemplate()
            return NodeParmTuple(hou_node, parm_name,
                                 parm_template, sesi_tuple)

        sesi_parm = hou_node.parm(parm_name)
        if not sesi_parm:
            return None
        parm_template = sesi_parm.parmTemplate()
        parm_type = parm_template.type()
        cls = mcls.PARM_TYPE_TO_CLASS.get(parm_type, HouParm)
        cls = cls.get_class_for_parm(parm_template)
        return cls(hou_node, parm_name, parm_template, sesi_parm)

class HouParm(object):
    """
    The base class to represent a Houdini node parameter.
    """
    __metaclass__ = MetaHouParm
    SUPPORTED_TYPES = []
    CAST_TYPE = None
    def __init__(self, hou_node, parm_name,
                 parm_template, sesi_parm=None):
        """
        Initializer for a Node parameter.
        """
        self._hou_node = hou_node
        self._parm_name = parm_name
        self._parm_template = parm_template
        self._sesi_parm = sesi_parm
        self._cast_type = None

        self._parm_method_names = []
        self.update_node_methods()

    @classmethod
    def get_class_for_parm(cls, parm_template):
        """
        Virtual method to control what HouParm class gets used
        for the specified parmater template. After the class is identied
        by the SUPPORTED_TYPES list.
        """
        return cls

    def update_node_methods(self, parm_method_names=None):
        """
        Updates the list of SESI parm method names
        that can be called vicariously on this HouParm instance.
        """
        if parm_method_names == None:
            parm_method_names = dir(self._sesi_parm)
        self._parm_method_names = parm_method_names

    def __getattr__(self, key):
        """
        Overrides the attribute retrieval method for HouParm instances.
        This method is only called if a python attribute or method of the
        requested name was not found.
        """

        # check for a method on the parm tuple for the requested attribute name
        #
        if not key.startswith('_'):
            method_names = self.__dict__.get('_parm_method_names', None)
            sesi_parm = self.__dict__.get('_sesi_parm', None)
            if sesi_parm and method_names and key in method_names:
                return getattr(sesi_parm, key)

        # no valid attribute could be found
        raise AttributeError('Unknown attribute "%s"' % key)

    def get_value(self):
        """
        Returns the value stored by this parameter object.
        """
        return self._sesi_parm.eval()

    def set_value(self, value):
        """
        Sets the value stored by this parameter object.
        """
        print value
        if self.CAST_TYPE:
            value = self.CAST_TYPE(value)
        self._sesi_parm.set(value)

    def __nonzero__(self):
        """
        Handles cast to boolean.
        """
        return bool(self.eval())

    def __float__(self):
        """
        Handles cast to float.
        """
        return self.evalAsFloat()

    def __int__(self):
        """
        Handles cast to integer.
        """
        return self.evalAsInt()

    def __str__(self):
        """
        Handles cast to string.
        """
        return self.evalAsString()

    def __unicode(self):
        """
        Handles cast to unicode.
        """
        return unicode(self.evalAsString())

    def __coerce__(self, other):
        """
        Handles cast to string.
        """
        if other == None:
            return (True, None)
        # coercion to primitive types
        #
        if isinstance(other, float):
            return (float(self), other)
        if isinstance(other, int):
            return (int(self), other)
        if isinstance(other, bool):
            return (bool(self), other)
        if isinstance(other, str):
            return (str(self), other)
        if isinstance(other, unicode):
            return (unicode(self), other)
        # coercion between parm objects
        #
        if isinstance(other, FloatNodeParm):
            return (float(self), float(other))
        if isinstance(other, IntNodeParm):
            return (int(self), int(other))
        if isinstance(other, ToggleNodeParm):
            return (bool(self), bool(other))
        if isinstance(other, MenuNodeParm):
            return (str(self), str(other))
        if isinstance(other, StringNodeParm):
            return (str(self), str(other))
        if isinstance(other, HouParm):
            return (id(self), id(other))
        # unknown type to coerce to
        return (self, False)

    def __repr__(self):
        return '<%s path "%s" value "%s" at %d>' % (self.__class__.__name__,
                                                    self._sesi_parm.path(),
                                                    str(self.get_value()), id(self))

class NodeParmTuple(object):
    """
    This class represents a multi-parm tuple
    on a HouNode (e.g. t,r,s on a geo node).
    """
    def __init__(self, hou_node, tuple_name,
                 parm_template, sesi_parm_tuple):
        """
        Initializer for a Node parameter tuple.
        """
        self._hou_node = hou_node
        self._tuple_name = tuple_name
        self._parm_template = parm_template
        self._sesi_parm_tuple = sesi_parm_tuple

        self._tuple_method_names = []
        self._sub_parms = [hou_node.get_node_parm(parm.name())
                           for parm in sesi_parm_tuple]
        self.update_parm_methods()

    def update_parm_methods(self, parm_method_names=None):
        """
        Updates the list of SESI parm tuple method names
        that can be called vicariously on a NodeParmTuple instance.
        """
        if parm_method_names == None:
            parm_method_names = dir(self._sesi_parm_tuple)
        self._parm_method_names = parm_method_names

    def get_value(self):
        return self._sesi_parm_tuple.eval()

    def set_value(self, *args):
        if len(args) == 1:
            args = args[0]
            if isinstance(args, NodeParmTuple):
                args = args.get_value()
        return self._sesi_parm_tuple.set(*args)

    def __len__(self):
        return len(self._sub_parms)

    def __getitem__(self, index):
        return self._sub_parms[index]

    def __setitem__(self, index, value):
        return self._sub_parms[index].set_value(value)

    def __iter__(self, index):
        return iter(self._sub_parms)

    def __repr__(self):
        path = '%s/%s' % (self._hou_node.path(), self._tuple_name)
        return '<%s path "%s" value "%s" at %d>' % (self.__class__.__name__,
                                                    path, str(self.get_value()),
                                                    id(self))

class NumericNodeParm(HouParm):

    # override common math operators
    def __add__(self, other):
        return self.get_value() + self.CAST_TYPE(other)
    def __sub__(self, other):
        return self.get_value() - self.CAST_TYPE(other)
    def __mul__(self, other):
        return self.get_value() * self.CAST_TYPE(other)
    def __floordiv__(self, other):
        return self.get_value() // self.CAST_TYPE(other)
    def __mod__(self, other):
        return self.get_value() % self.CAST_TYPE(other)
    def __pow__(self, other):
        return self.get_value() ** self.CAST_TYPE(other)
    def __lshift__(self, other):
        return self.get_value() << other
    def __rshift__(self, other):
        return self.get_value() >> other
    def __and__(self, other):
        return self.get_value() & self.CAST_TYPE(other)
    def __xor__(self, other):
        return self.get_value() ^ self.CAST_TYPE(other)
    def __or__(self, other):
        return self.get_value() | self.CAST_TYPE(other)
    def __div__(self, other):
        return self.get_value() / self.CAST_TYPE(other)
    def __truediv__(self, other):
        return self.get_value() / self.CAST_TYPE(other)

    # right side operations
    def __radd__(self, other):
        return self.CAST_TYPE(other) + self.get_value()
    def __rsub__(self, other):
        return self.CAST_TYPE(other) - self.get_value()
    def __rmul__(self, other):
        return self.CAST_TYPE(other) * self.get_value()
    def __rdiv__(self, other):
        return self.CAST_TYPE(other) / self.get_value()
    def __rtruediv__(self, other):
        return self.CAST_TYPE(other) / self.get_value()
    def __rfloordiv__(self, other):
        return self.CAST_TYPE(other) // self.get_value()
    def __rmod__(self, other):
        return self.CAST_TYPE(other) % self.get_value()
    def __rpow__(self, other):
        return self.CAST_TYPE(other) ** self.get_value()
    def __rlshift__(self, other):
        return other << self.get_value()
    def __rrshift__(self, other):
        return other >> self.get_value()
    def __rand__(self, other):
        return self.CAST_TYPE(other) & self.get_value()
    def __rxor__(self, other):
        return self.CAST_TYPE(other) ^ self.get_value()
    def __ror__(self, other):
        return self.CAST_TYPE(other) | self.get_value()

    # inplace operations
    def __iadd__(self, other):
        return self.get_value() + self.CAST_TYPE(other)
    def __isub__(self, other):
        return self.get_value() - self.CAST_TYPE(other)
    def __imul__(self, other):
        return self.get_value() * self.CAST_TYPE(other)
    def __idiv__(self, other):
        return self.get_value() / self.CAST_TYPE(other)
    def __itruediv__(self, other):
        return self.get_value() / self.CAST_TYPE(other)
    def __ifloordiv__(self, other):
        return self.get_value() // self.CAST_TYPE(other)
    def __imod__(self, other):
        return self.get_value() % self.CAST_TYPE(other)
    def __ipow__(self, other):
        return self.get_value() ** self.CAST_TYPE(other)
    def __ilshift__(self, other):
        return self.get_value() << other
    def __irshift__(self, other):
        return self.get_value() >> other
    def __iand__(self, other):
        return self.get_value() & self.CAST_TYPE(other)
    def __ixor__(self, other):
        return self.get_value() ^ self.CAST_TYPE(other)
    def __ior__(self, other):
        return self.get_value() | self.CAST_TYPE(other)
    def __neg__(self):
        return -self.get_value()
    def __pos__(self):
        return +self.get_value()
    def __abs__(self):
        return abs(self.get_value())
    def __invert__(self):
        return ~self.get_value()

class IntNodeParm(NumericNodeParm):
    SUPPORTED_TYPES = [hou.parmTemplateType.Int]
    CAST_TYPE = int

class ToggleNodeParm(NumericNodeParm):
    SUPPORTED_TYPES = [hou.parmTemplateType.Toggle]
    CAST_TYPE = bool

class FloatNodeParm(NumericNodeParm):
    SUPPORTED_TYPES = [hou.parmTemplateType.Float]
    CAST_TYPE = float

class MenuNodeParm(HouParm):
    SUPPORTED_TYPES = [hou.parmTemplateType.Menu]

class StringNodeParm(HouParm):
    SUPPORTED_TYPES = [hou.parmTemplateType.String]
    CAST_TYPE = str

    @classmethod
    def get_class_for_parm(cls, parm_template):
        """
        Get a HouParm class based on the parameters string type.
        """
        if not isinstance(parm_template, hou.StringParmTemplate):
            raise Exception('Unknown parmeter template type "%s"'
                            % type(parm_template).__name__)
        string_type = parm_template.stringType()
        if string_type == hou.stringParmType.Regular:
            return StringNodeParm
        elif string_type == hou.stringParmType.FileReference:
            return FileReferenceNodeParm
        elif string_type == hou.stringParmType.NodeReference:
            return NodeReferenceParm
        elif string_type == hou.stringParmType.NodeReferenceList:
            return NodeListReferenceParm
        return cls

    def expand(self, ignore_frame=False, ignore_names=None):
        """
        Expands expression globals in this string parameter
        optionally ignoring specific global variables.
        """
        self._ignore_frame = ignore_frame
        self._ignore_names = ignore_names
        path = self._sesi_parm.unexpandedString()
        return re.sub(r'\${?([a-zA-Z0-9_]+)}?', self._replace_var, path)

    def _replace_var(self, match_obj):
        """
        Replaces global variables in a path except frame place holders.
        """
        original_str = match_obj.group(0)
        var_name = match_obj.group(1)
        if self._ignore_frame and re.match('F[0-9]?$', var_name):
            return original_str
        if self._ignore_names and var_name in self._ignore_names:
            return original_str
        if var_name:
            value, err = hou.hscript('echo $%s' % var_name)
            value = str(value).rstrip('\n')
            if value and not err:
                # remove trailing new line
                return value
        return original_str

class FileReferenceNodeParm(StringNodeParm):
    """
    A parameter that references a file or file path.
    """
    def create_directory(self):
        """
        Creates any missing directories in this parameter file path.
        """
        path = self._sesi_parm.evalAsString()
        dir, file = os.path.split(path)
        if '.' in file:
            path = dir
        if not os.path.exists(path):
            os.makedirs(path)

    def expand_path(self):
        return self.expand(True)

class NodeReferenceParm(StringNodeParm):
    """
    A parameter that references another Houdini node.
    """
    def get_node(self):
        value = self._sesi_parm.evalAsString()
        return hou.node(value)

    def get_hou_node(self):
        from hou_node import get_hou_node
        value = self._sesi_parm.evalAsString()
        return get_hou_node(hou.node(value))

class NodeListReferenceParm(StringNodeParm):
    """
    A multiple node reference parameter.
    """
    def get_nodes(self):
        str_value = self._sesi_parm.evalAsString()
        values = str_value.split()
        nodes = []
        for value in values:
            if value.startswith('@'):
                node_bundle = hou.nodeBundle(value)
                if node_bundle != None:
                    nodes.extend(node_bundle.nodes())
            else:
                node = hou.node(value)
                if node:
                    nodes.append(node)
        return nodes

    def get_hou_nodes(self):
        from hou_node import get_hou_node
        return [get_hou_node(node) for node in self.get_nodes()]

Scene Hierarchy Class Structure

SceneGraph.zip

The three classes below are the basic building blocks I use in XNA to create a scene graph. The code provided here has been simplified and stripped of any pipeline specific content so that it can be easily reapplied.

SceneItem - SceneItem is a generic scene node that serves as the basic building block for creating a 3D scene graph. A scene item can be transformed by parenting it to a TransformNode.

(show)
Download
//SceneItem class...

public class SceneItem
{
       /*
        * CLASS PROPERTIES
        */
    /// <summary>
    /// visible state of the scene item
    /// </summary>
    protected bool visible;
    /// <summary>
    /// the scene item's parent (or null if none)
    /// </summary>
    protected SceneItem parent;
    /// <summary>
    /// the scene item's children
    /// </summary>
    protected List<SceneItem> children;

    /// <summary>
    /// the transfromation matrix that describes the
    /// translation, rotation and scale of this scene item
    /// </summary>
    private Matrix worldMatrix;
    /// <summary>
    /// indicates if the inverse matrix property is up to date
    /// </summary>
    private bool invNeedsUpdate;
    /// <summary>
    /// the inverse matrix of the world matrix
    /// </summary>
    private Matrix invWorld;

       /*
        * CONSTRUCTORS
        */
    /// <summary>
    /// main constructor for a scene item
    /// </summary>
    public SceneItem()
    {
        init();
    }

    private void init()
    {
        visible = true;
        parent = null;
        children = new List<SceneItem>();

        worldMatrix = Matrix.Identity;
        invWorld = Matrix.Identity;

        invNeedsUpdate = false;
    }
    
       /*
        * UNLOADING
        */
    /// <summary>
    /// unloads this scene item and all of its children
    /// </summary>
    /// <param name="disposeResources"></param>
    public virtual void Unload(bool disposeResources)
    {
        //remove from parent's hierarchy
        if (parent != null)
            parent.RemoveChild(this);

        if (children != null)
        {
            //unload all children...
            SceneItem[] childArray = children.ToArray();
            for (int i = 0; i < childArray.Length; i++)
                childArray[i].Unload(disposeResources);
            children.Clear();
        }
        children = null;
        parent = null;
        visible = false;
    }

       /*
        * ENCAPSULATORS
        */
    /// <summary>
    /// gets the current transformed position of this scene
    /// item in world coordinates
    /// </summary>
    public virtual Vector3 WorldPosition
    {
        get { return worldMatrix.Translation; }
    }

    /// <summary>
    /// gets /sets the world matrix
    /// transformation for this SceneItem
    /// </summary>
    public virtual Matrix WorldMatrix
    {
        get { return worldMatrix; }
        set
        {
            worldMatrix = value;
            invNeedsUpdate = true;

            //children inherit world matrix property
            foreach (SceneItem child in children)
                child.WorldMatrix = worldMatrix;
        }
    }

    /// <summary>
    /// the inverse of the world matrix
    /// </summary>
    public virtual Matrix WorldInverse
    {
        get
        {
            if (invNeedsUpdate)
            {
                invWorld = Matrix.Invert(worldMatrix);
                invNeedsUpdate = false;
            }
            return invWorld;
        }
    }

    /// <summary>
    /// indicates if this scene item is the child of a
    /// TransformNode object.
    /// </summary>
    public bool HasParentTransform
    {
        get { return (parent is TransformNode); }
    }

    /// <summary>
    /// gets the direct parent of this
    /// scene item as a TransformNode.
    /// Or null if there is not one.
    /// </summary>
    public virtual TransformNode ParentTransform
    {
        get { return (parent as TransformNode); }
    }

       /*
        * POSITION AND NORMAL TRANSFORMATIONS
        */
    /// <summary>
    /// transforms a position from the local space of this SceneItem node to the global space
    /// </summary>
    /// <param name="localPos">a position in local space</param>
    /// <returns>a position in global space</returns>
    public Vector3 LocalToGlobal(Vector3 localPos)
    {
        return Vector3.Transform(localPos, worldMatrix);
    }

    /// <summary>
    /// transforms a position from a global space to the local space of this SceneItem node
    /// </summary>
    /// <param name="globalPos">a position in global space</param>
    /// <returns>a position in local space</returns>
    public Vector3 GlobalToLocal(Vector3 globalPos)
    {
        return Vector3.Transform(globalPos, this.WorldInverse);
    }

    /// <summary>
    /// transforms a normal from the local space of this SceneItem node to the global space
    /// </summary>
    /// <param name="localNormal">a normal in local space</param>
    /// <returns>a normal in global space</returns>
    public Vector3 LocalToGlobalNormal(Vector3 localNormal)
    {
        return Vector3.TransformNormal(localNormal, worldMatrix);
    }

    /// <summary>
    /// transforms a normal from a global space to the local space of this SceneItem node
    /// </summary>
    /// <param name="globalPos">a normal in global space</param>
    /// <returns>a normal in local space</returns>
    public Vector3 GlobalToLocalNormal(Vector3 globalNormal)
    {
        return Vector3.TransformNormal(globalNormal, this.WorldInverse);
    }

    /// <summary>
    /// transforms a position from the local space of on SceneItem node to another
    /// </summary>
    /// <param name="localPos">the local position in the space of the SceneItem node "localSpace"</param>
    /// <param name="localSpace">the SceneItem node that has the "localPos"</param>
    /// <param name="targetSpace">the returned position will be in the local space of this SceneItem node</param>
    /// <returns>the position in the local space of "targetSpace"</returns>
    public static Vector3 LocalToLocal(Vector3 localPos,
			SceneItem localSpace, SceneItem targetSpace)
    {
        return targetSpace.GlobalToLocal(localSpace.LocalToGlobal(localPos));
    }

    /// <summary>
    /// transforms a position from the local space of one SceneItem node to another
    /// </summary>
    /// <param name="localPos">the local position in the space of the SceneItem node "localSpace"</param>
    /// <param name="currentSpace">the SceneItem node that has the "localPos"</param>
    /// <param name="targetSpace">the returned position will be in the local space of this SceneItem node</param>
    /// <returns>the position in the local space of "targetSpace"</returns>
    public static Vector3 LocalToLocalNormal(Vector3 localDirection,
				SceneItem currentSpace, SceneItem targetSpace)
    {
        return targetSpace.GlobalToLocalNormal(
		currentSpace.LocalToGlobalNormal(localDirection));
    }

    /// <summary>
    /// gets the matrix that will transform points
    /// from the current space to the target space
    /// </summary>
    /// <param name="currentSpace">the SceneItem node that is the current local space</param>
    /// <param name="targetSpace">the trnasformable node that is the target local space</param>
    /// <returns>a matrix that will transform points or directions from the current space to the target space</returns>
    public static Matrix GetTransform(SceneItem currentSpace, SceneItem targetSpace)
    {
        return (targetSpace.WorldMatrix * currentSpace.WorldInverse);
    }

    /// <summary>
    /// gets / sets the visiblity of
    /// this item and its children
    /// </summary>
    public bool Visible
    {
        get { return visible; }
        set { visible = value; }
    }


       /*
        * HIERARCHY CREATION / MANIPULATION
        */
    /// <summary>
    /// gets or sets the parent of this scene item
    /// </summary>
    public virtual SceneItem Parent
    {
        get { return parent; }
        set
        {
            if (value == null)
                parent = value;
            else
                value.AddChild(this);
        }
    }

    /// <summary>
    /// add achild to this scene item
    /// </summary>
    /// <param name="newChild">the child to add</param>
    public virtual void AddChild(SceneItem newChild)
    {
        if (newChild == this)
            throw new ArgumentException("Attempted to parent a scene item to itself.");
        if (newChild.descendant(this, 1, -1))
            throw new ArgumentException("Cannot make a scene item as one
								of it's own descendants");
        children.Add(newChild);

        //remove from old parent
        if (newChild.HasParent)
            newChild.parent.RemoveChild(newChild);

        //add to hierarchy
        newChild.parent = this;
        newChild.WorldMatrix = this.WorldMatrix;
    }

    /// <summary>
    /// remove all children from this scene item
    /// </summary>
    public virtual void RemoveAllChildren()
    {
        while (children.Count > 0)
        {
            //remove children from the top down
            RemoveChild(children[children.Count - 1]);
        }
        children.Clear();
    }

    /// <summary>
    /// remove the specified child from this scene item
    /// </summary>
    /// <param name="xChild">the child to be remove</param>
    public virtual void RemoveChild(SceneItem xChild)
    {
        if (children.Contains(xChild))
        {
            xChild.parent = null;
            xChild.WorldMatrix = Matrix.Identity;
            children.Remove(xChild);
        }
    }

    /// <summary>
    /// removes the child at the specified index from this scene item
    /// </summary>
    /// <param name="index">the index of the child to remove</param>
    public virtual void RemoveChildAt(int index)
    {
        if (index >= 0 && index < children.Count)
            this.RemoveChild(children[index]);
    }

    /// <summary>
    /// gets the child at the specified index
    /// </summary>
    /// <param name="index">the index in the children list</param>
    /// <returns>the child, or null if index is invalid</returns>
    public virtual SceneItem GetChildAt(int index)
    {
        if (index >= 0 && index < this.children.Count)
            return children[index];
        return null;
    }

    /// <summary>
    /// returns true if the specified scene item is
    /// an immediate child of this scene item
    /// </summary>
    public virtual bool Contains(SceneItem curChild)
    {
        return children.Contains(curChild);
    }

    /// <summary>
    /// returns true if the specified scene item is a descendant of this scene item
    /// </summary>
    /// <param name="startDepth">setting this value to 1 will cause the check to skip the first
    /// level of the hierarchy, 2 will skip the first two levels and so on</param>
    /// <param name="maxDepth">the maximum depth into the hierarchy past the start depth
    /// to check or -1 to check entire hirearchy</param>
    protected virtual bool descendant(SceneItem item, int startDepth, int maxDepth)
    {
        if (startDepth <= 0 && this == item)
            return true;

        startDepth--;
        if (maxDepth >= 0 && startDepth < -maxDepth)
            return false;

        for (int i = 0; i < children.Count; i++)
        {
            if (children[i].descendant(item, startDepth, maxDepth))
                return true;
        }
        return false;
    }

       /*
        * HIERARCHY ACCESORS
        */
    /// <summary>
    /// gets the list of children for this node
    /// </summary>
    public List<SceneItem> Children
    {
        get { return this.children; }
    }

    /// <summary>
    /// Gets the number of immediate
    /// children of this scene item
    /// </summary>
    public int Nuchildren
    {
        get { return children.Count; }
    }

    /// <summary>
    /// gets the total number of children,
    /// in the hierachy of this scene item
    /// </summary>
    public int NumTotalChildren
    {
        get
        {
            int totalChildren = 0;
            foreach(SceneItem child in children)
                totalChildren += child.NumTotalChildren;
            return totalChildren;
        }
    }

    /// <summary>
    /// gets true if this scene item has a parent node
    /// </summary>
    public bool HasParent
    {
        get { return (parent != null); }
    }

       /*
        * SIBLING SORTING
        */
    /// <summary>
    /// swap the children at the specified indeces
    /// </summary>
    /// <param name="index1">the index of the first child in the swap</param>
    /// <param name="index2">the index of the second child in the swap</param>
    public virtual void SwapChildrenAt(int index1, int index2)
    {
        if (index1 >= 0 && index1 < children.Count && index2 >= 0 &&
							index2 < children.Count)
        {
            SceneItem temp = children[index1];
            children[index1] = children[index2];
            children[index2] = temp;
        }
    }

    /// <summary>
    /// swap the render order of the two specified children
    /// </summary>
    /// <param name="item1">the first child in the swap</param>
    /// <param name="item2">the second child in the swap</param>
    public virtual void SwapChildren(SceneItem item1, SceneItem item2)
    {
        int index1 = children.IndexOf(item1);
        int index2 = children.IndexOf(item2);

        if (index1 >= 0 && index2 >= 0)
        {
            children[index1] = item2;
            children[index2] = item1;
        }
    }

    /// <summary>
    /// sets the specified child to be drawn last in
    /// the render order effectively bringing it to the front 
    /// </summary>
    public virtual void BringToFront(SceneItem curChild)
    {
        if (children.Contains(curChild))
        {
            int curIndex = children.IndexOf(curChild);
            if (curIndex < children.Count - 1)
            {
                children[curIndex] = children[children.Count - 1];
                children[children.Count - 1] = curChild;
            }
        }
    }

    /// <summary>
    /// sets the specified child to be drawn first in
    /// the render order effectively send it to the back
    /// </summary>
    public virtual void SendToBack(SceneItem curChild)
    {
        if (children.Contains(curChild))
        {
            int curIndex = children.IndexOf(curChild);
            if (curIndex > 0)
            {
                children[curIndex] = children[0];
                children[0] = curChild;
            }
        }
    }

    /// <summary>
    /// bring this item to the front
    /// of its parent's display order
    /// </summary>
    public virtual void BringToFront()
    {
        if (this.HasParent)
            parent.BringToFront(this);
    }

    /// <summary>
    /// send this item to the back of
    /// its parent's display order
    /// </summary>
    public virtual void SendToBack()
    {
        if (this.HasParent)
            parent.SendToBack(this);
    }

      /*
       * UPDATE AND RENDER
       */
    /// <summary>
    /// updates this node and all its children
    /// </summary>
    public virtual void Update(Matrix viewMatrix, Matrix projMatrix)
    {
        foreach (SceneItem child in children)
            child.Update(viewMatrix, projMatrix);
    }

    /// <summary>
    /// Renders this object and all its children if visibiltiy is on.
    /// </summary>
    public virtual void RenderChildren()
    {
        if (visible)
        {
            //render the object content itself
            Render();

            //render any children of this object
            foreach (SceneItem child in children)
                child.RenderChildren();
        }
    }

    /// <summary>
    /// Renders the content of just this scene item (not its children).
    /// This method is called by the RenderChild method if the object
    /// is visible.
    /// </summary>
    public virtual void Render()
    {
        //overwritten by any class with content to render
    }
}

TransformNode - The transform node uses the XNA matrix struct to create cumulative similarity transforms. This class provides a variety of encapsulators to manipulate translation, rotation and scale.

(show)
Download
//TransformNode class...

/// <summary>
/// Euler rotation axis order.
/// </summary>
public enum EulerRotateOrder
{
    XYZ,
    XZY,
    YXZ,
    YZX,
    ZXY,
    ZYX
}

public class TransformNode : SceneItem
{
    /// <summary>
    /// the ratio that converts radians to degrees
    /// </summary>
    public const float RADIANS_TO_DEGREES = 57.2957795f;
    /// <summary>
    /// the ratio that converts degrees to radians
    /// </summary>
    public const float DEGREES_TO_RADIANS = 0.0174532925f;

    /// <summary>
    /// the transformation matrix added in the
    /// hierarchy by this TransformNode
    /// </summary>
    protected Matrix mTransformation;
    /// <summary>
    /// the transformation added by the
    /// parent of this node
    /// </summary>
    protected Matrix mParentMatrix;

    //current translation
    protected EulerRotateOrder rotateOrder; //euler rotaion order
    protected Vector3 curRotation; //current rotation
    protected Vector3 curScale; //current scale

    //individual transformations
    protected Matrix translateMat; //translation matrix
    protected Matrix rotateMat; //rotation matrix
    protected Matrix scaleMat; //scale matrix

    /// <summary>
    /// constructor to initialze the transform node with
    /// the given name and rotation order
    /// </summary>
    /// <param name="newOrder">the euler rotation order</param>
    public TransformNode(EulerRotateOrder newOrder)
        : base()
    {
        rotateOrder = newOrder;

        init();
    }

    /// <summary>
    /// constructor to initialze the transform node with the
    /// given name and the specified transformation
    /// </summary>
    /// <param name="newTranslate">new translate offset</param>
    /// <param name="newRotate">new rotation (X,Y,Z indicate offset from 0 measured in radians)</param>
    /// <param name="newScale">new scale factor (no scale is (1,1,1)</param>
    /// <param name="newOrder">new euler rotation order</param>
    public TransformNode(Vector3 newTranslate, Vector3 newRotate,
        Vector3 newScale, EulerRotateOrder newOrder)
        : base()
    {
        rotateOrder = newOrder;
        curRotation = newRotate;
        curScale = newScale;

        translateMat = Matrix.CreateTranslation(newTranslate);
        scaleMat = Matrix.CreateScale(curScale);

        mTransformation = Matrix.Identity; //local transform

        updateRotation();
    }

    /// <summary>
    /// sets this transformation matrix to be the identity matrix
    /// </summary>
    private void init()
    {
        curRotation = Vector3.Zero;
        curScale = Vector3.One;

        //initialize to having no transformation
        mTransformation = Matrix.Identity;
        translateMat = Matrix.Identity;
        rotateMat = Matrix.Identity;
        scaleMat = Matrix.Identity;
    }

      /*
       * UPDATE TRANSFORMATIONS
       */
    /// <summary>
    /// update rotation using the Euler transforms
    /// </summary>
    protected virtual void updateRotation()
    {
        switch (rotateOrder)
        {
            case EulerRotateOrder.XYZ:
                rotateMat = Matrix.CreateRotationX(curRotation.X)
					* Matrix.CreateRotationY(curRotation.Y)
					* Matrix.CreateRotationZ(curRotation.Z);
                break;
            case EulerRotateOrder.XZY:
                rotateMat = Matrix.CreateRotationX(curRotation.X)
					* Matrix.CreateRotationZ(curRotation.Z)
					* Matrix.CreateRotationY(curRotation.Y);
                break;
            case EulerRotateOrder.YXZ:
                rotateMat = Matrix.CreateRotationY(curRotation.Y)
					* Matrix.CreateRotationX(curRotation.X)
					* Matrix.CreateRotationZ(curRotation.Z);
                break;
            case EulerRotateOrder.YZX:
                rotateMat = Matrix.CreateRotationY(curRotation.Y)
					* Matrix.CreateRotationZ(curRotation.Z)
					* Matrix.CreateRotationX(curRotation.X);
                break;
            case EulerRotateOrder.ZXY:
                rotateMat = Matrix.CreateRotationZ(curRotation.Z)
					* Matrix.CreateRotationX(curRotation.X)
					* Matrix.CreateRotationY(curRotation.Y);
                break;
            case EulerRotateOrder.ZYX:
                rotateMat = Matrix.CreateRotationZ(curRotation.Z)
					* Matrix.CreateRotationY(curRotation.Y)
					* Matrix.CreateRotationX(curRotation.X);
                break;
        }
        updateTranformation();
    }

    /// <summary>
    /// update the local transform matrix by combining the scale, rotation and translation matrices
    /// </summary>
    protected void updateTranformation()
    {
        //remove identity matrix from transformation product
        mTransformation = scaleMat * rotateMat * translateMat;
        updateCumulative();
    }

    /// <summary>
    /// gets the cumulative transformation matrix
    /// for this transform node
    /// </summary>
    public Matrix Transformation
    {
        get { return mTransformation; }
    }

    /// <summary>
    /// sets the world matrix of all child nodes to be
    /// Transformation * ParentMatrix
    /// </summary>
    private void updateCumulative()
    {
        base.WorldMatrix = mTransformation * mParentMatrix;
    }

    /// <summary>
    /// gets / sets the world transformation matrix
    /// </summary>
    /// <remarks>set the world matrix is really only setting
    /// the ParentMatrix property of the TransformNode. Children
    /// of this node will receive the cumulative world matrix of
    /// Transformation * ParentMatrix</remarks>
    public override Matrix WorldMatrix
    {
        get { return base.WorldMatrix; }
        set
        {
            mParentMatrix = value;
            updateCumulative();
        }
    }

      /*
       * RESET TRANSFORMATIONS
       */
    /// <summary>
    /// reset to no translation
    /// </summary>
    public void resetTranslate()
    {
        translateMat = Matrix.Identity;
        updateTranformation();
    }

    /// <summary>
    /// reset to no rotation
    /// </summary>
    public void resetRotate()
    {
        curRotation = Vector3.Zero;
        rotateMat = Matrix.Identity;
        updateTranformation();
    }

    /// <summary>
    /// reset to no scaling
    /// </summary>
    public void resetScale()
    {
        curScale = Vector3.One;
        scaleMat = Matrix.Identity;
        updateTranformation();
    }

      /*
       * TRANSLATION
       */
    /// <summary>
    /// offset translation by specified vector
    /// </summary>
    /// <param name="translate">amout to translate the transformation</param>
    public void Translate(Vector3 translate)
    {
        translateMat = Matrix.CreateTranslation(translateMat.Translation + translate);
        updateTranformation();
    }

    /// <summary>
    /// alternate name for position
    /// </summary>
    public Vector3 Translation
    {
        get { return this.Position; }
        set { this.Position = value; }
    }

    /// <summary>
    /// get / sets the position from
    /// the origin of this node
    /// </summary>
    /// <param name="translate"></param>
    public Vector3 Position
    {
        get { return translateMat.Translation; }
        set
        {
            translateMat = Matrix.CreateTranslation(value);
            updateTranformation();
        }
    }

    /// <summary>
    /// gets or sets the X translation of
    /// this transform node
    /// </summary>
    public float X
    {
        get { return translateMat.Translation.X; }
        set
        {
            translateMat = Matrix.CreateTranslation(
			new Vector3(value, translateMat.Translation.Y,
			translateMat.Translation.Z));

            updateTranformation();
        }
    }
    /// <summary>
    /// gets or sets the Y translation of this transform node
    /// </summary>
    public float Y
    {
        get { return translateMat.Translation.Y; }
        set
        {
            translateMat = Matrix.CreateTranslation(
			new Vector3(translateMat.Translation.X,
                                    value, translateMat.Translation.Z));

            updateTranformation();
        }
    }

    /// <summary>
    /// gets or sets the Z translation of this transform node
    /// </summary>
    public float Z
    {
        get { return translateMat.Translation.Z; }
        set
        {
            translateMat = Matrix.CreateTranslation(
			new Vector3(translateMat.Translation.X,
					translateMat.Translation.Y, value));

            updateTranformation();
        }
    }

      /*
       * ROTATIONS
       */
    /// <summary>
    /// offset rotation of node around X,Y, and Z axis
    /// using a standard Euler transformation
    /// </summary>
    /// <param name="rad"></param>
    public virtual void Rotate(Vector3 rad)
    {
        curRotation = curRotation + rad;
        updateRotation();
    }

    /// <summary>
    /// rotates around the X axis by the
    /// specified number of radians
    /// </summary>
    /// <param name="rad"></param>
    public virtual void RotateX(float rad)
    {
        curRotation.X += rad;
        updateRotation();
    }

    /// <summary>
    /// rotates around the Y axis by the
    /// specified number of radians
    /// </summary>
    /// <param name="rad"></param>
    public virtual void RotateY(float rad)
    {
        curRotation.Y += rad;
        updateRotation();
    }

    /// <summary>
    /// rotates around the Z axis by the
    /// specified number of radians
    /// </summary>
    /// <param name="rad"></param>
    public virtual void RotateZ(float rad)
    {
        curRotation.Z += rad;
        updateRotation();
    }

    /// <summary>
    /// get / sets the amount of rotation for the
    /// transform as Euler rotations in radians
    /// </summary>
    public virtual Vector3 Rotation
    {
        get { return curRotation; }
        set
        {
            curRotation = value;
            updateRotation();
        }
    }

    /// <summary>
    /// get / sets the amount of rotation for the
    /// transform as Euler rotations in degrees
    /// </summary>
    public virtual Vector3 RotationDegrees
    {
        get
        {
            Vector3 degRotation;
            degRotation.X = TransformNode.RADIANS_TO_DEGREES * curRotation.X;
            degRotation.Y = TransformNode.RADIANS_TO_DEGREES * curRotation.Y;
            degRotation.Z = TransformNode.RADIANS_TO_DEGREES * curRotation.Z;
            return degRotation;
        }
        set
        {
            curRotation.X = TransformNode.DEGREES_TO_RADIANS * value.X;
            curRotation.Y = TransformNode.DEGREES_TO_RADIANS * value.Y;
            curRotation.Z = TransformNode.DEGREES_TO_RADIANS * value.Z;

            updateRotation();
        }
    }

    /// <summary>
    /// get / sets the current rotation for the
    /// node around the X axis in radians
    /// </summary>
    public virtual float RotationX
    {
        get { return curRotation.X; }
        set
        {
            curRotation.X = value;
            updateRotation();
        }
    }
    
    /// <summary>
    /// get / sets the current rotation for the
    /// node around the Y axis in radians
    /// </summary>
    public virtual float RotationY
    {
        get { return curRotation.Y; }
        set
        {
            curRotation.Y = value;
            updateRotation();
        }
    }
    
    /// <summary>
    /// get / sets the current rotation for the 
    /// node around the Z axis in radians
    /// </summary>
    public virtual float RotationZ
    {
        get { return curRotation.Z; }
        set
        {
            curRotation.Z = value;
            updateRotation();
        }
    }
    
    /// <summary>
    /// rotates around an arbitrary axis, 
    /// WARNING: using this method will invalidate the
    /// use of other rotation methods
    /// </summary>
    /// <param name="axis">the axis to rotate around</param>
    /// <param name="deg">the angle in radians to rotate</param>
    public virtual void RotateAxis(Vector3 axis, float deg)
    {
        rotateMat = Matrix.Identity * Matrix.CreateFromAxisAngle(axis, deg) * rotateMat;
        updateTranformation();
    }

      /*
       * SCALING
       */
    /// <summary>
    /// modify scale factor by specified amount
    /// </summary>
    /// <param name="scale"></param>
    public void Scale(Vector3 scale)
    {
        curScale.X = scale.X * curScale.X;
        curScale.Y = scale.Y * curScale.Y;
        curScale.Z = scale.Z * curScale.Z;
        scaleMat = Matrix.CreateScale(curScale);
        updateTranformation();
    }

    /// <summary>
    /// get / sets the scale vector for the node
    /// </summary>
    /// <param name="translate"></param>
    public Vector3 CurrentScale
    {
        get { return curScale; }
        set
        {
            curScale = value;
            scaleMat = Matrix.Identity * Matrix.CreateScale(curScale);
            updateTranformation();
        }
    }

      /*
       * ENCAPSULATORS
       */
    /// <summary>
    /// gets / sets the euler rotation order
    /// </summary>
    public EulerRotateOrder RotateOrder
    {
        get { return rotateOrder; }
        set
        {
            rotateOrder = value;
            updateRotation();
        }
    }

    /// <summary>
    /// the translation part of the transformation matrix
    /// </summary>
    public Matrix TranslationMatrix
    {
        get { return translateMat; }
    }
    
    /// <summary>
    /// the scale part of the transformation matrix
    /// </summary>
    public Matrix ScaleMatrix
    {
        get { return scaleMat; }
    }
    
    /// <summary>
    /// the rotation part of the transformation matrix
    /// </summary>
    public Matrix RotationMatrix
    {
        get { return rotateMat; }
    }
}

BillboardTransform - A billboarding transform node that tracks another SceneItem's position. The primary use of this class is for creating 3D sprites.

(show)
Download
//BillboardTransform class...

public class BillboardTransform : TransformNode
{
    /// <summary>
    /// the default up direction to use when
    /// calculating the billboard facing direction
    /// </summary>
    public static Vector3 DEFAULT_UP = Vector3.UnitY;
    /// <summary>
    /// the up direction to use if forward axis == DEFAULT_UP
    /// </summary>
    public static Vector3 ALTERNATE_UP = Vector3.UnitX;

     /*
      * CLASS PROPERTIES
      */
    // the target the billboard will track
    protected SceneItem mTrackTarget;

    // the position of the tracker the last
    // time the billboard matrix was updated
    private Vector3 lastTrackerPosition;
    // the position of this transform node
    // the last time the billboard matrix was updated
    private Vector3 lastBillboardPosition;

    // a rotation matrix that can rotate
    // the billboard around its forward axis
    protected Matrix axisRotationMat;
    // the amoutn of rotation to apply around the forward axis
    protected float axisRotation;
    // the constraining up vector that orients the billboard
    protected Vector3 upVector;

     /*
      * CONSTRUCTOR
      */
    /// <summary>
    /// Basic constructor for a new billboard transform.
    /// </summary>
    /// <param name="newTrackTarget">The new target the billboard
    /// transform should track. Typically this is the camera.</param>
    public BillboardTransform(TransformNode newTrackTarget)
        : base(EulerRotateOrder.XYZ)
    {
        mTrackTarget = newTrackTarget;
        init();
    }

    private void init()
    {
        upVector = BillboardTransform.DEFAULT_UP;

        axisRotationMat = Matrix.Identity;
        lastTrackerPosition = Vector3.Zero;
        lastBillboardPosition = Vector3.Zero;
        axisRotation = 0;
    }

     /*
      * UPDATE ROTATION MATRIX
      */
    /// <summary>
    /// updates the rotation matrix to
    /// apply the current billboarding transform
    /// </summary>
    protected override void updateRotation()
    {
        if (mTrackTarget == null)
        {
            rotateMat = Matrix.Identity;
            return;
        }
        lastTrackerPosition = mTrackTarget.WorldPosition;
        lastBillboardPosition = this.WorldPosition;

        //determine billboard matrix
        Matrix lookAt = Matrix.Identity;
        lookAt.Forward = Vector3.Normalize(lastTrackerPosition - lastBillboardPosition);

        Vector3 up = upVector;
        if (up == lookAt.Forward)
            up = BillboardTransform.ALTERNATE_UP;

        lookAt.Right = Vector3.Normalize(Vector3.Cross(lookAt.Forward, up));
        lookAt.Up = Vector3.Cross(lookAt.Forward, lookAt.Right);

        //add axis rotation
        rotateMat = axisRotationMat * lookAt;
        base.updateTranformation();
    }


     /*
      * ENCAPSULATORS
      */
    /// <summary>
    /// The target which the billboard will track. Typically
    /// this will be the camera object though any transform
    /// node can be tracked.
    /// </summary>
    public SceneItem TrackTarget
    {
        get { return mTrackTarget; }
        set
        {
            mTrackTarget = value;
            lastTrackerPosition = Vector3.Zero;
            UpdateBillboard();
        }
    }
    /// <summary>
    /// the up direction to use in orienting the billboard
    /// </summary>
    public Vector3 UpVector
    {
        get { return upVector; }
        set { upVector = value; }
    }

    /// <summary>
    /// the rotation of the billboard around
    /// its forward axis measured in radians
    /// </summary>
    public virtual float AxisRotation
    {
        get { return axisRotation; }
        set
        {
            axisRotation = value;
            axisRotationMat = Matrix.CreateRotationZ(axisRotation);
            updateRotation();
        }
    }

     /*
      * UPDATE
      */
    /// <summary>
    /// updates the billboard transform if its position or
    /// its tracking target's position has changed
    /// </summary>
    public void UpdateBillboard()
    {
        if (mTrackTarget != null)
        {
            if (mTrackTarget.WorldPosition != lastTrackerPosition ||
                this.WorldPosition != lastBillboardPosition)
            {
                updateRotation();
            }
        }
    }

    public override void Update(Matrix viewMatrix, Matrix projMatrix)
    {
        base.Update(viewMatrix, projMatrix);
        UpdateBillboard();
    }

     /*
      * ROTATIONS OVERRIDE
      */
    // rotation overrides omitted
 
}