-
Notifications
You must be signed in to change notification settings - Fork 128
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
Changes from 25 commits
0e80eff
92fd765
96c8029
7f02e8c
76a1df7
338660d
24d7de4
3dd02ce
eb8c40a
ba45473
0af4f7d
2578597
bdc4a09
62ebd77
e885192
6b3b849
7d74425
c4fe43a
d370f8a
32b557e
f0fb628
4ecb955
ab7737d
b29f382
53186ff
20e32d0
5cf9ce7
4dcd147
6144f98
8334323
01a70c0
92986bc
ef29a14
d22d51d
4a6eb02
bca05d5
46996bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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")) | ||
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" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) |
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)) |
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 ...") | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a plug-in that is |
||||||||
|
||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||
|
||||||||
if "representations" not in instance.data: | ||||||||
instance.data["representations"] = [] | ||||||||
|
||||||||
# add extra blacklash for saveNodes in MaxScript | ||||||||
re_max_path = stagingdir + "\\\\" + filename | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 If it doesn't work otherwise, could you share the It sounds very confusing if only the last backslash needs extra escaping even though there could be multiple folders with backslashes. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems overly verbose?
Suggested change
|
||||||||
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) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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)) |
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) | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would simplify this:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 # -*- 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)] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had another quick peek over the 3ds Max One of these:
And then check it against the actual 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 | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. couldn't this be replaced with |
There was a problem hiding this comment.
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