10/26/2019

Сетки с Python & Blender: Круги и цилиндры



В последней части этой серии мы рассмотрим изготовление кругов и цилиндров. Они намного сложнее, чем кажется! Мы будем опираться на все из предыдущих частей, а также сделаем немного Bmesh для исправления нормалей.
 Давайте начнем импортировать обычные пакеты и Bmesh. Мы будем использовать его для исправления нормалей в конце. Также верните старый добрый и из предыдущей 

Ссылка http://sinestesia.co/blog/tutorials/python-tubes-cilinders/

Теги 05.10.2017 @ Учебники Blender , Meshes , Procedural , Python )


Серия учебников

Настроить

Давайте начнем импортировать обычные пакеты и Bmesh. Мы будем использовать его для исправления нормалей в конце. Также верните старый добрый и из предыдущей частиobject_from_data()set_smooth()






import bpy
import bmesh
import math


# ------------------------------------------------------------------------------
# Utility Functions

def set_smooth(obj):
    """ Enable smooth shading on an mesh object """

    for face in obj.data.polygons:
        face.use_smooth = True


def object_from_data(data, name, scene, select=True):
    """ Create a mesh object and link it to a scene """

    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(data['verts'], data['edges'], data['faces'])

    obj = bpy.data.objects.new(name, mesh)
    scene.objects.link(obj)

    scene.objects.active = obj
    obj.select = True

    mesh.validate(verbose=True)

    return obj

Первый шаг к созданию цилиндра - это создание  2D круга.

Делая круг

Во-первых, нам нужно поместить вершины в круг вокруг точки. Мы рассчитаем угол, под которым находится каждая вершина. Этот угол на самом деле является полярной координатой, поэтому, чтобы получить координаты X, Y, которые мы можем использовать, мы также конвертируем ее в декартову. Вы можете прочитать больше о математике на mathisfun.com и в Википедии. Конечно, мы передаем значение Z, поэтому нам не нужно его вычислять. Для начального круга Z будет ноль.






def vertex_circle(segments, z):
    """ Return a ring of vertices """
    verts = []

    for i in range(segments):
        angle = (math.pi*2) * i / segments
        verts.append((math.cos(angle), math.sin(angle), z))

    return verts

Мы можем использовать это сразу, чтобы сделать круг, но это будет только круг вершин. Давайте также добавим ребра тоже. Добавить ребра к кругу довольно просто. Просто обведите каждую вершину и соедините текущий индекс со следующим, затем добавьте последнее ребро, которое соединяет последний индекс с первым (ноль). Следуя урокам части 4, давайте создадим функцию для создания кругов.






def make_circle(name, segments=32):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    data['edges'] = [(i, i+1) for i in range(segments)]
    data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)

make_circle('Some Circle', 64)

Это не так уж много, но это хороший первый шаг.







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

Круги все время вверх

Что такое цилиндр, если не круг, выдавленный в трехмерном пространстве? Чтобы выдавить окружность по оси Z, мы можем выполнить цикл, задав ему растущие значения Z.vertex_circle()
Время make_cylinder()






def make_cylinder(name, segments=64, rows=4):
    """ Make a cylinder """

    data = { 'verts': [], 'edges': [], 'faces': [] }

    for z in range(rows):
        data['verts'].extend(vertex_circle(segments, z))

    scene = bpy.context.scene
    obj = object_from_data(data, name, scene)

    return obj

Notice возвращает список вершин, поэтому нам нужно использовать расширение вместо добавления, чтобы сохранить ту же структуру списка. Кроме того, мы не добавляем ребра здесь. Мы добавим эту сетку к фактическим граням, чтобы не было необходимости в ребрах. Создание граней следует логике из сеток в части 1. За исключением этого времени, оно идет по кругу.vertex_circle()






def face(segments, i, row):
    """ Return a face on a cylinder """

    ring_start = segments * row
    base = segments * (row + 1)

    return (base - 1, ring_start, base, (base + segments) - 1)

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






    for i in range(segments):
        for row in range(0, rows - 1):
            data['faces'].append(face(segments, i, row))

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







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






def face(segments, i, row):
    """ Return a face on a cylinder """

    if i == segments - 1:
        ring_start = segments * row
        base = segments * (row + 1)

        return (base - 1, ring_start, base, (base + segments) - 1)
    else:
        base = (segments * row) + i
        return (base, base + 1, base + segments + 1, base + segments)

Теперь у нас есть один красивый цилиндр.







