Reading UV Map Values

From The Foundry MODO SDK wiki
Jump to: navigation, search

Prerequisites

So what does it take to read the values from a UV Map? We need an API object called a Mesh Map Accessor to select our UV map, a Polygon Accessor to select polygons and read UV values from them, and a Storage Buffer Object to store those values in. When we use the term "select" with reference to the API, what we really mean is that we're choosing a specific thing to look at more closely. When we select a polygon, for instance, we aren't actually changing the currently selected polygons in modo. Instead, we're just telling the Python API to focus on a specific polygon so that we can then get some information about it. This is a very important principal to understand if you're new to working with the API or scripting systems.

Even though we're reading UV positions of individual vertices, we need to check the UV map by looking at polygons and then the vertices that belong to them. This is because UV vertices can be discontinuous and single vertex can have multiple UV map coordinates. By looking at each polygon and then at each of the vertices that make it up, we can be sure we aren't accidently skipping over these extra UV values.

Localizing a Mesh Object

Before you can interact with a Mesh through the Python API, you need to localize it as a mesh object. There's a lot of ways you can do this, but one of the quickest for this purpose is to create a LayerScan Object which returns the primary layer. We create layerscan items through the Layer Service:

  layerService = lx.service.Layer()  #Get the Layer Service
 
  # Call the ScanAllocate Method from the layer service to create a layerscan object
  # ScanAllocate requires a mode so we pass the symbol for Layerscan_primary to tell 
  # it that we want to deal with the primary mesh layer.  You can find symbols
  # for things like this by typing dir(lx.symbol) into the python interpreter in modo
 
  layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSCAN_PRIMARY)
 
  # Finally we call the MeshBase method to get our Mesh Object (lx.object.Mesh)
  # The MeshBase method could return multiple meshes, so we give it an argument of 0
  # to indicate we want to first mesh out of any that it might return
 
  localizedMesh = layerScanObject.MeshBase(0)
  • An important note on using LayerScan to localize a mesh: LayerScan Objects are primarily used for editing meshes. This means that when we localize a mesh with LayerScan, we're telling modo that our mesh is currently being edited. To make sure modo knows we're done editing the mesh, we need to (in a sense) turn off the editing mode for LayerScan by calling the LayerScan.Apply() method at the end of our script.

Now we have a localized Mesh object that we can work with. If you search the mesh object for what methods we can call on it, you'll see there are ways to access polygons, edges, points, maps, and lots of other attributes.

Accessing Map Values

The data from vertex maps such as UV coordinates can only be read into Storage Buffers. That means that before we can start reading vertex map values, we need to create a new storage object. The size and type of storage buffer can be set after we create it, or at the same time we create it by feeding the type and size in as arguments. The type determines what kind of data the buffer will store, and the size determines how many of those values the buffer can hold. Since we know that UV map coordinates will have 2 values (a U and V component) and these values are floats, we can create the appropriate storage object in a single line of python:

  storageBuffer = lx.object.storage('f',2)

The 'f' string indicates a float type, while the 2 indicates that our buffer will store two values at a time.

The next step is to create a Polygon Accessor to access polygons and a Mesh Map Accessor to access our UV map. This is easy with a localized Mesh Object (remember we called it localizedMesh):

  polyAccessor = localizedMesh.PolygonAccessor()
  mapAccessor = localizedMesh.MeshMapAccessor()

Getting Familiar with the API

So now we have everything we need to start reading values. We just need to call the appropriate methods on our objects. So how do we know what we methods we need and what each method requires to work correctly? This is where you really need to dig in to the Persistent Interpreter. Let's look at the Polygon Accessor object we've created and see what kind of polygon attributes we can access.

 dir(polyAccessor)

Dir(polyAccessor).png

So we can see that there's lots of different methods available for the Polygon Accessor Object, and if we want to find out how to use one, we just need to check its docs. To check the docs for the the Polygon Accessor's Normal() method, for instance, we'd type:

 polyAccessor.Normal.__doc__

and get

 vector normal = Normal()

The info from the docs is organized so that the information you get from a method is on the left, and the method you're checking is on the right. In this example, you can see "vector normal" is what you get back. The format of this is "data-type *space* what-the-data-is". So you get the polygon normal returned as a vector data type (a vector is a 3 element tuple which usually represents X,Y,Z or R,G,B). The right side of the equation you can see the method we asked about (Normal) with any required arguments inside of it. Since the right side is "Normal()" with no arguments, we can see that the normal method doesn't take any arguments. Instead, Normal (like a lot of other methods) requires you to select a polygon first (remember that select means to focus the API on, not to actually change the polygons that are selected in modo). Let's go through a few more methods just to point out some other data types:

 polyAccessor.Closest.__doc__
 (boolean,vector hitPos,vector hitNorm,float hitDist) = Closest(float maxDis,vector Pos)

So... when you call Closest() method, you are required to give 2 arguments. The first is a float which represents the max Distance to look for a polygon. The 2nd is a vector that represents the X,Y,Z position of the point that you want to search for polygons from. This is admittedly not spelled out very explicitly, so you often need to make certain inferences. Most of the returned info is understandable. We get a vector to indicate the hit position, a vector indicating the hit normal, a float indicating the hit distance. But what's the boolean? Usually, if a method returns a boolean and it's not obvious why, the boolean is a true/false statement as to whether the method succeeded or not. In this case, it indicates whether the Closest() method was actually able to find a polygon within the distance given to it in the arguments.

Down to Business

What about the method we actually want to use? We'll be using the MapEvaluate() Method. So a quick look at the docs for it gives us:

 boolean = MapEvaluate(id map,id point,float[] value)

So the MapEvaluatee method only returns a boolean for whether it succeeds or fails, not any values. That's because the values themselves are stored in the storage object we created a while ago. When you see a [] next to a data type in the requirements for a method, it means the method takes a storage object of that data type. So the "float[] value" in the requirements just means we need to feed the method a storage object. We also need an ID for the map we're reading, and an ID for the point we want to check. An ID (not to be confused with Index) is an integer which represents a specific element or item internally in modo. We can get vertex IDs by selecting a polygon (using the SelectByIndex() method) and then running theVertexByIndex() method. The UV map ID we can get by selecting our UV map in the mapAccessor we created, and then running the ID() method.

First off, let's select the UV map we want to read. We'll assume that we know the name of the UV map we want to read. In our Mesh Map Accessor object, there's a method to select vertex maps based on their Types and Names. The symbol we need to indicate a UV map type is found in dir(lx.symbol). Once the map is selected we can use the ID() method to return its ID.

  mapAccessor.SelectByName(lx.symbol.i_VMAP_TEXTUREUV,"Texture")
  mapID = mapAccessor.ID()

Now we need to get the point ID from our polygon accessor. We need to know how many vertices our current polygon has, and then get the Vertex ID from each one. First we select a polygon by Index, then use the VertexCount() method to see how many vertices it has. Then we can get the vertex ID from each one using the VertexByIndex() method and feed these into the MapValue() method to store the UV values in our storage object.

  uvValues = []  # Make a list to store our UV values
  polyAccessor.SelectByIndex(0)  # Select the polygon whose index is 0
  nVerts = polyAccessor.VertexCount()  # Store the number of verts that poly has as a variable
  for eachVert in range(nVerts):  #  For every one of the verts belonging to this polygon
      vertID = polyAccessor.VertexByIndex(eachVert)  #  Get the verts Index
      if polyAccessor.MapEvaluate(mapID, vertID, storageBuffer) == True:  
      ## If the MapEvaluate() method succeeds, the UV value should be stored in our storage object.  
      ## If it doesn't succeed, it means there isn't actually a UV value for the vert we were looking at
          values = storageBuffer.get()  # We use the get() method on storage objects to return its contents
          uvValues.append(values)

A Step Back

So let's put it all together to see where we're at. If we simplify some of the comments and the method searching, our code looks basically like this:

#python
import lx
 
## Localize our mesh ##
layerService = lx.service.Layer()
layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSCAN_PRIMARY)
localizedMesh = layerScanObject.MeshBase(0)
 
## Create a Storage Object and Accessors ##
storageBuffer = lx.object.storage('f',2)
polyAccessor = localizedMesh.PolygonAccessor()
mapAccessor = localizedMesh.MeshMapAccessor()
 
## Get Map ID ##
mapAccessor.SelectByName(lx.symbol.i_VMAP_TEXTUREUV,"Texture")
mapID = mapAccessor.ID()
 
## Select a polygon and read its vertex UV values into a list of tuples ##
uvValues = []
polyAccessor.SelectByIndex(0)
nVerts = polyAccessor.VertexCount()
for eachVert in range(nVerts):
    vertID = polyAccessor.VertexByIndex(eachVert)
    if polyAccessor.MapEvaluate(mapID, vertID, storageBuffer) == True:  
        values = storageBuffer.get()  
        uvValues.append(values)
# Remember we have to include the Apply() call to let modo know we're done editing the mesh
layerScanObject.Apply()

There's almost no chance that you'd just want to read the UV values for the vertices of a specific polygon, and nothing else. So if we wanted to, we could check how many polygons are in our mesh, then wrap up the bottom of our code with a 'for i in range(number_of_polygons)' loop. Then we could look at all the UV values for all the verts (even discontinuous ones) in our mesh. There's even a nice PolygonCount() method on our localized mesh that will tell us how many polygons we have. There's a trick to this, however. If we're looking at each vertex from each polygon, we're bound to get information about the same vertex twice. This means we'll have the same UV coordinates for each polygon that a vertex shares, which is going to be useless and possible harmful. We'd likely be editing vertices depending on whether they met some criteria, and we certainly wouldn't want to edit the same UV vertex twice. So we can fix this by checking our values list, and only add the new values to the list if they aren't already there. We'd also do any editing only after we make sure we haven't already editing this vertex, so the values would need to be stored after any editing happens.

#python
import lx
 
## Localize our mesh ##
layerService = lx.service.Layer()
layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSCAN_PRIMARY)
localizedMesh = layerScanObject.MeshBase(0)
 
## Create a Storage Object and Accessors ##
storageBuffer = lx.object.storage('f',2)
polyAccessor = localizedMesh.PolygonAccessor()
mapAccessor = localizedMesh.MeshMapAccessor()
 
## Get Map ID ##
mapAccessor.SelectByName(lx.symbol.i_VMAP_TEXTUREUV,"Texture")
mapID = mapAccessor.ID()
 
## Get the total polygons in our mesh and loop through them ##
## Select (Focus) on each one and get the uv values for its vertices ## 
uvValues = []
nPolys = localizedMesh.PolygonCount()
for eachPoly in range(nPolys):
    polyAccessor.SelectByIndex(eachPoly)
    nVerts = polyAccessor.VertexCount()
    for eachVert in range(nVerts):
        vertID = polyAccessor.VertexByIndex(eachVert)
        if polyAccessor.MapEvaluate(mapID, vertID, storageBuffer) == True:  
            values = storageBuffer.get()
            if values not in uvValues:    #We check to make sure this isn't a vertex we've already looked at
                # Any editing of this vertex's UV position would go here
                uvValues.append(values)
# Remember we have to include the Apply() call to let modo know we're done editing the mesh
layerScanObject.Apply()
lx.out(uvValues)

So there we are, a API script to spit out the UV coordinates for vertices in the current mesh. But that isn't the best way to handle this. For situations like this, when you're going to be looping through every vertex in your mesh, or every vertex map in your scene, or anything where you're going to be doing the same thing over and over, you can use what's called a Visitor to do the looping work for you.

