HOW TO ... FIT THE UVW MAPPING GIZMO
Code by Borislav "Bobo" Petrov
Last Edited: 2006/12/03
 
The UVW Mapping modifier has a FIT button in the UI which fits the gizmo to the underlying geometry
by centering the gizmo the the object or selection while preserving the current orientation, resetting the scaling
of the gizmo and setting the Length, Width and Height values depending on the mapping type and axis orientation.

Unfortunately, the FIT button is not directly exposed to MAXScript, but its functionality could be replicated because
the transformation matrix of the UVW Gizmo is exposed.

 
WHAT THE FIT FUNCTION DOES:
  • Let's assume the object is an arbitrary geometry with arbitrary position, rotation and scale. The node's PRS do not play any role because the modifier is applied in OBJECT space and the node transformations have not been applied yet.
  • An UVW Mapping modifier is applied to the object - normally it performs the fit automatically when first assigned.
  • Let's assume the Gizmo has been moved, rotated, scaled, the Width, Length and Height have been changed, the Axis might have been changed too, the user might have used the Normal Align tool or applied a script to perform some alignment, the gizmo could be any of the supported types etc. In other words, the gizmo is in some completely arbitrary state.
  • The gizmo's transformation matrix describes an offset from the object space of the node. In other words, when the gizmo's TM is the identity matrix, the gizmo is perfectly aligned and centered to the object space.
  • When fitting, the gizmo's scaling is reset back to 100% first. This is important to avoid skewing the results of the projection of geometry into the gizmo coordinate system.
  • Then, the vertices of the geometry the gizmo is applied to or a sub-set of the vertices if a Sub-Object selection is passed up are transformed into the space of the gizmo. The X, Y and Z coordinates of all vertices are compared and the min. and max. values are collected, thus finding the bounding box aligned to the map gizmo that encloses all processed vertices. Since the center of the gizmo can be anywhere relatively to the geometry at this point, the bounding box is not necessarily centered at the gizmo, but its X, Y and Z axes are parallel to the gizmo's axes.
  • The gizmo's form and orientation is dependent on the Axis specified in the UI. This does not affect the gizmo's transformation matrix, but controls which of the dimensions of the gizmo-aligned bounding box will be applied to which axes of the gizmo. When the Axis is set to Z, the X, Y and Z dimensions of the bounding box are assigned to the X, Y and Z of the gizmo. When the Axis is set to X ot Y, the Z of the bounding box will be assigned to the X or Y of the gizmo.
  • If no vertices have been passed to the modifier (SO is active but nothing is selected), the regular FIT function sets all dimensions to 10, probably to avoid getting a gizmo with a size of 0.
  • If there are vertices passed to the modifier, the actual values for the length, width and height are passed using 0.1% of the calculated sizes. This is probably to avoid having texels being cut off at the edges of a map. For example, if you create a plane with 100x100 units size and apply UVW Mapping to it, the Length and Width will both read 100.1 instead of 100.0
  • Depending on the mapping type, the bounding box values are used differently.
    • For Planar mapping, the X and Y are used for Width and Length, with the Height being being set to the Z but it is normally disabled in the UI.
    • For Cylindrical mapping, the Length and Width will be set to the larger of the two values corresponding to the radii of the cylinder's base. Since the base changes depending on the Axis settings, this requires some special handling. The height of the gizmo is always set to the 3rd axis of the bounding box.
    • For Spherical and Shrinkwrap mapping, all 3 values (Length, Width and Height) and set to the largest of the 3 dimensions of the bounding box.
    • For Box mapping, the values of the bounding box are used directly as the dimensions of the mapping
    • For Face and XYZ to UVW modes, the UVW Mapping behavior is to set the Width and Length to the maximum of the X and Y dimensions of the bounding box and the Height to the Z dimension. It is not clear why this is the default behavior, but the script recreates the same values just to stay close to the original.
  • Once the bounding box is applied to the Length, Width and Height of the modifier, the gizmo's center is moved to the middle of the calculated bounding box. The center is easily calculated as the half of the diagonal connecting the minimum and the maximum coordinates of the box (the two corners calculated by collecting the min. and max. X, Y and Z. To center the gizmo, the row4 component of the transformation matrix (the translation) is simply set to the middle of the bounding box' diagonal.
The following script describes the steps necessary to fit the UVW Mapping gizmo to the geometry using a MAXScript function.
It is NOT based on the source code of the UVW Mapping modifier but was reverse-engineered to provide the same results.
NOTE:
The UVW Mapping modifier operates on the INCOMING geometry below it.
Since the script cannot acquire the geometry at an arbitrary level of the modifier stack,
it is assumed that the script will be run when the UVW Mapping modifier is the TOP modifier on the stack.
If this is not the case, chances are that the geometry on top of the modifier stack might not be the same as
the geometry coming into the UVW Mapping modifier. In these cases, the results of the script might not match
the results of the built-in FIT button.
 
