В третьей части Диего расскажет про многогранники, сглаживание и что такое контекст в Блендере.
Первая часть: меши с 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: кубы и матрицы
ссылка на оригинал статьи https://habr.com/ru/post/647193/
Добавить комментарий