В третьей части Диего расскажет про многогранники, сглаживание и что такое контекст в Блендере.

Первая часть: меши с Python & Blender: двумерная сетка
Вторая часть: меши с Python & Blender: кубы и матрицы

В третьей части путешествия по миру Блендера и python займёмся икосферами. Другими словами, создадим икосаэдр, а потом уточним его до сферы. Заодно посмотрим, как можно сглаживать меш.

Икосаэдр — что это?

Икосаэдр — многогранник с двадцатью гранями. Их бывает много разных, нас же интересует правильный, выпуклый икосаэдр.

Хорошо, почему именно икосферы? Геометрия икосферы против обычной, основанной на UV координатах, удобнее в работе за счёт более равномерной сетки. Деформируя обычную сферу, можно получить странные результаты ближе к полюсам, так как плотность вертексов на полюсах выше. Икосферы же дают более органичный результат: геометрия распределена равномерно. Кроме того, икосферы ассиметричны.

Этот туториал сделан на основе кода Andreas Kahler, переписанного под третий пайтон и Блендер.

Настройки

Думаю, вы уже в курсе, что происходит в этом блоке. Импортируем необходимые модули и наметим структуру.

import bpy
from math import sqrt

# -----------------------------------------------------------------------------
# Настройки
name = 'Icosomething'
scale = 1
subdiv = 5

# -----------------------------------------------------------------------------
# Добавляем объект в сцену

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
выделение объекта...

...в версиях старше 2.79 реализовано так: obj.select_set(True)

Переменная subdiv отвечает за количество разбиений меша, scale за масштабирование объекта (вроде того, что мы делали в прошлый раз). Ноль в значении subdiv даст просто икосаэдр, а значения выше ноля уже приблизят к икосфере. Важный момент: поставив 9, вы получите меш с более чем пятью миллионами полигонов. Так что, в зависимости от мощщи вашей машины, стоит придерживаться более низких значений.

если вы-таки поставили 159 сабдивов

Одна из тех ситуаций, про которые я писал в посте о настройках Блендера: стоит до запуска скрипта включить консоль, и в случае чего завершить операцию принудительно.

От икосаэдра к сфере

Если мы просто добавим вершин к икосаэдру, мы лишь получим ту же форму с большим количеством вертексов. А нам нужно разбить меш так, чтобы новые вершины лежали на поверхности сферы.

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

Напишем функцию vertex(), которая будет определять и масштабировать эти координаты.

def vertex(x, y, z):
    """ Возвращаем координаты вертексов """

    length = sqrt(x**2 + y**2 + z**2)

    return [(i * scale) / length for i in (x,y,z)]

Создаём икосаэдр

Разобрались со сферой, и можем двигаться дальше. Как и с кубом из прошлого туториала, проще всего будет вбить координаты руками.

Один из простых способов построить икосаэдр — воспользоваться тремя золотыми прямоугольниками. Их вершины будут лежать в тех же координатах, что и вершины многогранника. Золотыми их делает золотое сечение: одна сторона прямоугольника относится ко второй примерно как 1:1.62, или, другими словами, как 1:φ. Координаты вершин прямоугольников находятся здесь: (0, ±1, ±φ), (±φ, 0, ±1) и (±1, ±φ, 0). Фи (φ) — значение золотого сечения, а ± определяет положение на оси координат относительно её начала.

В результате получим координаты для 12 вертексов, образующих 20 граней, каждые пять из которых имеют общую вершину. Посмотрите на рисунок:

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

# --------------------------------------------------------------
# Создаём икосаэдр

# Золотое сечение
PHI = (1 + sqrt(5)) / 2

verts = [
          vertex(-1,  PHI, 0),
          vertex( 1,  PHI, 0),
          vertex(-1, -PHI, 0),
          vertex( 1, -PHI, 0),

          vertex(0, -1, PHI),
          vertex(0,  1, PHI),
          vertex(0, -1, -PHI),
          vertex(0,  1, -PHI),

          vertex( PHI, 0, -1),
          vertex( PHI, 0,  1),
          vertex(-PHI, 0, -1),
          vertex(-PHI, 0,  1),
        ]


