2

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

Dan
  • 951
  • 6
  • 21

2 Answers2

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

obj=context.object
mw = obj.matrix_world
me=obj.data
bm = bmesh.new()
bm.from_mesh(me)

bm.transform(mw)
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
context.collection.objects.link(mt)

bm.free()
```
  • 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? – Dan Feb 05 '21 at 10:49
  • @Dan I finally modified it to correct this problem – Uneconscience UneSource Mar 15 '21 at 17:34
  • New version all in object mode and working even if cursor is not at zero – Uneconscience UneSource Sep 24 '21 at 14:53
  • 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 https://blender.stackexchange.com/a/237414/15543 for an evaluated mesh. – batFINGER Sep 24 '21 at 15:40
  • without it the empty is not well aligned and orientation of axis is different. (on a object with scale not applied) – Uneconscience UneSource Sep 24 '21 at 15:54
  • That's Absolute BS. Apply the transform to the bmesh, update the normals. Don't write bmesh back to mesh. – batFINGER Sep 24 '21 at 15:56
  • ok thanks to your explanations I think I will make it work in edit mode too now – Uneconscience UneSource Sep 24 '21 at 22:15
1

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

bpy.ops.object.empty_add( 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): bpy.ops.object.empty_add() 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.

batFINGER
  • 84,216
  • 10
  • 108
  • 233