Меши с помощью Python и Blender: икосферы

28 ноября 2017 16 комментариев Артем Слаква Скриптинг на Python

В третьей части серии мы займемся созданием икосаэдров, их подразделением и превращением в сферу. Мы также рассмотрим два способа настройки затенения мешей (сглаживания).
ico

Что такое икосаэдр?


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

Итак, почему икосферы? Икосферы имеют более равномерное распределение геометрии, нежели UV-сферы. Деформирование UV-сфер часто дает странные результаты вблизи полюсов из-за более высокой плотности геометрии, в то время как икосферы дают более четкий и органический результат. Вдобавок к этому, икосферы асимметричны, что помогает создавать органическую деформацию.

Этот урок основан на оригинальном коде икосаэдра Андреаса Келера, адаптированном к Python 3 и Blender.
ico-2

Настройка


Готов поспорить, вы уже знаете все это. Давайте начнем импорт, а затем перейдем к нашим обычным делам.

import bpy
from math import sqrt
 
# -----------------------------------------------------------------------------
# Settings
name = 'Icosomething'
scale = 1
subdiv = 5
 
# -----------------------------------------------------------------------------
# Add Object to Scene
 
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
 
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.objects.link(obj)
 
bpy.context.scene.objects.active = obj
obj.select = True

В разделе настроек переменная subdiv будет контролировать, сколько раз подразделять меш, а переменная scale будет простым параметром масштаба, как и в предыдущем уроке. Установка subdiv в значение 0 создаст икосаэдр (вместо икосферы). Обратите внимание, что значение subdiv равное 9 приведет к созданию меша с более чем 5 миллионами граней. Скорее всего вам нужно использовать значения ниже данного порога, конечно же, в зависимости от вашего оборудования.

Помещение сферы в икосферу

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

Чтобы это произошло, мы должны убедиться, что вершины, которые мы добавляем, лежат на единичной сфере. Единичная сфера является «мнимой» сферой с радиусом 1. Мы можем определить положение каждой точки (вершины) на единичной сфере с помощью простой формулы, а затем зафиксировать ее координаты. Подробнее на Википедии.

Для этого у нас будет функция vertex(), которая фиксируется в единичной сфере (и также масштабируется).

def vertex(x, y, z):
    """ Return vertex coordinates fixed to the unit sphere """
 
    length = sqrt(x**2 + y**2 + z**2)
 
    return [(i * scale) / length for i in (x,y,z)]

Создаем базовый икосаэдр


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

Одним из способов построения икосаэдра является рассмотрение его вершин как углов трех ортогональных золотых плоскостей. Эти плоскости называются золотыми, потому что они следуют правилу золотого сечения. Вершины этих плоскостей лежат на координатах (0, ± 1, ± φ), (± φ, 0, ± 1) и (± 1, ± φ, 0). Заметим, что буква φ (phi) представляет значение золотого сечения, а ± означает «отрицательный или положительный».

Эти комбинации приводят к созданию 12 вершин, которые создают 20 равносторонних треугольников с 5 треугольниками, встречающимися в каждой вершине. Ознакомьтесь с приведенной ниже диаграммой.
ico-3
Черт, это много математики! Однако, когда вы разбираетесь с этим, все становится довольно просто и прямолинейно (и повторяется). Вот код, который создает икосаэдр.

# --------------------------------------------------------------
# Make the base icosahedron
 
# Golden ratio
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 faces around point 0
         [0, 11, 5],
         [0, 5, 1],
         [0, 1, 7],
         [0, 7, 10],
         [0, 10, 11],
 
         # Adjacent faces
         [1, 5, 9],
         [5, 11, 4],
         [11, 10, 2],
         [10, 7, 6],
         [7, 1, 8],
 
         # 5 faces around 3
         [3, 9, 4],
         [3, 4, 2],
         [3, 2, 6],
         [3, 6, 8],
         [3, 8, 9],
 
         # Adjacent faces
         [4, 9, 5],
         [2, 4, 11],
         [6, 2, 10],
         [8, 6, 7],
         [9, 8, 1],
]

Стратегия для подразделения


Мы можем взять треугольник и разделить каждое ребро, создав на его месте три треугольника. По сути, треугольники превращаются в маленькие трифорсы. Обратите внимание, что когда я говорю «разделить», я не говорю о фактическом запуске оператора и разделении ребра. Под этим я подразумеваю создание новых вершин по середине каждого ребра и создании трех новых граней.
ico-4
Однако, если бы мы обходили все ребра и разделяли их, мы бы быстро столкнулись с теми же ребрами, которые мы уже разделили ранее. Это приведет к большому количеству дубликатов вершин и головной боли при попытке создания граней.

Чтобы этого не произошло, давайте сохраним список ребер, которые мы уже разделили (кеш), и проверим их перед очередным разделением. Этот кеш будет словарем. Ключами будут индекс вершин, упорядоченные от меньшего к большему. Таким образом, ключ останется прежним, независимо от того, как мы перебираем вершины ребра.

