Tuesday, February 23, 2010

Capping a face with a library cap piece

For the past 3 days I've been working on a solution to replace (cap) a face of varying orientations and size with a standardized library of cap pieces using MaxScript. I'm pretty sure I've got all the kinks worked out and it appears to be working as expected.

The steps to do this are as follows (Don't worry, I'll break down each step further down in the post). Note: I don't include a few steps here as they are pretty common sense that you need to them, for example, I don't include deleting the bounding boxes, attaching the objects or deleting the original faces.


  1. Determine the faces that need to be capped

  2. Determine a library cap piece

  3. Determine the face orientation

  4. Determine the center position of the face

  5. Build the random cap object

  6. Create a normal reference poly

  7. Create a bounding box cap piece

  8. Store the custom FFD data for the bounding box object and the cap object

  9. Determine the corner verts on the cap object and adjust the bounding box

  10. Apply the FFD deformation

  11. Flip inverted normals



I know it looks like a lot of steps, but they are all broken down into pretty simple functions. So lets get to the breakdown.


  1. Determine the faces that need to be capped

    This step is pretty simple, at Volition we use Material ID's to differentiate between material types. By knowing the Material ID of the faces I want to cap, I use an EditablePoly method called selectByMaterial. The commands looks like this : .EditablePoly.selectByMaterial .

    Once that command is run, I am able to use a polyOp method to get the selected faces: polyOp.getFaceSelection

  2. Determine a library cap piece

    This step is to project dependant to really be explained, it is all up to how the project wants to build and setup a cap library. I will explain the proposed idea for our library in another post.

    One important thing to note in this section though is the parameters my library pieces need to have to work correctly using my methods. For me there are two important things:

    1. Flagged corner verts using bitFlags

      This step requires the artist designing the cap pieces to select the 4 corner verts on a cap piece and "flag" then using vertex bit flags. The code to do that is below:



      fn set_vertex_bit_flags obj vertex_list bit_flag bit_value = (
      -- Unreserved bits
      if (bit_flag > 24 and bit_flag < 33) then (
      -- Build the bit flag
      bit_to_set = bit.set 0 bit_flag bit_value

      -- Set the vertex flag
      obj.setVertexFlags vertex_list bit_to_set
      ) else (
      messageBox "Invalid bit flag" title:"Invalid bit flag"
      )
      )

      fn get_vertex_bit_flags obj bit_flag bit_value = (
      -- Return value
      verts_with_flag = #{}

      -- Unreserved bits
      if (bit_flag > 24 and bit_flag < 33) then (
      -- Build the bit flag
      bit_to_get = bit.set 0 bit_flag bit_value

      -- Get the verts matching the flags
      verts_with_flag = polyOp.getVertsByFlag obj bit_to_get
      ) else (
      messageBox "Invalid bit flag" title:"Invalid bit flag"
      )

      -- Return the value
      verts_with_flag
      )




    2.  
    3. Triangulated mesh

      Due to how max handles winding orders of faces and poly creating, the mesh needs to be triangulated for the method I use to flip the normals to work. You'll see more on that later. This can be automated though using polyOp.ConnectVertices. Code is as follows:



      polyOp.setVertSelection <selection> #{1..(polyOp.getNumVerts <selection>)}
      <selection>.ConnectVertices()


       



  3. Determine the face orientation

    This one took a little bit to get right, but here are the steps


    1. Get the face normal of the face we are aligning too

      Pretty simple step, polyOp.getFaceNormal

    2. Get the edges from the face

      Again, pretty simple, polyOp.getEdgesUsingFace . This bitArray will be used in a further step

    3. Get the verts from the face

      polyOp.getVertsUsingFace . This bitArray will be used in a further step

    4. Look for the first vert that has 3 edges and store those edges

      This step is to make sure we get a corner vert, not floating vert on an edge. This may be unneccesary on your geometry, but for ours this is a neccessary step

    5. Determine the unused edges

      Subtracting the vert edges from the face edges will result in a bitArray of unused edges

    6. Determine the used edges


      Subtracting the vert edges from the unused edges will result in a bitArray of used edges

    7. Loop through all the used edges and build a list of verts used on each edge

      This will result in an array of coinciding verts that we will use to determine the face's orientation. Basically we are looking for a vert list that looks like the following image:


    8. Build our vectors

      Using the positions of our 3 verts, determine our vectors and determine the right vector on length.

      is determined by subtracting the second vert from the first vert. is determined by subtracting the last vert from the first vert.

      Once that is done, get the absolute length of both vectors, and using that length determine which is going to be used as the right vector.

      When that is determined, normalize both vectors and build your matrix using your two vectors and the face normal:



      matrix3 left_vector right_vector face_normal [0,0,0]


       



  4. Determine the center position of the face

    This is a pretty simple step as well, collect all of the vert positions using polyOp.getVert, add them all together and divide by the number of verts to get the average position (center of the face)

  5. Build the random cap object

    Simply clone the chosen cap object, this could be where you triangulate the cap as well.

  6. Create a normal reference poly

    To properly determine if the normals of the cap object were inverted in the transform, we need to create a reference poly on our cap object to get a baseline normal to compare against. By using the bounding box parameters of the cap object we can create a flat poly and flag the verts for later use. Code as follows:



    -- Get the vert positions
    vert_a_pos = [obj.min.x, obj.max.y, obj.min.z]
    vert_b_pos = [obj.max.x, obj.min.y, obj.min.z]
    vert_c_pos = [obj.max.x, obj.max.y, obj.min.z]

    -- Create the verts
    vert_a_ind = polyOp.createVert obj vert_a_pos
    vert_b_ind = polyOp.createVert obj vert_b_pos
    vert_c_ind = polyOp.createVert obj vert_c_pos

    -- Build the vert array
    vert_array = #(vert_a_ind, vert_b_ind, vert_c_ind)

    -- Make the polygon
    polyop.createPolygon obj vert_array


     

  7. Create a bounding box cap piece

    This is the box that will be used in the FFD (Free Form Deformation) calculation. This is created using the cap pieces bounding box positions. Be sure to reset the XForms and convert to a PolyObject

  8. Store the custom FFD data for the bounding box object and the cap object

    One of my collegues here, Will Smith, created a custom FFD function to use for this. We tried exploring using Max's FFD modifiers, but control was very limited and the coordinate system the FFD's use made this overly complex. We decided the best solution would be to write our own, which turns out wasn't very difficult. Since this isn't my code, I won't post it here, but essentialy we determine how much weight a vert on the FFD object (in this case our bounding box) has on the verts on the deforming object (in this case our cap object) based on the distance between the two positions. One thing to note though is that we needed to slightly scale up the bounding box to avoid division by zero and infinite numbers. I also added some checks in the function to prevent any floaters as well.

    If anyone is wondering about the math, we used the math found in this discussion on cgsociety.org for our baseline.

  9. Determine the corner verts on the cap object and adjust the bounding box

    This is where we properly adjust the bounding box to the corner verts of the face.

    To do this I loop through the verts that make up the outer edges of the face (verts with at least 3 edges) and find the closest vert based on distance. Since I know that the box I create has 8 verts, I am able to move the first 4 verts of the bounding box to the 4 corner positions of the face. The second step to this is to determine the amount I am moving the vert and then applying that amount to the bounding boxes corresponding vert, to get that index simply add 4 to the current bounding box vert you are working with.


  10. Apply the FFD deformation

    Again, using Will Smith's Custom FFD script, we apply the FFD deformation to the cap object according to the adjusted bounding box


  11. Flip inverted normals

    I'm going to avoid going on a "Why I Hate Max" rant here, but I still want to explain something about this step. As anyone who has messed with maxscript knows, having the modifier panel open slows down your maxscript quite a bit, it is always best for performance reason to have the create panel open unless you specifically need something in the modifier panel. Saying that, polyOp has a built in method for flipping face normals, polyOp.flipNormal, unfortunately it doesn't work unless the modifier panel is enabled, which slowed down the execution of my script by quite a large amount. After speaking to Jeff Hanna, he explained that the face normal is derived from the winding order of vertices, which led me to an experiment. The experiment was, would it be faster to rebuild the geometry and transfer the UVs with the correct winding order instead of opening the modifier panel and calling polyOp.flipNormal. What blew me away was the answer was a resounding yes.

    I won't go into the geometry creation or transfering of UV's, but I will explain how the reference poly comes into play here for determining if the normals are reversed.

    To determine if the cap object's normals were reversed in the transform, I get the normal of the face we are applying to the cap object to and the normal of the reference face. I then get the dot product of the reference face's normal and the cap object's face normal.

    If that value is less than 1, than I know that the normal has been reversed and I go ahead and recreate the geometry.



So there you have it, the basics for aligning a cap object to an arbitrary face.

No comments:

Post a Comment