ynput / ayon-maya

Maya addon for AYON
Apache License 2.0
3 stars 6 forks source link

Conversion of XYZ to XZY in terms of rotation during extract layout #17

Closed moonyuet closed 2 weeks ago

moonyuet commented 1 month ago

Created the issue for discussion

@LiborBatek The updated PR can successfully flip y axis to z axis. The only circumstance is not working when the y rotation and z rotation both filled out with different values Should we make a validator to make sure these y and z rotations are not filled at the same time to ensure it works correctly in unreal?

thats sounds really restrictive...as things living in 3 dimensional space, no?? I guess we have to solve it somehow...maybe someone can step in and help you out?

Yes It would be great if @antirotor or @BigRoy or even some potential community members can join to discuss on that and needs more research or discussion on figuring out as when y axis moves and it would move the z axis and vice versa. The numbers are varied under the condition of z-axis being over 90, and y axis being less than 90. Anyways, it would be great to have some discussion on how to fix the rotation when it has Y axis and Z axis.

Not sure if i gonna try on the graph discussed below but I think the best idea is to create another issue to discuss and do the decent fix. image It is resolved by https://github.com/ynput/ayon-maya/pull/64 Originally posted by @moonyuet in https://github.com/ynput/ayon-maya/issues/12#issuecomment-2222670311

BigRoy commented 1 month ago

Could you break down what it is exactly that you're trying to do - in the simplest steps / least words?

I feel like this is a problem that has been fixed many times before? (many exporters support toggles for this.) So it would mean we'd just need to find that algorithm/logic and use it as well.

I assume it is basically multiplying a transformation matrix with a "conversion matrix" to generate a new composed matrix that has e.g. the extra rotation in it.

Also commented here.

moonyuet commented 1 month ago

Could you break down what it is exactly that you're trying to do - in the simplest steps / least words?

  • You are looking to transform a transformation matrix from one scene up-axis format to another scene up-axis format?

I feel like this is a problem that has been fixed many times before? (many exporters support toggles for this.) So it would mean we'd just need to find that algorithm/logic and use it as well.

I assume it is basically multiplying a transformation matrix with a "conversion matrix" to generate a new composed matrix that has e.g. the extra rotation in it.

Also commented here.

do you mean multiplying the transform matrix with an inverse matrix?

BigRoy commented 1 month ago

Here's a very simple example:

from maya import cmds
import maya.api.OpenMaya as om
import math

# Convert Y-up axis to Z-up axis transformation
rotation = math.radians(-90)
xform = om.MTransformationMatrix()
xform.setRotation(om.MEulerRotation(rotation, 0, 0))
conversion_matrix = xform.asMatrix()

# Get object matrix
node = cmds.ls(sl=1)[0]
matrix = cmds.xform(node, query=True, worldSpace=True, matrix=True)
matrix = om.MMatrix(matrix)

# Rotate the object world-space around origin using the conversion matrix
matrix *= conversion_matrix

# Apply the matrix to test
cmds.xform(node, worldSpace=True, matrix=matrix)

Rotate a node around the origin along the world-space X-axis with -90 degrees so that what was pointing at the Y-axis is now pointing at the Z-axis. Basically converting the Y up axis to the Z as up axis.

Note how the rotation matrix applied doesn't change - this conversion matrix is the same to be applied for all nodes then.

You'd only need this conversion for root nodes, because child node would already 'reorient' based on the parent's transformation. :)

moonyuet commented 1 month ago

https://github.com/ynput/ayon-maya/assets/64118225/d50c2735-cdfb-49d1-8423-af8d88dc9f03

I have tested with Maya, the number of Y and Z axis isn't being switched accordingly in the scene.

BigRoy commented 1 month ago

2024-07-11.23-22-45.mp4

I have tested with Maya, the number of Y and Z axis isn't being switched accordingly in the scene.

