Есть очень удобная и мощная библиотека, которая значительно упрощает работу с полигональными 3D моделями или мешами под названием OpenMesh. Она предоставляет широкий набор операций для работы с 3D моделями и имеет версию в Python. В этой статье я покажу как работать с 3D моделями используя обертку OpenMesh для Python. Кому интересно, прошу под кат.

Начало работы с OpenMesh

Изначально OpenMesh написан на C++, но имеет обертку на языке Python, которую можно использовать для быстрой и легкой разработки. Давайте посмотрим какие операции предоставляет эта обертка.

Для начала установим пакет с помощью pip:

pip install openmesh

Создадим новый Python скрипт и импортируем модуль openmesh

import openmesh as om
import numpy as np

Создадим объект, представляющий 3D меш

mesh = om.TriMesh()

Добавим несколько вершин

# add a a couple of vertices to the mesh
vh0 = mesh.add_vertex([0, 1, 0])
vh1 = mesh.add_vertex([1, 0, 0])
vh2 = mesh.add_vertex([2, 1, 0])
vh3 = mesh.add_vertex([0,-1, 0])
vh4 = mesh.add_vertex([2,-1, 0])

и несколько полигонов

fh0 = mesh.add_face(vh0, vh1, vh2)
fh1 = mesh.add_face(vh1, vh3, vh4)
fh2 = mesh.add_face(vh0, vh3, vh1)

В OpenMesh вершина представлена объектом VertexHandle. Объекты VertexHandle передаются во многие методы библиотеки OpenMesh в качестве параметра.

Есть также альтернативный способ через питоновский список вершин

vh_list = [vh2, vh1, vh4]
fh3 = mesh.add_face(vh_list)

Стоит отметить, что OpenMesh также вводит специальный тип элемента в модели под названием Half-edge. Я не буду его рассматривать в данной статье. Подробнее о нем можно почитать здесь.

Манипуляция с отображением текстуры и координатами вершин

Я не буду раскрывать тему отображения текстуры (texture mapping) полигонов. Читатель может прочитать детальное объяснение этой темы здесь.

Получим координаты текстуры (UV texture coordinates) вершины:

tc = mesh.texcoord2D(vh)

tc представляет собой tuple. Значения координат u и v можно получить по индексу 0 и 1 соответственно.

Изменим координаты текстуры

uv_coords = [0.5, 0.2]
mesh.set_texcoord2D(vh, uv_coords)

Здесь vh - объект типа VertexHandle.

Получим точку с координатами вершины

point = mesh.point(vh)

point представляет собой tuple. Координаты точки можно получить по индексу 0 и 1

x, y = tc[0], tc[1]

Можно получить все точки вершин модели

point_array = mesh.points()

и использовать их для сдвига модели вдоль оси (например X)

point_array += np.array([1, 0, 0])

Важные замечания по работе с OpenMesh

Не советую использовать enumerate() при итерировании вершин в цикле. Вы можете получить неожиданное поведение, например одинаковые координаты текстуры UV для разных вершин.

При сохранение меша в файл OpenMesh по умолчанию не сохраняет координаты текстур для вершин (vt строки) в файле obj. Чтобы решить эту проблему нужно передать параметр vertex_tex_coord в метод write_mesh (источник):

om.write_mesh(‘test_out.obj’, mesh, vertex_tex_coord=True)

Также OpenMesh не сохраняет файл материалов mtl в файле obj. Для сохранения информации о материале используйте параметр face_color при чтении файла obj

mesh = openmesh.read_trimesh('test.obj', vertex_tex_coord=True, face_color=True)

и записи в файл

openmesh.write_mesh('test_out.obj', mesh, vertex_tex_coord=True, face_color=True)

То же касается и нормалей. Чтение модели obj с нормалями

mesh = openmesh.read_trimesh('test.obj', vertex_normal=True)

и записи в файл

openmesh.write_mesh('test_out.obj', mesh, vertex_normal=True)

Здесь важно использовать одинаковые параметры и при чтении и при записи. Например, если мы хотим получить и сохранить информации о материале нужно использовать параметр face_color в обоих методах read_trimesh и write_mesh.

При работе с OpenMesh я сделал интересное наблюдение: порядок индексов координат текстур вершин (индексы строк vt) меняется. Например для такой строки в исходном файле obj

f 1/1 2/2 3/3

Соответствующая строка в выходном файле obj может выглядеть примерно так

f 1/3 2/1 3/2

Итерации и циклы

Итерации над вершинами в меше

for vh in mesh.vertices():
    print(vh.idx())

Цикл for возвращает объекты vh типа VertexHandle. idx() возвращает индекс вершины.

Итерации над полигонами и гранями

# iterate over all edges
for eh in mesh.edges():
    print eh.idx()

# iterate over all faces
for fh in mesh.faces():
    print fh.idx()

Аналогично итератору над вершинами цикл for возвращает объекты fh типа FaceHandle.

Итерация над всеми half-edge в меше

for heh in mesh.halfedges():
    print heh.idx()

Над вершинами соседними с заданной

for vh_n in mesh.vv(vh):
    print(vh_n.idx())

Над гранями выходящими из заданной вершины

for eh in mesh.ve(vh1):
    print eh.idx()

Над полигонами, смежными с заданной вершиной

for fh in mesh.vf(vh1):
    print fh.idx()

Все то же самое можно проделать и с полигоном

# iterate over the face's vertices
for vh in mesh.fv(fh0):
    print vh.idx()
   
# iterate over the face's halfedges
for heh in mesh.fh(fh0):
    print heh.idx()

# iterate over the face's edges
for eh in mesh.fe(fh0):
    print eh.idx()
    
# iterate over all edge-neighboring faces
for fh in mesh.ff(fh0):
    print fh.idx()

Это все. Не так сложно, не правда ли. Удачи вам в работе с 3D мешами с использованием OpenMesh и до новых встреч.