Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data exchange cameras for 3d Studio Max #4376

Merged
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0e80eff
maya gltf texture convertor and validator
moonyuet Dec 22, 2022
92fd765
maya gltf texture convertor and validator
moonyuet Dec 22, 2022
96c8029
maya gltf texture convertor and validator
moonyuet Dec 22, 2022
7f02e8c
maya gltf texture convertor and validator
moonyuet Dec 22, 2022
76a1df7
Merge branch 'ynput:develop' into feature/OP-4668-maya-gltf-textures-…
moonyuet Jan 4, 2023
338660d
Merge branch 'ynput:develop' into feature/OP-4668-maya-gltf-textures-…
moonyuet Jan 17, 2023
24d7de4
improve the validator for gltf texture name
moonyuet Jan 17, 2023
3dd02ce
update the validator
moonyuet Jan 17, 2023
eb8c40a
update the validator for ORM
moonyuet Jan 17, 2023
ba45473
update the texture conversion of ORM
moonyuet Jan 17, 2023
0af4f7d
Update convert_gltf_shader.py
moonyuet Jan 17, 2023
2578597
Update convert_gltf_shader.py
moonyuet Jan 17, 2023
bdc4a09
Update convert_gltf_shader.py
moonyuet Jan 17, 2023
62ebd77
clean up the code for validator and add the config options for glsl s…
moonyuet Jan 18, 2023
e885192
Merge pull request #5 from ynput/feature/OP-4668-maya-gltf-textures-a…
moonyuet Jan 18, 2023
6b3b849
Merge branch 'develop' of https://github.com/moonyuet/OpenPype_fork i…
moonyuet Jan 19, 2023
7d74425
add extractors and validators for cameras
moonyuet Jan 26, 2023
c4fe43a
Delete convert_gltf_shader.py
moonyuet Jan 26, 2023
d370f8a
Delete validate_gltf_textures_names.py
moonyuet Jan 26, 2023
32b557e
Delete maya.json
moonyuet Jan 26, 2023
f0fb628
Delete schema_maya_publish.json
moonyuet Jan 26, 2023
4ecb955
hound fix
moonyuet Jan 26, 2023
ab7737d
hound fix
moonyuet Jan 26, 2023
b29f382
resolve conflict
moonyuet Jan 26, 2023
53186ff
resolve conflict
moonyuet Jan 26, 2023
20e32d0
clean up the extractors and validator
moonyuet Jan 26, 2023
5cf9ce7
fix typo
moonyuet Jan 26, 2023
4dcd147
fix typo
moonyuet Jan 26, 2023
6144f98
Merge branch 'develop' into feature/OP-4244-Data-Exchange-Cameras
moonyuet Jan 26, 2023
8334323
fix typo
moonyuet Jan 26, 2023
01a70c0
add loaders for fbx import and max scene import
moonyuet Jan 30, 2023
92986bc
hound fix
moonyuet Jan 30, 2023
ef29a14
Merge branch 'develop' into feature/OP-4244-Data-Exchange-Cameras
moonyuet Jan 30, 2023
d22d51d
Merge branch 'ynput:develop' into feature/OP-4244-Data-Exchange-Cameras
moonyuet Jan 30, 2023
4a6eb02
Merge branch 'feature/OP-4244-Data-Exchange-Cameras' of https://githu…
moonyuet Jan 30, 2023
bca05d5
Merge branch 'develop' into feature/OP-4244-Data-Exchange-Cameras
moonyuet Jan 30, 2023
46996bb
add camera family in abc loader
moonyuet Jan 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions openpype/hosts/max/plugins/create/create_camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating camera."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance


class CreateCamera(plugin.MaxCreator):
identifier = "io.openpype.creators.max.camera"
label = "Camera"
family = "camera"
icon = "gear"

def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
sel_obj = list(rt.selection)
_ = super(CreateCamera, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(subset_name)
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems wrong. I expect it's much safer to actually use the returned instances of the super process and taking the instance_node data

Suggested change
sel_obj = list(rt.selection)
_ = super(CreateCamera, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(subset_name)
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))
selection = list(rt.selection)
instance = super(CreateCamera, self).create(
subset_name,
instance_data,
pre_create_data
) # type: CreatedInstance
container = rt.getNodeByName(instance.data.get("instance_node"))
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in selection:
obj.parent = container

69 changes: 69 additions & 0 deletions openpype/hosts/max/plugins/publish/extract_camera_abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import os
import pyblish.api
from openpype.pipeline import publish
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)


class ExtractAlembicCamera(publish.Extractor):
"""
Extract Camera with AlembicExport
"""

order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Almebic Camera"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
label = "Extract Almebic Camera"
label = "Extract Alembic Camera"

hosts = ["max"]
families = ["camera"]
optional = True

def process(self, instance):
start = float(instance.data.get("frameStartHandle", 1))
end = float(instance.data.get("frameEndHandle", 1))

container = instance.data["instance_node"]

self.log.info("Extracting Camera ...")

