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

#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()

            location = loc,
            rotation = rot
    mt = bpy.context.object
    mt.empty_display_type = 'ARROWS'
    mt.empty_display_size = norm_length

bpy.context.view_layer.objects.active = obj

bm.free()  # free and prevent further access to bmesh
  • 1
    $\begingroup$ Try normalizing normal and tangent with .normalized(). $\endgroup$
    – scurest
    Sep 6, 2021 at 9:38
  • $\begingroup$ 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. $\endgroup$
    – batFINGER
    Sep 6, 2021 at 15:08

2 Answers 2


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.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)
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

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

  • $\begingroup$ 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! $\endgroup$
    – Dan
    Sep 7, 2021 at 12:46
  • $\begingroup$ 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). $\endgroup$
    – Dan
    Sep 7, 2021 at 12:53
  • 1
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Sep 7, 2021 at 13:00
  • $\begingroup$ Ok that's helpful, thanks! $\endgroup$
    – Dan
    Sep 7, 2021 at 13:04
  • 1
    $\begingroup$ M.to_euler() . Recommend use the python console to check things out M = Matrix() then M<tab> 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. $\endgroup$
    – batFINGER
    Sep 7, 2021 at 13:34

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]
                    mesh_from_eval = bpy.data.meshes.new_from_object(
                    face = mesh_from_eval.polygons[index]                    
                face = object.data.polygons[index]
            mw = object.matrix_world
            loc = mw @ face.center
            if object.mode == 'OBJECT':

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

                    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

                    context.scene.cursor.location = loc

            else:  # edit              
                if event.shift:
                    bm = bmesh.from_edit_mesh(me)
                    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
                    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):
        return {'RUNNING_MODAL'}

def register():

def unregister():

if __name__ == "__main__":

    # test call

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?


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.