Привет, Хабр! Меня зовут Глеб. Я работаю в компании Friflex над проектами по оцифровке спорта. Работая над idChess (приложением для распознавания и аналитики шахматных партий), мы расширяем наш датасет синтетическими данными. В качестве движка используем Blender. В этой статье рассмотрим основы взаимодействия с объектами, получение доступа через API, перемещение, масштабирование и вращение.

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

Далее в статье мы рассмотрим подробнее работу над объектами в Blender.

Интерфейс

Чтобы начать работу с API блендера, откроем python-консоль внутри приложения. Через нее мы будем запускать наши скрипты и наблюдать изменения на сцене.

Это гифка. Чтобы посмотреть, нажмите на изображение

Помимо консоли, в blender есть текстовый редактор, который позволяет запускать код прямо в приложении. Более подробно про редактор можно посмотреть тут

Если планируете импортировать или создавать свои библиотеки, то имейте в виду, что blender использует свое виртуальное окружение. Доступ к python-библиотекам можно получить по следующему пути: /blender/3.0/python/lib/python3.9

Объекты и коллекции

Список объектов и коллекций, с которыми мы можем работать, представлен в этом блоке:

Все объекты в списке наследуются от класса bpy.types.Object. Разберемся, как получить доступ к экземплярам.

cube = bpy.data.objects['Cube']

bpy.data.objects ведет себя, как словарь: доступны методы values, keys, items, get и т.д. А ключами являются названия объектов.

А что если мы хотим получить объекты, содержащиеся в какой-нибудь коллекции? Просто используем другой атрибут модуля bpy.data.

bpy.data.collections['Collection'].all_objects.values()
# [bpy.data.objects['Cube'], bpy.data.objects['Light'], bpy.data.objects['Camera']]

Заметим, что коллекции похожи на обычные папки за одним исключением: содержимым коллекции являются все вложенные в неё объекты — даже те, которые содержатся в дочерних коллекциях.

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

location

По умолчанию это свойство отвечает за смещение относительно центра сцены. Попробуем переместить объект «Cube».

print(cube.location)
# <Vector (0.0000, 0.0000, 0.0000)>
cube.location = (1, 0, 2)
# Эквивалентно
cube.location.xz = (1, 2)
# Эквивалентно
cube.location.x = 1
cube.location.z = 2
# Эквивалентно
cube.location[0] = 1
cube.location[2] = 2
print(cube.location)
# <Vector (1.0000, 0.0000, 2.0000)>
Это гифка. Чтобы посмотреть, нажмите на изображение

dimensions

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

print(cube.dimensions)
# <Vector (2.0000, 2.0000, 2.0000)>
cube.dimensions = (4, 2, 2)
# Эквивалентно
cube.dimensions.x = 4
# Эквивалентно
cube.dimensions[0] = 4
print(cube.dimensions)
# <Vector (4.0000, 2.0000, 2.0000)>

Однако заметим, что для некоторых объектов dimension работать не будет. Об этом мы поговорим далее, а пока посмотрим на простой пример.

camera = bpy.data.objects['Camera']
print(camera.dimensions)
# <Vector (0.0000, 0.0000, 0.0000)>
camera.dimensions.x = 2
print(camera.dimensions)
# <Vector (0.0000, 0.0000, 0.0000)>
Это гифка. Чтобы посмотреть, нажмите на изображение

scale

Масштабировать объект можно не только с помощью свойства dimensions, но и scale. В отличии от dimensions, scale не использует единицы измерения, а меняет масштаб относительно исходного размера.

print(cube.scale)
# <Vector (1.0000, 1.0000, 1.0000)>
cube.scale = Vector((2, 1, 1)) # Используется mathutils.Vector, встроенный в blender
# Эквиалентно
cube.scale.x = 2
Это гифка. Чтобы посмотреть, нажмите на изображение

rotation_euler

Для вращения объекта используется свойство rotation_euler. Оно немного отличается от location, dimensions и scale.

import math
print(cube.rotation_euler)
# <Euler (x=0.0000, y=0.0000, z=0.0000), order='XYZ'>
cube.rotation_euler = (
    math.radians(60),
    math.radians(45),
    math.radians(30)
    )
# Эквивалентно
cube.rotation_euler.x = math.radians(60)
cube.rotation_euler.y = math.radians(45)
cube.rotation_euler.z = math.radians(30)
print(cube.rotation_euler)
# <Euler (x=1.0472, y=0.7854, z=0.5236), order='XYZ'>

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

rotation = Euler((0, 0, math.radians(10))) # Используется mathutils.Euler, встроенный в blender
cube.rotation_euler.rotate(rotation)

При вращении объекта с помощью углов Эйлера, нужно помнить об одной их особенности — gimble lock. Про это можно прочитать тут.

Это гифка. Чтобы посмотреть, нажмите на изображение

bound_box

Это свойство доступно только для чтения. Оно возвращает 8 точек, описывающих границы объекта. В отличии от location, dimensions и scale, bound_box не использует mathutils.Vector.

print(type(cube.bound_box))
# <class 'bpy_prop_array'>
print(type(cube.bound_box[0]))
# <class 'bpy_prop_array'>

Для удобства преобразуем точки в mathutils.Vector:

def bounding_box(obj):
    return [Vector(point) for point in obj.bound_box]

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

def bounding_box_world(obj):
    return [obj.matrix_world @ point for point in bounding_box(obj)]

data

Ранее мы увидели, что dimensions применяется не ко всем объектам. Но что это значит? Если мы посмотрим на список объектов в нашем проекте, то заметим, что у нас есть три разные по своей природе сущности. Логично, что у камеры будет набор уникальных методов, которого не будет, например, у источника света. Но как это реализуется, если объекты Camera, Cube, Light являются экземплярами общего класса bpy.types.Object.

camera = bpy.data.objects['Camera']
camera.location = (10, 0, 0) # Меняем положения (Свойство объекта)
camera.data.lens = 5 # Меняем фокусное расстояние (Свойство камеры)

В свойстве data находится экземпляр другого класса, предоставляющий доступ к специализированным свойствам и методам. В примере выше, мы переместили камеру (свойство bpy.types.Object) и изменили фокусное расстояние (свойство bpy.types.Camera).

hide_viewport & hide_render

Эти свойства позволяют скрыть объект. hide_viewport отвечает за отображение объекта на сцене, а hide_render — за отображение на конечном изображении.

cube.hide_render = True
cube.hide_viewport = True
Это гифка. Чтобы посмотреть, нажмите на изображение

The end

Вы работали с blender и хотите обсудить прочитанное? Или поделиться своим опытом и знаниями, чтобы сделать статью более полной и полезной для новичков? Жду вас в комментариях!

P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!