10/24/2019

Сетки с Python & Blender: закругленные кубики

Сетки с Python & Blender: На этот раз мы делаем вещи по-другому. Мы собираемся создать файл JSON для данных сетки куба, а затем построить из него сетку. Таким образом, мы можем легко заменить данные сетки на любые другие, не внося больших изменений в реальный код.


Ссылка https://sinestesia.co/blog/tutorials/python-rounded-cube/

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


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

Настроить

На этот раз мы делаем вещи по-другому. Мы собираемся создать  файл JSON для данных сетки куба, а затем построить из него сетку. Таким образом, мы можем легко заменить данные сетки на любые другие, не внося больших изменений в реальный код.
Мы не остановимся, как только объект будет сделан и привязан к сцене. Мы преобразуем сетку, установим ее для сглаживания, добавим модификаторы и (необязательно) применим их. Это много кода, но, к счастью, это код, который можно легко использовать повторно. В качестве бонуса мы также добавим случайность к преобразованиям сетки, чтобы при каждом запуске сценария мы получали новый куб. Наконец, мы будем использовать простую формулу, чтобы модификатор скоса был непротиворечивым в разных масштабах.
Вот импорт. Нам нужен пакет ОС для модуля пути.






import bpy

import json
import os
from math import radians

from mathutils import Matrix

Управление сложностью

Прежде чем мы будем читать данные куба из файла, у нас должен быть способ преобразовать эту информацию в сетку и объект, связанный со сценой. К счастью, мы занимаемся этим с первой части, поэтому давайте начнем с абстрагирования создания объекта в его собственную функцию. Первое, что нужно рассмотреть, - это то, как мы хотим, чтобы данные передавались. Мы можем принять  путь файла JSON и открыть его здесь или даже  строку данных JSON для декодирования, но эти параметры ограничат функцию только работой с данными json. Что если мы сгенерировали вершины по коду?
Вместо этого функция ожидает один словарь с тремя записями (вершины, ребра, грани). Обратите внимание, что вам не нужно объявлять ребра, если у вас есть список граней, в этом случае список ребер будет пустым (но все равно должен существовать, иначе мы получим KeyError).






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

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






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

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

Чтение данных из  файла JSON

На данный момент я уверен, что вы, должно быть, спрашиваете « JSON ? Почему не CSV ?
Формат CSV предназначен для табличных данных и простых списков, но в этих файлах нам нужно хранить несколько вложенных списков. Каждый список (вершины или грани) содержит несколько списков значений (три или четыре соответственно).
JSON хорошо работает для этого, поскольку он отображает один в один со структурами данных Python. Это также очень компактный ( в отличие от XML ) и Python включает в себя  хороший пакет для кодирования / декодирования этих файлов. Другими возможными вариантами могут быть YAML и OBJ , но для них вам понадобится сторонний пакет. И, конечно, если вы действительно хотите, вы также можете сделать свой собственный формат.
После этого давайте начнем с создания файла json для куба.






{
    "verts":  [[1.0, 1.0, -1.0],
               [1.0, -1.0, -1.0],
               [-1.0, -1.0, -1.0],
               [-1.0, 1.0, -1.0],
               [1.0, 1.0, 1.0],
               [1.0, -1.0, 1.0],
               [-1.0, -1.0, 1.0],
               [-1.0, 1.0, 1.0]
               ],

    "edges": [],

    "faces": [[0, 1, 2, 3],
              [4, 7, 6, 5],
              [0, 4, 5, 1],
              [1, 5, 6, 2],
              [2, 6, 7, 3],
              [4, 0, 3, 7]
             ]
}

Чтобы создать это, я поместил списки вершин и граней в словарь и запустил json.dumps () . Затем я вставил результат в новый файл. Ленивый, но эффективный. Для чего-то такого простого вы также можете написать это вручную. Сохраните это как в той же папке, что и файл blend, где вы запускаете скриптcube.json
Прежде чем мы сможем прочитать файл, мы должны получить правильный и абсолютный путь к файлу json. Поскольку он сохраняется в том же месте, что и файл blend, мы можем смешать утилиты пути Blender с os.path, чтобы создать функцию, которая создает этот путь.






