Уроки по Blender: структура объектов, оптимизация, скрипты Python
Разбираемся в более продвинутых понятиях и инструментах.
Сохранитесь и подпишитесь: наш Telegram-канал «Чекпоинт» — уютное место, где мы рассказываем об играх и о том, как они создаются.
Год назад в рамках своего YouTube-канала Андрей записал подробный курс по Blender, в котором работает более 7 лет. Обучение начинается с основ, рассчитанных в первую очередь на новичков, но последующие уроки могут быть интересны и для продвинутых пользователей — в них Андрей затрагивает скрытые возможности софта. Сам материал записывался в версии программы 2.90.1, но знания актуальны как для ранних версий (от 2.80), так и для последней (3.0.0).
Делимся основными тезисами третьего и четвёртого видеоуроков, посвящённых структуре, оптимизации и рандомизации объектов через инструменты Blender и скрипты Python. В предыдущих уроках мы рассматривали интерфейс программы и простые операции, а также основы моделирования.
Структура объектов
Как известно, при создании нового проекта в сцене по умолчанию появляется куб. Это объект со своими данными, которые можно увидеть, если кликнуть на стрелку рядом с объектом Cube в Структуре проекта (Outliner). Данные объекта Cube — это меш Cube (иконка зелёного треугольника). Объект и его меш — это разные элементы.
У объекта есть название и характеристики: положение, вращение, масштаб и размеры. Они находятся на панели Трансформация (Transform) в верхней вкладке Элемент (Item) — саму панель можно легко вызвать горячей клавишей N.
Данные меша можно посмотреть в Режиме редактирования (Edit Mode): здесь во вкладке Трансформация — Элемент появятся новые данные, в том числе Медиана (Median). Эта характеристика отображает координаты среднего значения точек.
Видоизменим и переместим меш во вьюпорте и обратим внимание на его координаты. Затем вернёмся в Объектный режим (Object Mode) и обнаружим, что координаты объекта остались прежними (во вьюпорте можно заметить его ориджин — оранжевую точку, которая отвечает за позицию и центр объекта). Так мы видим, что объект и меш — две разные структуры в Blender.
Взаимоотношения объекта и меша
Теперь выясним, почему эта информация важна для пользователя. Для наглядности переименуем объект Cube в Cube object (двойной клик по названию в структуре проекта), а его меш — в Cube mesh. Эти элементы связаны между собой. Их отношения напоминают связь Parent — Child («родитель» — «ребёнок»), где Cube mesh будет «ребёнком» Cube object. При этом у объекта может быть одновременно только один «ребёнок», а меш может быть «ребёнком» и других объектов.
Создадим новый куб с помощью Shift + A. Как и в случае с первым кубом, у нового объекта будет свой меш. Но, перейдя во вкладку Настройки данных объекта (Object Data Properties), новый меш можно заменить на первый, который мы ранее переименовали в Cube mesh.
Таким образом, у двух объектов будет один и тот же меш (чуть ниже объясним, чем это полезно).
Примечание
На скриншоте два объекта после этой операции выглядят по-разному. Это связано с тем, что их позиция и настройки отличаются. Если убрать все изменения во вкладке Трансформация — Элемент, то получится два одинаковых куба.
Проведём ещё один эксперимент. Удалим предыдущие объекты, добавим новый куб и зададим его мешу новое название, например, cube mesh main. Он будет главным мешем для будущих операций.
Зайдём во вкладку Наложения вьюпорта (Viewport Overlays) — в первом уроке мы включали в ней ось Z, — и отметим галочкой пункт Статистика (Statistics). В левом верхнем углу появится информация о сцене.
Статистика показывает, что в сцене находится:
- 3 Объекта (Objects) — 1/3 означает, что один из них выделен;
- 8 Вершин (Vertices);
- 12 Рёбер (Edges);
- 6 Граней (Faces);
- 12 Треугольников (Triangles).
При создании нового куба или его дублировании (Shift + D) количество всех показателей геометрии увеличится вдвое, поскольку меш объекта также продублируется. Но если заменить меш нового объекта на предыдущий (в данном случае cube mesh main), то показатели не увеличатся.
Комбинация клавиш Alt + D упрощает процесс — с её помощью можно сразу же создавать новый объект с исходным мешем. Таким способом вы можете быстро сгенерировать множество идентичных объектов, сохранив начальное количество полигонов.
Эта операция значительно экономит время и ресурсы при просчёте сцены. Дело в том, что у каждого полигона существует множество внутренних характеристик: нормаль, шейдинг и прочие данные, которые не так очевидны на первый взгляд, но важны при рендеринге. Соответственно, графический движок должен просчитать все эти показатели для каждого полигона в сцене. При использовании одинаковых данных время расчёта значительно сокращается, даже если они принадлежат разным объектам.
Оптимизация масштабных сцен
Попробуем усложнить задачу: выделяем все объекты в сцене и копируем их через Alt + D несколько раз. Рекомендуем предварительно удалить или спрятать (H) камеру и источник света, чтобы случайно не создать их дубликаты.
Статистика на скриншоте показывает, что в сцене 310 объектов. Выделяем все объекты — это можно сделать при помощи клавиши A или окружности (Circle Select), вызвав инструмент кнопкой С и увеличив радиус колёсиком мыши. Если применить к одному из выделенных объектов модификатор Подразделение поверхности (Subdivision Surface) — о нём мы узнали во втором уроке, — количество граней увеличится. При наложении модификатора на все объекты в сцене сумма полигонов может вырасти до полумиллиона. Сделать это быстро можно при помощи горячих клавиш: Ctrl + 1, Ctrl + 2, Ctrl + 3 и Ctrl + 4 — для разных уровней сглаживания. Таким образом эффект от использования одних и тех же мешей пропадает, так как при использовании модификаторов происходит полный пересчёт геометрии объекта.
Примечание
Применить модификатор ко всем объектам можно и вручную — для этого его необходимо наложить на активный объект, выделить все остальные (A), нажать Ctrl + L и в открывшемся меню выбрать Copy Modifiers.
Обратный процесс работает аналогичным способом: удалив на активном объекте модификатор и применив Copy Modifiers, вы скопируете настройки на все выделенные объекты.
Однако аналогичная операция возможна и внутри объекта. Отменяем модификатор через Ctrl + Z и переходим в режим редактирования. Выделив все полигоны (А), открываем меню инструментов через ПКМ (или W, если на правую кнопку мыши у вас назначено выделение) и выбираем опцию Подразделить (Subdivide). В нижнем левом углу вьюпорта появится вкладка с одноимённым названием. Открываем её и выставляем значение Гладкость (Smoothness) на 1, а Количество разрезов (Number of Cuts) на 4.
В итоге получается почти тот же результат, что и при использовании модификатора через Ctrl + 4, но при этом сцена содержит 150 полигонов. Даже если продолжить дублирование с помощью Alt + D, количество граней останется прежним.
При этом можно выбрать любой объект, перейти в режим редактирования и изменить его на своё усмотрение, используя приёмы из второго урока. В этом случае все изменения будут автоматически применены и к остальным объектам, так как меш один и тот же.
А если один из объектов выделить в объектном режиме, то во вкладке Трансформация — Элемент ему можно задать уникальные параметры положения, вращения, масштабирования и размера по любой оси. В этом случае трансформация не затронет остальные объекты.
Получается, что при создании масштабных сцен можно обойтись одним мешем. Даже при 17 248 объектах статистика показывает, что в сцене всего 150 полигонов. При использовании отдельных мешей для каждого объекта их было бы порядка 8 млн. Это пример оптимизации.
Данный приём аналогично работает и с целыми коллекциями (о них мы узнали в первом уроке). Предположим, у вас есть коллекция из восьми разных моделей домов, которые вы хотите продублировать. При обычном копировании полигонаж сцены будет расти в геометрической прогрессии. Однако если щёлкнуть правой кнопкой по коллекции и выбрать Экземпляр в сцену (Instance to Scene), то группа моделей продублируется. Ещё это можно сделать через меню Добавить (Shift + A) — Экземпляр коллекции (Add — Collection Instance). При этом у вас останется ровно столько же полигонов, сколько было раньше.
Примечание
Имейте в виду, что коллекции могут накладываться одна на другую. Если на первый взгляд в сцене не прибавилось объектов, просто проверьте структуру проекта — скопированные коллекции отобразятся там.
Чтобы разбить продублированную коллекцию на отдельные объекты, необходимо её выделить, зайти в меню Применить (Ctrl + A) — Сделать экземпляры настоящими (Apply — Make Instances Real).
Экономия полигонов влияет не только на скорость работы в программе, но и на рендеринг. Это особенно важно при работе над анимацией — на рендере каждого кадра можно сэкономить до нескольких минут.
Теперь попробуем изменить объекты не по отдельности, а при помощи встроенных инструментов Blender и скриптов Python.
Рандомизация с помощью инструментов Blender
Возвращаемся в объектный режим и выделяем все объекты (на всякий случай ещё раз убедитесь, что не захватили камеру и источник света).
В Blender существует такая функция, как поиск команды (клавиша F3). В поисковике находим Случайную трансформацию (Randomize Transform) и выбираем появившийся пункт (если в сцене очень много объектов, то придётся немного подождать).
В левой нижней части экрана появится одноимённая вкладка — в ней можно рандомизировать положение, вращение и размер объектов по всем трём осям. Возьмём для примера пункт Вращение (Rotation): если в первом поле выставить максимальное значение 180, то у каждого из объектов будет свой угол разворота по оси X от 0° до 360° (от −180° до 180°). Проведём ещё один эксперимент и выставим Масштаб (Scale) на 5 — в этом случае каждый отдельный объект изменит масштаб на случайное значение от 1 до 5. Если в сцене много объектов, то придётся снова подождать.
Как вы могли заметить, проблема этой операции в том, что при огромном количестве объектов сцена обрабатывается довольно долго. Возможности Python, встроенные в Blender, могут значительно ускорить процесс рандомизации.
Рандомизация с помощью Python
Возвращаемся на шаг назад, до того, как мы рандомизировали объекты при помощи Случайной трансформации (Randomize Transform). Для начала поместим в отдельную коллекцию только те объекты, которые мы хотим рандомизировать (в данном случае кубы). Переименуйте её, например, в Cubes — это название мы в дальнейшем сможем использовать в коде как идентификатор коллекции.
Чтобы открыть консоль Python, наведите курсор на нижний край окна вьюпорта и аккуратно потяните вверх. В новом окне нажмите на Тип редактора (Editor Type) и выберите Консоль Python (Python Console).
Рандомизация в Python происходит с помощью модуля random. Импортируем модуль в рабочее пространство с помощью команды import и нажимаем Enter для подтверждения строки. После подключения модуля в Blender программа сможет обращаться к его функционалу. Модуль random по умолчанию входит в ту версию Python, которая поставляется вместе с Blender, и после импортирования программа может обращаться к нему и его методам.
Модуль random — это большой класс со множеством методов и подклассов. Их можно посмотреть, набрав в строке random. (точка после названия модуля или класса даёт доступ к его внутренней иерархии) и нажав Tab (в версиях ранее 2.82a — Ctrl + Space).
Примечание
Также Tab/Ctrl + Space работает как автозаполнение, с помощью которого можно быстрее вводить команды. Например, написав obj и нажав Tab, вы увидите, что программа выведет слово object.
Сейчас нам потребуется метод randint (). Если ввести в консоли random.randint () и нажать Tab (в версиях ранее 2.82a — Ctrl + Space), то отобразится техническая информация об этом методе.
Здесь говорится, что метод выдаёт случайное целое число в заданном диапазоне, включая крайние его значения. Попробуем ввести в скобках через запятую значения 0 и 100 и нажмём Enter.
Метод выдал число 39 (у вас может быть другой результат). Если обновить команду при помощи клавиши ↑ (вверх) и вновь нажать Enter, сгенерируется другое число в пределах между 0 и 100. Каждый раз при вводе команды оно будет меняться. Конечно, возможны и повторы, так как при случайном алгоритме результат непредсказуем.
Благодаря методу randint () мы можем присваивать объектам в Blender случайные значения масштаба и других параметров. Чтобы получить доступ к объектам, нам потребуется модуль bpy. В отличие от random он автоматически импортируется в консоль Python при запуске, поэтому добавлять его отдельно через команду import не нужно.
Модуль bpy включает в себя всё, что помогает пользователю взаимодействовать с интерфейсом Blender. Например, app включает всё, что касается взаимодействия с приложением, а context — всё, что связано с текущими элементами (активный объект, сцена, окно и так далее).
Нам потребуется класс data. Он содержит все данные текущего проекта в Blender, которые также можно посмотреть в Файле Blender (Blender File): информацию о коллекциях, объектах, камерах, источниках освещения, мешах и так далее.
Чтобы получить информацию об объектах (именно объектах, а не мешах), набираем в строке путь до класса, в котором они находятся (objects).
Если после этого нажать Tab (в версиях ранее 2.82a — Ctrl + Space), программа выдаст огромный список имён всех объектов в проекте. Чтобы найти конкретный объект, после указания пути до класса нужно поставить квадратные скобки и внутри в кавычках (двойных «» или одинарных ‘’) вписать его точное название.
Примечание
При указании имени важно соблюдать регистр (прописные и строчные буквы), например, «Cube» и «cube» — это совершенно разные объекты.
Если объект с таким именем существует, то при нажатии Enter он отобразится в строке. Если нет — программа выдаст ошибку.
Далее настраиваем цикл перебора объектов, который выполняется командой for. Для этого используется следующая конструкция: for название_переменной in список_объектов. В данном случае:
Буквально это можно перевести как «для каждого объекта из всех объектов в проекте:». Обратите внимание, что эта конструкция заканчивается двоеточием. В данном случае object — это имя переменной (вместо object можно вписать и другое слово — в остальном коде будет отвечать именно за эту переменную), в которую будет поочерёдно подставляться каждый объект из класса bpy.data.objects. С каждым из подставленных объектов можно производить те или иные действия.
Примечание
Слово object в Python зарезервировано и имеет ключевое значение в коде. Когда мы называем им переменную, доступ к изначальному значению в рамках исполняемой программы утрачивается. Поэтому по возможности лучше использовать в качестве имён переменных другие слова или сокращения: ob, obj и так далее.
Вводим for object in bpy.data.objects: и нажимаем Enter. Обратите внимание, что в начале следующей строки автоматически появился отступ. Отступ говорит программе, что эта строка относится к телу цикла и будет выполняться для каждого объекта. Зададим переменную, в которой будет подставляться рандомное значение:
scale — имя переменной (можно вписать любое слово, которое мы будем использовать в дальнейшем в значении данной переменной);
random.randint — модуль random и его метод randint, рассмотренные выше;
(5, 100) — диапазон случайных значений;
/100 — коррекция результата, полученного при выполнении метода randint (), — результат делится на 100.
Для нашей задачи необходимо задать диапазон случайных значений примерно от 0 до 1. Поскольку метод randint принимает и выдаёт только целые числа, мы зададим диапазон от 5 до 100 и поделим случайное значение на 100. В итоге у нас получается число между 0,05 и 1 (5/100 = 0,05 и 100/100 = 1).
Следующая задача — назначить полученное значение в качестве размера объекта. Подтверждаем предыдущую строку через Enter. Обратите внимание: в начале новой строки сохраняется такой же отступ, и это означает, что мы продолжаем описывать тело цикла. Набираем следующую команду:
object — переменная, которую мы использовали в конструкции for object in bpy.data.objects: и которая теперь обозначает каждый из объектов в сцене.
Имейте в виду, что значение scale прописано в скобках три раза через запятую. Ранее на панели Трансформация (Transform) вы могли заметить, что Масштаб (Scale) каждого объекта рассчитывается по трём осям: X, Y, Z. Поэтому команда object.scale = scale не сработает, так как в ней будет задано только одно значение (а их должно быть три). Чтобы задать значение переменной scale для всех осей, прописываем её в скобках три раза через запятые: object.scale = (scale, scale, scale).
Подтверждаем строку, удаляем отступы в начале следующей, поскольку дополнительных команд у нас не будет, и ещё раз нажимаем Enter. Немного ждём и получаем результат!
Все объекты в сцене стали разного размера. Если повторно выполнить прописанный нами цикл перебора (просто скопировав три строки), то масштаб объектов снова случайно изменится и сцена будет выглядеть иначе.
Легко заметить, что этот процесс происходит гораздо быстрее, чем при использовании Случайной трансформации (Randomize Transform). Чтобы засечь точное время обработки, напишем новый скрипт на основе предыдущего.
Ещё раз разделим окно вьюпорта, наведя курсор на верхний правый угол и потянув его влево. В новом окне меняем тип редактора на Редактор текста (Text Editor) — в нём нужный нам скрипт прописать будет легче, чем в консоли Python.
Нажимаем +Создать (+New). Перед тем, как копировать команды, прописанные в предыдущем шаге, нам нужно заново импортировать необходимые модули — текстовый редактор «не знает» операции, которые мы проводили в консоли Python. Импортируем модули с помощью команды import через запятую:
Ещё раз пройдёмся по модулям: bpy отвечает за взаимодействие с интерфейсом Blender, random поможет сгенерировать случайное число, time — засечь время операции. Обратите внимание, что в отличие от консоли Python, где часть модулей импортируется автоматически при запуске (например, bpy), в текстовом редакторе все модули необходимо добавлять вручную.
Далее через одну пустую строку мы пропишем функцию для выполнения определённой части кода. Функция — это часть кода, которую можно вызывать многократно. Признак функции — круглые скобки в конце. Перед тем, как использовать функцию в коде, её надо определить. Для этого используем следующую конструкцию:
def (от англ. define, «определить») — указывает, что начинается определение функции;
random_scale — название функции (пишется английскими и желательно строчными буквами, без пробелов и слешей);
() — часть синтаксиса функции; скобки могут оставаться пустыми или содержать некие параметры, которые обычно используются в теле функции;
: — указывает на то, что далее идёт тело функции.
После того как вы нажмёте Enter, редактор автоматически сделает отступ. Также его можно сделать, нажав клавишу Tab или четыре пробела. Отступы в Python очень важны — они означают, что все строки с одинаковым отступом относятся к одному и тому же блоку кода (телу функции, циклу и так далее).
В следующей строке перед основной частью функции мы засечём время. Для этого зададим переменную t1 и с помощью = присвоим ей значение текущего времени, используя метод perf_counter () из модуля time:
Метод — это готовая функция в рамках класса. Доступ к нему можно получить, указав название класса, к которому он относится, и через точку — имя самого метода. Обратите внимание, что def требуется только тогда, когда функцию нужно определить. Для вызова уже существующей функции достаточно прописать её имя с круглыми скобками в конце.
Теперь скопируем и вставим цикл, отвечающий за рандомизацию масштаба всех объектов, который мы прописали ранее в консоли:
Важно убрать лишние точки, которые автоматически скопировались из консоли, и правильно расставить все отступы, как показано на скриншоте (тело цикла обособляется новым отступом). Следующая после цикла строка с t2 фиксирует время на момент, когда функция была выполнена.
Теперь, чтобы посчитать длительность всего процесса, нам нужно из значения t2 вычесть t1 и отобразить где-то полученный результат. Для этого используем команду print () — это встроенный метод Python, с помощью которого можно выводить текстовую и числовую информацию на Системную консоль (не путать с консолью Python!). Её можно найти во вкладке Окно — Показать/скрыть системную консоль (Window — Toggle System Console).
Этот вариант подходит для пользователей Windows. Если у вас macOS/Linux, то Blender необходимо запустить из терминала, указав в нём полный путь до blender-launcher.exe. После этого терминал будет использоваться в качестве системной консоли.
Чтобы вывести в консоли разницу во времени между началом и окончанием работы функции, производим вычисление в скобках:
Сейчас функция существует только в виде инструкции и при запуске скрипта не сработает сама по себе. Для выполнения её нужно вызвать. Отделяем написанную функцию пустой строкой и в следующей строке копируем название функции random_scale () — без слова def, каких-либо отступов и двоеточия, но с круглыми скобками на конце. Это и будет вызов функции, которую мы определили ранее.
Теперь при запуске скрипта каждому объекту в сцене будет назначаться рандомное значение параметра scale, а в системной консоли — выводиться информация о времени, затраченном на выполнение операции. Остаётся только запустить скрипт кнопкой ▷ (в верхнем правом углу редактора) или комбинацией клавиш Alt + P.
После этого заходим в системную консоль и смотрим результаты — в нашем случае выполнение функции заняло 0,34 секунды:
Примечание
Конкретно этот скрипт работает для всех объектов в проекте. Помните, как в начале главы мы назвали коллекцию с кубами Cubes? Чтобы изменить объекты только в ней, фрагмент bpy.data.objects: необходимо заменить на bpy.data.collections[‘Cubes’].objects:, где ‘Cubes’ — название нужной коллекции.
Подведём итоги. Благодаря Python мы рандомизировали масштаб 17 248 объектов всего за треть секунды. Таким образом, базовые навыки программирования позволяют оптимизировать процессы и выполнять некоторые операции гораздо быстрее, чем через инструменты Blender.
В четвёртой части руководства мы познакомимся с основами анимации и рассмотрим несколько способов её воспроизведения.
Читайте остальные статьи из серии: