1

As you probably know, you can assign almost anything in Blender to variables, for instance

obj = C.object
scene = D.scenes['Scene']

I want to do the same with vertices you access from obj.data.vertices

#assign like this:
vert = obj.data.vertices[2]

I expected this to not be a problem, but I realized that if you change from Object Mode to Edit Mode and then back to Object Mode again, vert will not refer to obj.data.vertices[2] anymore, instead it's suddenly a different (and weird) vertex instance:

bpy.ops.object.mode_set(mode='OBJECT')

mesh = C.object.data vert = mesh.vertices[2] print(vert == mesh.vertices[2]) #prints 'True' print(vert.index)

bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT')

print(vert == mesh.vertices[2]) #prints 'False' print(vert in list(mesh.vertices)) #prints 'False' print(vert.index) #suddenly a very high random number, even if the mesh only has like 25 vertices total. sometimes negative.

So my question is: How can I assign vertices to variables without them becoming invalid?

My main intention is to save certain properties of vertices (like e.g. their selection state) together with the vertex they belong to at a specific point in a script and then at a later point compare these saved properties with the current ones. Example:

mesh = C.object.data
select_states = {}

for vert in mesh.vertices: select_states.update({vert: vert.select})

#do a bunch of different stuff, including multiple switching between Edit and Object Mode... #then, later:

changes = 0 for vert in select_states: if select_states[vert] != vert.select: #old vs current selection state changes += 1 print(changes,'vertices changed their selection state')

Due to various operations, using a bmesh is not possible. Vertices might get deleted or added, so using indices as an identifier doesn't work either.

I already checked the internet for answers to this question, but I didn't even find anyone who encountered this problem.

Cardboy0
  • 159
  • 8
  • 3
    Related https://blender.stackexchange.com/questions/123742/uniquely-addressing-vertices-in-python – batFINGER Aug 16 '21 at 08:06
  • This might be of interest too : https://developer.blender.org/T88914 Basically, it means that every time you access a vertex, a new python object is created from scratch. I assume it gets collected and re-assigned by the garbage collector once it is used, and keeping a reference to that object then leads to weird behaviour. – Gorgious Aug 16 '21 at 09:19

1 Answers1

1

Okay, if what user Gorgious commented on my question is indeed true, there likely isn't a direct way to assign a vertex to a variable. At least not in a stable way. I would still appreciate to be proven wrong though.

So I decided to switch to an indirect way, which is: We give vertices some kind of tag, and then later identify them by their tag again. This isn't optimal, but it does work for instance even if the vertex indices change. Note however that vertex objects don't support custom properties.

Below you can see code for a class I wrote that tags vertices by assigning them individual weights in a (new) Vertex Group. It hasn't experienced much testing and looks a bit complicated, but it can work correctly and has documentation included.

import bpy
import math
C = bpy.context
D = bpy.data

class tag_vertices: """ Helps keeping track of indivual mesh vertices by creating a vertex group and "tagging" them in it via individual weights

Unlike with most other objects in Blender, it seems like you cannot assign vertices to variables in a stable way. Once you switch to Edit and then back to Object mode, they lose their validity. 
See https://developer.blender.org/T88914 for more on that. 
To circumvent this, this class assigns each vertex an individual weight in a newly created vertex group and is then able to later identify the origianl "identity" of a vertex via its weight.

...

Attributes
----------
obj : bpy.types.Object
    The object whose vertices are being dealt with
VG : bpy.types.VertexGroup
    The vertex group in which the vertices have been assigned individual weights
dicts : list of dictionaries
    every dictionary is from different points of time, with the layout {weight_of_vertex: vertex_index or None}
increment : float
    Used by class functions; is the smallest allowed difference between weights; is based on the total amount of vertices in the mesh
highest_digit : int
    Used by class functions; position of the first digit after the decimal of increment that isn't a zero (increment == 0.0001 -> highest_digit == 4)

Methods
-------
create():
    Called automatically at the creation of a class instance. Assigns weights to all vertices in VG and saves first dictionary in dicts. Do not use it yourself.
add_current():
    Checks which vertex currently has which weight in VG and saves resulting dictionary in dicts attribute.
compare_indices(old_dict = 'DEFAULT', new_dict = 'DEFAULT'):
    Compares two dictionaries of attribute dicts and returns a dictionary with the following layout: {old_vertex_index: new_vertex_index} 
truncate_decimals(number, highest_digit):
    takes in a float number and returns same number with all digits after the highest_digit cut off. Used by other class methods.
get_highest_digit(float_number):
    takes in a float number and returns the position of the first digit that isn't a zero . Used by other class methods.
"""
#
def __init__(self, object = 'ACTIVE', VG_name = 'tagged_vertices'):
    """
    Creates object attributes, VG and calls create(). Cannot be called from edit mode

    Parameters
    ----------
        object : bpy.types.Object OR 'ACTIVE'
            The object that gets the new Vertex Group. If == 'ACTIVE', defaults to the currently active object.
        VG_name : str
            The desired name of the new Vertex Group.
    """
    #
    if C.mode == 'EDIT_MESH':
        raise RuntimeError('__init__ of tag_vertices class cannot be run while in Edit Mode')
    if object == 'ACTIVE':
        object = C.object
    self.obj = object
    VG_i_active = self.obj.vertex_groups.active_index #using active instead of active_index is possible too, but assigning active = None crashes Blender as of version 2.93.2
    self.VG = self.obj.vertex_groups.new(name = VG_name)
    self.obj.vertex_groups.active_index = VG_i_active #setting the previously active VG as active again, as creating a new one makes the new one active
    self.dicts = []
    self.create()
