Меши с Python & Blender: двумерная сетка

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Наиболее интересна функция 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

Заключение

Надеюсь, вам понравилось! Это самый простой пример создания меша, что я смог придумать, и дальше ещё много интересных штук, которые можно сделать. Вот несколько несложных вещей, на которых вы можете потренироваться:

  • разбить коэффициент масштабирования по осям;

  • добавить сетке смещение, чтобы она начиналась не в нулевых координатах;

  • красиво запаковать это в функции (или классы)

В следующем туториале перепрыгнем в трёхмерное пространство и сделаем куб.


Оригинал статьи (автор не прикрутил к сайту сертификат, браузер может ругаться.)

Источник: https://habr.com/ru/post/646527/


Интересные статьи

Интересные статьи

Функция property() используется для определения свойств в классах.Метод property() обеспечивает интерфейс для атрибутов экземпляра класса. Он инкапсулирует атрибуты экзем...
Взлёт искусственного интеллекта привёл к популярности платформ машинного обучения MLaaS. Если ваша компания не собирается строить фреймворк и развёртывать свои собственные модели, есть ша...
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...
Знаю, знаю, наверное вы сейчас думаете «что опять?!». Да, на хабре уже неоднократно писали о фреймворке FastAPI. Но я предлагаю рассмотреть этот инструмент немного подробнее и нап...
Для всех хабравчан, у которых возникло ощущение дежавю: Написать этот пост меня побудили статья "Введение в Python" и комментарии к ней. К сожалению, качество этого "введения" кхм… не будем о гру...