Мы просто пропускаем заглавные буквы, чтобы закончить петлю.

Укупорка цилиндра

Есть два способа закрыть этот цилиндр:
  • Ngons. Все вершины в кольце соединены как одно лицо.
  • Треугольный веер. Серия треугольников, соединяющихся с центральной вершиной.
Чтобы сделать треугольник веером, нам нужно поместить вершину в середине кольца, а затем зациклить кольцо, соединяя вершины с ним. С другой стороны, чтобы создать только Ngon, мы просто перебираем индексы вершин и помещаем их все в один кортеж. Это яснее увидеть в нижней части кода.






def bottom_cap(verts, faces, segments, cap='NGON'):
    """ Build bottom caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, 0))
        center_vert = len(verts) - 1

        [faces.append((i, i+1, center_vert)) for i in range(segments - 1)]
        faces.append((segments - 1, 0, center_vert))

    elif cap == 'NGON':
        faces.append([i for i in range(segments)])

    else:
        print('[!] Passed wrong type to bottom cap')

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






def top_cap(verts, faces, segments, rows, cap='NGON'):
    """ Build top caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, rows - 1))
        center_vert = len(verts) - 1
        base = segments * (rows - 1)

        [faces.append((base+i, base+i+1, center_vert))
                       for i in range(segments - 1)]

        faces.append((segments * rows - 1, base, center_vert))

    elif cap == 'NGON':
        base = (rows - 1) * segments
        faces.append([i + base for i in range(segments)])

    else:
        print('[!] Passed wrong type to top cap')

Теперь мы можем вызвать их в функции цилиндра. Поскольку мы уже передали тип cap, мы можем сделать его параметром и позволить вызывающей стороне пройти, чтобы отключить caps.make_cylinder()None






    if cap:
        bottom_cap(data['verts'], data['faces'], segments, cap)
        top_cap(data['verts'], data['faces'], segments, rows, cap)

Заполнение круга

Цилиндр - это вытянутый круг, помните? Поэтому нижнее кольцо такое же, как у первого круга, который мы сделали, и теперь мы можем использовать функцию для его заполнения. Обратите внимание, что если мы делаем грани для круга, нам не нужно устанавливать ребра.bottom_cap()






def make_circle(name, segments=32, fill=None):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    if fill:
        bottom_cap(data['verts'], data['faces'], segments, fill)
    else:
        data['edges'] = [(i, i+1) for i in range(segments)]
        data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)

Сгладить

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






    set_smooth(obj)

    if cap:
        bevel = obj.modifiers.new('Bevel', 'BEVEL')
        bevel.limit_method = 'ANGLE'

    obj.modifiers.new('Edge Split', 'EDGE_SPLIT')

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







Как видите, нормали нижней крышки направлены вверх, тогда как на самом деле они должны быть направлены вниз. Мы должны исправить это, чтобы получить хороший скос. Вы можете сделать это, перейдя в режим редактирования и нажав Но есть и способ в коде.CTRL+N

Исправление норм

Для исправления нормалей мы будем использовать один из операторов bmesh. Они отличаются от обычных операторов Блендера, поскольку они не зависят от контекста. Они будут работать до тех пор, пока вы дадите им действительный объект bmesh.
Bmesh - это специальный Blender API, который дает вам очень тесный доступ к внутреннему API редактирования сетки Это довольно быстро, чем другие методы и более гибко. Однако, когда дело доходит до создания сеток с нуля, Bmesh не предлагает ничего слишком отличного от других способов. Вот почему эта серия не касалась Bmesh до этого момента. Чтобы использовать bmesh, мы сначала создаем объект bmesh, затем заполняем его данными (в данном случае используя ). Как только мы закончим с этим, мы записываем новые данные в меш и освобождаем объект bmesh из памяти.from_mesh()
Я не буду слишком углубляться в Bmesh сейчас, так как это займет целый отдельный урок. Вы можете прочитать больше о Bmesh в документации . А пока давайте создадим функцию, которая берет сетку и исправляет ее нормали.






def recalculate_normals(mesh):
    """ Make normals consistent for mesh """

    bm = bmesh.new()
    bm.from_mesh(mesh)

    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    bm.to_mesh(mesh)
    bm.free()

Добавление вызова к этой функции приводит к финальному, удивительно выглядящему цилиндру.make_cylinder()







Окончательный код







import bpy
import bmesh
import math


# ------------------------------------------------------------------------------
# Utility Functions

