Стоимость использования 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 .
Комментарии
Отправить комментарий