#    
def create(self):
    """
    Called at the creation of a class instance. Assigns weights to all vertices in VG and saves first dictionary {weight: vertex_index} in dicts attribute.
    """
    #
    t_dict = {}
    #VG weights only accept values between 0 and 1, so we need to calculate how big our increment needs to be to not reach that weight of 1 with a vertex.
    self.increment = 1/len(self.obj.data.vertices)   #since weights in a VG can only be between 0.0 and 1.0, we need to calculate the smallest "distance" between weights we should use. 
    self.highest_digit = self.get_highest_digit(self.increment)
    self.increment = self.truncate_decimals(self.increment, self.highest_digit)  #an increment of 0.00384123 gets changed to 0.003, rounding up might mean that the last assigned vertex has a weight over 1.0
    weight = 0 #first vertex gets a weight of 0
    for index in range(len(self.obj.data.vertices)):
        self.VG.add([index], weight, 'ADD')
        t_dict.update({weight: index}) #using floats as keys might lead to problems due to how floats "actually" look, but that's a problem for future-us. Other methods might increase calculating time of this for-loop.
        weight = round(weight + self.increment, self.highest_digit) #floats behave weirdly sometimes, so 0.003 + 0.001 might actually give you 0.00399999999, which we don't want.
        #we must not use truncating on anything other than calculacting the increment value, since truncating a value like 0.00399999 returns 0.003
    self.dicts.append(t_dict)
    #Vertex Groups dont support custom properties, so we cannot save the increment to the VG object
#
def add_current(self):
    """
    Checks which vertex currently has which weight in VG and saves resulting dictionary {weight: vertex_index} in attribute dicts.
    """
    #
    t_dict = {}
    for index in range(len(self.obj.data.vertices)):
        try:
            weight = self.VG.weight(index)
            t_dict.update({round(weight, self.highest_digit): index})
        except:  #VG.weight(index) gives an error if vertex isn't in that VG
            None
    self.dicts.append(t_dict)
#    
def compare_indices(self, old_dict = 'DEFAULT', new_dict = 'DEFAULT'):
    """
    Compares two dictionaries of dicts and returns a dictionary with the following layout: {old_vertex_index: new_vertex_index, etc.}

    Parameters
    ----------
    old_dict : dictionary
        an older dictionary from attribute dicts. You most likely want to use the one that's from the time of creation, AKA the first in the list.
    new_dict : dictionary
        a newer dictionary from attribute dicts.

    Returns
    -------
        t_dict : dictionary
            {old_vertex_index: new_vertex_index or None, etc.}
    """
    #
    if old_dict == 'DEFAULT':
        old_dict = self.dicts[0]
    if new_dict == 'DEFAULT':
        new_dict = self.dicts[1]
    t_dict = {}
    for weight in old_dict:
        old_index = old_dict[weight]
        try:
            new_index = new_dict[weight]
        except:
            new_index = None
        t_dict.update({old_index: new_index})
    return t_dict
#
def truncate_decimals(self, number, highest_digit):    #truncating is the process of basically cutting off all numbers after a certain position in a number. The default math.trunc() function is only able to delete *all* decimals, so we have to make our own custom one to work with certain decimal places.
    """takes in a float number and returns same number with all digits after the highest_digit cut off"""
    multiplier = 10**highest_digit
    circumsized = math.trunc(number * multiplier) / multiplier
    return circumsized #lol
#
def get_highest_digit(self, float_number):  #returns the position of the "largest" number in a float. 0.00052013 would return 4, 0.00000123 would return 6.
    """takes in a float number and returns the position of the first digit that isn't a zero"""
    pos = 0
    while True:
        pos += 1
        if self.truncate_decimals(float_number, pos) > 0:
            break
    return pos

If I were to use that class in the example from my original question, it would look like this:

import bpy
C = bpy.context
D = bpy.data

obj = C.object #using C.object.data is not recommended since it can be unassigned by Blender select_states = {} for vert in obj.data.vertices: select_states.update({vert.index: vert.select}) #{0: True, 1: True, 2: False, etc.} t_verts = tag_vertices()

#do some stuff in viewport, such as deselecting, moving or deleting vertices, switching modes, etc.

t_verts.add_current() #needed to compare old_vs_new = t_verts.compare_indices() #returns a dictionary with this layout {original_index: current_index or None(if deleted)} changes = 0 #comparing selection states for old_index in select_states: new_index = old_vs_new[old_index] if new_index == None: changes += 1 else: old_state = select_states[old_index] new_state = obj.data.vertices[new_index].select if old_state != new_state: #new state can also be None changes += 1 print("a total of",changes,"vertices either changed their selection state or have been deleted")

Cardboy0
  • 159
  • 8
  • IMO it is indeed true. For me seeing your answer is a bit "full circle" . Using vgs or uvs to store other info, led to the advent of the data layers. Using data layers (as outlined in other link https://blender.stackexchange.com/questions/123742/uniquely-addressing-vertices-in-python) is akin to setting custom props on the verts.. – batFINGER Sep 11 '21 at 14:53
  • Yes, as I said it's not an optimal solution, just the best I could come up with. The answer you linked only works with bmesh AFAIK, something I tried to avoid. – Cardboy0 Sep 12 '21 at 08:54
  • Was tempted to close this on asking. Commented re the dupe a month ago. Ignored. Commented above, ignored & answer accepted (whatever). Closed question... ---> Prompted feedback. IMO Reinventing the wheel is never optimal. See edit to dupe, re another misunderstanding. – batFINGER Sep 12 '21 at 09:25