Hi TA.org,
Is it possible to convert an arbritrary point space in world space into an objects local space?
Plenty of examples going from object space to world space but how do I go from world space to object space for a single point?
Hi TA.org,
Is it possible to convert an arbritrary point space in world space into an objects local space?
Plenty of examples going from object space to world space but how do I go from world space to object space for a single point?
It really is as simple as multiplying the world space point by the objectâs WorldInverseMatrix.
Youâll have to make sure to multiply in the right order though. The pointâs world position comes first then the inverse matrix comes second.
Now, hopefully an explanation you can maybe follow. Feel free to ask more questions if you donât.
Think of how parenting behaves:
You start from where your parent is in worldspace, then add your own transforms on top of that to get your own world space.
In maya, you multiply matrices like this to say the same thing:
WorldMat = LocalMat * ParentWorldMat
You sometimes see this called âpost multiplyingâ because the parent world matrix comes after the child, and order matters when multiplying matrices. (Some other softwares use pre-multiplying. Maya uses post.)
Looking at that equation like youâre in algebra class, how would you isolate the LocalMat?
Youâd just divide both sides by ParentWorldMat, right?
But we canât do that because these are matrices and thereâs no such thing as âdividingâ ⌠but there is multiplying by the inverse.
So we canât divide by ParentWorldMat, but we can multiply by the ParentWorldInverseMat (aka. The parentInverseMatrix plug in Maya)
But again, these are matrices. We canât rearrange them after the fact. We have to pick: put the ParentWorldInverseMat on the right or the left.
I hope you see that the right side is correct like this:
WorldMat * ParentWorldInverseMat = LocalMat * ParentWorldMat * ParentWorldInverseMat
Because now you can see ParentWorldMat * ParentWorldInverseMat
. And a matrix multiplied by its inverse cancels out!
So youâre left with the equation WorldMat * ParentWorldInverseMat = LocalMat
Which is exactly what youâre trying to figure out
Thank you so much for the explanation @tfox_TD!
I might be asking for the wrong thing, basically this is what I want:
Iâm trying to axis snap according to an objects âObject modeâ X | Y | Z axis independently but in code.
Like this:
A Y axis snap would give this:
Doing a world space snap along a world space axis in code is as easy as getting the Locatorâs world space position, getting the Y value, and setting the Cubeâs Y value to that.
But how do I snap along the Cubeâs object space Y axis? (As shown in the pictures above)
Yeah, youâd convert the locatorâs worldspace into your boxâs local space. Then pull the y-coordinate of the locator and add it to the boxâs current y-coordinate. You canât just replace the y-coord, youâve gotta add it.
Haha my mistake
I tried that via the channel box and it didnât work (adding the value, but in code it works)
Didnât realize channel box displays values in world space
Thanks again
Oh crud, I may have been wrong. I think what I said would snap a child of the box along the given axis, but not the box itself.
Double check me.
I think you may have to do this instead:
offsetMat
offsetMat * boxLocalMatrix
to get the new local matrix of the boxI think thatâs what I did, it seems to work?
def get_pos_in_obj_space(obj, pt):
"""
Input obj and pt ws position as tuple
Return the input point position in the objects object space coordinate system
"""
def getWorldMatrix(obj):
return om.MMatrix(mc.xform(obj, q=True, matrix=True, ws=True))
# def getObjMatrix(obj):
# return om.MMatrix(mc.xform(obj, q=True, matrix=True, os=True))
osel_world_inverse = getWorldMatrix(obj).inverse()
cust_matrix_row = (pt[0], pt[1], pt[2], 1)
pt_ws = om.MMatrix((
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0),
cust_matrix_row,
))
ptpos_in_osel_objspace = (pt_ws * osel_world_inverse)
pt_os_x = ptpos_in_osel_objspace[12]
pt_os_y = ptpos_in_osel_objspace[13]
pt_os_z = ptpos_in_osel_objspace[14]
return (pt_os_x, pt_os_y, pt_os_z)
Later on I simply add the corresponding X Y Z to the objects existing X Y Z
I think that wonât work if both the box and its parent have arbitrary rotations.
Hereâs a setup with a working function
from maya import cmds
from maya.api import OpenMaya as om
def get_pos_in_obj_space(obj, pt):
"""
Input obj and pt ws position as tuple
Return the input point position in the objects parent space coordinate system
"""
def getMatrix(obj, world=True):
return om.MMatrix(
cmds.xform(
obj, q=True, matrix=True, worldSpace=world, objectSpace=not world
)
)
osel_world_inverse = getMatrix(obj).inverse()
osel_local_mat = getMatrix(obj, world=False)
# Don't have to use matrices if we don't need rotations
# Get the point in the object space
pt_ws = om.MPoint(pt)
pt_os = pt_ws * osel_world_inverse
# aligning the Y axis means zeroing out the x and z
pt_os[0] = 0.0
pt_os[2] = 0.0
# Get the aligned point in the parent space of the osel
new_osel_vals = pt_os * osel_local_mat
# These are the new values that can replace (not add to) the old transform values
return tuple(new_osel_vals)
loc1 = cmds.spaceLocator()[0]
loc2 = cmds.spaceLocator()[0]
loc3 = cmds.spaceLocator()[0]
loc4 = cmds.spaceLocator()[0]
cmds.parent(loc4, loc3)
cmds.parent(loc2, loc1)
cmds.setAttr(loc1 + ".t", -2, 1, -0.75, type="double3")
cmds.setAttr(loc1 + ".r", 18, -14, 5, type="double3")
cmds.setAttr(loc2 + ".t", -3, 3, 0, type="double3")
cmds.setAttr(loc2 + ".r", 39, -4, -39, type="double3")
cmds.setAttr(loc3 + ".t", 1, 0.5, -0.2, type="double3")
cmds.setAttr(loc3 + ".r", -22, -20, 0, type="double3")
cmds.setAttr(loc4 + ".t", -3.2, 3.4, 1.5, type="double3")
cmds.setAttr(loc4 + ".r", 12, -12, 3, type="double3")
pt = cmds.xform(loc4, query=True, translation=True, worldSpace=True)
newPt = get_pos_in_obj_space(loc2, pt)
# Edited to actually update the position of the locator, rather than print it
# Set the loc2 translation to the new point that is snapped along the y axis
cmds.setAttr(loc2 + ".t", *newPt, type="double3")
This is the expected value for loc2.t
after the command aligning its y-value with loc4
I found this by doing a Y-align in the UI, not by running the function.
[(-2.7950313411570025, 3.2845420690896105, 0.2828322955791683)]
HmâŚIâm confused.
I just want to vertex snap along an objects local Y axis to some position in 3d space. I canât get your latest code to work to do that?
But the code I posted works.
You know how the vertex snap works with the move tool? When the Y handle is active you can snap to a point in 3d space in object mode? Thatâs all Iâm trying to do, but in code.
What I posted seems to work I think? Iâm confused now haha.
Like this:
Before
After
I think your code does something more complicated?
The code I posted above is fine isnât it? (for my use case??)
Itâs only a little more complicated because itâs more general. My code will work if the object youâre snapping has a parent, or has any orientation. In your example, it looks like nothing has rotation values.
Also please note that I just updated the last 2 lines of my example in the previous post to actually perform the snap instead of just printing the new position.
Mine starts here
(This is what the scene would look like if you ran the latest code except for that last setAttr
line)
But then running that last line, does this
See how my locatorâs XZ plane is aligned with that locator in the background? Itâs the same position youâd get if you held down the V key and dragged to that locator.
Yeah your new code also works perfectly!
Sorry for the trouble @tfox_TD
I was using xform but if I use setAttr like you did in your script your code works also.
Just to clarify thoughâŚ
Arenât both functions doing the exact same thing?
My function is exactly the same up to this point:
Your code:
pt_os = pt_ws * osel_world_inverse
My code:
ptpos_in_objspace = (pt_ws_matrix * obj_world_inverse)
I add just the Y axis later on
Where as you do this:
pt_os[0] = 0.0
pt_os[2] = 0.0
# Get the aligned point in the parent space of the osel
new_osel_vals = pt_os * osel_local_mat
The only difference is the above line, isnât it?
As long as this below is being done in both functions:
pt_os = pt_ws * osel_world_inverse
Arenât both functions pretty much doing the exact same thing?
Itâs my lack of understanding thatâs driving me a little nuts haha
But if they are doing the same thing, why canât I use xform with you code (replacing not adding)?
I donât think so. The difference in my head is that yours is getting the offset in local space, where mine is getting the offset in parent space. The attribute values in the channelbox move the object in parent space, so thatâs the space you need to use to do this in a âpureâ form. (By pure, I mean that since Iâm setting the values directly, this is something I could do in a plugin with the api where I donât have access to xform)
Could you post how youâre using xform? I want to test your code with my locator setup to see if they match.
Yeah Iâm using the api just to query information, and rely on xform for all my modeling tools because it seems to automatically take care of undo in a single step.
Try the code below, uncomment the last couple of lines at the bottom (your section vs mine), they seem to give the same results, but I donât get why. Iâve never used setAttr, always been using xform for a while now.
Not sure why they cant be interchangeable for your function?
EDIT: As you said, setAttr refers to the parent space of the object, hence why it wont work with xform and my function
from maya.api import OpenMaya as om
def get_pos_in_obj_space(obj, pt):
"""
Input obj and pt ws position as tuple
Return the input point position in the objects local space coordinate system
"""
def getWorldMatrix(obj):
return om.MMatrix(mc.xform(obj, q=True, matrix=True, ws=True))
# def getObjMatrix(obj):
# return om.MMatrix(mc.xform(obj, q=True, matrix=True, os=True))
obj_world_inverse = getWorldMatrix(obj).inverse()
pt_matrix_row = (pt[0], pt[1], pt[2], 1)
pt_ws_matrix = om.MMatrix(
(
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0),
pt_matrix_row,
)
)
ptpos_in_objspace = pt_ws_matrix * obj_world_inverse
pt_os_x = ptpos_in_objspace[12]
pt_os_y = ptpos_in_objspace[13]
pt_os_z = ptpos_in_objspace[14]
print((pt_os_x, pt_os_y, pt_os_z))
return (pt_os_x, pt_os_y, pt_os_z)
def tfox_get_pos_in_obj_space(obj, pt):
"""
Input obj and pt ws position as tuple
Return the input point position in the objects parent space coordinate system
"""
def getMatrix(obj, world=True):
return om.MMatrix(
mc.xform(
obj,
q=True,
matrix=True,
worldSpace=world,
objectSpace=not world,
)
)
osel_world_inverse = getMatrix(obj).inverse()
osel_local_mat = getMatrix(obj, world=False)
# Don't have to use matrices if we don't need rotations
# Get the point in the object space
pt_ws = om.MPoint(pt)
pt_os = pt_ws * osel_world_inverse
print(pt_os)
# aligning the Y axis means zeroing out the x and z
pt_os[0] = 0.0
pt_os[2] = 0.0
# Get the aligned point in the parent space of the osel
new_osel_vals = pt_os * osel_local_mat
# These are the new values that can replace (not add to) the old transform values
return tuple(new_osel_vals)
loc1 = mc.spaceLocator()[0]
loc2 = mc.spaceLocator()[0]
loc3 = mc.spaceLocator()[0]
loc4 = mc.spaceLocator()[0]
mc.parent(loc4, loc3)
mc.parent(loc2, loc1)
mc.setAttr(loc1 + ".t", -2, 1, -0.75, type="double3")
mc.setAttr(loc1 + ".r", 18, -14, 5, type="double3")
mc.setAttr(loc2 + ".t", -3, 3, 0, type="double3")
mc.setAttr(loc2 + ".r", 39, -4, -39, type="double3")
mc.setAttr(loc3 + ".t", 1, 0.5, -0.2, type="double3")
mc.setAttr(loc3 + ".r", -22, -20, 0, type="double3")
mc.setAttr(loc4 + ".t", -3.2, 3.4, 1.5, type="double3")
mc.setAttr(loc4 + ".r", 12, -12, 3, type="double3")
pt = mc.xform(loc4, query=True, translation=True, worldSpace=True)
# # newPt = tfox_get_pos_in_obj_space(loc2, pt)
tfoxnewPt = tfox_get_pos_in_obj_space(loc2, pt)
dnewPt = get_pos_in_obj_space(loc2, pt)
# uncomment this section to test your code vs mine
# # Edited to actually update the position of the locator, rather than print it
# # Set the loc2 translation to the new point that is snapped along the y axis
# mc.setAttr(loc2 + ".t", *tfoxnewPt, type="double3")
# -----------------------------------------------------------------------------
# My function treats the new location as a delta vector from the current
# local space position of the object to be moved
# In the case of a Y snap I just add the Y component of dnewPT to the current
# local space position of the object to be moved aka locator2
loc2pos = mc.xform(loc2, query=True, translation=True, objectSpace=True)
movevector = (loc2pos[0], loc2pos[1] + dnewPt[1], loc2pos[2])
mc.xform(loc2, os=True, t=movevector)
p.s.
How do you post code with syntax highlighting on here?
p.p.s
Come to think of it my function is more a delta vector generator, its not the input points position in the objectâs local space, it is ONLY a delta vector, to get the true location of the input point in the objectâs local space, I would add ALL XYZ values of the point to the objectâs current local space position (which I was doing in the case of an XYZ snap) .
Ohhh, I get it now. Xform uses your delta directly in object space, and the end of my function (where I translate into parent space) is recreating what xform with objectSpace=True does under the hood.
And no idea how to get the syntax highlighting. I donât do anything more than type triple-backticks around the code I post.
I wanted to have a go at this - pseudo code (not tested):
So moving along an objects axis is in in object space (based on the tool settings) - this is not reflected in the channel box (as @tfox_TD explained) which is the coordinate space the object lives in i.e. its parent space or world if it has no parent. I always work in one coordinate system before converting into the objects space (this helps my brain from exploding). What Iâm doing is firstly computing the targets world transforms relative to the source. This is you object-space offset.
I then create a new clean transform with just the position part of the axis you want to align - directly pumping this value in to the channel box on the source will not be battle-hardened and only work if it has no parents. So I first multiply this transform by the world transform of the source transform to essentially compute an offset in object-world space.
Finally this new computed world transform needs to be applied relative to the source objects parent space (regardless of it not having a parent - itâs gunna be more robust). Lastly i set the the source objects translation to the new transforms position part - 4th row vector.
What this means - the channel box may very well get values on all three axes of the translation. But weâre snapping to the supplied tool-modes axes.
import maya.cmds as cmds
import maya.api.OpenMaya as apiOM
def snap_pos_axes(src, trg, axes="y"):
"""Snaps the source's position to a target
along its object-space for given axes.
:param str src: The source object.
:param str trg: The target object.
:param str axes: Axes you want to align
in object-space e.g. 'x', 'xy', 'xyz'.
"""
# Get objects transform in world space.
src_xfo = cmds.xform(src, q=True, ws=True, m=True)
src_mat = apiOM.MMatrix(src_xfo)
trg_xfo = cmds.xform(trg, q=True, ws=True, m=True)
trg_mat = apiOM.MMatrix(trg_xfo)
# Compute the targets local transform relative to
# the source.
local_tm = trg_mat * src_mat.inverse()
# Build a local offset matrix.
new_mat = apiOM.MMatrix()
for axis in axes:
index = "xyz".index(axis)
offset = local_tm.getElement(3, index)
new_mat.setElement(3, index, offset)
# Compute the new transform in object-space relative to
# the source's parent space.
parent_mat = apiOM.MMatrix(cmds.xform(
"{}.parentInverseMatrix".format(src))
parent_inv_tm = (new_mat * src_mat) * parent_mat
# Apply the offset in parent space.
for i, axis in enumerate("xyz"):
cmds.setAttr("{}.t{}".format(src, axis),
parent_inv_tm.getElement(3, i))
EDIT not a bug, just have to account for this (for my modeling tool):
Steps to recreate:
With @tfox_TD 's function
I basically didnât account for âtranslationâ of an objects pivot via only rotation. Thatâs what is throwing off the snap centers in the calculations.
I figured it out! Whats happening!
Check this out
Make cube at 0,0,0
Workaround may be to store the rotation values, zero transforms, and do the calculations from there, because if I do that it never âbreaksâ
If not I get results like this:
Rather than
Define âbreaksâ.
Also, yeah, I definitely didnât take pivot stuff into account. Personally, I really donât like pivot offsets. Itâs a needless complication, and extra calculations that arenât needed more than 99% of the time. If they could create a TransformV2 object without all that extra junk that we could choose to use in rigs, I would be a happy man.
This is how I overcame my issue, obviously when rigging I assume people zero out / freeze / bake transform but when modeling anything goes?
Both your guys solutions are great, thanks for the input @tfox_TD @chalk
The âbreakingâ was regarding this: (Iâm sure thereâs a term for it?)
Imagine rolling a cube in +Z starting at 0,0,0 by moving its pivot and rotating it âforwardâ. This is what is throwing off the snap tool calculations.
It was hard to figure out what was going on haha when things would mysteriously not snap to the right locations while testing. It occurs when Iâve âmovedâ an object via lots of random rotations / pivot moves.
For my snap tool, I run through the objects and prebake pivots based on this calculation:
(it seems to work)
def checkRotatePivotOffset(obj):
"""
Checks if there is a relative difference between
object current worldspace position + pivots object space position
VS pivot worldspace position
Returns True if an offset exists
Returns False if there is NO offset
:param obj str: object name
"""
rotpiv_name = obj + '.rotatePivot'
rotpivws = om.MVector(mc.xform(rotpiv_name, q=1, ws=1, t=1))
rotpivos = om.MVector(mc.xform(rotpiv_name, q=1, os=1, t=1))
objposws = getObjWsPosVector(obj)
rotpivws2 = objposws + rotpivos
if rotpivws != rotpivws2:
return True
else:
return False
So, you might already have this working but Iâd like to propose an alternate method.
You can reliably do exactly what you need all in world space with some simple vector math and hierarchy wonât affect the result. The benefit of this is that the object-space axis you use is completely arbitrary. This is how I would do it
"""
For two selected transforms, snap the first to the position of the second along
the given local transform axis
"""
import pymel.core as pm
## The local-space axis of the first transform node.
## This is set to Y by default but could be set to any arbitrary axis, or combination of
## axes such as X/Y or Z/Y.
localAxisA = pm.dt.Vector(0, 1, 0).normal()
## Get the selected transform nodes
transformNodeA, transformNodeB = pm.selected(type='transform')
## Record the world-space positions of the transforms (returned as pm.dt.Vector)
positionA = transformNodeA.getTranslation(worldSpace=True)
positionB = transformNodeB.getTranslation(worldSpace=True)
## Multiply the local axis by transform A's matrix to get the axis in world-space
worldAxisA = localAxisA * transformNodeA.getAttr('matrix')
## Get a world-space vector from transform A to transform B
vectAB = positionB - positionA
## Use a dot product to "project" the vector from transform A to B onto the world-space
## axis of transform A
dot = vectAB.dot(worldAxisA)
translateDelta = worldAxisA * dot
## Add the result to the current position of transform A and set it
newPositionA = positionA + translateDelta
transformNodeA.setTranslation(newPositionA, worldSpace=True)
(Edit: added python syntax highlighting to the script)