[Maya-Python]UV Pivot Tool [Part 1]

Hello!

I started working on a tool for Maya that will store positions into vertices UVs.
If you think “what’s it for?” well, you could use it for a lot of shader operations in game engines, especially vertex shader operations.
I will show something using this when it’s finished.

This article will go through the first part of the tool, which consists in storing the selected locator (or any other transform) position in the selected vertices UVs, explaining the UI, the usage of scriptCtx for user selection callback and the uvs creation and set through the selection mode.

Here is a snapshot of the current UI:

UI.JPG

I wouldn’t definitely call that a complex tool, but hey, that’s just the beginning 😉

Let’s start having a look to the UI code.

import maya.cmds as cmds

class UVPivotUI:

    @classmethod
    def show_ui(cls):
        win = cls()
        win.create()
        return win

    # inizialization
    def __init__(self):
        self.window = 'UVPivot Main Window'
        self.title = 'UV Pivot'
        self.size = (400, 350)
        self.action_name = 'Apply and Close'

        self.main_layout = None

        self.help_menu = None
        self.help_menu_items = None

        self.common_btn_size = ((self.size[0] - 18) / 3, 26)

        # Buttons
        self.action_btn = None
        self.apply_btn = None
        self.close_btn = None

        self.mode_radio = None
        self.history_checkbox = None

We start with a simple definition of the class and a class method that will return our window object.
The class itself doesn’t have anything special, just some variables like the window and title, the size, the actions and other initialized variables.
This is very similar to another article I wrote in this blog.

After defining the class, we can start defining some methods.

    def create(self):
        if cmds.window(self.window, exists=True):
            cmds.deleteUI(self.window, window=True)

        self.window = cmds.window(
                                    self.window,
                                    title=self.title + ' | %s' % self.window,
                                    widthHeight=self.size,
                                    menuBar=True,
        )

The previous bit of code will take care of creating the window and killing it if it already exists, this will prevent multiple instances of the same tool being started at the same time.

        # set layout and attachment rules for responsive resizing
        self.main_layout = cmds.formLayout(nd=100)

        self.common_menu()
        self.common_buttons()

        options_border = cmds.tabLayout(
            scrollable=True,
            tabsVisible=False,
            height=1
        )
        cmds.formLayout(
            self.main_layout, e=True,
            attachForm=(
                [options_border, 'top', 0],
                [options_border, 'left', 2],
                [options_border, 'right', 2]
            ),
            attachControl=(
                [options_border, 'bottom', 5, self.apply_btn]
            )
        )
        self.display_options()

        cmds.showWindow()

We defined our main layout creating a formLayout as its child, then we spawned the window.

    # menu definition
    def common_menu(self):
        self.help_menu = cmds.menu(label='Help')
        self.help_menu_items = cmds.menuItem(
            label='Help on %s' % self.title,
            command=self.help_menu_cmd
        )

    # buttons
    def common_buttons(self):
        self.action_btn = cmds.button(
            label=self.action_name,
            height=self.common_btn_size[1],
            command=self.action_btn_cmd
        )

        self.apply_btn = cmds.button(
            label='Apply',
            height=self.common_btn_size[1],
            command=self.apply_btn_cmd
        )

        self.close_btn = cmds.button(
            label='Close',
            height=self.common_btn_size[1],
            command=self.close_btn_cmd
        )

        # attachment rules for the buttons
        cmds.formLayout(
            self.main_layout, e=True,
            attachForm=(
                [self.action_btn, 'left', 5],
                [self.action_btn, 'bottom', 5],
                [self.apply_btn, 'bottom', 5],
                [self.close_btn, 'right', 5],
                [self.close_btn, 'bottom', 5]
            ),
            attachPosition=(
                [self.action_btn, 'right', 1, 33],
                [self.close_btn, 'left', 0, 67]
            ),
            attachControl=(
                [self.apply_btn, 'left', 4, self.action_btn],
                [self.apply_btn, 'right', 4, self.close_btn]
            ),
            attachNone=(
                [self.action_btn, 'top'],
                [self.apply_btn, 'top'],
                [self.close_btn, 'top']
            )
        )

    def help_menu_cmd(self, *args):
        cmds.launch(web='http://ue4techarts.com')

    def action_btn_cmd(self, *args):
        self.apply_btn_cmd()
        self.close_btn_cmd()

    def apply_btn_cmd(self, *args):
        mode = cmds.radioButtonGrp(self.mode_radio, q=True, select=True)
        history_checkbox = cmds.checkBox(self.history_checkbox, q=True, value=True)
        execute(mode=mode, history_checkbox=history_checkbox)

    def close_btn_cmd(self, *args):
        cmds.deleteUI(self.window, window=True)

    def display_options(self):
        cmds.frameLayout(
            label='Detection Mode',
            collapsable=False,
        )

        self.mode_radio = cmds.radioButtonGrp(
            label='Detection mode:',
            labelArray2=[
                'Selection',
                'Closer',
            ],
            numberOfRadioButtons=2,
            select=1,
            columnAlign=(1,"left")
        )

        cmds.frameLayout(
            label='Mesh Settings',
            collapsable=False
        )

        self.history_checkbox = cmds.checkBox(
            label='Delete History before applying',
            al='center'
        )

All the rest of the code is generating the buttons and assigning them callbacks (e.g. def action_btn_cmd).

The next bit of code is more interesting.

def start_ctx():
    print "Starting Context."

def create_context():
    locator_ctx = cmds.scriptCtx(title="Test Ctx", setNoSelectionPrompt="select Locator and vertices.",
                                 toolStart='python("start_ctx()");',
                                 finalCommandScript='python("check_selection()");',
                                 totalSelectionSets=1,
                                 setAutoComplete=False,
                                 toolCursorType="create")
    cmds.setToolTo(locator_ctx)

We will use scriptCtx from cmds to wait for the user selection.
We could also use scriptJob, but I prefer to use this since the scriptJobs are a bit more a pain to manage.
To use scriptCtx, we need three functions:

  1. start_ctx: the function called when the tool starts.
  2. create_context: a function to call to actually create the scriptCtx context
  3. end_ctx: the function called when the tool is executed.

In our case, the tool is executed once the user press “Enter“, so we just want to check the selection.

In the previous code you can see how I set

'python("check_selection()");',

that will assign the “check_selection()” function we will define to the end_ctx callback.


def execute(**kwargs):
    mode = kwargs["mode"]
    history_checkbox = kwargs["history_checkbox"]

    if mode == 1:
        cmds.confirmDialog(message="Select the locator and the vertices, finally press Enter.", button=["Ok"])
        create_context()

def check_selection():
    selection = cmds.ls(sl=True)
    locator = None
    vertices = None
    for ob in selection:
        if cmds.objectType(ob) == "mesh":
            vertices = ob
        elif cmds.objectType(ob) == "transform":
            locator = ob

    if vertices and locator:
        assign_uv_to_vertices(vertices, locator)
    else:
        cmds.confirmDialog(message="Error. Selection not valid.", button=["Ok"])

The previous code contains the execute() function, which is called directly by the UI class with the Apply button, and the check_selection() function, that will run some basic checks on the selection before calling the most useful function: assign_uv_to_vertices(vertices,locator).

def assign_uv_to_vertices(vertices, locator):
    cmds.select(vertices)
    all_mesh_uv_sets = cmds.polyUVSet(q=True, auv=True)
    uv_set_a = "pivot_painter_set_1"
    uv_set_b = "pivot_painter_set_2"

    def create_pivot_painter_uvs(original_uv_set):
        cmds.polyUVSet(copy=True, uvs=original_uv_set, nuv=uv_set_a)
        cmds.polyUVSet(copy=True, uvs=original_uv_set, nuv=uv_set_b)

create_pivot_painter_uvs() is used later to create the 2 UV sets used by the tool.

    def apply_uvs():
        cmds.select(vertices)
        locator_position = cmds.getAttr("{}.translate".format(locator))[0]
        cmds.polyUVSet(uvs=uv_set_a, cuv=True)
        cmds.polyEditUV(relative=False, uValue=float(locator_position[2]), vValue=float(locator_position[0]),
                        uvs=uv_set_a)
        cmds.polyUVSet(uvs=uv_set_b, cuv=True)
        cmds.polyEditUV(relative=False, uValue=float(locator_position[1]), vValue=0,
                        uvs=uv_set_b)

This is the code that actually stores the locator position.
Remember that we need two UVsets because we need to store XYZ values, and an UVset can only store two values being a float2.
If you can’t understand why I am putting the Z in the X, the Y in the X and the Y in the Z: It’s because I want to use this tool in combination with UE4, which has a different coordinate system than Maya.

We will probably add some checks later to ask the user which engine he is using, in order to use an appropriate coordinate system.

    if len(all_mesh_uv_sets) == 1:
        user_input = cmds.confirmDialog(message="Maya Pivot Painter Tool needs 2 uv sets to store values. "
                                   "Can we create them for you?", button=["Ok.", "Cancel."])
        if user_input == "Ok.":
            create_pivot_painter_uvs(all_mesh_uv_sets[0])
            apply_uvs()
            return
        if user_input == "Cancel.":
            return

    elif len(all_mesh_uv_sets) == 2:
        user_input = cmds.confirmDialog(message="Maya Pivot Painter Tool needs 2 uv sets to store values. "
                                                "Can we create them for you?", button=["Use my second one and create "
                                                                                       "a third one (if it doesn't exist).",
                                                                                       "Ok, create 2 new ones.", "Cancel."])
        if user_input == "Use my second one and create a third one (if it doesn't exist).":
            original_second_uv_set = all_mesh_uv_sets[1]

            cmds.polyUVSet(rename=True, uvs=original_second_uv_set, nuv=uv_set_a)
            create_pivot_painter_uvs(all_mesh_uv_sets[0])
            apply_uvs()
            return

        elif user_input == "Ok, create 2 new ones.":
            create_pivot_painter_uvs(all_mesh_uv_sets[0])
            apply_uvs()
            return

        elif user_input == "Cancel.":
            return

    elif len(all_mesh_uv_sets) >= 2:
        user_input = cmds.confirmDialog(message="Maya Pivot Painter Tool needs 2 uv sets to store values. "
                                                "Can we create them for you?", button=["Use my first and second existing "
                                                                                       "Uv Sets.",
                                                                                       "Ok, create 2 new ones.", "Cancel."])
        if user_input == "Use my first and second existing Uv Sets.":
            uv_set_a = all_mesh_uv_sets[1]
            uv_set_b = all_mesh_uv_sets[2]

        elif user_input == "Ok, create 2 new ones.":
            create_pivot_painter_uvs(all_mesh_uv_sets[0])
            apply_uvs()
            return
        elif user_input == "Cancel.":
            return

UVPivotUI().show_ui()

Hopefully this will be helpful to someone, if you have any question please let me know.
See you soon with the part 2, in which we will use not the selected locator but we’ll catch the closest one to the vertices based on some users input!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s