Почему важно узнавать подробности до старта работы
Появилась задачка: взять примерно сто тридцать шотов, настроить в них освещение, пофиксить проблемы при наличии, отправить на рендер. Софт — Autodesk Maya, а каждый шот представляет из себя отдельный файл с анимацией и всеми пирогами. И так двадцать пять раз, потому что двадцать пять эпизодов.
Когда я брался за задачку, наивно посчитал, что можно выкатывать эпизод в месяц. Примерно те же тайминги озвучили заказчики.
Жестоко ошибся: сначала стоило выяснить рабочую процедуру, благо есть у кого.
Выяснив её, я пришёл в ужас. По всему выходило, что на один эпизод улетает от полутора месяцев.
Как жить, когда ты уже не узнал подробностей
Поборов в себе судорожное желание найти новых заказчиков, сел думать, что можно сделать в этих обстоятельствах.
Я задокументировал процесс, и расписал временные ресурсы.
На множестве операций 3D-артист простаивает. Для примера, одна лишь загрузка Maya занимает минуту-две.
Звучит смешно, понимаю. Смешно перестаёт быть при взгляде на эту табличку:

Все шесть операций человек делает пару кликов на операцию, а потом задумчиво тупит в монитор. Очень не люблю такой формат работы, он высаживает всё внимание за пару часов.
Оптимизировать пайплайн проблематично: модельки русские, анимация иранская, собирается это всё и вовсе третьей стороной, плагины под проект написаны также иранцами.
Лезть во всё это великолепие руками страшно: во-первых, есть риск заблудиться навсегда, а во-вторых, убрав паузу из какого-нибудь батника можно невзначай всю телегу поломать.
Зато можно имитировать оператора ПК питоном! Я собрал у коллег информацию об ошибках, возникающих в процессе работы. Убедился в том, что большая их часть — типовые, а также не помешают работе скрипта. И начал продумывать план побега, который скостит время простоя оператора.
Первым делом разметил таски двумя цветами: обязательное присутствие мозга при выполнении, и необязательное.
Так я вычленил операции, которые можно безболезненно перевесить на скрипт.
Как решалось
Несколько раз пользовавшись библиотекой PyWinAuto, обратился к ней опять.
Людей на рендере много и у них разные машины, потому координатный подход к автоматизации отмёлся — буду ловить кнопки по названиям.
Поехали
Импортнём нужные библиотеки.
import pywinauto as pw # для работы с GUI import os # для работы с файлами и папками from time import sleep # для таймеров import logging # для ведения лога import sys # для вывода информации в терминал from PIL import ImageGrab # для скриншотов from datetime import datetime # для записи времени в название скриншота import matplotlib.pyplot as plt # на случай острого желания построить пару графиков
Первое, чего захочется — безудержного логирования! Возможно, даже со скриншотами. Но это не точно, поэтому повесим скриншот на булинь.
def log(file_name, massage, screenshot = False): logging.basicConfig( format="%(asctime)s %(message)s", encoding='utf-8', level=logging.DEBUG, handlers=[ logging.FileHandler("log\\" + file_name + '.log'), logging.StreamHandler(sys.stdout) ]) logging.debug(massage) if screenshot == True: now = datetime.now() current_time = now.strftime("%H_%M_%S_") myscreen = ImageGrab.grab() myscreen.save("log\\" + current_time + file_name + '.jpg')
Второе — получить список файлов с явками и паролями. Исходные файлы лежат в отдельной папке рядом со скриптом, так что всё удобно.
def get_files_list(): path = os.getcwd() # подхватываем адрес рабочей папки files = os.listdir(path + '\\animate') # собираем список на обработку return(files)
Третье — обработать каждый файлик. Соберу конструкцию, которую дальше буду дополнять.
def process(): files = get_files_list() for file in files: open_shot(file) baking_animation_curves(file) get_assembly(file)
Откроем файл в лоб через проводник. Допускаю, что есть более простые методы.
def open_shot(file): pw.Application().start('explorer.exe "C:\\Program Files"') # стартуем проводник app = pw.Application(backend="uia").connect(path="explorer.exe", title="Program Files") # подключаемся к нему dlg = app["Program Files"] # подключаемся к окошку dlg['Address: C:\Program Files'].wait("ready") # ждём готовности поисковой строки dlg.type_keys("%D") # переключаемся на неё хоткеем dlg.type_keys(file, with_spaces=True) # ввпечатываем туда файл dlg.type_keys("{ENTER}") app.kill() # закрываем проводник
Окно Майи появляется секунд через тридцать после обращения к файлу. Напишем функцию, которая проверит появление окна, и заодно даст доступ скрипту к Майе.
def maya_connect(): try: # пробуем подключиться к окошку: app = pw.Application(backend="uia").connect(title_re=".* - Autodesk Maya 2020.4:") print("Connected!") return app # если окошка нет: except pw.findwindows.ElementNotFoundError: # Можно взять просто time.sleep(), но так мы # дадим понять пользователю, что скрипт не висит for i in ['. ', '.. ', '...']: sys.stdout.write('\r'"Сonnecting" + i) sleep(1) return maya_connect()
Теперь надобно понять, что файл открылся, и с ним можно работать. Вариант повесить на таймер не годится: файлы весят по-разному, с-во берут разное время на загрузку.
PyWinAuto предлагает app.wait_cpu_usage_lower(threshold=5) — план хорош!
Проверим, как выглядит кривая потребления CPU Майей:
код
def get_app_load(app):
load_points = [1] # список для записи загрузки # начинается с единицы, чтобы сработал цикл timer = [0] # список для подсчёта секунд со старта записи нагрузки n = 0 # счётчик времени # пока сумма значений нагрузки за последние 60 секунд не станет нулевой, # собираем значения while sum(load_points[-60:]) != 0: load = app.cpu_usage() if load < 0.5: # убираем случайные всплески load = 0 load_points.append(load) n += 1 timer.append(n) sleep(1) plt.plot(timer, load_points) plt.show()

