В общем понимании “петля” или в терминологии 3D – “луп” (с английского loop – петля) обычно представляет собой последовательное выделение нескольких точек, ребер или полигонов меша.
Однако во внутренней структуре меша присутствует отдельный элемент, который тоже называется “луп” (будем придерживаться терминологии) и представляет собой условную комбинацию одного вертекса с одним ребром меша. Попробуем разобраться, для чего нужны эти “лупы”.
Рассмотрим отдельный полигон, имеющий 4 вертекса и 4 ребра.
Полигон имеет 4 лупа:
- Вертекс 0 + ребро 1
- Вертекс 1 + ребро 2
- Вертекс 3 + ребро 3
- Вертекс 2 + ребро 0
Лупы не сортируются в соответствии с индексами вертексов или ребер. Определить, какой из лупов считается условно первым можно с помощью свойства полигона loop_start, возвращающего индекс ребра, которое берется за точку отсчета:
1 2 |
bpy.context.object.data.polygons[0].loop_start # 0 |
Луп всегда направлен против часовой стрелки в правосторонней системе координат (нормаль полигона направлена вверх). Луп всегда выходит из вертекса и идет вдоль ребра.
В нашем примере первый луп выходит из вертекса с индексом 2 и идет вдоль ребра с индексом 0.
Общее количество лупов полигона можно получить с помощью свойства полигона loop_total:
1 2 |
bpy.context.object.data.polygons[0].loop_total # 4 |
А список индексов лупов на полигоне – через range свойство loop_indices:
1 2 |
[i for i in bpy.context.object.data.polygons[0].loop_indices] # [0, 1, 2, 3] |
В самом меше хранится список всех лупов этого меша:
1 2 |
bpy.context.object.data.loops[:] # [bpy.data.meshes['Plane'].loops[0], bpy.data.meshes['Plane'].loops[1], bpy.data.meshes['Plane'].loops[2], bpy.data.meshes['Plane'].loops[3] |
Если луп полигона – это просто вертекс плюс ребро, луп в структуре меша имеет собственный тип:
1 |
<class 'bpy.types.MeshLoop'> |
Через MeshLoop можно получить индексы принадлежащих этому лупу вертекса и ребра:
1 2 3 4 |
bpy.context.object.data.loops[0].vertex_index # 0 bpy.context.object.data.loops[0].edge_index # 1 |
Кажется, что лупы в структуре меша не имеют особой ценности, однако это не так. Например, для получения соответствия полигонов меша с полигонами его развертки (UV) используются механизмы лупов.
С помощью лупа на полигоне можно получить соответствующий ему луп на UV-развертке меша:
1 2 |
type(bpy.context.object.data.uv_layers.active.data[bpy.context.object.data.polygons[0].loop_start]) # <class 'bpy.types.MeshUVLoop'> |
А через луп полигона развертки – получить координаты его вертекса:
1 2 |
bpy.context.object.data.uv_layers.active.data[bpy.context.object.data.polygons[0].loop_start].uv # Vector((0.00010003960051108152, 9.998001041822135e-05)) |
Таким образом вертекс меша соотносится с его положением на развертке, что дает возможность манипулировать наложением текстуры.
Механизм лупов реализован так же и во внутреннем формате меша Blender – BMesh.
Механизм лупов в BMesh реализован гораздо шире. Луп имеет свой тип:
1 |
<class 'bmesh.types.BMLoop'> |
и большой набор свойств и методов.
Однако принцип реализации остается неизменным: луп представляет собой комбинацию вертекс + ребро.
Для получения лупов, принадлежащих полигону, нужно выполнить следующий код:
1 2 3 4 5 6 7 8 9 10 11 |
import bmesh bpy.ops.object.mode_set(mode='OBJECT') meshdata = bpy.context.object.data # active mesh b_mesh = bmesh.new() b_mesh.from_mesh(meshdata) b_mesh.faces.ensure_lookup_table() print(b_mesh.faces[0].loops[:]) b_mesh.free() bpy.ops.object.mode_set(mode='EDIT') # [<BMLoop(0x0000020234E24E30), index=0, vert=0x0000020234C07450/0, edge=0x0000020234C2ED30/1, face=0x0000020234BAF410/0>, <BMLoop(0x0000020234E24E78), index=1, vert=0x0000020234C07488/1, edge=0x0000020234C2ED80/2, face=0x0000020234BAF410/0>, <BMLoop(0x0000020234E24EC0), index=2, vert=0x0000020234C074F8/3, edge=0x0000020234C2EDD0/3, face=0x0000020234BAF410/0>, <BMLoop(0x0000020234E24F08), index=3, vert=0x0000020234C074C0/2, edge=0x0000020234C2ECE0/0, face=0x0000020234BAF410/0>] |
Для каждого лупа очень просто получить полигон, которому принадлежит луп, ребро, вдоль которого он идет, и вертекс, из которого луп выходит.
1 2 3 4 5 6 7 |
loop = b_mesh.faces[0].loops[0] loop.face # <BMFace(0x000000000BD28EA0), index=0, totverts=4> loop.edge # <BMEdge(0x000000000BCC8ED0), index=1, verts=(0x000000000BCC0E70/0, 0x000000000BCC0EA8/1)> loop.vert # <BMVert(0x000000000BCC0E70), index=0> |
Второй вертекс (в который приходит луп), можно получить с помощью метода other_vert ребра BMEdge:
1 2 |
loop.edge.other_vert(loop.vert) # <BMVert(0x000000000BCC0EA8), index=1> |
Для ребер и вертексов BMesh так же есть возможность получить лупы, проходящие через них при помощи свойства link_loops.
1 2 3 4 5 6 |
b_mesh.edges.ensure_lookup_table() b_mesh.edges[0].link_loops[:] # [<BMLoop(0x000000000BCE8F68), index=3, vert=0x000000000BCC0EE0/2, edge=0x000000000BCC8E80/0, face=0x000000000BD28EA0/0>] b_mesh.verts.ensure_lookup_table() b_mesh.verts[0].link_loops[:] # [<BMLoop(0x000000000BCE8E90), index=0, vert=0x000000000BCC0E70/0, edge=0x000000000BCC8ED0/1, face=0x000000000BD28EA0/0>] |
Метод
1 |
ensure_lookup_table() |
выполняется для каждого набора полигонов, ребер и вертексов BMesh для того, чтобы упорядочить таблицы индексов этих элементов.
Кроме упрощенного доступа к лупам, в BMesh предусмотрен очень удобный механизм перехода между соседними лупами.
Возьмем для примера любой луп полигона. Вызов свойства
1 |
link_loop_prev |
всегда вернет предыдущий по порядку луп полигона.
Свойство
1 |
link_loop_next |
по аналогии всегда возвращает следующий по порядку луп полигона.
А так как у полученный луп имеет те же самые свойства link_loop_prev и link_loop_next, можно например получить луп, лежащий на противоположной стороне полигона:
1 |
link_loop_next.link_loop_next |
или
1 |
link_loop_prev.link_loop_prev |
Еще одно удобное свойство для перехода между лупами
1 |
link_loop_radial_prev |
и
1 |
link_loop_radial_next |
позволяет перейти от текущего лупа к лупу, лежащему на том же ребре но на соседнем полигоне и направленному в противоположную сторону.
В большинстве случаев для обычной геометрии мешей link_loop_radial_next и link_loop_radial_prev указывают на один и тот же луп.
Они будут указывать на разные лупы только если в одно общее ребро сходятся больше двух полигонов. В этом случае каждый следующий вызов link_loop_radial_next укажет на луп, лежащий на следующем по порядку полигоне ребра, а link_loop_radial_prev – на предыдущий.
Наглядной демонстрацией перехода между лупами может служить пример выделения нескольких ребер друг за другом (подобное выделение производится кликом с зажатой клавишей alt). Возьмем за начало отсчета одно выделенное ребро на меше. Добавим к выделению еще 3 ребра, лежащие по направлению следования исходного.
Для того, чтобы перейти от текущего ребра к последующему, нужно выполнить цепочку переходов между лупами:
1 |
link_loop_next.link_loop_radial_next.link_loop_next |
и выделить ребро, принадлежащее полученному лупу.
Повторяя алгоритм перехода между лупами и выделяя полученные ребра необходимое число раз, получим требуемое выделение.
Полный код скрипта, выделяющего 3 следующих ребра, начиная с выделенного:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import bpy import bmesh bpy.ops.object.mode_set(mode='OBJECT') meshdata = bpy.context.object.data # active mesh data b_mesh = bmesh.new() b_mesh.from_mesh(meshdata) b_mesh.edges.ensure_lookup_table() selected_edge = [edge for edge in b_mesh.edges if edge.select][0] start_loop = selected_edge.link_loops[0] for i in range(3): next_loop = start_loop.link_loop_next.link_loop_radial_next.link_loop_next next_loop.edge.select = True start_loop = next_loop b_mesh.to_mesh(meshdata) b_mesh.free() bpy.ops.object.mode_set(mode='EDIT') |
При всем удобстве механизма лупов, не всегда лупы ведут себя предсказуемо.
Например на срезе меша, если полигон не имеет соседнего полигона, свойства link_loop_radial_next (link_loop_radial_prev) и link_loop_next (link_loop_prev) укажут на один и тот же луп.
Поэтому команда
1 |
link_loop_next.link_loop_radial_next.link_loop_next |
в случае попадания на срез меша укажет на луп, принадлежащий противоположному ребру того же полигона, а не на перпендикулярное ребро соседнего полигона, как может быть ожидалось.
В тоже время, если срез заполнен энгоном, та же самая комбинация переходов выведет именно на ожидаемое перпендикулярное ребро соседнего полигона.
как найти loop_start через bmesh?
b_mesh.faces[0].loops[0]