# Set a polygon's normal's orientation as the objects local rotation orientation (if every polygon is an object)

So,

Each of my polygons is a separate object. Their default normals point like this:

But their orientation or local axes are like so:

How can I make their local axes to be like so, but automatically?

I guess particles might be one solution for this exact case (if you emit new objects on top of the existing ones) but let's say the original objects are triangles and I wouldn't want to change their shape and pointing direction at all, only their local orientation axes, would that be possible?

Cheers!

• The most simple answer is no. It's not possible using commonly available built in Blender functions, but if you need it for transformations in edit mode, then setting Normal as transformation orientation will do the job. It's also possible via Python script. – Mzidare Sep 19 '18 at 17:43
• You can hack it manually, by snapping a cube to the plane's face, with 'Align Rotation' switched on, Ctrl-J joining the plane to the (active) cube, and deleting the cube's part of the joined mesh in Edit Mode. But i guess that's not what you mean by automatic. – Robin Betts Sep 19 '18 at 19:27
• Robin Betts - haha, thanks, I was just experimenting with that, it's nice that you can actually delete all the vertices or mesh data of an object and still use it for aligning and joining other objects into it. – Manu Järvinen Sep 19 '18 at 19:45
• I think this procedure will be shortened in 2.8 - where the 3D cursor will have an orientation. – Robin Betts Sep 19 '18 at 23:07

A much much much faster script to do this

EDIT

Noticed the edit re other answer from OP, and given zero Feedback thought I'd have another look. As OP noted in answer, Using that many operators is going to give terrible performance

The rate determining step in old version was using face split and separate by loose parts operators, the latter is notoriously slow.

Here is another take on this with No operators used at all.

The object created from each face has its origin at face center and local Z axis along its face normal. Could use edges to align the x or y axis along the face..

For convenience adds them to a collection named "FaceCollection"

import bpy
from mathutils import Vector

context = bpy.context

z = Vector((0, 0, 1))

face_col = bpy.data.collections.new(
"FaceCollection",
)

for ob in context.selected_objects:

mw = ob.matrix_world
mwi = mw.inverted()
for f in ob.data.polygons:
me = bpy.data.meshes.new(f"Face{f.index}")
me.from_pydata(
[ob.data.vertices[i].co - f.center for i in f.vertices],
[],
[range(len(f.vertices))]
)
fo = bpy.data.objects.new(
f"Face{f.index}",
me
)
# face space
zf = f.normal
q = zf.rotation_difference(z)
M =  q.to_matrix().to_4x4()
me.transform(M)
fo.matrix_world = mw.to_3x3().to_4x4() @ M.inverted()
fo.matrix_world.translation = mw @ f.center

# remove originals?
#bpy.data.objects.remove(ob)


This takes a couple of seconds on an old machine with a 4-icosphere with 1280 faces.

End EDIT

NOTE Appears to be a logic error in this when not Identity Matrix World, didn't bother to fix

Used as answer to Calculating and exporting global rotation of faces created from splitting an isosphere (possible dupe?) thought it would be good here too

Shows origin and local orientation of one split off face

• Adds an icosphere, edge splits, separates and returns to object mode

• Selected objects are the result of separate modifier.

• For each face object get its lone face and calculate the global center and normal. See how far the normal is rotated from default quaternion.