faces = [
         # 5 полигонов вокруг нулевой точки
         [0, 11, 5],
         [0, 5, 1],
         [0, 1, 7],
         [0, 7, 10],
         [0, 10, 11],

         # Полигоны вокруг
         [1, 5, 9],
         [5, 11, 4],
         [11, 10, 2],
         [10, 7, 6],
         [7, 1, 8],

         # 5 полигонов вокруг третьей точки
         [3, 9, 4],
         [3, 4, 2],
         [3, 2, 6],
         [3, 6, 8],
         [3, 8, 9],

         # Полигоны вокруг
         [4, 9, 5],
         [2, 4, 11],
         [6, 2, 10],
         [8, 6, 7],
         [9, 8, 1],
]

Планируем сабдив

Можно разбить треугольник, разрезав посередине каждое ребро. Получим четыре треугольника, эдакий трифорс. Говоря "разрезать", я имею в виду добавить новых вертексов в середину каждого ребра, а не операцию "разрезать".

Однако, просто пробежав по разделяемым рёбрам, мы наткнёмся на уже обработанные. Это добавит дублей вертексов, и строить на них новые полигоны будет непросто.

Избежим этого, сохраняя обработанные рёбра в отдельный список, и сверяясь с ним на следующей итерации разбиения. Списком будет словарь. В качестве ключей возьмём индексы вершин, от меньших к большим. Вне зависимости от того, как мы перебираем рёбра, ключи останутся теми же.

middle_point_cache = {}

def middle_point(point_1, point_2):
		"""Находим центральную точку и её отображение на единичной сфере"""

    # Чекаем, прошлись ли по этому ребру, чтобы не плодить дубли вертексов
    smaller_index = min(point_1, point_2)
    greater_index = max(point_1, point_2)

    key = '{0}-{1}'.format(smaller_index, greater_index)

    if key in middle_point_cache:
        return middle_point_cache[key]

    # Если не прошлись, добавляем вертекс
    vert_1 = verts[point_1]
    vert_2 = verts[point_2]
    middle = [sum(i)/2 for i in zip(vert_1, vert_2)]

    verts.append(vertex(*middle))

    index = len(verts) - 1
    middle_point_cache[key] = index

    return index

Координата центральной вершины — это сумма координат двух других, делённая на два. Добавим её в записнушку и вернём индекс для списка граней.

Делаем сабдив

Создав функцию middle_point(), можно приступить к сабдиву.

Каждое разбиение создаст новый пустой список для полигонов, а в конце мы заменим список полигонов, который уже был, на новый. Потом пройдёмся по каждому полигону, найдём центральные точки его граней, сохраним индексы, и построим на них четыре полигона, как на рисунке выше.

# Сабдив
# --------------------------------------------------------------

for i in range(subdiv):
    faces_subdiv = []

    for tri in faces:
        v1 = middle_point(tri[0], tri[1])
        v2 = middle_point(tri[1], tri[2])
        v3 = middle_point(tri[2], tri[0])

        faces_subdiv.append([tri[0], v1, v3])
        faces_subdiv.append([tri[1], v2, v1])
        faces_subdiv.append([tri[2], v3, v2])
        faces_subdiv.append([v1, v2, v3])

    faces = faces_subdiv

Сгладим икосаэдр

Теперь у нас есть меш, близкий к сфере, но всё ещё сверкающий гранями. Давайте визуально сгладим их.

Плавное сглаживание освещения (Smooth Shading) — свойство граней меша. Для того, чтобы меш выглядел гладенько, надо накинуть Smooth Shading на все его полигоны. То же самое делает кнопка Set smooth в интерфейсе:

bpy.ops.object.shade_smooth()

Эта строчка сработает в нашем скрипте, потому что контекст будет верным. Но попытка использовать её же в другой ситуации может обернуться ошибкой incorrect context. Context — такая божественная переменная, в которой лежит информация об актуальном состоянии Блендера: положение мыши, текущий режим, и многое другое. Можно изменить контекст перед вызовом оператора, но пока нет простого способа узнать, что именно каждый оператор ожидает увидеть в контексте.

Зато есть способ провернуть то же самое более низкоуровненно:

for face in mesh.polygons:
    face.use_smooth = True

В рамках скриптов в Блендере под "низкоуровненно" подразумевается избежать операторов, и сразу обратиться к методам и свойствам объекта. Другой пример низкого уровня — from_pydata().

