Привет! Понадобилось процедурно генерировать сложную модель, и пока я копал, как это делается, нашёл несколько статей от Diego Gangl, cg артиста и разработчика Блендера. Они славные для новичка, понимающего в моделировании, и не умеющего в код. Это перевод одной из них. Неточности и ошибки автора я поместил под спойлеры.
Процедурная генерация мешей даёт уйму возможностей. Можно сделать модель, чьё состояние зависит от событий в реальном мире, заняться генеративным артом, моделировать формы, основанные на матфункциях, или даже создавать контент для игр. Блендер — прекрасный выбор инструмента. Это комбайн для моделирования и анимации, и у него есть жирный и хорошо документированный Python API.
Важная заметка: сохраняйтесь чаще, особенно ковыряясь со скриптами!
Начнём-с
Меш и объект для Блендера — разные вещи. Взаимосвязь такая: создаём меш → привязываем к объекту → привязываем объект к сцене.
Стартанём с импорта bpy и пары переменных.
import bpy
# Настройки
name = 'Gridtastic'
rows = 5
columns = 10
Переменная name
используется и для объекта, и для меша. Переменные rows
и columns
будут определять координаты вертексов меша. Дальше настроим добавление меша и объекта. Создадим меш, потом объект с привязкой к нему меша , потом привяжем объект к сцене. Сразу создадим пустышки для вертексов и полигонов, и чуть позже накидаем в них данных.
verts = []
faces = []
# Создаём меш
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
# Создаём объект и привязываем к сцене
obj = bpy.data.objects.new(name, mesh)
bpy.scene.collection.objects.link(obj)
# Выделяем объект
bpy.context.view_layer.objects.active = obj
obj.select = True
...если бездумно копипастнуть
ничего не произойдёт, кроме печального AttributeError: module 'bpy' has no attribute 'scene'. Опечатка автора статьи в том, что строка привязки должна выглядеть так: bpy.context.scene.collection.objects.link(obj)
Кроме того, выделение объекта в версиях старше 2.79 реализовано так: obj.select_set(True)
Наиболее интересна функция from_pydata()
. Она и создаёт меш, исходя из трёх списков: вершин, рёбер и граней. Подробнее об этой функции в родной документации.
Сетка из вершин
Резонно начать с первого вертекса. Каждый вертекс описывается тремя координатами: по X, Y, Z осям. Поскольку мы делаем двумерную сетку, координата Z будет всегда равна нулю. Первый вертекс расположим в нулевых координатах сцены, они же нули глобальных координат. Иначе говоря, в координатах (0, 0, 0). По сути, вертекс это кортеж из трёх чисел с плавающей точкой. Перепишем список с вертексами таким образом:
verts = [(0, 0, 0)]
Попробуйте запустить скрипт, и вы увидите одинокую точку. Она даже редактируема. Пора сделать ряд точечек. Нам потребуется цикл, накидывающий столько вершин, сколько колонок мы обозначили.
verts = [(x, 0, 0) for x in range(columns)]
Поскольку range()
возвращает целые числа, кооордината X каждой вершины будет по сути номером колонки. Что означает, что ширина колонки равна одному блендер-юниту. Снова прогнав скрипт, мы увидим десять вертексов в ряд. До сетки из вертексов осталась капля: добавить строчки. Сделаем их так же циклом:
verts = [(x, y, 0) for x in range(columns) for y in range(rows)]
Победа! Налицо сетка из вертексов.
Пора создать полигоны, но сначала осознаем, как это работает.
Полигон
У каждого вертекса есть индекс. Как только рождается новая вершинка, так ей тут же присваивают порядковый номер. Но самая первая будет с индексом 0.
Полигон — кортеж из индексов вертексов. Для формирования полигона их может быть от трёх до бесконечности. Кроме того, эти индексы — натуральные числа. Если вставить дробное значение, Блендер не крашнется, но округлит его. Ну, а поскольку мы хотим четырёхугольные полигоны, для каждого полигона нам потребуется четыре индекса вертексов. Каких? Ну, можно прикинуть на глаз, но есть более клёвый вариант: включить режим отладки. Откройте питон-консоль Блендера, и вбейте туда команду:
примечание про версии Блендеров
Код ниже — для Блендра до версии 2.8. Ежели у вас Блендер в диапазоне 2.8-3, нужная галочка лежит тут: Edit → Preferences → Interface → Display → Developer Extras. Теперь во Viewport Displays (в Edit Mode) появится чекбокс Indices.
bpy.app.debug = True
Я надеюсь, что если вы и почерпнёте что-то из этого туториала, то это будет режим отладки. Это лучшее, что может с вами произойти, пока вы пишете скрипт или даже пилите аддон. Для просмотра индексов вершин свежесозданной сетки, перейдите в режим редактирования объекта и выделите нужную вершину. В N-панели во вкладке Mesh Display Panel поставьте галочку на Indices, чекбокс появится в колонке Edge Info. Если этого чекбокса нет, вероятно, режим отладки выключен. Теперь все выделенные вершины будут показывать свои индексы.
Сосредоточимся на первом полигоне. Его образуют вертексы с индексами 0, 1, 5 и 6. Попробуем:
faces = [(0, 1, 5, 6)]
Запускаем скрипт, и видим странную картину: как будто бы мы соединили неправильные вертексы.
На самом деле вертексы правильные, но порядок записи некорректен. Дело в том, что индексы в кортеже должны располагаться начиная с нижнего левого угла, и против часовой стрелки:
примечания про порядок обхода вершин
Первое: абсолютно неважно, с какой вершины начнётся обход, лишь бы по кругу.
Второе: обход по часовой или против часовой влияет на направление нормали полигона, но никак не влияет на его построение.
То есть на самом деле нужна была такая последовательность: 0, 5, 6, 1. Исправим строчку кода, и снова запустим скрипт:
Вот теперь хорошечно. Когда возникают такие проблемы, попробуйте поменять местами индексы в первой или второй паре. А теперь веселье: надо понять, как составить список вершин для ряда из полигонов. Если присмотреться, увидим такую закономерность:
все индексы прибавляют по пять пять по оси X
первый индекс равен нулю, второй на единицу больше
Первый индекс каждого полигона — номер столбца, умноженный на количество колонок. Второй смещается на единицу относительно первого, так что просто добавим её. Накидаем код и проверим выхлоп:
for x in range(columns - 1):
print(x * rows)
print((x + 1) * rows)
Почему у количества колонок отнимается единица? Потому что на десять вертексов приходится девять рёбер, то есть нам нужно девять пар чисел.
Третий и четвертый индексы будут равны (x + 1) * rows + 1
и x * rows + 1
соответственно. Добавим единицу к X перед умножением, чтобы сместить индекс во вторую строчку.
Цикл, который выведет наборы индексов для каждого полигона в строке:
for x in range(columns - 1):
print('first:', x * rows)
print('second:', (x + 1) * rows)
print('third:', (x + 1) * rows + 1)
print('fourth:', x * rows + 1)
print('---')
Делаем меш
Со всеми этими знаниями мы можем приступить к созданию первого ряда полигонов. Но прежде завернём кусок кода для полигонов в функцию, для большего удобства и читаемости. Кроме того, я докинул создание полигонов построчно. На каждый следующий ряд индексы возрастают на единицу, так что просто добавляем номер ряда в конце.
def face(column, row):
""" Создаём полигон """
return (column* rows + row,
(column + 1) * rows + row,
(column + 1) * rows + 1 + row,
column * rows + 1 + row)
Добавим в код полигонов эту функцию, как мы сделали это с кодом вертексов:
faces = [face(x, y) for x in range(columns - 1) for y in range(rows - 1)]
Отнимаем у количетсва строк единицу по тем же причинам, что и отнимали у колонок. Запустим скрипт и возрадуемся.
Вот и всё! Только что вы своими руками написали скрипт, генерирующий двумерную сетку. Дальше ещё немного хитростей.
Масштабирование
Мы можем регулировать количество вертексов, составляющих сетку, но размер ячейки всегда будет равен одному блендер-юниту. Поправим же это.
Достаточно простого умножения координат на число, определяющее масштаб. Добавим переменную для этого числа. Можно вписать её прям к вертексам, но будет лучше сделать отдельную функцию.
size = 1
def vert(column, row):
""" Создаём точку """
return (column * size, row * size, 0)
verts = [vert(x, y) for x in range(columns) for y in range(rows)]
Попробуем поменять значение size
на что-нибудь другое, и посмотрим, что получится.
Финальный код
import bpy
# Настройки
name = 'Gridtastic'
rows = 5
columns = 10
size = 1
# Функции
def vert(column, row):
""" Создаём точку """
return (column * size, row * size, 0)
def face(column, row):
""" Создаём полигон """
return (column* rows + row,
(column + 1) * rows + row,
(column + 1) * rows + 1 + row,
column * rows + 1 + row)
# Циклы для списка координат и вертексов
verts = [vert(x, y) for x in range(columns) for y in range(rows)]
faces = [face(x, y) for x in range(columns - 1) for y in range(rows - 1)]
# Создаём меш
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
# Создаём объект и привязвыем к сцене
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
# Выделяем объект
bpy.context.view_layer.objects.active = obj
obj.select = True
Заключение
Надеюсь, вам понравилось! Это самый простой пример создания меша, что я смог придумать, и дальше ещё много интересных штук, которые можно сделать. Вот несколько несложных вещей, на которых вы можете потренироваться:
разбить коэффициент масштабирования по осям;
добавить сетке смещение, чтобы она начиналась не в нулевых координатах;
красиво запаковать это в функции (или классы)
В следующем туториале перепрыгнем в трёхмерное пространство и сделаем куб.
Оригинал статьи (автор не прикрутил к сайту сертификат, браузер может ругаться.)
Комментарии (10)
Kyushu
20.01.2022 10:31-1Ну, сетку называть меш это еще куда не шло, но почему вершины стали вертексами, вы же не нызываете ребра еджами? Уже даже не помню, почему Blender у меня оказался невостребованным и я перешел на gmsh для генерации сеток.
goshkalinin Автор
20.01.2022 15:26Язык — гибкая штука! Например, на форуме рендер.ру первое упоминание слова "вертекс" я нашёл датированным 2000 годом. Упоминаний эйджей меньше, слово "ребро" куда популярнее, но тем не менее, в современном русском оно существует, то есть — используется.
Gmsh же готовит сетку для всяких солверов, или я путаю?Kyushu
20.01.2022 15:39Да, gmsh строит поверхностную и пространственные сетки. Преимущесвенно для моделирования, но поверхностную сетку можно использовать и для визуализации. Про Blender я понял, что это совсем не то, что мне нужно.
jushinen
20.01.2022 15:13Небольшое изменение на сегодняшний день (версия 2.93.2+)
obj.select = True -> obj.select_set(True)
v0nd1
Спасибо большое, интересно. Пока еще не работал в блендере из-за плохого Пк, но обязательно попробую повторить.
goshkalinin Автор
камон, слабая печка потянет и блендер, и пару десятков полигонов
...ну разве если печка из начала века. Тогда стоит качнуть блендер образца 2.59
v0nd1
у меня i3 со встроенной видеокартой (видеокарты грубо говоря нет вообще) Сомневаюсь, что у меня пойдёт, но старые версии попробовать стоит, спасибо.
domix32
Процессор конечно может немного напрячься, но врядли какие-то серьезные проблемы возникнут с этим. Чай не 3д макс или майя которые сами по себе несколько гигов весят.
osmanpasha
Вы попробуйте сначала новую версию, если у вас комп потянет win7, то и блендер потянет. Возможны варианты в скорости работы с файлами большой сложности, а также времени рендера сцен, но один меш из статьи блендер вам точно нарисует.