Майя дискретно освобождает ресурсы процессора в процессе работы, так что ждать cpu_lower смысла нет.
Совместим таймер и чек нагрузки на процессор:
def app_status(app): """если последние 30 секунд Майя не трогала процессор, будем считать, что она освободилась""" load_points = [1] while sum(load_points[-30:]) != 0: load = app.cpu_usage() if load < 0.5: # убираем случайные всплески load = 0 load_points.append(load) for i in ['. ', '.. ', '...']: sys.stdout.write('\r'"Maya is working now" + i) sleep(0.33)
Будем пользовать эту конструкцию для ожидания конца операций.
Файл открыли, можно теперь и автоматизировать что-нибудь
...предварительно обложившись инструментами. Нам понадобится:
Функция, печатающая дерево элементов в выбранном диалоге:
# для печати в файл dlg.print_control_identifiers(depth = None, filename = "MayaControls.txt") # или просто вывода dlg.print_control_identifiers(depth = None)
И функция, рисующая зелёный прямоугольник вокруг выбранного элемента:
# или не зелёный, тут как настроишь :) dlg.draw_outline(colour='green') dlg['ShowMenuItem2'].draw_outline(colour='red') dlg.MultyDoTask.draw_outline(colour='blue')
Как обращаться к элементам
Диалог — dlg — это коробка с графическими элементами. А главное окно приложения — тоже диалог, потому обращаться к интересующему можно через dlg = pw.Desktop(backend="uia")["Anything"].
Независимо от способа работы с диалогом, через connect() или Desktop, понадобится определить, что этот диалог отличает от собратьев. Часто достаточно названия, отображаемого на экране.
Внутри диалога лежит не просто графика, а контроллеры: кнопки, чекбоксы, тому подобное и... другие диалоги.
Подключаемся к диалогу:
# так: dlg = pw.Desktop(backend="uia")["Untitled - Notepad"] # либо так: app = pw.application.Application(backend="uia").connect(title = "Untitled - Notepad") dlg = app["Untitled - Notepad"]
Затем ищем и что-то делаем с необходимым элементом:
dlg["File"].click_input() # клик по кнопке "File"
Питон шибко быстрее графического интерфейса, а тем более, монструозной софтины. Так что перед совершением действия просим скрипт подождать:
# всё вместе, или любая комбинация: dlg["File"].wait("exists enabled visible ready active").click_input()
Eсли искомая кнопка в интерфейсе легко находится скриптом по названию — всё ок.
Если находится не та — печатаем идентификаторы, находим уникальное имя кнопки — и всё опять ок.
Дерево идентификаторов может выглядеть странно.
В первую голову надо удостовериться в правильно выбранном бэкенде при подключении к приложению:

Нет нужды выводить элементы всего окошка. Стоит выбрать только интересующую область.
Ну а кроме того, можно настроить глубину раскопок:
# так в вывод уйдут только первые две ступени dlg["File"].print_control_identifiers(depth = 2)

Этих знаний достаточно для натыка по Maya 3D, а так — возможности PyWinAuto гораздо шире.
Натыкиваем операции, вставляем между ними проверку потребления ресурсов, и отпускаем скрипт в свободное плавание!
Напоследок
При массовой обработке файлов очень, очень стоит заворачивать конструкции в Try/Except: обидно узнать, что скрипт крашнулся в два часа ночи, и всю ночь машинка стояла без дела.
Проект бодро ползёт к финалу.
Кроме процессов, мы оптимизировали сцены, а это тоже славно режет время на обработку.
Хочу сказать спасибо разработчику PyWinAuto, потому что без него яб лёг и умер.