def get_filename(filepath):
    """ Return an absolute path for a filename relative to the blend's path """

    base = os.path.dirname(bpy.context.blend_data.filepath)
    return os.path.join(base, filepath)

Использование гарантирует, что это будет работать кроссплатформенно. Теперь мы можем прочитать файл как любой старый файл в Python и передать егоos.path.joinobject_from_data()






    with open(get_filename('cube.json'), 'r') as jsonfile:
        mesh_data = json.load(jsonfile)

    scene = bpy.context.scene
    obj = object_from_data(mesh_data, 'Cubert 2', scene)

Помните этот подробный параметр в Вы увидите сообщение, напечатанное в терминале, например:validate()
BKE_mesh_validate_arrays: verts(8), edges(12), loops(24), polygons(6)
BKE_mesh_validate_arrays: finished
И, очевидно, вы увидите знакомый куб, сидящий посреди сцены.

Матричное преобразование

Процесс считывания данных меша в объекты завершен, но ничего сложного. Давайте вернем матричные преобразования из части 2, чтобы оживить ситуацию. Но вместо непосредственного применения матриц к объектам мы сделаем это через функцию. Эта новая функция будет заботиться о генерации матриц из более простых параметров и применять их независимо от того, передаем ли мы блок данных сетки или объект.
Параметры будут очень простыми:
  • Объект или сетка для преобразования
  • Положение как кортеж из трех значений
  • Масштаб как кортеж масштабирования для каждой оси
  • Вращение как кортеж, где первое значение - это поворот в градусах, а второе - строка, представляющая ось, которую необходимо повернуть. Это должна быть строка, которая принимает.Matrix.Rotation()
Последние три являются необязательными, и если они опущены, матрица будет умножена на единицу (аналогично умножению на единичную матрицу). Как мы можем определить, что является первым параметром? Мы используем принцип EAFP («Проще просить прощения, чем разрешения»). Мы знаем, что объекты имеют  matrix_worldсвойство и имеют  метод. Мы можем попытаться использовать первое, и если мы потерпим неудачу, то попробуем использовать второе. Если мы все еще потерпели неудачу, то параметр не может быть преобразован, и мы можем вызвать ошибку.transform()
Это исключение может быть поймано выше в стеке, когда мы вызовем эту функцию позже.






def transform(obj, position=None, scale=None, rotation=None):
    """ Apply transformation matrices to an object or mesh """

    position_mat = 1 if not position else Matrix.Translation(position)

    if scale:
        scale_x = Matrix.Scale(scale[0], 4, (1, 0, 0))
        scale_y = Matrix.Scale(scale[1], 4, (0, 1, 0))
        scale_z = Matrix.Scale(scale[2], 4, (0, 0, 1))

        scale_mat = scale_x * scale_y * scale_z
    else:
        scale_mat = 1

    if rotation:
        rotation_mat = Matrix.Rotation(radians(rotation[0]), 4, rotation[1])
    else:
        rotation_mat = 1

    try:
        obj.matrix_world *= position_mat * rotation_mat * scale_mat
        return
    except AttributeError:
        # I used return/pass here to avoid nesting try/except blocks
        pass

    try:
        obj.transform(position_mat * rotation_mat * scale_mat)
    except AttributeError:
        raise TypeError('First parameter must be an object or mesh')

Собираем все вместе, контроль ошибок

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






def make_object(datafile, name):
    """ Make a cube object """

    subdivisions = 0
    roundness = 2.5

    position = (0, 0, 1)
    scale = (1, 1, 1)
    rotation = (20, 'X')

    with open(datafile, 'r') as jsonfile:
        mesh_data = json.load(jsonfile)

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

    transform(obj, position, scale, rotation)
    set_smooth(obj)

roundnessИ subdivisionsпеременные управляют скоса и уточняющих модификаторы , которые мы добавим в следующем разделе. Обратите внимание, что мы также берем путь к файлу json в новой функции, так что мы можем запускать все это в разных сетках.
Теперь мы можем добавить некоторую базовую обработку ошибок, вызвав эту функцию внутри блока try.