SCRIPT
fn UVWMapFit theObj theMap useSel:#none=
(
--RESET THE GIZMO SCALE FIRST:

--Multiply the gizmo's transformation with the inverse of a matrix created from its scale factor
theMap.gizmo.transform *= inverse (scaleMatrix theMap.gizmo.transform.scalepart)
--RESULT: the gizmo scaling will be reset to [1,1,1]

--GET THE MESH IN OBJECT SPACE BEFORE PRS AND WSM
--NOTE that this will get the mesh from TOP of the modifier stack just before PRS transforms.
--This means that the script SHOULD be applied to the UVW_Mapping modifier only when it is
--the TOP modifier on the stack, otherwise the geometry used might not be the geometry
--that the modifier is really applied to.
theMesh = theObj.mesh

--TRANSFORM ALL VERTICES IN GIZMO SPACE AND FIND MIN. AND MAX. OF GIZMO BBOX
minX = minY = minZ = 100000000 --initialize  min.values to very large numbers
maxX = maxY = maxZ = -100000000 --initialize max.values to very low numbers
theTM = theMap.gizmo.transform --store the transformation matrix of the UVW gizmo in a variable
theTMInv = inverse theTM --calculate the inverse of the gizmo TM and store in another variable
 

--DEPENDING ON THE OPTIONAL 3RD ARGUMENT, GET THE VERTICES TO OPERATE ON:
theVerts = case useSel of
(
--If no selection is defined, use a bitarray containing ALL vertices.
#none: #{1..theMesh.numverts}

--If vertex selection is requested, get the selection from the mesh
#verts: (getVertSelection theMesh)

--if edge selection is requested, convert the edge selection to vertex list:
#edges: (meshop.getVertsUsingEdge theMesh (getEdgeSelection theMesh))

--if face selection is requested, convert the face selection to vertex list:
#faces: (meshop.getVertsUsingFace theMesh (getFaceSelection theMesh))
)
 

for v in theVerts do --loop through all vertices in the bitarray defined above
(
  theVert = (getVert theMesh v) * theTMInv --get the vertex position in gizmo space

--Record min. and max. values for X, Y and Z:
 
if theVert.x > maxX do maxX = theVert.x
  if theVert.y > maxY do maxY = theVert.y
  if theVert.z > maxZ do maxZ = theVert.z
  if theVert.x < minX do minX = theVert.x
  if theVert.y < minY do minY = theVert.y
  if theVert.z < minZ do minZ = theVert.z
)--end v loop


delete theMesh --delete the TriMesh value from memory

--CALCULATE THE GIZMO-ALIGNED BOX SIZE
case theMap.axis of --take into account axis orientation
(
  0: (
    X = maxZ - minZ  --gizmo's Z axis is aligned to the object's local X axis
    Y = maxY - minY
    Z =maxX - minX
  )
  1: (
    X = maxX - minX
    Y = maxZ - minZ --gizmo's Z axis is aligned to the object's local Y axis
    Z =maxY - minY
  )
  2: (
    X = maxX - minX
    Y = maxY - minY
    Z = maxZ - minZ --gizmo's Z axis is aligned to the object's local Z axis
  )
)--end case

if theVerts.numberset == 0 then --if no vertices processed, set all sizes to 10
  X = Y = Z = 10
else --if any vertices are processed, add 0.1 % padding
(
  X += 0.001*X
  Y += 0.001*Y
  Z += 0.001*Z
)

--Set the values for length, width and height in the general case
theMap.length = Y
theMap.width = X
theMap.height= Z

case theMap.maptype of --now take into account the mapping type
(
  default: theMap.width = theMap.length = amax #(X,Y) --get the bigger of the two values for width and length
  0: () --do nothing for planar mode - will use the above general case
  1: (
    case theMap.axis of --special axis handling for cylinder!
    (
      0: (theMap.width = theMap.length = amax #(X,Z); theMap.height = Y)
      1: (theMap.width = theMap.length = amax #(Y,Z); theMap.height = X)
      2: (theMap.width = theMap.length = amax #(X,Y); theMap.height = Z)
    )--end axis case
  )
  2: theMap.width = theMap.length = theMap.height = amax #(X,Y,Z) --radius from the largest of the 3 values
  3: theMap.width = theMap.length = theMap.height = amax #(X,Y,Z) --radius from the largest of the 3 values
  4: () --do nothing for box mode - will use the above general case
)--end maptype case

--CALCULATE THE CENTER IN GIZMO SPACE - IT IS THE MIDDLE OF THE BOX'S DIAGONAL
theCenter = ([maxX,maxY,maxZ] + [minX,minY,minZ])/2.0
--CONVERT THE CENTER INTO OBJECT SPACE BY MULTIPLYING WITH THE GIZMO TM
theCenter *= theTM
--THEN CENTER THE GIZMO
theTM.row4 = theCenter --set the translation part of the matrix to the new center
theMap.gizmo.transform = theTM --and assign the matrix back to the gizmo transformation
)--end function

 

CALLING THE FUNCTION
--Print the old gizmo transform.
--If you hit FIT in the UI before running the script and then run this line,
--the results from the script should be identical to the result of the FIT:

theMap.gizmo.transform

--Call the funtion, specifying the object to get the geometry from, the modifier to fit and the selection level to use:
UVWMapFit $Teapot01 $Teapot01.uvw_Mapping useSel:#none

 

Want to learn more about vector and matrix operations in MAXScript?
Check out this 5 hours introduction into the world of mathematics for 3ds Max users: