Python Tools - Session 10A

Level of Detail (LOD) Creator

March 10, 2020

Session Navigation

Overview

This session creates a tool for generating multiple levels of detail (LOD) for 3D meshes. Users select a mesh and specify the number of LODs and reduction percentage. The tool duplicates the mesh and progressively reduces polygon count for each LOD level, then creates a Maya LOD group compatible with game engines.

Key Concepts

  • Polygon reduction for LOD creation - using pm.polyReduce() to lower triangle counts
  • Mesh duplication and progressive iteration - duplicating each LOD level from the previous one
  • Maya LOD groups for game engine compatibility - using pm.mel.LevelOfDetailGroup()
  • UI widget properties vs. values - understanding that UI widgets must be queried with getValue()
  • Multiple LOD level chaining - passing each reduced mesh as the source for the next level
  • xrange loops for iteration - counting through LOD levels with xrange()

Code

The full LOD creator tool (lod_creator.py):

"""
#############################################################################
filename    lod_creator.py
author      Matt Osbond 
dP email    m.osbond@digipen.edu
course      CS115
Brief Description:
    Session 10A - Tuesday Mar 10th
    Simple LOD tool
#############################################################################
"""

import pymel.core as pm

def create_lod_level(mesh_node, reduction):
    """Create a single new LOD level
    This will duplicate the given mesh then reduce it by the given amount
    """
    # Duplicate the mesh - rememering to capture the FIRST element in the returned list (the zero'th index)
    dupe = pm.duplicate(mesh_node, rr=True)[0]

    # Poly reduce the duplicate
    pm.polyReduce(dupe, percentage=reduction)

    # Return the duplicate mesh transform
    return dupe

class LodUI():
    """Base UI class
    """

    def __init__(self):

        # Create a window handle name
        # This is only ever seen by maya
        # Note: DO NOT INCLUDE SPACES IN THIS! Things will break...
        self.handle = 'LodCreator'

        # Check if the window exists, and delete it if it does
        if pm.window(self.handle, exists=True):
            pm.deleteUI(self.handle)

        # Create the window, defining the title and size
        # We pass in the window handle first, as that forces Maya to use it - which means we can check for it (above)
        with pm.window(self.handle, title='LOD Creator', width=400, height=100):

            # Start a column layout
            with pm.columnLayout():

                # Create an integer slider to define the number of LODs
                # This is cast to a "class property" (self.ui_numlods) so we can query it in the method "create_lods"
                self.ui_numlods = pm.intSliderGrp(label='Num LODs', field=True, value=3, min=1, max=10)

                # Create a float slider to control the reduction percentage
                self.ui_perc = pm.floatSliderGrp(label='Reduce By', field=True, value=50, min=10, max=90)

                # Create a button to trigger things
                # The trigger command for the method "create_lods" is attached using a callback
                pm.button(label='Create LODs', w=100, h=30, command=pm.Callback(self.create_lods))

        # Show the window created with the window handle
        pm.showWindow(self.handle)

        
    def create_lods(self):
        """Class method to create the LODs on the selected objects
        """
        # Query the selected objects
        # We cast this to a new variable, as the selection will change as we go - we want the original selection
        selected_objects = pm.ls(sl=True)

        # Iterate through each selected object
        for selected_obj in selected_objects:

            # Create a new variable to track the current mesh - assign it to be the source mesh for now
            current_mesh = selected_obj

            # Create a list of all LOD levels for the current object
            lod_levels = [current_mesh]

            # FOR loop that will count up to the number in the slider field
            # Note: the ui property "self.ui_perc" is a UI widget, NOT the actual value
            # To get the value, we have to use "getValue()" to query the widget data
            for i in xrange(0, self.ui_numlods.getValue()):

                # Call the create LOD function, using the current mesh as the source
                # However, we also re-cast the "current_mesh" variable to be the result from the function (the duplicate)
                # This lets us "chain" the LODs, by passing in the previously created mesh
                current_mesh = create_lod_level(current_mesh, self.ui_perc.getValue())

                # Add the current LOD level to our LOD list for the current source mesh
                lod_levels.append(current_mesh)

            # Create an LOD group that's compatible with game engines
            # Maya expects the meshes to be selected, so we have to select all meshes for the CURRENT source mesh
            # (meaning, we do this once for each object in our original selection)
            pm.select(lod_levels)

            # Run the LOD group command
            pm.mel.LevelOfDetailGroup()

Live coding notes from the session (notes.py):

import pymel.core as pm
import random

def create_lod_level(node, perc):
    dupe = pm.duplicate(node, rr=True)[0]
    pm.polyReduce(dupe, percentage=perc)
    return dupe

    
class LodUI():
    """Base UI class
    """
    def __init__(self):
        """Initialization
        """
        # Set the window handle name. This is what Maya sees.
        self.handle = 'LODCreator'

        # Delete the window if it exists
        if pm.window(self.handle, exists=True):
            pm.deleteUI(self.handle)

        # Delete the window preferences if they exist
        if pm.windowPref(self.handle, exists=True):
            pm.windowPref(self.handle, remove=True)

        # Create the window using a context
        with pm.window(self.handle, title='LOD Creator', w=400, h=100):
            # Each layout can be creaed using contexts too
            with pm.columnLayout():

                # Number of LODs
                self.ui_int_lodcount = pm.intSliderGrp(label='Num LODs', field=True, value=3, min=1, max=8)

                # Reduction percentage
                self.ui_float_perc = pm.floatSliderGrp(label='Reduce By', field=True, value=50, min=10, max=90)

                # Create the three buttons, attaching each one their method
                pm.button(label='Create LODs', w=100, h=40, command=pm.Callback(self.create_lods))

                
        # Show the UI at the end of initialization
        pm.showWindow(self.handle)

        
    def create_lods(self):
        selected_items = pm.ls(sl=True)

        for node in selected_items:
            if node.nodeType() == 'mesh' or node.getShape().nodeType() == 'mesh':
                print ('Creating LODs for %s' %node)

                lod_meshes = [node]
                for i in xrange(0, self.ui_int_lodcount.getValue()):
                    print i
                    node = create_lod_level(node, self.ui_float_perc.getValue())
                    lod_meshes.append(node)
                pm.select(lod_meshes)
                pm.mel.LevelOfDetailGroup()

LodUI()
← Prev Next ->