Creating a Visitor

  • This section is a little more complicated, and assumes you have a little familiarity with object oriented programming. If you aren't familiar with these ideas, there's tons of good resources on the web for understanding python classes that might be worth reading up on

Visitors are API objects you can create for a specific use, and then call in like a plug-in. Any API objects that have a method called Enumerate() can use a Visitor object to traverse their data sets. Using a visitor object to look at a lot of data is typically a lot faster than simply looping through it, so it's a good idea to use visitors when you can. Since Enumerate() is one of the methods shown on our polyAccessor when we checked dir(polyAccessor), we can use a visitor to enumerate the polygons which the accessor can select. Basically that means that we can create a visitor that will cycle through all of the polygons in our primary mesh layer, and do the same thing to each one. For our purposes, this is really just the bottom part of the script we've already created.

So how do we set a visitor up? Since visitors act like plug-ins, we need to include its definition in our python file and put that file in a lxserv folder. That way, modo imports it on launch and properly interprets it as a plugin. In your user folder there's a "scripts" folder where you can create a subdirectory called "lxserv" if it doesn't exist. In this python file, we need to create a visitor class, and have it inherit the Visitor Super Class from the lxifc module that's included with modo's SDK. That means we need to import the lxifc module (as well as the lx module) at the start of our script/plugin. We'll call the class uvVisitor, because that's essentially what it is, but you can name your visitors anything you'd like.

#python
import lx, lxifc
 
class uvVisitor(lxifc.Visitor):

Inside of our uvVisitor class, we'll define two different methods. These are both inherited methods and need to be named exactly as they are. We'll def the __init__() method, where we set up which arguments we need to give the visitor in order for it to be able to work corectly, and the vis_Evaluate() method, which is the method that fires when we call an Enumerate() method from somewhere else.

#python
import lx, lxifc
 
class uvVisitor(lxifc.Visitor):                  
''' A new visitor object with inherited properties from the lxifc.Visitor class
    We just need a Polygon Accessor and Mesh Map Accessor to call the MapEvaluate()
    method as we cycle through polygons and check UV values '''
    def __init__(self, polyAccessor, mapID):     # The initial setup method 
        self.polyAccessor = polyAccessor
        self.mapID = mapID
        self.storage = lx.object.storage('f',2)  
# We'll create the storage item within our visitor object 
# so that we don't need to feed in more variables than necessary
        self.values = []                         
# We'll also create an empty list where we can save the UV values
 
    def vis_Evaluate(self):                           
# When the vis_Evaluate() method is running, it's as though each polygon
# gets selected (in the API sense). That means we can skip the step of 
# selecting a polygon by it's index (polyAccessor.SelectByIndex(0) in our early script)
        nVerts = self.polyAccessor.VertexCount()
        for eachVert in range(nVerts):
            vertID = self.polyAccessor.VertexByIndex(eachVert)
            if self.polyAccessor.MapValue(self.mapID, vertID, self.storage) == True:  
                currentValue = self.storage.get()
                if currentValue not in self.values:      
                    self.values.append(currentValue)

Notice that the bottom section of the visitor is almost exactly the same as our early code. Visitors might seem like a slightly complicated concept, but it's just a different way of organizing the same processes you would go through in a while or for loop. So how do we get this visitor to interact with our other code, and how do we use it? Well the best way would probably be to create a command which calls everything for us, but that's a little beyond the scope of this walkthrough. Instead, we're just going to write a new function outside of the visitor class we created that we can import and call from the same python file. So what do we put in this new method? Most of it will be familiar stuff from our earlier code, but there's a few gaps we need to fill in. Back to the interpreter!

If we check the docs on our Polygon Accessor's Enumerate() method, here's what we get:

 poly.Enumerate.__doc__
 Enumerate(integer mode, object visitor, object monitor)

