I want to align an empty (Arrows) to a selected face normal with the empty's Z axis aligning to the normal direction (and location which I have working). When the target objects rotation is 0 it's fine... but if I rotate the target object and then select a face I can't work out how to add the object's matrix_world rotation to the empty's matrix_world rotation (I'm thinking this should be easy but can't solve it atm)... here's the code so far. (Add an Arrows Empty named 'Empty' and select one face on an object). any help greatly appreciated.

import mathutils
import bmesh

obj = bpy.context.edit_object
mw = obj.matrix_world.copy()
loc, rot, scale = obj.matrix_world.decompose()

bm = bmesh.from_edit_mesh(obj.data)

# select one face - testing    
for f in bm.faces: 
    if f.select:
        f_location = mw @ f.calc_center_median()
        print("location: ", f_location) #correct
        DirectionVector = f.normal 

bpy.data.objects['Empty'].rotation_mode = 'QUATERNION'
bpy.data.objects['Empty'].rotation_quaternion = DirectionVector.to_track_quat('Z','Y')
bpy.data.objects['Empty'].location = f_location

#bpy.data.objects['Empty'].matrix_world.to_euler = rot
  • $\begingroup$ Thanks @batFINGER, I'll check those out. $\endgroup$
    – Dan
    Jan 11, 2021 at 23:07

2 Answers 2

import bpy
from bpy import context
from mathutils import Matrix, Vector
import bmesh

mw = obj.matrix_world
bm = bmesh.new()

bm.normal_update() # if the obj has rotation
f = bm.select_history.active #last face selected
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("Empty", None)
mt.empty_display_type = 'ARROWS'
mt.empty_display_size = 1
mt.matrix_world = M

  • $\begingroup$ Thanks for this answer... but the final rotation of the empty doesn't match a selected face rotation if the object is scaled non-uniformly. The object scale is applied to the empty, but the empty's orientation is already aligned with the face so it effects it incorrectly... perhaps applying the object scale to the empty before matching the rotation would work? $\endgroup$
    – Dan
    Feb 5, 2021 at 10:49
  • $\begingroup$ @Dan I finally modified it to correct this problem $\endgroup$ Mar 15, 2021 at 17:34
  • $\begingroup$ New version all in object mode and working even if cursor is not at zero $\endgroup$ Sep 24, 2021 at 14:53
  • $\begingroup$ Suggest there is no need to apply transforms, then re - apply the inverse on the Mesh Can all be done directly on bmesh as shown here blender.stackexchange.com/a/237414/15543 for an evaluated mesh. $\endgroup$
    – batFINGER
    Sep 24, 2021 at 15:40
  • $\begingroup$ without it the empty is not well aligned and orientation of axis is different. (on a object with scale not applied) $\endgroup$ Sep 24, 2021 at 15:54

Using the instancer.

enter image description here

If we add an object at global origin, set it as the child of our mesh, turn on instancing and set to faces then an instance of each object is created at the face center and aligned to the face normal as calculated internally by blender.

In a number of your questions

Vertex as x,y co-ordinates from face center for animated object

Face location, rotation world matrix with x, y, z and bmesh

it has been noted that using

t = f.calc_tangent_edge_pair()

will flip flop on faces when there are a number of same length edges.

Here is a test script, select a mesh object

  • Adds an empty and makes it a dupli-face ie an instance is created for each face aligned to each face normal

  • Iterate over the instance objects, make a copy of the matrix world

  • Match to mesh objects polygons. Contend they are instance in polygon index order

Test script. Run with Mesh object selected, active in OBJECT mode.

import bpy
from bpy import context
from mathutils import Matrix

ob = context.object
mw = ob.matrix_world
scene = context.scene
# clear all empties
bpy.data.batch_remove((ob for ob in scene.objects if ob.type == 'EMPTY'))
# add an empty to instance
        location=(0, 0, 0)
mt = context.object
mt.parent = ob
S = Matrix.Diagonal(mw.to_scale().to_4d())
mt.matrix_parent_inverse = S.inverted()

mt.empty_display_type = 'SINGLE_ARROW'
ob.instance_type = 'FACES' 

# get the matrix worlds of the instances

dg = context.evaluated_depsgraph_get()

mats = [i.matrix_world.copy() for i in dg.object_instances if i.is_instance and i.object.name == mt.name]

# Same number 
print(len(mats), len(ob.data.polygons))

# contend their order matches  

for i, mat in enumerate(mats):
    mt = context.object
    mt.name = f"Face{i}"
    mt.show_name = True
    mt.empty_display_type = 'ARROWS'
    mt.matrix_world =  mat

Note theoretically this should also work on modified mesh.

enter image description here

But it does not.. may make a question regarding.


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.