6
$\begingroup$

So,

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

But their orientation or local axes are like so: enter image description here

How can I make their local axes to be like so, but automatically? enter image description here

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!

$\endgroup$
4
  • $\begingroup$ 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. $\endgroup$
    – Mzidare
    Commented Sep 19, 2018 at 17:43
  • $\begingroup$ 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. $\endgroup$
    – Robin Betts
    Commented Sep 19, 2018 at 19:27
  • $\begingroup$ 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. $\endgroup$ Commented Sep 19, 2018 at 19:45
  • 1
    $\begingroup$ I think this procedure will be shortened in 2.8 - where the 3D cursor will have an orientation. $\endgroup$
    – Robin Betts
    Commented Sep 19, 2018 at 23:07

3 Answers 3

6
$\begingroup$

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"

enter image description here

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

        face_col.objects.link(fo)
    
    # remove originals?
    #bpy.data.objects.remove(ob)        
context.collection.children.link(face_col)       

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

enter image description here 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.primitive_ico_sphere_add(enter_editmode=True)
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

$\endgroup$
2
$\begingroup$

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.modifier_add(type = 'EDGE_SPLIT')
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.ops.mesh.primitive_cube_add()
    #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
    bpy.ops.object.particle_system_add()
    obj.particle_systems['ParticleSettings'].settings.type = 'HAIR'
    obj.particle_systems['ParticleSettings'].settings.use_advanced_hair = True
    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.collection_link(collection=CollectionName)
    
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.modifier_add(type = 'EDGE_SPLIT')
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.ops.mesh.primitive_cube_add(layers=(True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True))
    #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
    bpy.ops.object.particle_system_add()
    obj.particle_systems['ParticleSystem'].settings.type = 'HAIR'
    obj.particle_systems['ParticleSystem'].settings.use_advanced_hair = True
    #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.group_link(group=GroupName)

bpy.ops.object.select_same_group(group=GroupName)
$\endgroup$
1
$\begingroup$

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.

result

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

$\endgroup$
10
  • $\begingroup$ 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 $\endgroup$ Commented Sep 19, 2018 at 23:28
  • $\begingroup$ 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. $\endgroup$
    – binweg
    Commented Sep 20, 2018 at 6:40
  • $\begingroup$ 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 $\endgroup$ Commented Sep 20, 2018 at 6:46
  • 1
    $\begingroup$ Alright! So, for Linux and Windows it does this: i.imgur.com/ptAhzAm.gif $\endgroup$ Commented Sep 20, 2018 at 11:00
  • 1
    $\begingroup$ 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 :) $\endgroup$ Commented Sep 20, 2018 at 11:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .