4

I'm trying to adapt the excellent information in this post https://blender.stackexchange.com/a/119342/18323 to also include x,y rotations of faces and for a case where any modifiers are taken into account with bpy.context.evaluated_depsgraph_get() ...it's close but the rotations are slightly off when scaling non-uniformly? What have I missed?

import bmesh
import mathutils

obj = bpy.context.object

Remove empties

bpy.data.batch_remove((o for o in bpy.context.scene.objects if o.type == 'EMPTY'))

norm_length = 1

#Get the object scale and matrix_world data bpy.ops.object.mode_set(mode='OBJECT')

#Create a bmesh of the object bm = bmesh.new() # create an empty BMesh dg = bpy.context.evaluated_depsgraph_get() bm.from_object(obj, dg)

mw = obj.matrix_world

def create_rotation_matrix_from_face(mx, face):

N = mx.inverted_safe().transposed().to_3x3().normalized()
normal = N @ face.normal
tangent = N @ face.calc_tangent_edge_pair()
binormal = normal.cross(tangent)
rot = mathutils.Matrix()
rot[0].xyz = -tangent
rot[1].xyz = -binormal
rot[2].xyz = normal
return rot.transposed()

#Calculate normal transformations for the selected faces in the bmesh for face in bm.faces: mat = create_rotation_matrix_from_face(mw, face) loc = mw @ face.calc_center_median() rot = mat.to_track_quat().to_euler()

bpy.ops.object.empty_add(
        location = loc,
        rotation = rot
    )
mt = bpy.context.object
mt.empty_display_type = 'ARROWS'
mt.empty_display_size = norm_length
mt.select_set(True)    

bpy.context.view_layer.objects.active = obj obj.select_set(True)

bm.free() # free and prevent further access to bmesh

Dan
  • 951
  • 6
  • 21
  • 1
    Try normalizing normal and tangent with .normalized(). – scurest Sep 06 '21 at 09:38
  • Quick note: have updated linked question to not use the add empty object operator. Multiple operator calls can slow things down significantly. Often use it in test scripts and then remove, decided to leave adding empties in answer. Anyhoo. suggest adding an empty to check tangent vector. Another option is to apply the matrix to the evaluated bmesh and use normal drirectly. – batFINGER Sep 06 '21 at 15:08

2 Answers2

5

From the bmesh directly.

Suggest to get the global normals from an evaluated bmesh, may as well apply the transform (world matrix) and update the normals.

enter image description here

import bpy
import bmesh
from bpy import context
norm_length = 2

bpy.ops.object.mode_set() bpy.data.batch_remove((o for o in context.scene.objects if o.type == 'EMPTY'))

ob = context.object dg = context.evaluated_depsgraph_get()

bm = bmesh.new() bm.from_object(ob, dg) bm.transform(ob.matrix_world) bm.normal_update() for f in bm.faces: n = f.normal mt = bpy.data.objects.new(f"n{f.index}", None) mt.location = f.calc_center_median() mt.rotation_euler = n.to_track_quat().to_euler() mt.empty_display_type = 'SINGLE_ARROW' mt.empty_display_size = norm_length context.collection.objects.link(mt)

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

Or to create a World Matrix from face tangent.

as in question then, for unit scale empties, using normalized vectors

enter image description here

n = f.normal
t = f.calc_tangent_edge_pair().normalized()
bt = n.cross(t).normalized()

M = Matrix([t, bt, n]).transposed().to_4x4() M.translation = f.calc_center_median() mt = bpy.data.objects.new("n{f.index}", None) mt.matrix_world = M

or unnormalized

enter image description here

n = f.normal
t = f.calc_tangent_edge_pair()
bt = n.cross(t)

The results shown from cube with following transform

