Reverse transforms

So I got 5 objects in my scene. One is called master. The other ones have the exact same topology but the transforms have been frozen. I need to do some reverse engineering here to get the original transforms. I used to have a tool to do that in Maya. You selected I think 3 vertices and 1 edge and you rand a script and it did it. It was magical. I need Blender magic now. Anyone can help? (in the attached file, there are only 5 objects for testing but in what I’m working on, I have 52 000! They are seats in a stadium. I need to place characters in front of the seat. Once I get the right transform info, it should be trivial to add the characters with geometry nodes).

objects.blend.zip (104.9 KB)

look for 1D scripts and Mesh Align Plus
BTW, great yt channel - very informative and full of practise knowledge

2 Likes

Thank you so much for the nice comments! :slight_smile:
1D script and Mesh align Plus are super cool but I think I will need a script in order to apply it to 50k objects.

right, 50k :slight_smile: I thought if objects have the same topology it only needs to change one and apply to the others. ( but then you have to do that in edit mode). it seems that it requires some script to do the job in these case

Hi Bob,

nothing easier than this.

objects_with_script.zip (110.1 KB)

The script loops over the selected objects and creates a linked copy of the active object. It also transforms the copies according to a transformation calculated by matching 3 specified points. In case your stadium seats have the points with IDs 0,1,2 too close together choose 3 random ones, that are further apart. You can activate point ID display in edit mode, if you enable the developer extras:

import bpy
import mathutils

# Choose 3 arbitrary points and specify their indices
# If points with IDs 0,1,2 are too close together on your
# real geometry, choose some other ones, that are further
# apart.
v0_index = 0
v1_index = 1
v2_index = 2


def trafo(o):
    """ Compute an affine transformation matrix to the specified points """
    mesh = o.data
    if not mesh.polygons:
        print("Skipping ", o, ": not a mesh")
    v0 = mesh.vertices[v0_index].co
    v1 = mesh.vertices[v1_index].co
    v2 = mesh.vertices[v2_index].co
    e01 = v1 - v0
    e02 = v2 - v0
    
    x = e01.normalized()
    y = e01.cross(e02).normalized()
    z = x.cross(y).normalized()
    t = mathutils.Matrix.Translation(v0)
    t.col[0] = x.resized(4)
    t.col[1] = y.resized(4)
    t.col[2] = z.resized(4)
    return t

def align(o, proto, inv_proto_trafo):
    """ Compute the transformation between proto and o,
        Create a linked copy of proto and transform it to
        the computed location """
    t_o = trafo(o)
    t_new = t_o @ inv_proto_trafo
    new_obj = proto.copy()
    new_obj.matrix_local = t_new
    bpy.context.collection.objects.link(new_obj)


# Get the trafo of the prototype (the active object)
proto = bpy.context.active_object
inv_proto_trafo = trafo(proto).inverted()

# Create copies of the prototype and transform them
for o in bpy.context.selected_objects:
    if o == proto:
        continue
    align(o, proto, inv_proto_trafo)
1 Like

Nice answer, but not entirely accurate. While this works for the small sample set on vertex index 0,1,2. as mentioned in the original post the topology may be the same but the geometry is not. If you try vert indices 32, 35, 37 from the master (located on one of the top faces) you will observe some unwanted behavior since these vertices are not the same index in other objects.

Yes, you’re right, it behaves differently. This means, that the precondition, the topology is the same on all copies, is not valid. So some other operation additionally to freezing the transform was done, or the system, where the transforms had been frozen in, is not maintaining the topology.
I think one would need to perform some other strategy to get the coordinates of matching points. One strategy could be to calculate a best fitting oriented bounding box and use that one. This however requires the object to have some extend and not being cube-like (which might happen on a stadium seat). Maybe Bob could provide more information which features could be used to determine a corresponding transformation.

Well, I found the Maya script. You need to use it in a specific way in order to make it work. Once the script is loaded, it pops up a tool. You selected the objects you want to orient and press the “objects to rebuilt” button. Then you select a vertex on the reference object, press “Base Pivot” button. You then select an edge oriented on the Z axis (and it has to touch the previous vertex) and click the “Z Axis Aim”. Then select another edge on the Y axis, again, touching the pivot vertex, “Y Axis Aim” button and finally you click the “REBUID” button. Maybe that can inspire you.

#en_transformRebuild.py
#What: for rebuilding object transforms, on one or multiple objects
#How: select objects first, click button.  select poly components for z and Y axes.
    # then run!
#note: Base pivot, Yaxis aim and Zaxis aim are calculated based on averaged center location of selected components.
import maya.cmds as cmds
import math