Visually to me, this is exactly doing what it should. Whatever is pointing Z-axis (in world) in your scene is now after the script pointing Y-axis. Which as mentioned before should "Basically converting the Y up axis to the Z as up axis."

We don't care about the rotation values - we care about the scene's orientation from? No?

moonyuet commented 1 month ago

2024-07-11.23-22-45.mp4 I have tested with Maya, the number of Y and Z axis isn't being switched accordingly in the scene.

Visually to me, this is exactly doing what it should. Whatever is pointing Z-axis (in world) in your scene is now after the script pointing Y-axis. Which as mentioned before should "Basically converting the Y up axis to the Z as up axis."

We don't care about the rotation values - we care about the scene's orientation from? No?

Perhaps you are right. I implemented the code with some changes of basis matrix on the extract layout. The Y and Z rots are correct, scaleXYZ are correct, only that the translation and X rot is with a bit strange number I will update the clip tomorrow to explain about that

MustafaJafar commented 1 month ago

Perhaps you are right. I implemented the code with some changes of basis matrix on the extract layout. The Y and Z rots are correct, scaleXYZ are correct, only that the translation and X rot is with a bit strange number I will update the clip tomorrow to explain about that

So, you want to convert the axis rotation while preserving the the scene rotation values ?

moonyuet commented 1 month ago

I have edited the extract layout to export the data, the basis identity matrix ensure the axis would go xzy instead of xyz (There is some progress for sure after the code tweak)

import json
import math
import os

from ayon_api import get_representation_by_id
from ayon_maya.api import plugin
from maya import cmds
from maya.api import OpenMaya as om

class ExtractLayout(plugin.MayaExtractorPlugin):
    """Extract a layout."""

    label = "Extract Layout"
    families = ["layout"]
    project_container = "AVALON_CONTAINERS"
    optional = True

    def process(self, instance):
        # Define extract output file path
        stagingdir = self.staging_dir(instance)

        # Perform extraction
        self.log.debug("Performing extraction..")

        if "representations" not in instance.data:
            instance.data["representations"] = []

        json_data = []
        # TODO representation queries can be refactored to be faster
        project_name = instance.context.data["projectName"]

        for asset in cmds.sets(str(instance), query=True):
            # Find the container
            project_container = self.project_container
            container_list = cmds.ls(project_container)
            if len(container_list) == 0:
                self.log.warning("Project container is not found!")
                self.log.warning("The asset(s) may not be properly loaded after published") # noqa
                continue

            grp_loaded_ass = instance.data.get("groupLoadedAssets", False)
            if grp_loaded_ass:
                asset_list = cmds.listRelatives(asset, children=True)
                # WARNING This does override 'asset' variable from parent loop
                #   is it correct?
                for asset in asset_list:
                    grp_name = asset.split(':')[0]
            else:
                grp_name = asset.split(':')[0]
            containers = cmds.ls("{}*_CON".format(grp_name))
            if len(containers) == 0:
                self.log.warning("{} isn't from the loader".format(asset))
                self.log.warning("It may not be properly loaded after published") # noqa
                continue
            container = containers[0]

            representation_id = cmds.getAttr(
                "{}.representation".format(container))

            representation = get_representation_by_id(
                project_name,
                representation_id,
                fields={"versionId", "context"}
            )

            self.log.debug(representation)

            version_id = representation["versionId"]
            # TODO use product entity to get product type rather than
            #    data in representation 'context'
            repre_context = representation["context"]
            product_type = repre_context.get("product", {}).get("type")
            if not product_type:
                product_type = repre_context.get("family")

            json_element = {
                "product_type": product_type,
                "instance_name": cmds.getAttr(
                    "{}.namespace".format(container)),
                "representation": str(representation_id),
                "version": str(version_id)
            }

            loc = cmds.xform(asset, query=True, translation=True)
            rot = cmds.xform(asset, query=True, rotation=True, euler=True)
            scl = cmds.xform(asset, query=True, relative=True, scale=True)

            json_element["transform"] = {
                "translation": {
                    "x": loc[0],
                    "y": loc[1],
                    "z": loc[2]
                },
                "rotation": {
                    "x": math.radians(rot[0]),
                    "y": math.radians(rot[1]),
                    "z": math.radians(rot[2])
                },
                "scale": {
                    "x": scl[0],
                    "y": scl[1],
                    "z": scl[2]
                }
            }

            row_length = 4
            t_matrix_list = cmds.xform(asset, query=True, worldSpace=True, matrix=True)

            transform_mm = om.MMatrix(t_matrix_list)
            transform = om.MTransformationMatrix()
            transform.setRotation(om.MEulerRotation(math.radians(-90), 0, 0))

            conversion_matrix = transform.asMatrix()
            transform_mm *= conversion_matrix
            t_matrix_list = list(transform_mm)

            t_matrix = []
            for i in range(0, len(t_matrix_list), row_length):
                t_matrix.append(t_matrix_list[i:i + row_length])

            json_element["transform_matrix"] = [
                list(row)
                for row in t_matrix
            ]

            basis_list = [
                1, 0, 0, 0,
                0, 0, 1, 0,
                0, 1, 0, 0,
                0, 0, 0, 1
            ]

            basis_mm = om.MMatrix(basis_list)
            basis = om.MTransformationMatrix(basis_mm)

            b_matrix_list = list(basis.asMatrix())
            b_matrix = []

            for i in range(0, len(b_matrix_list), row_length):
                b_matrix.append(b_matrix_list[i:i + row_length])

            json_element["basis"] = []
            for row in b_matrix:
                json_element["basis"].append(list(row))

            json_data.append(json_element)

        json_filename = "{}.json".format(instance.name)
        json_path = os.path.join(stagingdir, json_filename)

        with open(json_path, "w+") as file:
            json.dump(json_data, fp=file, indent=2)

        json_representation = {
            'name': 'json',
            'ext': 'json',
            'files': json_filename,
            "stagingDir": stagingdir,
        }
        instance.data["representations"].append(json_representation)

        self.log.debug("Extracted instance '%s' to: %s",
                       instance.name, json_representation)

