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.zipA 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)// 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)// 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.zipA 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)# 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)# 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.zipThe 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)//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)//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)//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 }