Привет! Понадобилось процедурно генерировать сложную модель, и пока я копал, как это делается, нашёл несколько статей от 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 на что-нибудь другое, и посмотрим, что получится.

bu: blender unit
bu: blender unit

Финальный код

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)


  1. v0nd1
    19.01.2022 20:40

    Спасибо большое, интересно. Пока еще не работал в блендере из-за плохого Пк, но обязательно попробую повторить.


    1. goshkalinin Автор
      19.01.2022 20:42

      камон, слабая печка потянет и блендер, и пару десятков полигонов

      ...ну разве если печка из начала века. Тогда стоит качнуть блендер образца 2.59


      1. v0nd1
        19.01.2022 21:22

        у меня i3 со встроенной видеокартой (видеокарты грубо говоря нет вообще) Сомневаюсь, что у меня пойдёт, но старые версии попробовать стоит, спасибо.


        1. domix32
          20.01.2022 15:58

          Процессор конечно может немного напрячься, но врядли какие-то серьезные проблемы возникнут с этим. Чай не 3д макс или майя которые сами по себе несколько гигов весят.


        1. osmanpasha
          21.01.2022 18:05

          Вы попробуйте сначала новую версию, если у вас комп потянет win7, то и блендер потянет. Возможны варианты в скорости работы с файлами большой сложности, а также времени рендера сцен, но один меш из статьи блендер вам точно нарисует.


  1. Kyushu
    20.01.2022 10:31
    -1

    Ну, сетку называть меш это еще куда не шло, но почему вершины стали вертексами, вы же не нызываете ребра еджами? Уже даже не помню, почему Blender у меня оказался невостребованным и я перешел на gmsh для генерации сеток.


    1. goshkalinin Автор
      20.01.2022 15:26

      Язык — гибкая штука! Например, на форуме рендер.ру первое упоминание слова "вертекс" я нашёл датированным 2000 годом. Упоминаний эйджей меньше, слово "ребро" куда популярнее, но тем не менее, в современном русском оно существует, то есть —  используется.

      Gmsh же готовит сетку для всяких солверов, или я путаю?


      1. Kyushu
        20.01.2022 15:39

        Да, gmsh строит поверхностную и пространственные сетки. Преимущесвенно для моделирования, но поверхностную сетку можно использовать и для визуализации. Про Blender я понял, что это совсем не то, что мне нужно.


  1. jushinen
    20.01.2022 15:13

    Небольшое изменение на сегодняшний день (версия 2.93.2+)

    obj.select = True -> obj.select_set(True)


    1. goshkalinin Автор
      20.01.2022 15:16

      Совершенно проплыло мимо внимания, спасибо!