def _exec_obj_mode(self, context): all_type_err = True # no obj is of type mesh vert_count_err = False # less than 2 vertices selected if self.join_select: coords = np.empty((0, 3), dtype=float) ob = context.object if context.object else \ context.selected_objects[0] mat_wrld_inv = np.array(ob.matrix_world.inverted()) for o in context.selected_objects: if o.type == 'MESH': all_type_err = False else: continue ocoords = sbio.get_vecs(o.data.vertices) if o is not ob: mat = mat_wrld_inv @ np.array(o.matrix_world) ocoords = sbt.transf_pts(mat, ocoords) coords = np.concatenate((coords, ocoords)) if len(coords) > 1: self._core(context, ob, coords, context.selected_objects) else: vert_count_err = True else: for o in context.selected_objects: if o.type == 'MESH': all_type_err = False else: continue # If we align to axes and the ob is rotated, we can't # use Blender's bounding box. Instead, we have to find # the global bounds from all global vertex positions. # This is because for a rotated object, the global # bounds of its local bounding box aren't always equal # to the global bounds of all its vertices. # If we don't align to axes, we aren't interested in the # global ob bounds anyway. rot = np.array(o.matrix_world.to_euler()) coords = sbio.get_vecs(o.data.vertices) \ if self.align_to_axes and rot.dot(rot) > 0.001 \ else np.array(o.bound_box) if len(coords) > 1: self._core(context, o, coords, [o]) else: vert_count_err = True if vert_count_err: self.report({'ERROR_INVALID_INPUT'}, "A selection must at least contain two vertices") if all_type_err: self.report({'ERROR_INVALID_INPUT'}, "An object must be of type mesh")
def execute(self, context): obs_in_editmode = context.objects_in_mode_unique_data bpy.ops.object.mode_set(mode='OBJECT') try: for o in obs_in_editmode: viewdir = Vector((0, 0, -1)) for area in context.screen.areas: if area.type != 'VIEW_3D': continue r3d = area.spaces[0].region_3d if r3d is None: continue viewdir.rotate(r3d.view_rotation) break # For every face normal, calculate the dot product # with the view direction nrmls = get_vecs(o.data.polygons, attr='normal') dotprdcs = np.dot(nrmls, np.array(viewdir)) # Select each face with dot product entry > 0 selflags = np.greater(dotprdcs, 0) o.data.polygons.foreach_set('select', selflags) finally: bpy.ops.object.mode_set(mode='EDIT') return {'FINISHED'}
def execute(self, context): for o in context.selected_editable_objects: if o.type != 'MESH': continue coords = get_vecs(o.data.vertices) # Reconstruct basis vectors via Singular Value Decomposition _, _, v = np.linalg.svd(coords) # Ensure basis vectors are as close to world axes as # possible: if a basis vector points in the opposite # direction, invert it i3 = np.identity(3) for i in range(3): if np.dot(v[i], i3[i]) < 0: v[i] = -v[i] # Apply new basis to mesh set_vals(o.data.vertices, coords @ v.T) # Modify object rotation so object stays in the same place, # even with its mesh changed o.matrix_basis @= Matrix(append_row_and_col(v.T)) return {'FINISHED'}
def execute(self, context): if self.method == 'RED': meth = get_red elif self.method == 'GRE': meth = get_green elif self.method == 'BLU': meth = get_blue else: meth = avg_rgb for o in context.selected_editable_objects: if o.type != 'MESH': continue cols = o.data.vertex_colors.active if not cols: continue # Get colors of all mesh loops # These loops don't always match the vertex count and # are not stored at the correct vertex indices. cs = get_vecs(cols.data, attr='color', vecsize=4) # For every loop, get its vertex index lops = o.data.loops vindcs = get_scalars(lops, attr='vertex_index', dtype=np.int) # Find the indices of 'vindcs' at which a unique entry is # found for the first time. _, i_vindcs = np.unique(vindcs, return_index=True) # This index list 'i_vindcs' filters out all redundant # entries in 'cs' and sorts them so each color lands at # the index of its corresponding vertex. # Then calculate the (unique) weights of the colors via # the chosen method. weights = meth(cs[i_vindcs]) u_weights = np.unique(weights) vg = o.vertex_groups.new(name=cols.name) for w in u_weights: # Get the indices of one weight value and add it to # the vertex group. indcs = np.where(weights == w)[0] vg.add(indcs.tolist(), w, 'REPLACE') return {'FINISHED'}
def _find_parts(self, context): for o in context.objects_in_mode: data = o.data parts = TreeDict(acc=concat) coords = get_vecs(data.vertices) # choose comparison method method = np.linalg.norm if self._method == 0 else np.prod for indcs in get_parts(from_edit_mesh(data).verts): bounds, _ = get_bounds_and_center(coords[indcs]) # calculate comparison value from bounding box, # round to create less bins in dict for better # performance key = round(method(bounds), self._resolution) parts[key] = indcs self._data.append((data.vertices, parts))
def _find_parts(self, obs): for o in obs: # get parts, each a vertex index list bob = bmesh.new() bob.from_mesh(o.data) parts = get_parts(bob.verts) del bob # choose comparison method method = np.linalg.norm if self._method == 0 else np.prod # create dict of parts and their comparison value partdict = TreeDict(acc=concat) coords = get_vecs(o.data.vertices) for indcs in parts: bounds, _ = get_bounds_and_center(coords[indcs]) # calculate comparison value from bounding box, # round to create less bins in dict for better # performance key = round(method(bounds), self._resolution) partdict[key] = indcs self._data.append((o.data.vertices, partdict))
def execute(self, context): # TARGET target = context.object tdata = target.data tverts = tdata.vertices teuler = target.matrix_world.to_euler() if tdata.is_editmode: # ensure newest changes from edit mode are visible to data target.update_from_editmode() # get selected vertices in target sel_flags_target = sbio.get_scalars(tdata.vertices) tcoords = sbio.get_vecs(tverts)[sel_flags_target] else: trot = np.array(teuler) # If we align to axes and the target is rotated, we can't # use Blender's bounding box. Instead, we have to find the # global bounds from all global vertex positions. # This is because for a rotated object, the global bounds of # its local bounding box aren't always equal to the global # bounds of all its vertices. # If we don't align to axes, we aren't interested in the # global target bounds anyway. tcoords = sbio.get_vecs(tverts) \ if self.axes_align \ and trot.dot(trot) > 0.001 \ else np.array(target.bound_box) if len(tcoords) < 2: self.report({'ERROR_INVALID_INPUT'}, "Select at least 2 vertices") return {'CANCELLED'} tworldmat = np.array(target.matrix_world) if self.axes_align: # If we align sources to world axes, we are interested in # the target bounds in world coordinates. tcoords = sbt.transf_pts(tworldmat, tcoords) # If we align sources to axes, we ignore target's rotation. trotmat = np.identity(3) tbounds, tcenter = sbio.get_bounds_and_center(tcoords) if not self.axes_align: # Even though we want the target bounds in object space if # align to axes is false, we still are interested in world # scale and center. tbounds *= np.array(target.matrix_world.to_scale()) tcenter = sbt.transf_point(tworldmat, tcenter) # target rotation to later apply to all sources trotmat = np.array(teuler.to_matrix()) # SOURCE error_happened = False for source in context.selected_objects: if source is target: continue if source.type != 'MESH': continue sdata = source.data sverts = sdata.vertices if sdata.is_editmode: # get selected vertices in source source.update_from_editmode() sverts_all = sbio.get_vecs(sdata.vertices) sselflags = sbio.get_scalars(sdata.vertices) sverts_sel = sverts_all[sselflags] if len(sverts_sel) < 2: error_happened = True continue else: sverts_sel = np.array(source.bound_box) sbounds, scenter = sbio.get_bounds_and_center(sverts_sel) sbounds_recpr = np.reciprocal( sbounds, # prevent division by 0 out=np.ones_like(sbounds), where=sbounds != 0, ) # assemble transformation matrix later applied to source transf_mat = \ sbmm.to_transl_mat(tcenter) @ \ sbmm.append_row_and_col( trotmat @ sbmm.to_scale_mat(tbounds) @ sbmm.euler_to_rot_mat(np.array(self.rot_offset)) @ sbmm.to_scale_mat(sbounds_recpr) ) @ \ sbmm.to_transl_mat(-scenter) if sdata.is_editmode: # somehow the mesh doesn't update if we stay in edit # mode bpy.ops.object.mode_set(mode='OBJECT') # transform transformation matrix from world to object # space transf_mat = np.array(source.matrix_world.inverted()) \ @ transf_mat # update every selected vertex with transformed # coordinates sverts_all[sselflags] = \ sbt.transf_pts(transf_mat, sverts_sel) # overwrite complete vertex list (also non-selected) sbio.set_vals(sverts, sverts_all) bpy.ops.object.mode_set(mode='EDIT') else: source.matrix_world = Matrix(transf_mat) if error_happened: self.report( {'ERROR_INVALID_INPUT'}, "Select at least 2 vertices per selected source object") return {'FINISHED'}
def get_sel_verts(o): # ensure newest changes from edit mode are # visible to data o.update_from_editmode() sel_flags = sbio.get_scalars(o.data.vertices) return sbio.get_vecs(o.data.vertices)[sel_flags], sel_flags
def execute(self, context): source = context.object try: sdata = source.data sgeom = sdata.polygons except AttributeError: self.report( {'ERROR_INVALID_INPUT'}, "The active object needs to have a mesh data block.", ) return {'CANCELLED'} # get comparison values for source scvals = get_vecs(sgeom, 'center') if self.in_wrld_crds: # transform source to world coordinates mat = np.array(source.matrix_world) scvals = transf_pts(mat, scvals) # build KD-Tree from comparison values kd = KDTree(len(sgeom)) for i, v in enumerate(scvals): kd.insert(v, i) kd.balance() # get values to transfer from source stvals = get_scalars(sgeom, 'material_index', np.int8) all_meshless = True # for error-reporting for target in context.selected_objects: if target is source: continue try: tdata = target.data tgeom = tdata.polygons all_meshless = False except AttributeError: continue # get comparison values for target tcvals = get_vecs(tgeom, 'center') if self.in_wrld_crds: # transform target to world coordinates mat = np.array(target.matrix_world) tcvals = transf_pts(mat, tcvals) ttvals = np.empty(len(tgeom), dtype=np.int32) # for every comparison point in target, find closest in # source and copy over its transfer value for ti, tv in enumerate(tcvals): _, si, _ = kd.find(tv) ttvals[ti] = stvals[si] # set values to transfer to target set_vals(tgeom, ttvals, 'material_index') tmats = tdata.materials if self.assign_mat: # transfer assigned materials for i, m in enumerate(sdata.materials): if i < len(tmats): tmats[i] = m else: tmats.append(m) if all_meshless: self.report( {'ERROR_INVALID_INPUT'}, "No selected target object has a mesh data block.", ) return {'CANCELLED'} return {'FINISHED'}
def execute(self, context): mode = context.mode selobs = context.selected_objects arms = [o for o in selobs if o.type == 'ARMATURE'] if not arms: self.report({'ERROR_INVALID_INPUT'}, "Select exactly one armature.") return {'CANCELLED'} arm = arms[0] bone1, bone2 = None, None for b in arm.data.bones: if b.select: if bone1 is None: bone1 = b else: bone2 = b break if bone2 is None: self.report({'ERROR_INVALID_INPUT'}, "Select exactly two bones.") return {'CANCELLED'} # Transform bones into world system arm2wrld = np.array(arm.matrix_world) b1 = transf_point(arm2wrld, bone1.head_local) b2 = transf_point(arm2wrld, bone2.head_local) dims = np.array(self.axes) # Vector from bone1 to bone2, if wished projected onto the # axis/plane isolated through the 'axes' parameter by zeroing # coordinates in ignored dimensions b1b2 = (b2 - b1) * dims # Squared distance between the bones sqrbdist = np.linalg.norm(b1b2)**2 if sqrbdist == 0: # If both bones are in the same position after projection, # we can't interpolate between them. # This way, bone1 is assigned a weight of one. sqrbdist = 1e-15 for o in selobs: if o.type != 'MESH': continue # Get selected vertices o.update_from_editmode() verts = o.data.vertices selflags = get_scalars(verts) pts = get_vecs(verts)[selflags] # Also transform vertices into the world system pts = transf_vecs(o.matrix_world, pts) # Calc vector from bone1 to every point and zero coordinates # of dimensions that are ignored. b1pts = (pts - b1) * dims # Calc dot product of this vector and the vector from bone1 # to bone2 # Get final weight by dividing through the squared bone # distance weights = np.dot(b1b2, b1pts.T) / sqrbdist # breakpoint() # Get indices of selected vertices indcs = np.arange(len(selflags))[selflags] vgs = o.vertex_groups try: vg1 = vgs[bone1.name] except KeyError: self.report( {'ERROR_INVALID_INPUT'}, (f"Vertex group '{bone1.name}' does not exist " f"on object '{o.name}'"), ) continue if self.bidirect: try: vg2 = vgs[bone2.name] except KeyError: self.report( {'ERROR_INVALID_INPUT'}, (f"Vertex group '{bone2.name}' does not " f"exist on object '{o.name}'"), ) continue # The mesh doesn't update in edit mode. bpy.ops.object.mode_set(mode='OBJECT') try: for i, w in zip(indcs, weights): # No clue why add expects a one-element list, but # that's how it is. # item() converts the NumPy int to a native int idx = [i.item()] vg1.add(idx, 1 - w, 'REPLACE') if self.bidirect: vg2.add(idx, w, 'REPLACE') finally: # This is so stupid Blender! Why not make those # strings match!? if mode == 'PAINT_WEIGHT': bpy.ops.object.mode_set(mode='WEIGHT_PAINT') else: bpy.ops.object.mode_set(mode='EDIT') return {'FINISHED'}
def sample_mesh(mesh, samplecnt=1024, mask=None): """ Draw N random samples on the surface of a triangle mesh. Parameters ---------- mesh : bpy.types.Mesh Blender mesh to sample from. samplecnt : int = 1024 Number of samples to use. mask : Iterable or None = None Iterable specifying the faces from which to sample either by passing their index in the mesh's face list as an Integer iterable or as a Bool iterable where that specific index is set to True. If None is passed, every face is sampled. Returns ------- out : numpy.ndarray 2D array with shape (N, 3), containing the coordinates of the N drawn sample points. """ # Load mesh into bmesh, bob = bm.new() bob.from_mesh(mesh, face_normals=False) if mask is not None: bfaces = np.array(bob.faces) invmask = np.ones(len(bfaces), dtype=np.bool) invmask[mask] = False bm.ops.delete(bob, geom=bfaces[invmask], context='FACES') del bfaces, invmask # triangulate it, bm.ops.triangulate(bob, faces=bob.faces) # and save it back to a temporary mesh, so that we can use # Blenders' fast-access foreach functions to get vectorized data. mesh = bpy.data.meshes.new("tmp") bob.to_mesh(mesh) del bob # Accumulate all triangle areas in the mesh to sample each triangle # with a probability proportional to its surface area. areas = get_scalars(mesh.polygons, 'area', np.float64) areas = np.cumsum(areas) # Choose N random floats between 0 and the sum of all areas. rdareas = np.random.uniform(0., areas[-1], samplecnt) # For each random float, find the index of the triangle with the # highest, but less equal cumulative area (the left neighbor of the # randomly drawn area). rdindcs = np.searchsorted(areas, rdareas) # These vectorized calculations eat up a lot of memory. Make some # unneeded data applicable for garbage collection. del areas, rdareas # Get the vertex coordinates. pts = get_vecs(mesh.vertices) # Get the vertex indices for each triangle. tris = get_vecs(mesh.polygons, 'vertices', dtype=np.int32) bpy.data.meshes.remove(mesh) # Inner indexing operation: For each randomly chosen triangle index, # insert the actual vertex indices of the corresponding triangle # into the array. # Outer indexing operation: For each vertex index in the triangle # array, insert its actual vertex coordinates. # This 3D array holds a lot of redundant data now, this probably # scales pretty badly with the sample count and the number of # triangles. tris = pts[tris[rdindcs, :]] del rdindcs, pts # For each sample, draw two random floats that determine where on # the triangle the sample point is placed. # This is done via the following formula, with the triangle's # vertex coordinate vectors A, B, C: # P = (1 - sqrt(r1))*A + sqrt(r1)*(1 - r2)*B + sqrt(r1)*r2*C r1 = np.sqrt(np.random.rand(samplecnt)) r2 = np.random.rand(samplecnt) # Calculate coefficients for each vertex A, B, C coef = np.stack( (1 - r1, r1 * (1 - r2), r1 * r2), axis=1, ).reshape(-1, 3, 1) del r1, r2 # For each triangle sum up A, B, and C, leaving one point per # sample. Remember, one triangle can be several times in 'tris', # but a different sample is drawn from its surface every time. return np.sum(coef * tris, axis=1)
def _find_concave_patches(self, context): """ A patch is a set of connected faces. For each patch containing at least two faces facing each other, return a list of vertex indices making up such a patch, together with its center point and diameter. A patch's border is along edges between faces facing away from each other (think ridges). """ self._meshes.clear() for o in context.objects_in_mode_unique_data: o.update_from_editmode() data = o.data polys = data.polygons bob = bm.from_edit_mesh(data) bfaces = bob.faces bfaces.ensure_lookup_table() # bmesh has to recalculate face centers, so get them # directly from the mesh data instead centrs = get_vecs(polys, attr='center') # Bool array of vertex indices already visited. # Unselected faces will be True already. flags = get_scalars(polys) # Will contain a list of tuples. First entry is the list of # face indices of the patch. Second is the maximum angle # between two neighboring faces in the patch. # This list is only needed to not delete vertices while we # iterate the mesh. patches = [] for i, new in enumerate(flags): if not new: continue flags[i] = False # Faces to visit stack = [bfaces[i]] # Face indices of the patch findcs = [] # Maximum dot product between two neighboring faces in # the patch. maxdot = 0 while stack: f = stack.pop() n = f.normal c = centrs[f.index] findcs.append(f.index) # Push all faces connected to f on stack... for l in f.loops: f2 = l.link_loop_radial_next.face i2 = f2.index # The dot product between f's normal and the # vector between both face's centers is a # simple way to measure if they are parallel # (=0), concave (>0), or convex (<0). angl = n.dot(normalize(centrs[i2] - c)) # but only if not already checked and f and f2 # are not convex (don't face away from each # other) if flags[i2] and angl > -1e-3: maxdot = max(maxdot, angl) flags[i2] = False stack.append(f2) if len(findcs) > 2: # pihalf: transform dot product result to rad angle patches.append((findcs, maxdot * pihalf)) del flags # second representation of patches, this time as a tuple of # face indices, max angle, and diameter patches2 = [] for findcs, maxangl in patches: bounds, _ = get_bounds_and_center(centrs[findcs]) patches2.append((findcs, maxangl, np.linalg.norm(bounds))) self._meshes.append((data, patches2))