Меши с Python & Blender: икосферы
В третьей части Диего расскажет про многогранники, сглаживание и что такое контекст в Блендере.
Первая часть: меши с 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: кубы и матрицы