• Use this to create the global matrix aligned to normal, origin at center. The object space inverse is applied to the mesh with transfrom (makes origin (0, 0, 0).

• The global transform is given to the object by way of world matrix.

Script.

import bpy
from mathutils import Quaternion

context = bpy.context
bpy.ops.mesh.edge_split()
bpy.ops.mesh.separate(type='LOOSE')

bpy.ops.object.mode_set()
z = Quaternion()
for ob in context.selected_objects:
print(ob)
me = ob.data
mw = ob.matrix_world
mwi = mw.inverted()
f = ob.data.polygons[0]
c = mw @ f.center
n = mw @ f.normal

q = z.rotation_difference(n.to_track_quat())
M =  q.to_matrix().to_4x4()
M.translation = c
me.transform(mwi @ M.inverted())
ob.matrix_world = M



Related. Another example of making a matrix based on known orthogonals.

https://blender.stackexchange.com/a/177331/15543

Yay! I actually managed myself to make a brute-force artist-made stupid script that seemed to actually work well for the many types of situations one faces with 100s of different polygon-objects.

Usage:

• Select an object with less polygons than 2000 (otherwise it takes ages, 500 polys takes like 30 seconds)

• Run the script

## Blender 2.80+:

The Collection this makes is an invisible one, like a group used to be in Blender 2.79b. You can remove this by going to Outliner and in the drop-down menu select Blender File and in there Collections.

# WARNING: This is quite a heavy script. It takes like 30-60 seconds to handle 512 faces alone.

## SETTINGS ##
PolygonName = "PolygonObject.000"
CollectionName = "PolygonCollection"

## CODE ##
import bpy
bpy.ops.collection.create(name=CollectionName)
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.select_all(action = 'SELECT')
bpy.ops.mesh.mark_sharp()
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.modifier_apply(modifier="EdgeSplit")
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.separate(type = 'LOOSE')
bpy.ops.object.mode_set(mode = 'OBJECT')

for obj in bpy.context.selected_objects:
#bpy.context.active_object.show_axis = True
bpy.context.active_object.name = "NormalAxisObject"
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.delete(type ='FACE')
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.context.active_object.rotation_mode = 'YXZ'
#bpy.context.active_object.show_axis = True
bpy.context.active_object.name = "NormalAxisObject"
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.delete(type ='FACE')
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.context.active_object.rotation_mode = 'YXZ'

bpy.context.view_layer.objects.active = obj
obj.particle_systems['ParticleSettings'].settings.type = 'HAIR'
obj.particle_systems['ParticleSettings'].settings.count = 1
obj.particle_systems['ParticleSettings'].settings.userjit = 1
obj.particle_systems['ParticleSettings'].settings.render_type = 'OBJECT'
obj.particle_systems['ParticleSettings'].settings.instance_object = bpy.data.objects['NormalAxisObject']
obj.select_set(state=True)
bpy.ops.object.duplicates_make_real()
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects['NormalAxisObject'].select_set(state=True)
bpy.ops.object.delete()

bpy.data.objects['NormalAxisObject.001'].select_set(state=True)
bpy.context.view_layer.objects.active = bpy.data.objects['NormalAxisObject.001']
bpy.ops.object.select_all(action='DESELECT')

obj.select_set(state=True)
bpy.data.objects['NormalAxisObject.001'].select_set(state=True)
bpy.context.view_layer.objects.active = bpy.data.objects['NormalAxisObject.001']
bpy.ops.object.join()
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.data.objects['NormalAxisObject.001'].name = PolygonName

bpy.ops.object.select_same_collection(collection=CollectionName)


## Blender 2.79b:

# WARNING: This is quite a heavy script. It takes like 30-60 seconds to handle 512 faces alone.

## SETTINGS ##
PolygonName = "PolygonObject.000"
GroupName = "PolygonGroup"

## CODE ##
import bpy
bpy.ops.group.create(name=GroupName)
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.select_all(action = 'SELECT')
bpy.ops.mesh.mark_sharp()
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.modifier_apply(apply_as = 'DATA', modifier="EdgeSplit")
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.separate(type = 'LOOSE')
bpy.ops.object.mode_set(mode = 'OBJECT')

for obj in bpy.context.selected_objects:
#bpy.context.active_object.show_axis = True
bpy.context.active_object.name = "NormalAxisObject"
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.delete(type ='FACE')
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.context.active_object.rotation_mode = 'YXZ'

bpy.context.scene.objects.active = obj
obj.particle_systems['ParticleSystem'].settings.type = 'HAIR'
#obj.particle_systems['ParticleSystem'].settings.hair_length = 1
obj.particle_systems['ParticleSystem'].settings.count = 1
obj.particle_systems['ParticleSystem'].settings.userjit = 1
obj.particle_systems['ParticleSystem'].settings.render_type = 'OBJECT'
obj.particle_systems['ParticleSystem'].settings.dupli_object = bpy.data.objects['NormalAxisObject']
obj.select = True
bpy.ops.object.duplicates_make_real()
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects['NormalAxisObject'].select = True
bpy.ops.object.delete()

bpy.data.objects['NormalAxisObject.001'].select = True
bpy.context.scene.objects.active = bpy.data.objects['NormalAxisObject.001']
#bpy.context.object.rotation_euler[0] = bpy.context.object.rotation_euler[0] - 1.57079633 # put Z as the up axis
bpy.ops.object.select_all(action='DESELECT')

obj.select = True
bpy.data.objects['NormalAxisObject.001'].select = True
bpy.context.scene.objects.active = bpy.data.objects['NormalAxisObject.001']
bpy.ops.object.join()
bpy.ops.object.transform_apply(scale=True)
bpy.data.objects['NormalAxisObject.001'].name = PolygonName

bpy.ops.object.select_same_group(group=GroupName)


If you calculate the right rotation matrix and transform the vertices with the inverse of that matrix while assigning the object transforms with the Euler angles of the matrix, then the axis will point in the right direction while the meshes haven't moved.

As a prerequisite you have to specify how exacly the new axes should be aligned. If we assume that the z axis points into the direction of the normal then we need to find a way to define the rotation of the x and y axes around the z. In the example script at the bottom I assume that the x axis should point to the midpoint of the two vertices with the highest x value. An alternative could be that you're taking the average of the vertices with the correct vertex indices, a single vertex at random or maybe the vertex with the greatest distance to the center for an irregular polygon.

The rotation matrix then is given by

from mathutils import Matrix

rot = Matrix.Identity(3)
rot[0] = guide                # x
rot[1] = normal.cross(guide)  # y
rot[2] = normal               # z
rot = rot.transposed()


where normal is the polygon's normal and guide is a normalized vector to uniquely define the rotation around the normal.

Once you have that matrix, you can easily rotate the mesh (with the matrix's inverse) as well as assign the object's transforms to counter that:

import bpy
from mathutils import Matrix

objs = [obj for obj in bpy.data.objects if obj.name.startswith('Plane')]

for obj in objs:
verts = obj.data.vertices
normal = obj.data.polygons[0].normal

sorted_verts = sorted((v.co for v in verts), key=lambda co: co.x, reverse=True)
guide = (sorted_verts[0] + sorted_verts[1]).normalized()

# Vector rejection in case the guide isn't perpendicular to the normal
# e.g. when the object origin isn't in the polygon's plane
guide_projection = guide.dot(normal) * normal
guide_rejection = guide - guide_projection
guide = guide_rejection.normalized()

rot = Matrix.Identity(3)
rot[0] = guide
rot[1] = normal.cross(guide)
rot[2] = normal
rot = rot.transposed()

for v in verts:
v.co = rot.inverted() * v.co

obj.rotation_euler = rot.to_euler()


N.B.: My script will fail if a plane is rotated so that the normal points in the global x axis and the sorting function will pick to vertices opposite of each other so that the guide vector is zero. Production code would probably get a better sorting function ;-)

• Wow! Thanks! Worked wonderfully for a couple of isolated planes! But then, if I apply this for a larger experiment, say, an Icosphere that has all its polygons separated as individual objects (mark all edges as sharp, edgesplit modifier, apply, separate by loose parts, pivots centered to geometry), and then the script is run: all the faces nudge a little bit in their rotation :/ - I actually managed myself to make a brute-force artist-made stupid script that seemed to actually work better for the many types of situations one faces with 100s of different polygon-objects. I'll post it here – Manu Järvinen Sep 19 '18 at 23:28
• I'm glad you got it working. But just out of curiosity: How much nudging are you talking about? I just tested the script with an icosphere and detected no visible wiggle. Initially I assumed there are rounding errors with the matrix calculations, and some additional normalization, e.g. for the cross product, would help. But unfortunately (or fortunately?) I can't notice a bug. – binweg Sep 20 '18 at 6:40
• Hey! That’s wonderful news! I rather use and accept your method than my hackiest hack if it works. I’ll see if I can reproduce the nudging and post an image of it soon. I try both Win, Mac and Linux – Manu Järvinen Sep 20 '18 at 6:46
• Alright! So, for Linux and Windows it does this: i.imgur.com/ptAhzAm.gif – Manu Järvinen Sep 20 '18 at 11:00
• AHA! When setting the pivot points I did "Origin to Geometry" for that GIF. And the script works nicely if I do "Origin to Center of Mass (Surface)", for example :) – Manu Järvinen Sep 20 '18 at 11:05