def set_smooth(obj):
    """ Enable smooth shading on an mesh object """

    for face in obj.data.polygons:
        face.use_smooth = True


def object_from_data(data, name, scene, select=True):
    """ Create a mesh object and link it to a scene """

    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(data['verts'], data['edges'], data['faces'])

    obj = bpy.data.objects.new(name, mesh)
    scene.objects.link(obj)

    scene.objects.active = obj
    obj.select = True

    mesh.update(calc_edges=True)
    mesh.validate(verbose=True)

    return obj


def recalculate_normals(mesh):
    """ Make normals consistent for mesh """

    bm = bmesh.new()
    bm.from_mesh(mesh)

    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    bm.to_mesh(mesh)
    bm.free()


# ------------------------------------------------------------------------------
# Geometry functions

def vertex_circle(segments, z):
    """ Return a ring of vertices """
    verts = []

    for i in range(segments):
        angle = (math.pi*2) * i / segments
        verts.append((math.cos(angle), math.sin(angle), z))

    return verts


def face(segments, i, row):
    """ Return a face on a cylinder """

    if i == segments - 1:
        ring_start = segments * row
        base = segments * (row + 1)

        return (base - 1, ring_start, base, (base + segments) - 1)

    else:
        base = (segments * row) + i
        return (base, base + 1, base + segments + 1, base + segments)


def bottom_cap(verts, faces, segments, cap='NGON'):
    """ Build bottom caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, 0))
        center_vert = len(verts) - 1

        [faces.append((i, i+1, center_vert)) for i in range(segments - 1)]
        faces.append((segments - 1, 0, center_vert))

    elif cap == 'NGON':
        faces.append([i for i in range(segments)])

    else:
        print('[!] Passed wrong type to bottom cap')


def top_cap(verts, faces, segments, rows, cap='NGON'):
    """ Build top caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, rows - 1))
        center_vert = len(verts) - 1
        base = segments * (rows - 1)

        [faces.append((base+i, base+i+1, center_vert))
                       for i in range(segments - 1)]

        faces.append((segments * rows - 1, base, center_vert))

    elif cap == 'NGON':
        base = (rows - 1) * segments
        faces.append([i + base for i in range(segments)])

    else:
        print('[!] Passed wrong type to top cap')


# ------------------------------------------------------------------------------
# Main Functions

def make_circle(name, segments=32, fill=None):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    if fill:
        bottom_cap(data['verts'], data['faces'], segments, fill)
    else:
        data['edges'] = [(i, i+1) for i in range(segments)]
        data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)


def make_cylinder(name, segments=64, rows=4, cap=None):
    """ Make a cylinder """

    data = { 'verts': [], 'edges': [], 'faces': [] }

    for z in range(rows):
        data['verts'].extend(vertex_circle(segments, z))

    for i in range(segments):
        for row in range(0, rows - 1):
            data['faces'].append(face(segments, i, row))

    if cap:
        bottom_cap(data['verts'], data['faces'], segments, cap)
        top_cap(data['verts'], data['faces'], segments, rows, cap)


    scene = bpy.context.scene
    obj = object_from_data(data, name, scene)
    recalculate_normals(obj.data)
    set_smooth(obj)

    bevel = obj.modifiers.new('Bevel', 'BEVEL')
    bevel.limit_method = 'ANGLE'

    obj.modifiers.new('Edge Split', 'EDGE_SPLIT')

    return obj


# ------------------------------------------------------------------------------
# Main Code

#make_circle('Circle', 64)
make_cylinder('Cylinder', 128, 4, 'TRI')

Заворачивать

Вот и все для этого урока и всей серии! Я надеюсь, что эти учебники были полезны для вас, или, по крайней мере, выучили новый трюк или два. Мы говорили о создании 2D сеток, кубов, икосфер, кругов и цилиндров. Мы также занялись отладкой, управлением сложностью, настройкой плавного затенения, добавлением и применением модификаторов, пересчетом нормалей и чтением данных из файлов. Это довольно много, но это только начало. В Blender есть много других возможностей для изучения и изучения!
Что вы можете сделать для себя:
  • Используйте Bmesh вместо того, чтобы строить мешfrom_pydata()
  • Сделайте новый, который делает спирали вместо круговvertex_circle()
  • Реорганизуйте функции шапки в одну, которая может сделать обе шапочки, одну или ни одной.
Есть вопросы или предложения по новым учебникам? Оставьте комментарий ниже!

Комментариев нет:

Отправить комментарий