How to match two Normals direction?

I write a simple script.
Create two Cube and change vertice[0] a little ~
Then, match their Location and Normals direction.
But , There’s some sticky in Normal-match-part.
I’ve try many many times.
So. I really need your help.

Thanks for your Help~




import bpy




import random
from math import degrees




def parallel(vct, p):
    """
    vct : vector line
    p : points
    return : parallel vector with vct
    """
    #tmp var in caculate
    pt1 = p.copy()
    for i in pt1:
        i+=1
    #new point in vct line
    pt1 = pt1.project(vct)
    pt2 = p.project(vct)
    #offset
    offset = pt2-p
    pp = pt1-offset
    #final
    newVct = pp
    print("
new point:%s" % (pp))
    if newVct[0]==newVct[1]==newVct[2]==0:
        print('error vector.zero')
    return newVct




def MatchPoint0(context, ob1, ob2):
    """
    static = ob1
    dynamic = ob2
    """
    vt1 = ob1.data.vertices[0]
    vt2 = ob2.data.vertices[0]
    print('Check location ::%s' % (vt1.co))
    
    #location match
    m1 = ob1.matrix_world.copy()
    m2 = ob2.matrix_world.copy()
    loc1 = m1 * vt1.co
    loc2 = m2 * vt2.co
    bpy.ops.object.select_all(action='DESELECT')
    context.scene.objects.active = ob2
    ob2.select = True
    bpy.ops.transform.translate(value=loc1 - loc2)
    context.scene.cursor_location = loc1
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') 
    
    #So, We can keep on caculate rotation match
    #Caculate again cuz of ob2 is moved
    m2 = ob2.matrix_world.copy()
    n1 = m1 * vt1.normal.copy().normalized()
    n2 = m2 * vt2.normal.copy().normalized()
    angle = n1.angle(n2)
    print(degrees(angle))
    dot = n1.dot(n2)
    print(dot)
    
    ###
    ### I'm Confusing in this section too much time, How to match there two Normals? Please ~
    ###
    if dot>0:
        angle *= -1
    cross = n1.cross(n2)
    cross = parallel(cross, loc1)
    bpy.ops.transform.rotate(value=angle, axis=cross)
    return




def PrepareMesh(ob):
    """init"""
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.editmode_toggle()
    vt0 = ob.data.vertices[0]
    vt0.select = True
    for i in range(len(vt0.co)):
        vt0.co[i] += random.uniform(0.1, 1)
    ob.data.update()
    ob.data.show_normal_vertex = True
    print('ob Name: %s -vt0- ::
location: %s  
normal: %s
' % (ob.name, vt0.co, vt0.normal))
    return
    
def main(context):
    print('

--->>>')
    # Creating and Preparing
    bpy.ops.mesh.primitive_cube_add(view_align=True, location=(0, 0, 0), rotation=(10, 20, 30))
    ob1 = context.active_object
    PrepareMesh(ob1)
    ob2 = bpy.ops.mesh.primitive_cube_add(view_align=True, location=(3, 0.5, 0), rotation=(40, 50, 60))
    ob2 = context.active_object
    PrepareMesh(ob2)
    
    # Main working
    # static = ob1
    # dynamic = ob2
    MatchPoint0(context, ob1, ob2)












class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"




    def execute(self, context):
        main(context)
        return {'FINISHED'}








def register():
    bpy.utils.register_class(SimpleOperator)








def unregister():
    bpy.utils.unregister_class(SimpleOperator)








if __name__ == "__main__":
    register()




    # test call
    bpy.ops.object.simple_operator()





I think you need to remove the ‘parallel’ call. If you have the angle between 2 vectors then you just rotate around their cross product to align them. You might want to verify that “n1.dot(n2) != 1”. In that case they are the same vector and I think the cross product will give you something funny.

Noraml-Match-Part have been checked many many times.
(Line 39 ~ Line 59)

Still Need Help ~~

Thanks~ :smiley:


import bpy

import random
from math import degrees, radians
from mathutils import Vector


def MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2):
    context.scene.objects.active = ob2
    ob1.select = ob2.select = True
    bpy.ops.object.join()
    ob = context.active_object
    bpy.ops.object.editmode_toggle()
    return

def MatchPoint0(context, ob1, ob2):
    """
    static = ob1
    dynamic = ob2
    """
    vt1 = ob1.data.vertices[0]
    vt2 = ob2.data.vertices[0]
    
    
    #location match
    m1 = ob1.matrix_world.copy()
    m2 = ob2.matrix_world.copy()
    loc1 = m1 * vt1.co.copy()
    loc2 = m2 * vt2.co.copy()
    bpy.ops.object.select_all(action='DESELECT')
    context.scene.objects.active = ob2
    ob2.select = True
    bpy.ops.transform.translate(value=loc1 - loc2)
    context.scene.cursor_location = loc1
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') 
    
    #So, We can keep on caculate rotation match
    #Caculate again cuz of ob2 is moved
    m2 = ob2.matrix_world.copy()
    n1 = m1 * vt1.normal.copy()
    n2 = m2 * vt2.normal.copy()
    
    angle = n1.angle(n2)
    cross = n1.cross(n2)

    print('before::
axis: %s 
angle: %s
' % (cross, angle))
    euler = m2.Rotation(angle, 4, cross).to_euler()
    ob2.rotation_euler = euler
    #bpy.ops.transform.rotate(value=angle, axis=cross)
    
    bpy.ops.object.editmode_toggle()
    bpy.ops.object.editmode_toggle()
    m2 = ob2.matrix_world.copy()
    n1 = m1 * vt1.normal.copy()
    n2 = m2 * vt2.normal.copy()
    angle = n1.angle(n2)
    cross = n1.cross(n2)
    print('after::
axis: %s 
angle: %s
' % (cross, angle))
    return

def PrepareMesh(ob):
    """init"""
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.editmode_toggle()
    vt0 = ob.data.vertices[0]
    vt0.select = True
    for i in range(len(vt0.co.copy())):
        vt0.co[i] += random.uniform(0.1, 1)
    ob.data.update()
    ob.data.show_normal_vertex = True
    print('ob Name: %s -vt0- ::
location: %s  
normal: %s
' % (ob.name, vt0.co, vt0.normal))
    return
    
def main(context):
    print('

--->>>')
    # Creating and Preparing
    bpy.ops.mesh.primitive_cube_add(view_align=True, location=(0, 0, 0), rotation=(10, 20, 30))
    ob1 = context.active_object
    PrepareMesh(ob1)
    ob2 = bpy.ops.mesh.primitive_cube_add(view_align=True, location=(3, 0.5, 0), rotation=(40, 50, 60))
    ob2 = context.active_object
    PrepareMesh(ob2)
    
    # Main working
    # static = ob1
    # dynamic = ob2
    MatchPoint0(context, ob1, ob2)
    MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2)
    # Goto 3dView check two normals
    



class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"

    def execute(self, context):
        main(context)
        return {'FINISHED'}


def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.simple_operator()


I know how to match the elements in the same mesh.
Match the elments in the different that’s seems more difficult.

Help me ~ Please ~


import bpy

import random
from math import degrees, radians
from mathutils import Vector


def MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2):
    context.scene.objects.active = ob2
    ob1.select = ob2.select = True
    bpy.ops.object.join()
    ob = context.active_object
    bpy.ops.object.editmode_toggle()
    return

def MatchPoint0(context, ob1, ob2):
    """
    static = ob1
    dynamic = ob2
    """
    vt1 = ob1.data.vertices[0]
    vt2 = ob2.data.vertices[0]
    
    #location match
    m1 = ob1.matrix_world.copy()
    m2 = ob2.matrix_world.copy()
    loc1 = m1 * vt1.co.copy()
    loc2 = m2 * vt2.co.copy()
    bpy.ops.object.select_all(action='DESELECT')
    context.scene.objects.active = ob2
    ob2.select = True
    bpy.ops.transform.translate(value=loc1 - loc2)
    context.scene.cursor_location = loc1
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN')
    
    #So, We can keep on caculate rotation match
    #Caculate again cuz of ob2 is moved
    m2 = ob2.matrix_world.copy()
    n1 = (m1 * vt1.normal.copy()).normalized()
    print('v1  ::  lNor:%s gNor:%s' % (vt1.normal.copy(), n1))
    n2 = (m2 * vt2.normal.copy()).normalized()
    print('v2  ::  lNor:%s gNor:%s' % (vt2.normal.copy(), n2))
    
    angle = n1.angle(n2)
    cross = n1.cross(n2)
    dot = n1.dot(n2)
    
    print('before::
axis: %s 
angle: %s 
dot: %s
' % (cross, angle, dot))
    if dot>0:
        angle *= -1
    #euler = m2.Rotation(angle, 4, cross).to_euler()
    #ob2.rotation_euler = euler
    bpy.ops.transform.rotate(value=angle, axis=cross)
    
    ##############################################################
    m2 = ob2.matrix_world.copy()
    n1 = (m1 * vt1.normal.copy()).normalized()
    n2 = (m2 * vt2.normal.copy()).normalized()
    angle = n1.angle(n2)
    cross = n1.cross(n2)
    dot = n1.dot(n2)
    print('before::
axis: %s 
angle: %s 
dot: %s' % (cross, angle, dot))
    return

def PrepareMesh(ob):
    """init"""
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.editmode_toggle()
    vt0 = ob.data.vertices[0]
    vt0.select = True
    for i in range(len(vt0.co.copy())):
        vt0.co[i] += random.uniform(0.1, 1)
    ob.data.update()
    ob.data.show_normal_vertex = True
    print('ob Name: %s -vt0- ::
location: %s  
normal: %s
' % (ob.name, vt0.co, vt0.normal))
    return
    
def main(context):
    print('

--->>>')
    # Creating and Preparing
    bpy.ops.mesh.primitive_cube_add(view_align=True, location=(0, 0, 0), rotation=(10, 20, 30))
    ob1 = context.active_object
    PrepareMesh(ob1)
    ob2 = bpy.ops.mesh.primitive_cube_add(view_align=True, location=(3, 0.5, 0), rotation=(40, 50, 60))
    ob2 = context.active_object
    PrepareMesh(ob2)
    
    # Main working
    # static = ob1
    # dynamic = ob2
    context.tool_settings.mesh_select_mode = (True, False, False)
    MatchPoint0(context, ob1, ob2)
    MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2)
    # Goto 3dView check two normals
    



class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"

    def execute(self, context):
        main(context)
        return {'FINISHED'}


def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.simple_operator()



The problem is in how you transform the normals. The python API does not make an automatic distinction between position and normal vectors, but they need to be transformed differently by matrices.

For normals the translation component should be ignored (as normals are invariant under translations). That’s the cause of the mismatch here. Another issue happens when an object has non-uniform scale, in that case you need to use the inverse transpose.

Here’s the modified code that should work, note NormalTransformMatrix:

import bpy


import random
from math import degrees, radians
from mathutils import Vector




def MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2):
    context.scene.objects.active = ob2
    ob1.select = ob2.select = True
    bpy.ops.object.join()
    ob = context.active_object
    bpy.ops.object.editmode_toggle()
    return


def NormalTransformMatrix(m):
    m_normal = m.inverted().transposed()
    m_normal[0][3] = 0.0
    m_normal[1][3] = 0.0
    m_normal[2][3] = 0.0
    
    return m_normal


def MatchPoint0(context, ob1, ob2):
    """
    static = ob1
    dynamic = ob2
    """
    vt1 = ob1.data.vertices[0]
    vt2 = ob2.data.vertices[0]
    
    #location match
    m1 = ob1.matrix_world.copy()
    m2 = ob2.matrix_world.copy()
    loc1 = m1 * vt1.co.copy()
    loc2 = m2 * vt2.co.copy()
    bpy.ops.object.select_all(action='DESELECT')
    context.scene.objects.active = ob2
    ob2.select = True
    bpy.ops.transform.translate(value=loc1 - loc2)
    context.scene.cursor_location = loc1
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN')
    
    #So, We can keep on caculate rotation match
    #Caculate again cuz of ob2 is moved
    m1 = NormalTransformMatrix(ob1.matrix_world.copy())
    m2 = NormalTransformMatrix(ob2.matrix_world.copy())
    n1 = (m1 * vt1.normal.copy()).normalized()
    print('v1  ::  lNor:%s gNor:%s' % (vt1.normal.copy(), n1))
    n2 = (m2 * vt2.normal.copy()).normalized()
    print('v2  ::  lNor:%s gNor:%s' % (vt2.normal.copy(), n2))
    
    angle = n1.angle(n2)
    cross = n1.cross(n2)
    dot = n1.dot(n2)
    
    print('before::
axis: %s 
angle: %s 
dot: %s
' % (cross, angle, dot))
    if dot>0:
        angle *= -1
    #euler = m2.Rotation(angle, 4, cross).to_euler()
    #ob2.rotation_euler = euler
    bpy.ops.transform.rotate(value=angle, axis=cross)
    
    ##############################################################
    m1 = NormalTransformMatrix(ob1.matrix_world.copy())
    m2 = NormalTransformMatrix(ob2.matrix_world.copy())
    n1 = (m1 * vt1.normal.copy()).normalized()
    n2 = (m2 * vt2.normal.copy()).normalized()
    angle = n1.angle(n2)
    cross = n1.cross(n2)
    dot = n1.dot(n2)
    print('before::
axis: %s 
angle: %s 
dot: %s' % (cross, angle, dot))
    return


def PrepareMesh(ob):
    """init"""
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bpy.ops.object.editmode_toggle()
    vt0 = ob.data.vertices[0]
    vt0.select = True
    for i in range(len(vt0.co.copy())):
        vt0.co[i] += random.uniform(0.1, 1)
    ob.data.update()
    ob.data.show_normal_vertex = True
    print('ob Name: %s -vt0- ::
location: %s  
normal: %s
' % (ob.name, vt0.co, vt0.normal))
    return
    
def main(context):
    print('

--->>>')
    # Creating and Preparing
    bpy.ops.mesh.primitive_cube_add(view_align=True, location=(0, 0, 0), rotation=(10, 20, 30))
    ob1 = context.active_object
    PrepareMesh(ob1)
    ob2 = bpy.ops.mesh.primitive_cube_add(view_align=True, location=(3, 0.5, 0), rotation=(40, 50, 60))
    ob2 = context.active_object
    PrepareMesh(ob2)
    
    # Main working
    # static = ob1
    # dynamic = ob2
    context.tool_settings.mesh_select_mode = (True, False, False)
    MatchPoint0(context, ob1, ob2)
    MergeMeshSoWeCanCheckTheNormals(context, ob1, ob2)
    # Goto 3dView check two normals
    






class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"


    def execute(self, context):
        main(context)
        return {'FINISHED'}




def register():
    bpy.utils.register_class(SimpleOperator)




def unregister():
    bpy.utils.unregister_class(SimpleOperator)




if __name__ == "__main__":
    register()


    # test call
    bpy.ops.object.simple_operator()

By the way, most of the .copy() calls in the code are not needed, though they don’t hurt.

Dear brecht.

Thank you very much!!!