enter image description here

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • Thanks @batFINGER. Could you help me understand how I can store the correct rotation as a x,y,z Euler though?... (I didn't explain it properly in the question sorry) I need a variable that contains the x,y,z Euler for my use case... in your example you went straight to M = Matrix([t, bt, n]).transposed().to_4x4() ...I'm still trying to wrap my head around all this, thanks for your help! – Dan Sep 07 '21 at 12:46
  • Oh hold on I can just do something simple here like rot = M.Rotation.to_euler() right? (Seeing as you just created the matrix anyway). – Dan Sep 07 '21 at 12:53
  • 1
    The three axes form the columns of the matrix. The Matrix class uses row order. Hence the transpose. (May also see transpose used to invert normalized orthogonal matrices like a rotation) And yes. re Euler. Prefer setting a matrix as it works regardless of rotation mode. – batFINGER Sep 07 '21 at 13:00
  • Ok that's helpful, thanks! – Dan Sep 07 '21 at 13:04
  • Getting an error with rot = M.Rotation.to_euler() ... no attribute 'to_euler()' ... what's the correct way? – Dan Sep 07 '21 at 13:32
  • 1
    M.to_euler() . Recommend use the python console to check things out M = Matrix() then M The convention is a capital letter is a class and can be used as a constructor R = Matrix.Rotation(angle, size, axis) to create an instance of aa matrix R. Mixing the two rarely works. – batFINGER Sep 07 '21 at 13:34
  • I think you forgot the f string here - mt = bpy.data.objects.new(f"n{f.index}", None) – Dan Sep 07 '21 at 13:43
  • Hey @batFINGER, I know this is resolved and prob should be a separate question but when using this in an animation on equal sided faces (eg. Square) the x, y rotations flip all over the place (every frame)... https://docs.blender.org/api/current/mathutils.html?highlight=to_euler#mathutils.Matrix.to_euler... is using the euler_compat (Euler) argument the solution here? – Dan Sep 09 '21 at 08:08
  • This is always going to be an issue using the calc tangent method to create matrix. Sounds like need to do a test if all edges are equal and devise your own calc_tangent* methods.by choosing the most axis aligned pair. The track quat method lets you choose UP, just like a tracking constraint. yeah prob warrants a new question. – batFINGER Sep 09 '21 at 08:27
3

Using a ray cast, this is snapping cursor on face centers (pressing F), with rotation (F + shift). in object mode, edit mode, with subdivision modifier My script was on a active object following the cursor but I simplified for the example. this is quite the same enter image description here

import bpy
import bmesh
from mathutils import Matrix
from bpy_extras import view3d_utils

class ModalOperator(bpy.types.Operator): bl_idname = "object.modal_operator" bl_label = "Simple Modal Operator"

def ray_cast(self, context, event):

    scene = context.scene
    region = context.region
    rv3d = context.region_data
    coord = event.mouse_region_x, event.mouse_region_y
    viewlayer = context.view_layer
    if bpy.app.version >= (2, 91, 0): #well actually under 2.93 but I let it
        viewlayer = viewlayer.depsgraph
    # get the ray from the viewport and mouse
    view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
    ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)

    return scene.ray_cast(viewlayer, ray_origin, view_vector)


def face_center(self, context, event):

    result, location, normal, index, object, matrix = self.ray_cast(
        context, event)

    if object:
        subsurf = [mod for mod in object.modifiers if mod.type == 'SUBSURF']
        if subsurf:
            depsgraph = context.evaluated_depsgraph_get()
            if context.mode == 'OBJECT':
                object_eval = object.evaluated_get(depsgraph)
                face = object_eval.data.polygons[index]
            else:
                mesh_from_eval = bpy.data.meshes.new_from_object(
                    object_eval)
                face = mesh_from_eval.polygons[index]                    
        else:
            face = object.data.polygons[index]

        mw = object.matrix_world
        me=object.data
        loc = mw @ face.center

        if object.mode == 'OBJECT':

            if event.shift: # + rotation copy
                bm = bmesh.new()
                bm.from_mesh(me)
                bm.transform(mw)
                bm.normal_update() # if the obj has rotation

                bm.faces.ensure_lookup_table()
                f = bm.faces[index]
                n = f.normal
                t = f.calc_tangent_edge_pair().normalized()
                bt = n.cross(t).normalized()

                M = Matrix([t, bt, n]).transposed().to_4x4() #rotation
                loc = M.translation = f.calc_center_median()

                context.scene.cursor.location = (0,0,0) 
                context.scene.cursor.matrix = M # or on an object obj.matrix_world = M

                bm.free()
            else:
                context.scene.cursor.location = loc

        else:  # edit              

            if event.shift:
                bm = bmesh.from_edit_mesh(me)
                bm.faces.ensure_lookup_table()
                f = bm.faces[index]
                n = f.normal@mw.inverted()
                t = f.calc_tangent_edge_pair().normalized()@mw.inverted()
                bt = n.cross(t).normalized()

                R = Matrix([t, bt, n]).transposed().to_4x4()                    

                context.scene.cursor.matrix = R
                context.scene.cursor.location = loc
                bmesh.update_edit_mesh(me) 

            else:
                context.scene.cursor.location = loc

def modal(self, context, event):

# to face center
    if event.type == 'F' and event.value == 'PRESS':
        self.face_center(context, event)
        return {'RUNNING_MODAL'}

    elif event.type in {'RIGHTMOUSE', 'ESC'}:
        return {'CANCELLED'}

    elif event.type in {'SPACE', 'RET'}:
        return {'FINISHED'}

    return {'RUNNING_MODAL'}

def invoke(self, context, event):
    context.window_manager.modal_handler_add(self)
    return {'RUNNING_MODAL'}


def register(): bpy.utils.register_class(ModalOperator)

def unregister(): bpy.utils.unregister_class(ModalOperator)

if name == "main": register()

# test call
bpy.ops.object.modal_operator('INVOKE_DEFAULT')

on an object instead of cursor in edit mode I did

mw = context.object.matrix_world 
n = f.normal @ mw.inverted()  
t = f.calc_tangent_edge_pair().normalized() @ mw.inverted()

and here is an example in object mode on a active object Align empty to normal with object matrix world rotation?