When I extract layout in maya with the code above, and then import the layout, it shows wrong translation, and the rotation is wrong. You can find this scenegrab helpful to understand the issue image

https://github.com/user-attachments/assets/0323dd6c-9f16-4250-a26b-b21c884db888

BigRoy commented 1 month ago

I don't understand - Houdini has a different up-axis - and due to this you need to rotate your objects so it faces the correct direction. As soon as you start applying a rotation, the values being different is correct behavior, right? And if the object already had rotation - to rotate it in world-space (to convert Y-up to Z-up) would basically mean you're rotating across multiple euler angles (depending on the current values and rotation order).

In short, the values not looking like the exact values in Maya in the outliner is entirely correct.

The rotation values in Unreal in the attributes are irrelevant - the primary point is that we convert the transformations to be valid for Unreal so we need to check the object visually in the scene instead, not compare the attributes.

If you do not want to mess with the rotation values you may be be better off importing everything into a group - and then you apply the rotation to the group. Then all child transforms can remain completely unaffected.

BigRoy commented 1 month ago

Side note: You seem to be exporting the transform matrix but also individual translate, rotate, scale into the JSON. I'm not sure why. The transformation matrix defines the translate, rotate, scale (and shear) OR vice-versa. If one changes, the other should as well. So you'd only need to store one of the two, usually the matrix - unless for whatever reason you need rotation values below/above 360 degrees span (e.g. outside -180 and +180) because the matrix can't hold that information.

moonyuet commented 1 month ago

I did some other test within Maya and read along the unreal code. I guess we can tweak the code from both sides. It could be some converting issues on the matrix, where somehow the there is some multiplication between scale and rotation.