# -----------------------------------------------------------------------------
# Main code and error control

try:
    make_object(get_filename('cube.json'), 'Cubert 2')

except FileNotFoundError as e:
    print('[!] JSON file not found. {0}'.format(e))

except PermissionError as e:
    print('[!] Could not open JSON file {0}'.format(e))

except KeyError as e:
    print('[!] Mesh data error. {0}'.format(e))

except RuntimeError as e:
    print('[!] from_pydata() failed. {0}'.format(e))

except TypeError as e:
    print('[!] Passed the wrong type of object to transform. {0}'.format(e))

Вы можете распечатать ошибки, записать их в журнал или, если вы звоните из оператора, вы также можете показать всплывающее окно. Интересным моментом является то, что, поскольку весь основной код включен, мы можем остановить сценарий в любое время, когда обнаружим ошибку. Фактически, мы можем остановить скрипт в любой произвольной точке, просто вернувшись из этой функции. Если бы мы поместили код на уровне модуля вместо функции (как мы делали в предыдущих частях), у нас не было бы элегантного способа остановить скрипт. В то время как Python предлагает и остановить выполнение, они также убивают Blender.execute()make_object()sys.exit()raise SystemExit()
Достаточно поговорить об обработке ошибок, чтобы создать целый отдельный учебник, но, надеюсь, это даст вам некоторые идеи.

Добавление и применение модификаторов

Это урок о закругленных кубах, не так ли? Тогда давайте их окружим! Вы можете сделать это, вручную изменив координаты вершин. Кошачьи кодирование имеет отличный учебник по этому вопросу (для Unity). Но это Блендер, и в нашем распоряжении есть артиллерия модификаторов. Давайте будем ленивыми и используем модификатор скоса для округления кубов.






    bevel = obj.modifiers.new('Bevel', 'BEVEL')
    bevel.segments = 10
    bevel.width = roundness / 10

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






    if subdivisions > 0:
        subdiv = obj.modifiers.new('Subdivision', 'SUBSURF')
        subdiv.levels = subdivisions
        subdiv.render_levels = subdivisions

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






def apply_modifiers(obj, scene, render=False):
    """ Apply all modifiers on an object """

    settings_type = 'PREVIEW' if not render else 'RENDER'

    obj.data = obj.to_mesh(scene, True, settings_type)
    obj.modifiers.clear()

Параметр типа параметров ожидает одну из двух строк: 'PREVIEW'и 'RENDER'Они управляют тем, какие модификаторы и какие настройки в модификаторах применяются, в зависимости от того, включены ли они для области просмотра или визуализации. Например, модификатор subsurf имеет два уровня (область просмотра и визуализация). Если вы хотите применить уровень области просмотра, вы пройдете PREVIEW.







Добавление некоторой случайности

Чтобы сделать наш код немного более причудливым, почему бы не рандомизировать некоторые части? Мы можем использовать произвольный модуль Питона . Есть много способов добавить случайность. Например, мы можем создать случайное значение в диапазоне, используя или сделать некоторую математику со случайным значением (обычно умножение). отлично подходит для этого, потому что он возвращает значения в диапазоне 0.0−1.0. Попробуйте изменить эти строки, и вы получите новый куб при каждом запуске скрипта.uniform()random()






from random import random, uniform
# [...]
    position = (uniform(-5,5), uniform(-5,5), uniform(-5,5))
    scale = (5 * random(), 5 * random(), 5 * random())

Между прочим, нет никакой научной причины использовать 5, мне это просто нравится. Теперь, когда мы меняем масштаб объекта, было бы неплохо, если бы мы могли также рассчитать размер скоса, чтобы обеспечить равномерность округлости между всеми кубами? Это так же просто, как деление на среднее значение по шкале.






    mod.width = (roundness / 10) / (sum(scale) / 3)

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






    try:
        mod.width = (roundness / 10) / (sum(scale) / 3)
    except DivisionByZeroError:
        mod.width = roundness / 10

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







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







import bpy

