(Source code is located in the Developer Kit \plug-ins directory.)
The splitUV command is a MEL command used to unshare or "split" the selected UVs of a polygonal mesh. A UV is a point on a 2D texture plane that is used to map textures onto a mesh. To properly associate these UVs with a mesh, each face can either be mapped or unmapped. If a face is mapped, each vertex belonging to that face is associated with a UV. This relationship is known as a face-vertex relationship. (For more information, see Face-Vertices.)
For a better understanding of what splitting a UV means, try the following steps in Maya:
The Cut UV Edges command operates on edges of a mesh while splitUV operates on a UV.
UVs are stored in a single linear array, indexed by face vertices. Each face vertex indexes a specific UVId, and in turn each UVId represents a single UV point on a texture map. Thus the same number of faces that share the UV would index a shared UVId. (For more information, see UVs.)
You need to add a number of UVs (at the same 2D coordinates) equal to the total number of faces that share the UV minus one. Subtract one knowing you already have at least one UV already associated to a shared face, which leaves one less face and UV to create and associate. Now associate each of the new UVs to one of the faces that shared the original UV, leaving one of the faces indexing the original UV itself. This leaves each face with an unshared UV at their respective face-vertex location.
First gather your input—in this case the selected UVs. Obtain the selection list and filter it for the first object you find with selected UVs. For simplicity, only the first object encountered with selected UVs is taken. You can easily extend this to operate on multiple objects.
// Get the active selection list and filter for poly objects. // Also create a selection list iterator to parse the list // MSelectionList selList; MGlobal::getActiveSelectionList( selList ); MItSelectionList selListIter( selList ); selListIter.setFilter( MFn::kMesh ); // Declare a dagPath and component to store a reference to the // mesh object and selected components // MDagPath dagPath; MObject component; // Now parse the selection list for poly objects with selected // UVs // for( ; !selListIter.isDone(); selListIter.next() ) { selListIter.getDagPath( dagPath, component ); // Check for selected UVs // if( component.apiType() == MFn::kMeshMapComponent ) { break; } } // Now we break down the component object into an int array // of UVIds // MFnSingleIndexedComponent compFn( component ); MIntArray selUVs; compFn.getElements( selUVs );
Now you have the object to operate on and the selected UVs. Before you can perform the operation, you need to collect some preprocessing data—which faces share each selected UV and the UV vertex associations. Finally, you must cover for the possible appearance of multiple UV sets. Since UVs from only one UV set can be selected at any particular time, you only need to access the current active UV set and operate on that set.
// Declare our processing variables // MObject mesh; MString selUVSet; MIntArray selUVFaceIdMap; MIntArray selUVFaceOffsetMap; MIntArray selUVLocalVertIdMap; // Make sure our dagPath points to a shape node. That is where // the topological/geometry data is stored (not on the transform) // dagPath.extendToShape(); mesh = dagPath.node(); // Initialize a mesh function set to our mesh // MFnMesh meshFn( mesh ); // Get the current UV set name // meshFn.getCurrentUVSetName( selUVSet); // Now parse the mesh for face and vertex UV associations // MItMeshPolygon polyIter( mesh ); int i; int j; int offset = 0; int selUVsCount = selUVs.length(); for( i = 0; i < selUVsCount; i++ ) { // Append the current offset in the faceId map to the // faceOffset map so we have an index reference into the // faceId map for each selected UV. In other words, // for each offset value, we have a number of faces equal // to (faceOffsetMap[i+1] – faceOffsetMap[i]) that share // the UV that that offset value represents. // selUVFaceOffsetMap.append( offset ); // Parse the mesh for faces which share the current UV // for( ; !polyIter.isDone(); polyIter.next() ) { // Only continue if the face is mapped // if( polyIter.hasUVs() ) { // Now parse the vertices of each face and check for // the current UV // int polyVertCount = polyIter.polygonVertexCount(); for( j = 0; j < polyVertCount; j++ ) { int UVIndex = 0; polyIter.getUVIndex( j, UVIndex ); // If we find the UV on this face, append the faceId, // append the local vertex (relative to the current // face) and increment our offset. // if( UVIndex == selUVs[i] ) { selUVFaceIdMap.append( polyIter.index() ); selUVLocalVertIdMap.append(j); offset++; break; } } } } } // Finally append the last offset value so we can obtain the // number of faces that share the last UV in the list. // selUVFaceOffsetMap.append( offset );
The face and face-vertex associations are stored using a similar technique used by Maya to store topological data for polygons. This technique is apparent in the code where the list of faces shared by each selected UV is accumulated. Rather than creating a multi dimensional array to store the list of faces shared by each selected UV, store it all in a single dimensional array (a data array). To do this you create another single dimensional array (an index array) to store the index values of the data array where each selected UV begins its list of faces.
For each UV, parse the mesh for any faces sharing that particular UV. This is accomplished by parsing the face-vertices of each face in the mesh, looking at the associated UVId and comparing the UVId with the current UV. Now store the face Id and the local vertex Id (relative to the current face, enumerating from 0 to (faceVertexCount – 1)). Make note of the local vertex Id rather than the global or mesh vertex Id because UVs are assigned on a face-vertex basis.
// Declare UV count variables so we can keep create and // keep track of the indices of the new UVs // int currentUVCount = meshFn.numUVs( selUVSet ); // For each UV in our list of selected UVs, split the UV. // for( i = 0; i < selUVsCount; i++ ) { // Get the current faceId map offset // offset = selUVFaceOffsetMap[i]; // Get the U and V values of the current UV // float u; float v; int UVId = selUVs[i]; meshFn.getUV( uvId, u, v, &selUVSet ); // Get the number of faces sharing the current UV // int faceCount = selUVFaceOffsetMap[i+1]–selUVFaceOffsetMap[i]; // Arbitrarily choose that the last faceId in the list // of faces sharing this UV will keep the original UV // for( j = 0; j < faceCount – 1; j++ ) { // Create a new UV (setUV dynamically sizes the UV array // if the index value passed in exceeds the length of the // UV array) with the same 2D coordinates as our UV. // meshFn.setUV( currentUVCount, u, v, &selUVSet ); // Get the face and face-vertex info so we can assign // our newly created UV to one of the faces in the list // of faces sharing this UV // int localVertId = selUVLocalVertIdMap[offset]; int faceId = selUVFaceIdMap[offset]; // Associate the UV with the particular face vertex // meshFn.assignUV( faceId, localVertId, currentUVCount, &selUVSet ); // Increment our counters so we can create another new UV // at the currentUVCount index. Increment the offset, so we // can associate the next new UV with the next face in the // the list of faces sharing this UV // currentUVCount++; offset++; } }
There are two primary methods which are called to perform the actual split:
Call the first method, setUV(), to create a new UVId. The method automatically grows the UV array to accommodate the index passed into the method. Thus in the code you can see a variable named currentUVCount which is continuously incremented after each new UV. currentUVCount keeps track of the index that is one element greater than the highest element in the UV array. Incrementing it after each iteration through the loop allows you to create a new UV, one at a time.
Call the second and last method, assignUV(), to associate a given UVId with a face and face-vertex.
There are many intricacies involving modifying a polygonal mesh, including construction history and tweaks. If the mesh does not have history, you could attempt to unshare the UVs directly on the mesh itself. If the mesh has history, any DG evaluation from a node upstream in the construction history overwrites the mesh on the mesh node and the modifications made directly to the mesh would be lost. Even if that were the case, the existence of tweaks would change the appropriate place to write the modifications on the mesh.
You need to look at the mesh node, analyze its state, and apply your operation accordingly. The MPxCommand class polyModifierCmd (see polyModifierCmd example) was developed with the splitUV command to aid in abstracting the handling of construction history and tweaks. polyModifierCmd is a mid level command class designed for commands which intend to modify a polygonal mesh. It provides an outlet through which a poly command can take its code to modify a mesh directly and seamlessly integrate it into the Maya framework, accounting for both construction history and tweaks.
polyModifierCmd is a good example of using the API and demonstrates how construction history and tweaks work.
Before you proceed with this section, read the polyModifierCmd example. This section steps through the implementation of a command based on polyModifierCmd.
There are three main pieces of the polyModifierCmd that must be handled:
The factory is the lowest level of the splitUVCmd, which performs the operation given a set of inputs. The inputs are an integer array of UV Ids and a reference to the mesh you are about to modify. The rest fits inside the factory. The splitUVFty factory class interface is shown below.
There are two methods of deploying the factory. One is through the splitUV node and the other is directly through the command for certain exception cases where the node is not applicable. The splitUV node is used for cases where you want to build or add to an existing history chain in the DG.
When a DG evaluation is propagated via a dirtied node, the DG evaluates from the top of the history chain where a copy of the original mesh (original referring to the node’s state denoting the start of this history) is located. It then takes a copy of that mesh and passes it in through each node in order, where the mesh is altered through each node evaluation. Once it reaches the final shape, you have a mesh placed onto the shape which holds all the modifications stored in the history chain. The splitUV node needs to take in a mesh input as well as an input of which UVs to modify, and pass that data down to an instance of the factory. The resulting mesh is then passed out through a mesh output attribute.
class splitUVNode : polyModifierNode { public: splitUVNode(); virtual ~splitUVNode(); virtual MStatus compute(const MPlug& plug, MDataBlock& data); static void* creator(); static MStatus initialize(); private: // Note: There needs to be an MObject handle for each // attribute on the node. These handles are needed for // setting and getting values later. The standard inMesh // and outMesh attributes are already inherited from // polyModifierNode, thus we only need to declare splitUVNode // specific attributes // static MObject uvList; // Node type identifier // static MTypeId id; // We instantiate a copy of our splitUV factory on the node // for it to perform the splitUV operation // splitUVFty fSplitUVFactory };
The standard node interface is in the above class declaration. The only differences to note reside in the private members. From the class hierarchy, splitUVNode inherits an inMesh and outMesh attribute from polyModifierNode. We add yet another attribute to the node, specific to the splitUVNode, which consists of our only other input—the list of UVs to operate on.
Notice that the node class has an instance of a splitUV factory. You create a distinct factory for each node so that the splitUVFty implementation would require no foreknowledge of which node is calling the operation. Continuing with the basic node setup, implement the basic methods in the above interface, creating and associating attributes, assigning a type id, etc.:
MTypeId splitUVNode::id( 0x34567 ); MStatus splitUVNode::creator() { return new splitUVNode(); } MStatus splitUVNode::initialize() { MStatus status; MFnTypedAttribute attrFn; uvList = attrFn.create("inputComponents", "ics", MFnComponentListData::kComponentList); // To be stored during file-save attrFn.setStorable(true); inMesh = attrFn.create("inMesh", "im", MFnMeshData::kMesh); // To be stored during file-save attrFn.setStorable(true); // outMesh is read-only because it is an output attribute // outMesh = attrFn.create("outMesh", "om", MFnMeshData::kMesh); attrFn.setStorable(false); attrFn.setWritable(false); // Add the attributes we have created for the node // status = addAttribute( uvList ); if( !status ) { status.perror("addAttribute"); return status; } status = addAttribute( inMesh ); if( !status ) { status.perror("addAttribute"); return status; } status = addAttribute( outMesh ); if( !status ) { status.perror("addAttribute"); return status; } // Set up a dependency between the inputs and the output. // This will cause the output to be marked dirty when the // input changes. The output will then be recomputed the // next time it is requested. // status = attributeAffects( inMesh, outMesh ); if( !status ) { status.perror("attributeAffects"); return status; } status = attributeAffects( uvList, outMesh ); if( !status ) { status.perror("attributeAffects"); return status; } return MS::kSuccess; }
Finally, we turn towards the implementation of our compute() method. The compute method is not overly complex. Since we have the factory to perform the operation, all the compute method needs to do is provide the factory with the references to the proper mesh to modify.
Start as all plug-in nodes should and look to handle the ‘state’ attribute, inherited from MPxNode, on the node. The ‘state’ attribute holds a short integer representing how the DG treats the node. In a sense it is an override mechanism to alter how the node is treated during a DG evaluation. With respect to plug-in nodes, the only state of concern is the ‘HasNoEffect’ or ‘PassThrough’ state, where the node is ignored entirely. For the node to behave as though it were transparent, you need to redirect the inMesh to the outMesh without altering the mesh passing through. Otherwise the node behaves normally.
Following the node state check, grab the UVs from the component list and the input mesh, assign the input mesh to the output mesh, and pass in these references to the factory. Assigning the input mesh to the output mesh allows you to operate directly on the output mesh, so that the output mesh will hold the modified mesh. From there, let the factory take care of the rest of the operation.
MStatus splitUVNode::compute(const MPlug& plug, MDataBlock& data) { MStatus status = MS::kSuccess; // Retrieve our state attribute value // MDataHandle stateData = data.outputValue(state,&status); MCheckStatus( status, "ERROR getting state" ); // Check for the HasNoEffect/PassThrough state // // (stateData is stored as a short) // // (0 = Normal) // (1 = HasNoEffect/PassThrough) // (2 = Blocking) // .. // if( stateData.asShort() == 1 ) { MDataHandle inputData = data.inputValue(inMesh,&status); MCheckStatus(status, "ERROR getting inMesh"); MDataHandle outputData = data.outputValue(outMesh,&status); MCheckStatus(status, "ERROR getting outMesh"); // Simply redirect the inMesh to the outMesh // outputData.set(inputData.asMesh()); } else { // Check which output value we have been asked to compute. // If the node doesn’t know how to compute it, return // MS::kUnknownParameter. // if( plug == outMesh ) { MDataHandle inputData = data.inputValue( inMesh, &status ); MCheckStatus(status, "ERROR getting inMesh"); MDataHandle outputData = data.outputValue( outMesh, &status ); MCheckStatus(status, "ERROR getting outMesh"); // Now, we retrieve the input UV list // MDataHandle inputUVs = data.inputValue( uvList, &status ); MCheckStatus(status, "ERROR getting uvList"); // Copy the inMesh to the outMesh so we can operate // directly on the outMesh // outputData.set(inputData.asMesh()); MObject mesh = outputData.asMesh(); // Retrieve the UV list from the component list // // Note, we use a component list to store the components // because it is more compact memory wise. (ie. // comp[81:85] is smaller than comp[81],...,comp[85]) // MObject compList = inputUVs.data(); MFnComponentListData compListFn( compList ); unsigned i; int j; MIntArray uvIds; for( i = 0; i < compListFn.length(); i++ ) { MObject comp = compListFn[i]; if( comp.apiType() == MFn::kMeshMapComponent ) { MFnSingleIndexedComponent uvComp(comp); for( j = 0; j < uvComp.elementCount(); j++ ) { int uvId = uvComp.element(j); uvIds.append(uvId); } } } // Set the mesh object and uvList on the factory // fSplitUVFactory.setMesh( mesh ); fSplitUVFactory.setUVIds( uvIds ); // Now, call the factory to perform the splitUV // status = fSplitUVFactory.doIt(); // Mark the outputMesh as clean // outputData.setClean(); } else { status = MS::kUnknownParameter; } } return status; }
Now that we have a splitUVNode, the last thing left on the list to do is the splitUV command. This is the piece that ties everything together. It is from here that the method for modifying the mesh is chosen (although implicitly through polyModifierCmd). The command manages the operation and is the highest level of interfacing with the user.
As a child class of polyModifierCmd, the splitUVCmd does not have much work to do other than override some specific polyModifierCmd methods and retrieve the input.
class splitUV : polyModifierCmd { public: splitUV(); virtual ~splitUV(); static void* creator(); bool isUndoable(); // MPxCommand inherited methods // MStatus doIt( const MArgList& ); MStatus redoIt(); MStatus undoIt(); // polyModifierCmd inherited methods // MStatus initModifierNode( MObject modifierNode ); MStatus directModifier( MObject mesh ); private: // Private methods // bool validateUVs(); MStatus pruneUVs(); // Private members // // Selected UVs // // We store two copies of the UVs, one that is passed down to // the node and another kept locally for the directModifier. // Note, the MObject member, fComponentList, is only ever // accessed during a single call of a plugin, never between // calls where its validity is not guaranteed. // MObject fComponentList; MIntArray fSelUVs; // splitUV Factory // // This factory is for the directModifier to have access to // operate directly on the mesh. // splitUVFty fSplitUVFactory; };
This looks much like the standard MPxCommand class interface. However there are a few differences due to the polyModifierCmd inheritance as well as some performance enhancing methods. Two methods need to be overridden:
initModifierNode() is the chance for a command to initialize any inputs aside from the inMesh on the modifier node, which in our case is the splitUVNode. This is not restricted to input initialization, but can be catered towards custom node initialization if desired. This method is called before the modifier node is placed in the history chain, if the creation of history is permissible. For example, in our case we’d like to initialize the uvList input on our splitUVNode:
MStatus splitUV::initModifierNode( MObject modifierNode ) { MStatus status; // Tell the splitUVNode which UVs to operate on. // MFnDependencyNode depNodeFn( modifierNode ); MObject uvListAttr; uvListAttr = depNodeFn.attribute( "inputComponents" ); // Pass the component list down to the node. // To do so, we create/retrieve the current plug // from the uvList attribute on the node and simply // set the value to our component list. // MPlug uvListPlug( modifierNode, uvListAttr ); status = uvListPlug.setValue( fComponentList ); return status; }
directModifier() is a method called only in a specific exception case where the mesh node has no history and the preference to record history is turned off. The consequence of this state is that the user does not wish to have any history chain at all. So in effect, the polyModifierCmd is forbidden to use the DG. As a result we modify the mesh directly. The polyModifierCmd description discusses the implications of this state in more detail as well as alternative approaches. However all we need to know is that we need to provide a method to operate on the mesh directly, which if you recall, we completed in the concepts section. It is for this reason that the command also holds an instance of the factory as well as a copy of the UVs to modify in an integer array format (as opposed to a component list for the splitUVNode).
You might wonder why we store two copies of the selected UVs in different formats. The reason for this is that an MObject is never guaranteed to be valid between plug-in calls (including redoIt() calls). Since the directModifier() would be called in a redoIt() case, it would rely on the validity of the MObject component list between calls. As such we’ve stored two copies on the command. Alternatively, one could choose to modify the node so that it receives an integer array as a node input rather than a component list to streamline the operation, however it’s a balancing issue of performance vs. storage.
Using these inputs we have the following simple directModifier() method:
MStatus splitUV::directModifier( MObject mesh ) { MStatus status; fSplitUVFactory.setMesh( mesh ); fSplitUVFactory.setUVIds( fSelUVs ); // Now, call the factory to perform the splitUV // status = fSplitUVFactory.doIt(); return status; }
Before we look at the performance enhancing methods, let’s take a peek at the MPxCommand inherited methods. These methods will give us a better appreciation of how the performance of the command can be slightly tweaked:
The doIt() method is our main method which retrieves the input and caters the rest of the operation to the various parts, overseeing the entire operation. The doIt() method is the method used to initialize the command and perform the operation as the name would suggest. And much to that effect, the splitUV’s doIt() method does exactly that.
We begin by parsing the selection list for any objects where UVs are selected, just the same as we did in our original implementation. Following that we initialize the polyModifierCmd settings, call our performance enhancing methods and issue the doModifyPoly() method, which can be viewed as the polyModifierCmd’s version of doIt(). Additionally we scatter the appropriate error messages in the code to inform the user of improper use of the command.
MStatus splitUV::doIt( const MArgList& ) { MStatus status; MSelectionList selList; MGlobal::getActiveSelectionList( selList ); MItSelectionList selListIter( selList ); selListIter.setFilter( MFn::kMesh ); // Initialize our component list // MFnComponentListData compListFn; compListFn.create(); // Parse the selection list // bool found = false; bool foundMultiple = false; for( ; !selListIter.isDone(); selListIter.next() ) { MDagPath dagPath; MObject component; selListIter.getDagPath( dagPath, component ); if( component.apiType() == MFn::kMeshMapComponent ) { if( !found ) { // Add the components to our component list. // ‘component’ holds all selected components // on the given object, so only a single call // to add is needed. // compListFn.add( component ); fComponentList = compListFn.object(); // Locally store the selected UVIds in the command // int array member, fSelUVs // MFnSingleIndexedComponent compFn( component ); compFn.getElements( fSelUVs ); // Ensure that our DAG path is pointing to a // shape node. Set the DAG path for polyModifierCmd. // dagPath.extendToShape(); setMeshNode( dagPath ); found = true; } else { // Since we are only looking for whether or not there // are multiple objects with selected UVs, break out // once we have found one other object. // foundMultiple = true; break; } } } if( foundMultiple ) { displayWarning( "Only operates on first found mesh" ); } // Set the modifier node type for polyModifierCmd // setModifierNodeType( splitUVNode::id ); if( found ) { if( validateUVs() ) { // Now, pass control over to polyModifierCmd // status = doModifyPoly(); if( status == MS::kSuccess ) { setResult( "splitUV command succeeded!" ); } else { setResult( "splitUV command failed!" ); } } else { displayError( "Selected UVs are not splittable" ); status = MS::kFailure; } } else { displayError( "Unable to find selected UVs" ); status = MS::kFailure; } return status; }
The undo/redo mechanism is supported by the undoIt() and redoIt() methods inherited from MPxCommand. These methods often use cached data from the first doIt() to execute. This is what splitUV does as well as polyModifierCmd, which supports its own undo/redo in the form of undoModifyPoly() and redoModifyPoly(). Since splitUV relies on polyModifierCmd to perform the operation, the undo/redo redirects the undo/redo to polyModifierCmd. As a result, the splitUV’s undoIt() and redoIt() methods are very straightforward:
MStatus splitUV::redoIt() { MStatus status; status = redoModifyPoly(); if( status == MS::kSuccess ) { setResult( "splitUV command succeeded!" ); } else { setResult( "splitUV command failed!" ); } return status; } MStatus splitUV::undoIt() { MStatus status; status = undoModifyPoly(); if( status == MS::kSuccess ) { setResult( "splitUV undo succeeded!" ); } else { setResult( "splitUV undo failed!" ); } return status; }
The call to validateUVs() in the doIt() method is a performance enhancing method. Though this method is primarily a pre-condition check on the selected UVs, it increases the optimal performance of the command by pruning the selected UV list of UVs that cannot be split. This potentially saves the operation unnecessary loops. However, an extra pass of the mesh is required to check for UVs that cannot be split, but only on the very first call to the command. Any successive redoIt() calls or node evaluations are faster.
To define when a UV is invalid and unable to be split, look at the definition of the operation. splitUV provides each face which shares a particular UV with it’s own unique and unshared UV. Thus a UV can only be split if it is shared by more than one face. Subsequently, the validateUVs() method parses the mesh and retrieves the face sharing information for each UV, marking valid UVs. The valid UV list is sent to the pruneUVs() method which replaces the component list and locally stored integer array of selected UVs.
bool splitUV::validateUVs() { // Get the mesh that we are operating on // MDagPath dagPath = getMeshNode(); MObject mesh = dagPath.node(); // Get the number of faces sharing each UV // MFnMesh meshFn( mesh ); MItMeshPolygon polyIter( mesh ); MIntArray selUVFaceCountArray; int i; int j; int count = 0; selUVsCount = fSelUVs.length(); for( i = 0; i < selUVsCount; i++ ) { for( ; !polyIter.isDone(); polyIter.next() ) { if( polyIter.hasUVs() ) { int UVIndex = 0; polyIter.getUVIndex( j, UVIndex ); // If we have a matching UVId, then we have a // face which shares this UV, so increment the // count. // if( UVIndex == fSelUVs[i] ) { count++; break; } } } selUVFaceCountArray.append( count ); } // Now, check to make sure that at least one UV has more than // one face sharing it. So long as we have at least one valid // UV, we should proceed with the operation by returning true // bool isValid = false; MIntArray validUVIndices; for( i = 0; i < selUVsCount; i++ ) { if( selUVFaceCountArray > 1 ) { isValid = true; validUVIndices.append(i); } } if( isValid ) { pruneUVs( validUVIndices ); } return isValid; } MStatus splitUV::pruneUVs( MIntArray& validUVIndices ) { MStatus status = MS::kSuccess; unsigned i; MIntArray validUVIds; for( i = 0; i < validUVIndices.length(); i++ ) { int uvIndex = validUVIndices[i]; validUVIds.append( fSelUVs[uvIndex] ); } // Replace our local int array of UVIds // fSelUVs.clear(); fSelUVs = validUVIds; // Build our list of valid components // MFnSingleIndexedComponent compFn; compFn.create( MFn::kMeshMapComponent ); compFn.addElements( validUVIds ); MObject component = compFn.object(); // Replace the component list // MFnComponentListData compListFn; compListFn.create(); compListFn.add( component ); fComponentList = compListFn.object(); return status; }
For further details on the implementation of the splitUV command, look at the source code provided in the plug-ins directory of the developer’s kit.