from maya.api import OpenMaya as om
row_length = 4
basis_list = [
    1, 0, 0, 0,
    0, 0, 1, 0,
    0, 1, 0, 0,
    0, 0, 0, 1
]
# identity matrix to have (x, z, y, w)
basis_mm = om.MMatrix(basis_list)
basis = om.MTransformationMatrix(basis_mm)

b_matrix_list = basis.asMatrix()

t_matrix_list = cmds.xform("pSphere1", query=True, matrix=True, worldSpace=True)

transform_mm = om.MMatrix(t_matrix_list)
transform = om.MTransformationMatrix(transform_mm)
t = transform.translation(om.MSpace.kWorld)
t = om.MVector(t.x, t.z, t.y)
transform.setTranslation(t, om.MSpace.kWorld)
transform.rotateBy(
    om.MEulerRotation(0, 0, 0), om.MSpace.kWorld)
transform.scaleBy([1.0, 1.0, 1.0], om.MSpace.kObject)
t_matrix_list = transform.asMatrix()
t_matrix_inverse_list = transform.asMatrixInverse()

test_matrix = t_matrix_list * b_matrix_list
# test_matrix = t_matrix_inverse_list * t_matrix_list * b_matrix_list    # M^-1 *M*I
cmds.xform("pSphere2", matrix=test_matrix, worldSpace=True)

I also did some calculations to figure out the matrix formula to converting Y to Z to find out their relationship.

from maya.api import OpenMaya as om
# t.x = 10, t.y=9, t.z=8, r.x = 30, r.y=60, r.z=90, s.x=10, s.y=11, s.z=12
worldMatrix_1 = cmds.xform('pSphere1', query=True, matrix=True, worldSpace=True)
# t.x = 10, t.y=8, t.z=9, r.x = 30, r.y=90, r.z=60, s.x=10, s.y=12, s.z=11
worldMatrix_2 = cmds.xform('pSphere2', query=True, matrix=True, worldSpace=True)  
world_Matrix_mm_1 = om.MMatrix(worldMatrix_1)
world_transform_1 = om.MTransformationMatrix(world_Matrix_mm_1)

# find the inverse
world_1_matrix_list = world_transform_1.asMatrixInverse()

world_Matrix_mm_2 = om.MMatrix(worldMatrix_2)
world_transform_2 = om.MTransformationMatrix(world_Matrix_mm_2)

world_2_matrix_list = world_transform_2.asMatrix()

# Not a inverse of world_1_matrix_list * H = world_2_matrix_list
H = world_2_matrix_list *world_1_matrix_list
# maya.api.OpenMaya.MMatrix(((0.86602540378443859659, -0.2272727272727274872, -0.36084391824351613742, 0), (0.51961524227066357984, 0.88146840206423893171, 0.39951905283832911397, 0), 
#(0.27499999999999980016, -0.53349364905389040636, 0.74067831006786766235, 0), (-0.13660254037844390962, -0.016637518353838032237, -0.026415608175648364053, 1)))

I would be tweaking on both ayon-maya, ayon-unreal (hard-coded a little bit) to see if this outcome can help to get the correct attributes for Alembic EDIT: I am actually overthinking a little bit, I can use the original code to improve.

moonyuet commented 1 month ago

I have used the code below to manually put the axis, the matrix would be changed depending on what value we get from cmds.xform("the asset", matrix=True, query=True)

import unreal
transform_matrix = unreal.Matrix(
    [1.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 1.0]
)
t = transform_matrix.transform()
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
ar = unreal.AssetRegistryHelpers.get_asset_registry()
for asset in sel_objects:
    obj = ar.get_asset_by_object_path(asset.get_path_name()).get_asset()
    actor = unreal.EditorLevelLibrary.spawn_actor_from_object(
        obj, t.translation
    )
    actor.set_actor_rotation(t.rotation.rotator(), False)
    actor.set_actor_scale3d(t.scale3d)

and get this conclusion below so far IMG_20240801_235600