import json
import os
from math import radians
from random import random, uniform

from mathutils import Matrix


# -----------------------------------------------------------------------------
# Functions

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


def transform(obj, position=None, scale=None, rotation=None):
    """ Apply transformation matrices to an object or mesh """

    position_mat = 1 if not position else Matrix.Translation(position)

    if scale:
        scale_x = Matrix.Scale(scale[0], 4, (1, 0, 0))
        scale_y = Matrix.Scale(scale[1], 4, (0, 1, 0))
        scale_z = Matrix.Scale(scale[2], 4, (0, 0, 1))

        scale_mat = scale_x * scale_y * scale_z
    else:
        scale_mat = 1

    if rotation:
        rotation_mat = Matrix.Rotation(radians(rotation[0]), 4, rotation[1])
    else:
        rotation_mat = 1

    try:
        obj.matrix_world *= position_mat * rotation_mat * scale_mat
        return
    except AttributeError:
        # I used return/pass here to avoid nesting try/except blocks
        pass

    try:
        obj.transform(position_mat * rotation_mat * scale_mat)
    except AttributeError:
        raise TypeError('First parameter must be an object or mesh')


def apply_modifiers(obj, scene, render=False):
    """ Apply all modifiers on an object """

    settings_type = 'PREVIEW' if not render else 'RENDER'

    obj.data = obj.to_mesh(scene, True, settings_type)
    obj.modifiers.clear()


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

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


def get_filename(filepath):
    """ Return an absolute path for a filename relative to the blend's path """

    base = os.path.dirname(bpy.context.blend_data.filepath)
    return os.path.join(base, filepath)


# -----------------------------------------------------------------------------
# Using the functions together

def make_object(datafile, name):
    """ Make a cube object """

    subdivisions = 0
    roundness = 2.5
    position = (uniform(-5,5), uniform(-5,5), uniform(-5,5))
    scale = (5 * random(), 5 * random(), 5 * random())
    rotation = (20, 'X')

    with open(datafile, 'r') as jsonfile:
        mesh_data = json.load(jsonfile)

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

    transform(obj, position, scale, rotation)
    set_smooth(obj)

    mod = obj.modifiers.new('Bevel', 'BEVEL')
    mod.segments = 10
    mod.width = (roundness / 10) / (sum(scale) / 3)

    if subdivisions > 0:
        mod = obj.modifiers.new('Subdivision', 'SUBSURF')
        mod.levels = subdivisions
        mod.render_levels = subdivisions

    #apply_modifiers(obj, scene)

    return obj



# -----------------------------------------------------------------------------
# Main code and error control

try:
    make_object(get_filename('cube.json'), 'Rounded Cube')

except FileNotFoundError as e:
    print('[!] JSON file not found. {0}'.format(e))

except PermissionError as e:
    print('[!] Could not open JSON file {0}'.format(e))

except KeyError as e:
    print('[!] Mesh data error. {0}'.format(e))

except RuntimeError as e:
    print('[!] from_pydata() failed. {0}'.format(e))

except TypeError as e:
    print('[!] Passed the wrong type of object to transform. {0}'.format(e))

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

Это было много, но, надеюсь, вы увидите, как написание модульного и простого кода может помочь контролировать сложность. Нечитаемый код - это не поддерживаемый код , не стоит недооценивать важность поддержания чистоты и модульности. Помните, что кто-то должен будет прочитать и разобраться в этом коде в будущем. И этот кто-то, вероятно, будет вами.
Вещи, которые вы можете для себя:
  • Поместите это в класс или оператор
  • Добавьте некоторую случайность в расположение вершин, прежде чем подключать их к from_pydata ().
  • Рассчитайте количество сегментов в модификаторе скоса по отношению к значению подразделений, чтобы полученная геометрия была более ровной.
  • Попробуйте создать файлы JSON для других сеток. Все, что вам нужно сделать, это поместить вершины, ребра и грани в словарь, вызвать одну из функций кодирования json и сохранить ее в файл.

В последнем уроке из этой серии мы вернемся к созданию фигур. Далее: круги и цилиндры

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

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