stagingdir = self.staging_dir(instance)
filename = "{name}.abc".format(**instance.data)
path = os.path.join(stagingdir, filename)

# We run the render
self.log.info("Writing alembic '%s' to '%s'" % (filename,
stagingdir))

export_cmd = (
f"""
AlembicExport.ArchiveType = #ogawa
AlembicExport.CoordinateSystem = #maya
AlembicExport.StartFrame = {start}
AlembicExport.EndFrame = {end}
AlembicExport.CustomAttributes = true

exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport
Comment on lines +38 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it that the path does not need escaping for backslashes here but it did need it for the extract max scene raw?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's only for max scene export. If you are using abc/fbx export, you dont need the path escaping for backslashes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you are right. let me double check.


""")

self.log.debug(f"Executing command: {export_cmd}")

with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(export_cmd)

self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []

representation = {
'name': 'abc',
'ext': 'abc',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
path))
68 changes: 68 additions & 0 deletions openpype/hosts/max/plugins/publish/extract_camera_fbx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
import pyblish.api
from openpype.pipeline import publish
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)


class ExtractCameraFbx(publish.Extractor):
"""
Extract Camera with FbxExporter
"""

order = pyblish.api.ExtractorOrder - 0.2
label = "Extract Fbx Camera"
hosts = ["max"]
families = ["camera"]

def process(self, instance):
container = instance.data["instance_node"]

self.log.info("Extracting Camera ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.fbx".format(**instance.data)

filepath = os.path.join(stagingdir, filename)
self.log.info("Writing fbx file '%s' to '%s'" % (filename,
filepath))

# Need to export:
# Animation = True
# Cameras = True
# AxisConversionMethod
fbx_export_cmd = (
f"""

FBXExporterSetParam "Animation" true
FBXExporterSetParam "Cameras" true
FBXExporterSetParam "AxisConversionMethod" "Animation"
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true

exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP

""")

self.log.debug(f"Executing command: {fbx_export_cmd}")

with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(fbx_export_cmd)

self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []

representation = {
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
filepath))
67 changes: 67 additions & 0 deletions openpype/hosts/max/plugins/publish/extract_max_scene_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import pyblish.api
from openpype.pipeline import publish
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)


class ExtractMaxSceneRaw(publish.Extractor):
"""
Extract Raw Max Scene with SaveSelected
"""

order = pyblish.api.ExtractorOrder - 0.2
label = "Max Scene(Raw)"
antirotor marked this conversation as resolved.
Show resolved Hide resolved
hosts = ["max"]
families = ["camera"]

def process(self, instance):
container = instance.data["instance_node"]

# publish the raw scene for camera
self.log.info("Extracting Camera ...")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a plug-in that is ExtractMaxSceneRaw I wouldn't expect it to log this.


stagingdir = self.staging_dir(instance)
filename = "{name}.max".format(**instance.data)

max_path = os.path.join(stagingdir, filename)
self.log.info("Writing max file '%s' to '%s'" % (filename,
max_path))
Comment on lines +38 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're using f string formatting down below we could so here too.

Suggested change
self.log.info("Writing max file '%s' to '%s'" % (filename,
max_path))
self.log.info(f"Writing max file: {max_path}/{filename}")


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

# add extra blacklash for saveNodes in MaxScript
re_max_path = stagingdir + "\\\\" + filename
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we'd also need to escape any potential backslashes in the path to stagingdir, no?
Wouldn't it be easier to just force forward slashes, then you'd need no escaping?
Backslashes joining a path like this seems very Windows-only too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it's just for the slash in-between stagingdir and filename for this specific case.

Copy link
Collaborator

@BigRoy BigRoy Jan 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, but why? :) I don't see anything in the docs that would make it so that only that part of the filename needs extra escaping.

Likely this works too re_max_path = os.path.join(stagingdir, filename) (which means you could just use max_path defined already) if it supports backslashes just fine. And if not then this works too: re_max_path = max_path.replace("\\", "/")

If it doesn't work otherwise, could you share the re_max_path value + the value of the raw_export_cmd + the error that gets generated.

It sounds very confusing if only the last backslash needs extra escaping even though there could be multiple folders with backslashes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is done for double backslashes, maybe something like:

re_max_path = os.path.normpath(max_path).replace("\\", "\\\\")

to be sure that they are everywhere?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most apps also deal with it fine if you force forward slashes instead - it usually makes the output easier to read so if it works I'd recommend:

os.path.normpath(max_path).replace("\\", "/")

# saving max scene
raw_export_cmd = (
f"""
sel = getCurrentSelection()
for s in sel do
(
select s
f="{re_max_path}"
print f
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly verbose?

Suggested change
print f

saveNodes selection f quiet:true
)
""") # noqa
self.log.debug(f"Executing Maxscript command: {raw_export_cmd}")

with maintained_selection():
# need to figure out how to select the camera
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(raw_export_cmd)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rt.execute(raw_export_cmd)
rt.execute(f'saveNodes selection "{path}" quiet:true')

Having never used 3ds Max or its Maxscript API. Why couldn't you just use saveNodes directly over your current selection and instead need to do it per node as you're doing?

Looking at this Saving Nodes documentation it should just be capable of saving a 'collection of nodes' including the selection?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming path here has no 'special characters' that needs escaping. Simplest way is likely to make path = path.replace("\\", "/") beforehand.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming path here has no 'special characters' that needs escaping. Simplest way is likely to make path = path.replace("\\", "/") beforehand.

it's just the slash in between stagingdir and filename needs escape, the rest doesn't need that. I know it sounds weird but if you escape all of them or not escaping them, the publisher is not able to publish the max scene


self.log.info("Performing Extraction ...")
representation = {
'name': 'max',
'ext': 'max',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
max_path))
2 changes: 1 addition & 1 deletion openpype/hosts/max/plugins/publish/extract_pointcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class ExtractAlembic(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Pointcache"
hosts = ["max"]
families = ["pointcache", "camera"]
families = ["pointcache"]

def process(self, instance):
start = float(instance.data.get("frameStartHandle", 1))
Expand Down
55 changes: 55 additions & 0 deletions openpype/hosts/max/plugins/publish/validate_camera_contents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt


class ValidateCameraContent(pyblish.api.InstancePlugin):
"""Validates Camera instance contents.

A Camera instance may only hold a SINGLE camera's transform
"""

order = pyblish.api.ValidatorOrder
families = ["camera"]
hosts = ["max"]
label = "Camera Contents"
camera_type = ["$Free_Camera", "$Target_Camera",
"$Physical_Camera", "$Target"]

def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Camera instance must only include"
"camera (and camera target)")

def get_invalid(self, instance):
"""
Get invalid nodes if the instance is not camera
"""
invalid = list()
container = instance.data["instance_node"]
self.log.info("Validating look content for "
"{}".format(container))

con = rt.getNodeByName(container)
selection_list = self.list_children(con)
validation_msg = list()
for sel in selection_list:
# to avoid Attribute Error from pymxs wrapper
sel_tmp = str(sel)
for cam in self.camera_type:
if sel_tmp.startswith(cam):
validation_msg.append("Camera Found")
else:
validation_msg.append("Camera Not Found")
if "Camera Found" not in validation_msg:
invalid.append(sel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would simplify this:

Suggested change
for sel in selection_list:
# to avoid Attribute Error from pymxs wrapper
sel_tmp = str(sel)
for cam in self.camera_type:
if sel_tmp.startswith(cam):
validation_msg.append("Camera Found")
else:
validation_msg.append("Camera Not Found")
if "Camera Found" not in validation_msg:
invalid.append(sel)
for sel in selection_list:
# to avoid Attribute Error from pymxs wrapper
sel_tmp = str(sel)
found = False
for cam in self.camera_type:
if sel_tmp.startswith(cam):
found = True
break
if not found:
self.log.error("Camera not found")
invalid.append(sel)

Copy link
Collaborator

@BigRoy BigRoy Jan 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think check code-wise is odd to read.

We seem to be comparing the object type to see if it's a camera.

I'd search on how to explicitly get the object type instead of assuming it's the start of the string for str(object) of a 3ds max object so that we could turn the whole validation .py file into something along these lines:

# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt


def is_valid_camera_node(node):
    obj_type = node.type()  # or whatever the valid method is for that
    return obj_type in {"$Free_Camera", "$Target_Camera", 
                        "$Physical_Camera", "$Target"}
    

class ValidateCameraContent(pyblish.api.InstancePlugin):
    """Validates Camera instance contents.

    A Camera instance may only hold a SINGLE camera's transform
    """

    order = pyblish.api.ValidatorOrder
    families = ["camera"]
    hosts = ["max"]
    label = "Camera Contents"

    def process(self, instance):
        invalid = self.get_invalid(instance)
        if invalid:
            raise PublishValidationError("Camera instance must only include"
                                         "camera (and camera target)")

    def get_invalid(self, instance):
        """Get invalid nodes if the instance is not camera"""
        
        container = rt.getNodeByName(instance.data["instance_node"])

        invalid = list()
        for child in list(container.Children):
            if not is_valid_camera_node(child):
                invalid.append(child)
                
        return invalid

Potentially even:

        invalid = [obj for obj in container.Children if not is_valid_camera_node(obj)]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had another quick peek over the 3ds Max pymxs docs. I think it should be something along these lines:

One of these:

rt.superClassOf(node) 
rt.classOf(node)

And then check it against the actual rt types:

def is_valid_camera_node(node):
    obj_type = rt.classOf(node)
    return obj_type in {rt.Free_Camera, rt.Target_Camera,
                        rt.Physical_Camera, rt.Target}

Having never used 3ds Max I'm not sure that's anywhere remotely valid - but it seemed like that should be roughly it. :) But do consider it pseudocode!

# go through the camera type to see if there are same name
return invalid

def list_children(self, node):
children = []
for c in node.Children:
children.append(c)
return children
Copy link
Member

@antirotor antirotor Jan 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't this be replaced with list(node.Children)?

Loading