Reading UV Map Values

From The Foundry MODO SDK wiki
Revision as of 00:12, 20 January 2014 by Chris Hague (Talk | contribs) (Creating a Visitor)

Jump to: navigation, search


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_LAYERSSCAN_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)

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 ='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.



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:


and get

 vector normal = Normal()

The info from the docs is organized so that the information you get from a method is on the right, and the method you're checking is on the left. 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:

 (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 MapValue() Method. So a quick look at the docs for it gives us:

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

So the MapValue 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 indices 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.

  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()  #  Get the verts Index
      if polyAccessor.MapValue(mapID, vertID, storageBuffer) == True:  
      ## If the MapValue() 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

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:

## Localize our mesh ##
layerService = lx.service.Layer()
layerScanObject = layerService.ScanAllocate(lx.symbol.f_LAYERSSCAN_PRIMARY)
localizedMesh = layerScanObject.MeshBase(0)
## Create a Storage Object and Accessors ##
storageBuffer ='f',2)
polyAccessor = localizedMesh.PolygonAccessor()
mapAccessor = localizedMesh.MeshMapAccessor()
## Get Map ID ##
mapID = mapAccessor.ID()
## Select a polygon and read its vertex UV values into a list of tuples ##
uvValues = []
nVerts = polyAccessor.VertexCount()
for eachVert in range(nVerts):
    vertID = polyAccessor.VertexByIndex()
    if polyAccessor.MapValue(mapID, vertID, storageBuffer) == True:  
        values = storageBuffer.get()  

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. But that isn't the best way to handle this. For situations like this, when you're going to be looping through ever 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