Стоимость использования Python всегда ищет более простые способы сделать что-то. Это относится и к Blender. Недавно я обнаружил, что копирую и вставляю из кода тот же самый старый шаблон BMesh и спрашиваю себя, не существует ли способа сделать это менее повторяющимся. Есть конечно!
Всегда пишите код, так как будто программист,
который будет поддерживать ваш код, будет жестоким психопатом, который знает,
где вы живете. Джон Ф. Вудс
Моей первой идеей было создание функции, которая принимает другую функцию (функция высокого порядка в функциональном жаргоне). Эта функция создаст объект bmesh, запустит заданную ему функцию, а затем отправит данные BMesh в меш и освободит его. Но это было своего рода ограничение. Вы можете запустить только одну вещь, или вам придется сгруппировать их в другую функцию. Другая идея заключалась в том, чтобы создать класс-обертку, но мне все равно пришлось бы вручную его освобождать. Кроме того, я не большой поклонник классов.
Открытие файла дало мне лучшую идею: контекстные менеджеры.
Менеджеры контекста - это специальные объекты в Python, которые позволяют вам определять специальные контексты времени выполнения. Обычно для управления ресурсами. Вы создаете / открываете ресурсы, когда заходите в контекст, используете его, и когда вы покидаете ресурс, он автоматически закрывается / освобождается.
Менеджеры контекста - это специальные объекты в Python, которые позволяют вам определять специальные контексты времени выполнения. Обычно для управления ресурсами. Вы создаете / открываете ресурсы, когда заходите в контекст, используете его, и когда вы покидаете ресурс, он автоматически закрывается / освобождается.
Рассмотрим
open
функцию / менеджер контекста, например. # This snippet...
with open('some/file', 'r') as the_file:
do_something(the_file)
# ...is doing this behind the scenes
the_file = open('some/file', 'r')
do_something(the_file)
close(the_file)
Немного сахара для мирской задачи открытия файлов. Но он автоматически заботится о закрытии файлов (чтобы мы не пропускали дескрипторы) и аккуратно группирует весь код на новом уровне отступов. А как же БМеш? Объект BMesh не слишком отличается от ресурса Python. Мы создаем его, передаем и, наконец, отправляем в меш и освобождаем (или позволяем, если выпадают из области видимости).
Менеджеры контекста определяются как классы с тремя специфическими методами:
__init__
, __enter__
и __exit__
. Вы можете добавить свой, конечно, но это минимум, необходимый. Давайте посмотрим на первую реализацию.class Bmesh_from_obj():
def __init__(self, obj):
# Register parameters as class properties
self.obj = obj
def __enter__(self):
# Create and return bmesh object
self.bm = bmesh.new()
self.bm.from_object(self.obj)
return self.bm
def __exit__(self, *args):
# Clean up: send bmesh to mesh and free()
self.bm.to_mesh(obj.data)
self.bm.free()
При этом мы уже можем использовать
bmesh_obj
в качестве менеджера контекста (в объектном режиме). Обратите внимание, что возвращать значение в __enter__
необязательно, вы можете создать with
блок, который не привязывает переменную. Также, если мы нажмем исключение __init__
или __enter__
никогда не попадем внутрь блока with. С другой стороны, если мы идем внутрь, __exit__
метод всегда вызывается, и мы можем использовать его для обработки исключений, подобных этому: def __exit__(self, exc_type, exc_value, exc_traceback):
self.bm.to_mesh(obj.data)
self.bm.free()
if exc_type:
print('Oh no')
print(f'{exc_value}. Trace: {exc_traceback}')
Я оставлю обработку исключений для вас, хотя. Каждый проект индивидуален, и вы можете захотеть обработать исключения раньше или позже (позволяя им подняться). Кроме исключений, мы можем сделать еще две вещи, чтобы улучшить это: упростить код и поддерживать режим редактирования. Давайте сначала упростим.
Мы можем использовать декоратор для функции вместо написания целого класса. Сначала нам нужно импортировать
@contextmanager
из contextlib. Модуль contextlib полностью посвящен менеджерам контекста, и его документация - это то место, куда можно обратиться, если вы хотите углубиться в них. Как бы тогда выглядела эта функция?from contextlib import contextmanager
@contextmanager
def bmesh_from_obj(obj):
# Create bmesh object and yield it
yield bm
# Code inside the with block gets executed here
# Clean up (we're leaving the with block)
Заметьте , что мы получали Bm вместо того , чтобы вернуться , как мы делали в в
__enter__
методе раньше. Декоратору нужна функция для возврата одного итератора генератора значений, который затем становится целью оператора with (часть «as»). Вот и все для инициализации. Код внутри with
блока выполняется сразу после yield, и как только мы достигаем конца, он выполняет остальную часть функции.
Давайте добавим параметр mode и уточним это:
@contextmanager
def bmesh_from_obj(obj, mode):
"""Context manager to auto-manage BMesh."""
if mode == 'EDIT_MESH':
bm = bmesh.from_edit_mesh(obj.data)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
yield bm
# Send to mesh and clean up
bm.normal_update()
if mode == 'EDIT_MESH':
bmesh.update_edit_mesh(obj.data)
else:
bm.to_mesh(obj.data)
bm.free()
Теперь мы можем передать переменную режима из контекста и использовать BMesh в любом режиме. Вот пример из руководства по экструзии.
with bmesh_from_obj(obj, bpy.context.mode) as bm:
# Get geometry to extrude
bm.faces.ensure_lookup_table()
faces = [bm.faces[0]] # For a plane
faces = [bm.faces[5]] # For the top face of the cube# Extrude
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
# Move extruded geometry
translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]
up = Vector((0, 0, 1))
bmesh.ops.translate(bm, vec=up, verts=translate_verts)
bmesh.ops.delete(bm, geom=faces, context=DEL_FACES)
# Remove doubles
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
Что если мы хотим отправить данные BMesh новому объекту? Мы можем добавить небольшую служебную функцию, чтобы создать новый пустой объект и передать его менеджеру контекста.
def new_obj(obj_name):
"""Add a new object to the scene."""
# Make new object when leaving context manager
mesh = bpy.data.meshes.new(obj_name)
obj = bpy.data.objects.new(obj_name, mesh)
bpy.context.scene.objects.link(obj)
return obj
# (Later)
with bmesh_obj(new_obj('myobj'), bpy.context.mode):
pass
Финальный код
Я сделал пример немного более интересным для окончательного примера. Попробуйте! Этот код создает простую скрученную форму. Он начинается с плоскости, несколько раз выдавливает ее, вращает и масштабирует некоторые края. Как вы можете видеть в
extrude
функции, мы также можем абстрагировать некоторые общие операции bmesh в функции, чтобы повторно использовать код или сделать его более выразительным. Все может пойти в with
блоке.import bpy
import bmesh
from bpy.types import Object
from bmesh.types import BMFace, BMVert
from mathutils import Vector
from contextlib import contextmanager
from math import radians
from mathutils import Matrix
# ------------------------------------------------------------------------------
# BMesh Context manager
# ------------------------------------------------------------------------------
@contextmanager
def bmesh_from_obj(obj, mode):
"""Context manager to auto-manage bmesh regardless of mode."""
if mode == 'EDIT_MESH':
bm = bmesh.from_edit_mesh(obj.data)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
yield bm
bm.normal_update()
if mode == 'EDIT_MESH':
bmesh.update_edit_mesh(obj.data)
else:
bm.to_mesh(obj.data)
bm.free()
# ------------------------------------------------------------------------------
# Bmesh / Utils functions
# ------------------------------------------------------------------------------
def new_obj(obj_name):
"""Add a new object to the scene."""
# Make new object when leaving context manager
mesh = bpy.data.meshes.new(obj_name)
obj = bpy.data.objects.new(obj_name, mesh)
bpy.context.scene.objects.link(obj)
return obj
def extrude(bm, faces, direction, remove=True):
"""Extrude a set of faces in a direction"""
# Extrude
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]
bmesh.ops.translate(bm, vec=Vector(direction), verts=translate_verts)
if remove:
bmesh.ops.delete(bm, geom=faces, context=5)
return [f for f in extruded['geom'] if isinstance(f, BMFace)]
# ------------------------------------------------------------------------------
# Testing
# ------------------------------------------------------------------------------
# Add a new (empty) object
obj = new_obj('Bmesh test')
# We could also pass an existing object
# obj = bpy.context.object
with bmesh_from_obj(obj, bpy.context.mode) as bm:
# A grid with segments of 1 is a plane
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1)
# We need to call this since we are accesing faces by index
bm.faces.ensure_lookup_table()
faces = [bm.faces[0]]
new_faces = extrude(bm, faces, (0, 0, 1), False)
# Keep a copy of these verts for later
middle_verts = new_faces[0].verts[:]
# Another extrusion because why not
top_faces = extrude(bm, new_faces, (0, 0, 3))
# Give it a thin waist
bmesh.ops.scale(bm, vec=Vector((0.25, 0.25, 1)), verts=middle_verts)
# Add a small rotation at the top
bmesh.ops.rotate(bm, verts=top_faces[0].verts, cent=(0, 0, 0),
matrix=Matrix.Rotation(radians(15), 3, 'Z'))
# Unnecesary but for demo purposes...
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
Посмотрите мою серию уроков по созданию сетки, если вы ищете больше идей.
Если вы заинтересованы в получении дополнительной информации о менеджерах контекста, я рекомендую обратиться к документации модуля contextlib. Также проверьте оригинальное предложение PEP для этой функции: PEP343 .
Комментариев нет:
Отправить комментарий