Кроме того, что мы не работаем с контекстом, такой подход часто оказывается более гибким, и не зависит от работы с операторами. Например, сейчас мы можем сгладить не все полигоны, а лишь какие-то из них, а оператор затронет все грани.

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

import bpy
from math import sqrt

# -----------------------------------------------------------------------------
# Настройки

scale = 1
subdiv = 5
name = 'Icosomething'


# -----------------------------------------------------------------------------
# Функции

middle_point_cache = {}


def vertex(x, y, z):
    """ Возвращаем координаты вертексов """

    length = sqrt(x**2 + y**2 + z**2)

    return [(i * scale) / length for i in (x,y,z)]


def middle_point(point_1, point_2):
		"""Находим центральную точку и её отображение на единичной сфере"""

    # Чекаем, прошлись ли по этому ребру, чтобы не плодить дубли вертексов
    smaller_index = min(point_1, point_2)
    greater_index = max(point_1, point_2)

    key = '{0}-{1}'.format(smaller_index, greater_index)

    if key in middle_point_cache:
        return middle_point_cache[key]

    # Если не прошлись, добавляем вертекс
    vert_1 = verts[point_1]
    vert_2 = verts[point_2]
    middle = [sum(i)/2 for i in zip(vert_1, vert_2)]

    verts.append(vertex(*middle))

    index = len(verts) - 1
    middle_point_cache[key] = index

    return index


# -----------------------------------------------------------------------------
# Создаём икосаэдр

# Золотое сечение
PHI = (1 + sqrt(5)) / 2

verts = [
          vertex(-1,  PHI, 0),
          vertex( 1,  PHI, 0),
          vertex(-1, -PHI, 0),
          vertex( 1, -PHI, 0),

          vertex(0, -1, PHI),
          vertex(0,  1, PHI),
          vertex(0, -1, -PHI),
          vertex(0,  1, -PHI),

          vertex( PHI, 0, -1),
          vertex( PHI, 0,  1),
          vertex(-PHI, 0, -1),
          vertex(-PHI, 0,  1),
        ]


faces = [
         # 5 полигонов вокруг нулевой точки
         [0, 11, 5],
         [0, 5, 1],
         [0, 1, 7],
         [0, 7, 10],
         [0, 10, 11],

         # Полигоны вокруг
         [1, 5, 9],
         [5, 11, 4],
         [11, 10, 2],
         [10, 7, 6],
         [7, 1, 8],

         # 5 полигонов вокруг третьей точки
         [3, 9, 4],
         [3, 4, 2],
         [3, 2, 6],
         [3, 6, 8],
         [3, 8, 9],

         # Полигоны вокруг
         [4, 9, 5],
         [2, 4, 11],
         [6, 2, 10],
         [8, 6, 7],
         [9, 8, 1],
        ]


# -----------------------------------------------------------------------------
# Сабдив

for i in range(subdiv):
    faces_subdiv = []

    for tri in faces:
        v1 = middle_point(tri[0], tri[1])
        v2 = middle_point(tri[1], tri[2])
        v3 = middle_point(tri[2], tri[0])

        faces_subdiv.append([tri[0], v1, v3])
        faces_subdiv.append([tri[1], v2, v1])
        faces_subdiv.append([tri[2], v3, v2])
        faces_subdiv.append([v1, v2, v3])

    faces = faces_subdiv


# -----------------------------------------------------------------------------
# Добавляем объект на сцену

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


# -----------------------------------------------------------------------------
# Сглаживаем

#bpy.ops.object.shade_smooth()

for face in mesh.polygons:
    face.use_smooth = True

Заключение

Конец третьего туториала! Если проснулся интерес к шарообразным объектам, стоит побольше почитать про единичную сферу и её применение к нормалям объекта.

Вот что можно сделать самостоятельно:

  • оптимизировать код (не обязательно хранить ключ как строку)

  • использовать матрицы вращения и перемещения из предыдущего туториала

  • убрать масштабирование и заменить его на матрицу

В следующий раз вернёмся к кубу, сгладим ему рёбра, разберёмся с модификаторами, и сделаем ещё кой-чего.


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

Настроить Блендер для комфортной работы: пост

Первая часть: меши с Python & Blender: двумерная сетка
Вторая часть: меши с Python & Blender: кубы и матрицы