class en_transformRebuild(object):
    def __init__(self,*args):
        self.en_transformRebuildWin = 'en_transformRebuildWin'
        if cmds.window(self.en_transformRebuildWin, exists=True):
            cmds.deleteUI(self.en_transformRebuildWin)
        self.buildWin()

    def buildWin(self,*args):
            
        self.en_transformRebuildWin = cmds.window(self.en_transformRebuildWin, t="Transformation Rebuilder",w=150, h=295,s=True,tb=True, bgc= [0.3,0.4,0.2])
        
        cmds.columnLayout()

        cmds.separator(style="none",w=150,h=10)
        
###BUTTONS      
        cmds.text( label='Rebuild Transforms') 
        cmds.text( label='For Multiple objects.') 
        cmds.separator(style="none",w=150,h=10)
        cmds.text( label='When running on more than 1')
        cmds.text( label='object,make sure all objects') 
        cmds.text( label='have same Topology') 
        
        cmds.separator(style="none",w=150,h=15)
        
        cmds.rowColumnLayout( numberOfColumns=1 )

        self.btnobjectSel = cmds.button(label = "Objects To Rebuild",ann="Select only Polygon Objects that you would like to rebuild transforms for", w=150,command= self.objectSel_RUN)
        
        cmds.separator(style='in',w=150,h=10)
        
        self.btnbaseVerts = cmds.button(label = "BasePivot",ann="Select The components(VERTS,Edges,Faces) you would like to have as the pivot location",w=150, command= self.baseVerts_RUN)
        self.btntipVerts = cmds.button(label = "Z Axis Aim",ann="Select The components(VERTS,Edges,Faces) you would like to have as the Z aim axis",w=150, command= self.tipVerts_RUN)
        self.btnorientVerts = cmds.button(label = "Y Axis Aim",ann="Select The components(VERTS,Edges,Faces) you would like to have as the Y aim axis",w=150, command= self.orientVerts_RUN)
        
        cmds.separator(style='in',w=150,h=10)
        
        self.btnxformRestore = cmds.button(label = "REBUILD!",ann="Run the script, based on the selections used above!",w=150, command= self.xformRestore_RUN)

        cmds.showWindow(self.en_transformRebuildWin )
        
            
    def objectSel_RUN(self,*args):
        
        self.initialSel = cmds.ls(sl=True,l=True)
        self.sourceObj = self.initialSel[0]
        self.targetObjs = self.initialSel[1:]
    
    def baseVerts_RUN(self,*args):
        cmds.ConvertSelectionToVertices()
        self.baseVertsTmp = cmds.ls(sl=True,l=True)
        self.baseVertsObjTmp = cmds.ls(sl=True,o=True,l=True)[0]
        self.baseVertsObj = cmds.listRelatives(self.baseVertsObjTmp,p=True,f=True)[0]
        
        self.baseVerts = []
        for self.obj in self.baseVertsTmp:
            if str(self.baseVertsObj) in self.obj:
                self.baseVertsTmp2 = self.obj.split(".")[-1]
                self.baseVertsTmp2 = "." + str(self.baseVertsTmp2)
                self.baseVerts.append(self.baseVertsTmp2)
            
    def tipVerts_RUN(self,*args):
        cmds.ConvertSelectionToVertices()
        self.tipVertsTmp = cmds.ls(sl=True,l=True)
        self.tipVertsObjTmp = cmds.ls(sl=True,o=True,l=True)[0]
        self.tipVertsObj = cmds.listRelatives(self.tipVertsObjTmp,p=True,f=True)[0]
        
        self.tipVerts = []
        for self.obj in self.tipVertsTmp:
            if str(self.tipVertsObj) in self.obj:
                self.tipVertsTmp2 = self.obj.split(".")[-1]
                self.tipVertsTmp2 = "." + str(self.tipVertsTmp2)
                self.tipVerts.append(self.tipVertsTmp2)    
                
    
    def orientVerts_RUN(self,*args):
        cmds.ConvertSelectionToVertices()
        self.orientVertsTmp = cmds.ls(sl=True,l=True)
        self.orientVertsObjTmp = cmds.ls(sl=True,o=True,l=True)[0]
        self.orientVertsObj = cmds.listRelatives(self.orientVertsObjTmp,p=True,f=True)[0]
        
        self.orientVerts = []
        for self.obj in self.orientVertsTmp:
            if str(self.orientVertsObj) in self.obj:
                self.orientVertsTmp2 = self.obj.split(".")[-1]
                self.orientVertsTmp2 = "." + str(self.orientVertsTmp2)
                self.orientVerts.append(self.orientVertsTmp2)
    
    
    def xformRestore_RUN(self,*args): 
        cmds.select(cl=True)
        
        ##################################################################################################
        #GET SCALE OFFSETS################################################################################
        ##################################################################################################
        self.edgeVal = 1
        ##get lengths of each target edge
        self.lengthList = []
        for self.obj in self.initialSel:
            cmds.select(str(self.obj) + ".e["+str(self.edgeVal)+"]")
            self.edgeSel=cmds.ls(sl=True,fl=True)
            cmds.ConvertSelectionToVertices()
            self.p=cmds.xform(self.edgeSel,q=True,t=True,ws=True)
            self.length = math.sqrt(math.pow(self.p[0]-self.p[3],2)+math.pow(self.p[1]-self.p[4],2)+math.pow(self.p[2]-self.p[5],2))
            self.lengthList.append(self.length) 
        #get average length
        self.avgLength = sum(self.lengthList)/float(len(self.lengthList))
        self.percentList = []
        # percentage finder
        for self.myLength in self.lengthList:
            self.tempPercent = self.myLength/self.avgLength
            self.percentList.append(self.tempPercent)
            
        #################################################################################################
        ##################################Scale FIND END#################################################
        #################################################################################################
        
        self.jointList = []
        #MAIN LOOP, APPLIES OFFSET SCALE, MAKES JOINTS, CONSTRAINTS, ETC.
        for self.i,self.obj in enumerate(self.initialSel):
            #MAKE NAMES OF VERTS FOR TARGET MESHES.
            self.targBaseVerts = []
            self.targTipVerts = []
            self.targOrientVerts = []
            for self.item in self.baseVerts:
                self.tmp1 = str(self.obj) + str(self.item)
                self.targBaseVerts.append(self.tmp1)
            for self.item in self.tipVerts:
                self.tmp2 = str(self.obj) + str(self.item)
                self.targTipVerts.append(self.tmp2)
            for self.item in self.orientVerts:
                self.tmp3 = str(self.obj) + str(self.item)
                self.targOrientVerts.append(self.tmp3)

            #get position of base verts
            self.posBase = cmds.xform(self.targBaseVerts, q=True, ws=True, t=True)
            self.xposBase =  self.posBase[0::3]
            self.yposBase =  self.posBase[1::3]
            self.zposBase =  self.posBase[2::3]
            self.xposBase = sum(self.xposBase)/len(self.xposBase)
            self.yposBase = sum(self.yposBase)/len(self.yposBase)
            self.zposBase = sum(self.zposBase)/len(self.zposBase)
            
            #get position of tip verts
            self.posTip = cmds.xform(self.targTipVerts, q=True, ws=True, t=True)
            self.xposTip =  self.posTip[0::3]
            self.yposTip =  self.posTip[1::3]
            self.zposTip =  self.posTip[2::3]
            self.xposTip = sum(self.xposTip)/len(self.xposTip)
            self.yposTip = sum(self.yposTip)/len(self.yposTip)
            self.zposTip = sum(self.zposTip)/len(self.zposTip)
            
            #get position of orient verts
            self.posOrient = cmds.xform(self.targOrientVerts, q=True, ws=True, t=True)
            self.xposOrient =  self.posOrient[0::3]
            self.yposOrient =  self.posOrient[1::3]
            self.zposOrient =  self.posOrient[2::3]
            self.xposOrient = sum(self.xposOrient)/len(self.xposOrient)
            self.yposOrient = sum(self.yposOrient)/len(self.yposOrient)
            self.zposOrient = sum(self.zposOrient)/len(self.zposOrient)
            
            #create locators based on positions
            self.myOrientLocator = cmds.spaceLocator(n='orientLocator')[0]
            cmds.setAttr(str(self.myOrientLocator) + '.translateX', self.xposOrient)
            cmds.setAttr(str(self.myOrientLocator) + '.translateY', self.yposOrient)
            cmds.setAttr(str(self.myOrientLocator) + '.translateZ', self.zposOrient)
            self.myTipLocator = cmds.spaceLocator(n='tipLocator')[0]
            cmds.setAttr(str(self.myTipLocator) + '.translateX', self.xposTip)
            cmds.setAttr(str(self.myTipLocator) + '.translateY', self.yposTip)
            cmds.setAttr(str(self.myTipLocator) + '.translateZ', self.zposTip)
            
            cmds.select(self.obj,r=True)
            #NEED TO MOVE PIVOT TO BASE LOC
            cmds.move( self.xposBase,self.yposBase,self.zposBase, str(self.obj) + ".scalePivot", str(self.obj) + ".rotatePivot",ws=True)
            #scale up object based on offsets
            #cmds.scale(1/percentList[i],1/percentList[i],1/percentList[i], str(obj),r=True)
        
            cmds.select(cl=True)
            #######JOINTS#######
        
            self.mainJoint = cmds.joint(p=(self.xposBase,self.yposBase,self.zposBase), n= str(self.obj) + "_joint")
            self.jointList.append(self.mainJoint)
            
            cmds.scale(self.percentList[self.i],self.percentList[self.i],self.percentList[self.i], self.mainJoint,r=True)
            
            #aim constraint.  get orientation for joint
            cmds.select(self.myTipLocator,r=True)
            cmds.select(self.mainJoint,add=True)
            self.myAimConstraint = cmds.aimConstraint(offset=(0 ,0 ,0),weight=1, aimVector=(0, 0, 1), upVector=(0, 1, 0), worldUpType="object", worldUpObject=str(self.myOrientLocator))
            cmds.delete(self.myAimConstraint)
            cmds.delete(self.myOrientLocator,self.myTipLocator)
            
            cmds.parentConstraint(self.mainJoint,self.obj,mo=True,w=True)
            cmds.scaleConstraint(self.mainJoint,self.obj,mo=True,w=True)
            
        #####################################################################
        ############################BAKE TRANSFORMS##########################
        #####################################################################
        
        
        self.newJoints = cmds.duplicate(self.jointList)
        #move joints back to origin, in order to send object back by themselves.
        for self.obj in self.jointList:
            cmds.setAttr(str(self.obj) + ".tx",0)
            cmds.setAttr(str(self.obj) + ".ty",0)
            cmds.setAttr(str(self.obj) + ".tz",0)
            cmds.setAttr(str(self.obj) + ".rx",0)
            cmds.setAttr(str(self.obj) + ".ry",0)
            cmds.setAttr(str(self.obj) + ".rz",0)
            cmds.setAttr(str(self.obj) + ".sx",1)
            cmds.setAttr(str(self.obj) + ".sy",1)
            cmds.setAttr(str(self.obj) + ".sz",1)
        
        #DELETE CONSTRAINTS and freez transforms on original objects
        for self.obj in self.initialSel:
            self.myPConst = cmds.listRelatives(self.obj,ad=True,typ="parentConstraint")
            self.mySConst = cmds.listRelatives(self.obj,ad=True,typ="scaleConstraint")
            cmds.delete(self.myPConst)
            cmds.delete(self.mySConst)
            cmds.makeIdentity(self.obj,apply=True,t=True,r=True,s=True)
        #DELETE ORIGINAL JOINTS
        
        
        cmds.delete(self.jointList)
        #send objects to joint locations
        for self.obj,self.jnt in zip(self.initialSel,self.newJoints):
            self.myTX =cmds.getAttr(str(self.jnt) +".tx")
            self.myTY =cmds.getAttr(str(self.jnt) +".ty")
            self.myTZ =cmds.getAttr(str(self.jnt) +".tz")
            self.myRX =cmds.getAttr(str(self.jnt) +".rx")
            self.myRY =cmds.getAttr(str(self.jnt) +".ry")
            self.myRZ =cmds.getAttr(str(self.jnt) +".rz")
            self.mySX =cmds.getAttr(str(self.jnt) +".sx")
            self.mySY =cmds.getAttr(str(self.jnt) +".sy")
            self.mySZ =cmds.getAttr(str(self.jnt) +".sz")
            
            cmds.setAttr(str(self.obj) + ".tx", self.myTX)
            cmds.setAttr(str(self.obj) + ".ty", self.myTY)
            cmds.setAttr(str(self.obj) + ".tz", self.myTZ)
            cmds.setAttr(str(self.obj) + ".rx", self.myRX)
            cmds.setAttr(str(self.obj) + ".ry", self.myRY)
            cmds.setAttr(str(self.obj) + ".rz", self.myRZ)
            cmds.setAttr(str(self.obj) + ".sx", self.mySX)
            cmds.setAttr(str(self.obj) + ".sy", self.mySY)
            cmds.setAttr(str(self.obj) + ".sz", self.mySZ)
            cmds.delete(self.jnt)
en_transformRebuild()

It’s been too long, since I’ve been scripting in Maya (back then it didn’t even support python yet, only mel). However if I understand that script correctly it’s using the same principle as my blender script. Differences are

  • It supports scaling (which I ignored), but that’s not too difficult
  • It uses an averaged location of multiple selected vertices to calculate the axis constraints.
  • Assist friendly user interface

The issues with e.g. indices 32, 35, 37 nezumi mentioned earlier, would I guess also happen with the Maya script.

1 Like