We need to give the Enumerate() method an integer that specifies the mode, a visitor object (an instance of the class we just created), and a monitor object (if we want a progress monitor to come up during enumeration). The mode refers to the way that our Visitor object is going to mark a polygon as already looked at once it gets done with it. In the python API there's only one mark mode: lx.symbol.iMARK_ANY. So our first argument for Enumerate() will be lx.symbol.iMARK_ANY. Now we need to create an instance of our Visitor Class. To do this, we need to come up with a variable name for this instance, and set it equal to our Visitor Class name with any needed variables included. So here's what this looks like:

def execute():
    # We start out with the setup code we wrote earlier to localize the mesh and all that
    # This time, however, it's inside of a function so we can call it from python
 
    ## Localize our mesh ##
    layerService = lx.service.Layer()
    layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSCAN_PRIMARY)
    localizedMesh = layerScanObject.MeshBase(0)
 
    ## Create Accessors (Remember we're creating our storage object inside the visitor class)##
    polyAccessor = localizedMesh.PolygonAccessor()
    mapAccessor = localizedMesh.MeshMapAccessor()
 
    ## Get Map ID ##
    mapAccessor.SelectByName(lx.symbol.i_VMAP_TEXTUREUV,"Texture")
    mapID = mapAccessor.ID()
 
    ## Now we have what we need to create our Visitor Instance ##
    visitorInstance = uvVisitor(polyAccessor, mapID)
 
    ## With an instance of our Visitor class, we can now call Enumerate() on our polyAccessor
    polyAccessor.Enumerate(lx.symbol.iMARK_ANY, visitorInstance, 0)    
    ## We put 0 instead of a monitor object, because we won't be using a progress monitor for this example
 
    ## Remember we have to include the Apply() call to let modo know we're done editing the mesh
    layerScanObject.Apply()
 
    # After we've run the polyAccessor.Enumerate() method, our visitor class has created a list within
    # the visitorInstance called "values".  We can get to it as shown below.
    lx.out(visitorInstance.values)

So now we just need to put all of this in a file, and learn how to call it from the persistent interpreter. Here's all the code put together, with some briefer comments:

#python
import lx, lxifc
 
class uvVisitor(lxifc.Visitor):                  
    def __init__(self, polyAccessor, mapID):  
        self.polyAccessor = polyAccessor
        self.mapID = mapID
        self.storage = lx.object.storage('f',2)  
        self.values = []
 
    def vis_Evaluate(self):
        nVerts = self.polyAccessor.VertexCount()
        for eachVert in range(nVerts):
            vertID = self.polyAccessor.VertexByIndex(eachVert)
            if self.polyAccessor.MapEvaluate(self.mapID, vertID, self.storage):
                currentValue = self.storage.get()
                if currentValue not in self.values:    
                    self.values.append(currentValue)
 
def execute():
    ## Localize our mesh ##
    layerService = lx.service.Layer()
    layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSCAN_PRIMARY)
    localizedMesh = layerScanObject.MeshBase(0)
 
    ## Create a Storage Object and Accessors ##
    polyAccessor = localizedMesh.PolygonAccessor()
    mapAccessor = localizedMesh.MeshMapAccessor()
 
    ## Get Map ID ##
    mapAccessor.SelectByName(lx.symbol.i_VMAP_TEXTUREUV,"Texture")
    mapID = mapAccessor.ID()
 
    # Enumerate the polygons in our mesh, get UV values, make sure modo knows 
    # we're done editing the mesh and output the values from our UVs
    visitorInstance = uvVisitor(polyAccessor, mapID)
    polyAccessor.Enumerate(lx.symbol.iMARK_ANY, visitorInstance, 0)
    layerScanObject.Apply()
    for i in visitorInstance.values:
        lx.out(i)

If we save this to a .py File, we can import it from the interpreter and manually call its execute() method. The file has to be inside the lxserv folder before modo starts in order for modo to see it, though. For instance, if we save it as 'readuv.py' in our user scripts/lxserv folder, typing this into the python interpreter will run it:

 import readuv
 readuv.execute()

If we look in the results of the python interpreter, we should see a list of UV values for our the current mesh, as long as it has a uv map named "Texture" in it.