middle_point_cache = {}
 
def middle_point(point_1, point_2):
    """ Find a middle point and project to the unit sphere """
 
    # We check if we have already cut this edge first
    # to avoid duplicated verts
    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]
 
    # If it's not in cache, then we can cut it
    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

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

Подразделение


С помощью функции middle_point() мы можем перейти к циклу и созданию подразделений.

На каждом уровне подразделений мы создаем новый пустой список граней, а в конце мы заменяем исходный список граней новым. Затем мы проходим через каждую грань, находим среднюю точку для трех ребер, сохраняем индексы и создаем из них 4 новые грани (помните диаграмму выше).

# Subdivisions
# --------------------------------------------------------------
 
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 на панели инструментов:

bpy.ops.object.shade_smooth()

Это будет прекрасно работать для этого скрипта, потому что контекст подходит для оператора. В других случаях вы можете обнаружить, что оператор отказывается работать из-за «неправильного контекста (incorrect context)». Контекст в Blender — это своего рода божественная переменная, которая содержит информацию о текущем состоянии приложения. Это включает в себя такие вещи, как положение курсора мыши, активная область и многое другое. Вы можете переопределить контекст при вызове оператора, но в настоящее время нет простого способа узнать, что каждый оператор хочет увидеть в качестве контекста.

К счастью, есть еще один способ сделать это «низкоуровневым» способом, установив сглаживание для каждой грани в цикле.

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

В мире скриптов Blender «Низкий уровень» относится к пропуску операторов и прямому доступу к методам и атрибутам объектов. from_pydata() — еще один пример работы на низком уровне.

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

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


import bpy
from math import sqrt
 
# -----------------------------------------------------------------------------
# Settings
 
scale = 1
subdiv = 5
name = 'Icosomething'
 
 
# -----------------------------------------------------------------------------
# Functions
 
middle_point_cache = {}
 
 
def vertex(x, y, z):
    """ Return vertex coordinates fixed to the unit sphere """
 
    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):
    """ Find a middle point and project to the unit sphere """
 
    # We check if we have already cut this edge first
    # to avoid duplicated verts
    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]
 
    # If it's not in cache, then we can cut it
    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
 
 
# -----------------------------------------------------------------------------
# Make the base icosahedron
 
# Golden ratio
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 faces around point 0
         [0, 11, 5],
         [0, 5, 1],
         [0, 1, 7],
         [0, 7, 10],
         [0, 10, 11],
 
         # Adjacent faces
         [1, 5, 9],
         [5, 11, 4],
         [11, 10, 2],
         [10, 7, 6],
         [7, 1, 8],
 
         # 5 faces around 3
         [3, 9, 4],
         [3, 4, 2],
         [3, 2, 6],
         [3, 6, 8],
         [3, 8, 9],
 
         # Adjacent faces
         [4, 9, 5],
         [2, 4, 11],
         [6, 2, 10],
         [8, 6, 7],
         [9, 8, 1],
        ]
 
 
# -----------------------------------------------------------------------------
# Subdivisions
 
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
 
 
# -----------------------------------------------------------------------------
# Add Object to Scene
 
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
 
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.objects.link(obj)
 
bpy.context.scene.objects.active = obj
obj.select = True
 
 
# -----------------------------------------------------------------------------
# Smoothing
 
#bpy.ops.object.shade_smooth()
 
for face in mesh.polygons:
    face.use_smooth = True

Завершение


На этом завершается третья часть этой серии. Если вы заинтересованы в создании сфер или преобразовании точек/объектов к сферическим формам, почитайте больше о единичной сфере и том, как применять ее вдоль нормалей.

Вещи, которые вы можете сделать для себя:

  • Оптимизируйте код (подсказка: вам не нужно хранить ключ в виде строки)
  • Примените матрицы вращения и перемещения из предыдущей части
  • Удалите переменную масштаба и вместо этого используйте матрицу

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

источник урока

О сайте

На данном сайте Вы сможете найти множество уроков и материалов по графическому
редактору Blender.

Контакты

Для связи с администрацией сайта Вы можете воспользоваться следующими контактами:

Email:
info@blender3d.com.ua

Следите за нами

Подписывайтесь на наши страницы в социальных сетях.

На сайте Blender3D собрано огромное количество уроков по программе трехмерного моделирования Blender. Обучающие материалы представлены как в формате видеоуроков, так и в текстовом виде. Здесь затронуты все аспекты, связанные с Blender, начиная от моделирования и заканчивая созданием игр с применением языка программирования Python.

Помимо уроков по Blender, Вы сможете найти готовые 3D-модели, материалы и архивы высококачественных текстур. Сайт регулярно пополняется новым контентом и следит за развитием Blender.