Skip to content

Commit

Permalink
Maya: implement matchmove publishing (#5445)
Browse files Browse the repository at this point in the history
* OP-6360 - allow export of multiple cameras as alembic

* OP-6360 - make validation of camera count optional

* OP-6360 - make ValidatorCameraContents optional

This validator checks number of cameras, without optionality publish wouldn't be possible.

* OP-6360 - allow extraction of multiple cameras to .ma

* OP-6360 - update defaults for Ayon

Changes to Ayon settings should also bump up version of addon.

* OP-6360 - new matchmove creator

This family should be for more complex sets (eg. multiple cameras, with geometry, planes etc.

* OP-6360 - updated camera extractors

Added matchmove family to extract multiple cameras.
Single camera is protected by required validator.

* OP-6360 - added matchmove to reference loader

* Revert "OP-6360 - make ValidatorCameraContents optional"

This reverts commit 4096e81.

* Revert "OP-6360 - update defaults for Ayon"

This reverts commit 4391b25.

* OP-6360 - performance update

Number of cameras might be quite large, set operations will be faster than loop.

* Revert "OP-6360 - make validation of camera count optional"

This reverts commit ee3d91a.

* OP-6360 - explicitly cast to list for Maya functions

cmds.ls doesn't like sets in some older versions of Maya apparently. Sets are used here for performance reason, so explicitly cast them to list to make Maya happy.

* OP-6360 - added documentation about matchmove family

* OP-6360 - copy input planes

* OP-6360 - expose Settings to keep Image planes

Previous implementation didn't export Image planes in Maya file, to keep behavior backward compatible new Setting was added and set to False.

* OP-6360 - make both camera extractors optional

In Settings Alembic extractor was visible as optional even if code didn't follow that.

* OP-6360 - used long name

* OP-6360 - fix wrong variable

* Update openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py

Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>

* OP-6360 - removed shortening of varible

* OP-6360 - Hound

* OP-6360 - fix wrong key

* Update openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py

Co-authored-by: Toke Jepsen <tokejepsen@gmail.com>

* Update openpype/hosts/maya/api/lib.py

Co-authored-by: Toke Jepsen <tokejepsen@gmail.com>

* Update openpype/hosts/maya/plugins/publish/extract_camera_alembic.py

Co-authored-by: Toke Jepsen <tokejepsen@gmail.com>

* OP-6360 - fix wrong variable

* OP-6360 - added reattaching method

Image planes were attached wrong, added method to reattach them properly.

* Revert "Update openpype/hosts/maya/api/lib.py"

This reverts commit 4f40ad6.

* OP-6360 - exported baked camera should be deleted

Forgotten commenting just for development.

* OP-6360 - updated docstring

* OP-6360 - remove scale keys

Currently parentConstraint from old camera to new one doesn't work for keyed scale attributes. To key scale attributes doesn't make much sense so as a workaround, keys for scale attributes are checked AND if they are diferent from defaults (1.0) publish fails (as artist might want to actually key scale). If all scale keys are defaults, they are temporarily removed, cameras are parent constrained, exported and old camera returned to original state.

* OP-6360 - cleaned up resetting of scale keys

Batch calls used instead of one by one.
Cleaned up a return type as key value is no necessary as we are not setting it, just key.

* OP-6360 - removed unnecessary logging

* OP-6360 - reattach image plane to original camera

Image plane must be reattached before baked camera(s) are deleted.

* OP-6360 - added context manager to keep image planes attached to original camera

Without this image planes would disappear after removal of baked cameras.

* OP-6360 - refactored contextmanager

* OP-6360 - renamed flag

Input connections are not copied anymore as they might be dangerous. It is possible to epxlicitly attach only image planes instead.

* OP-6360 - removed copyInputConnections

Copying input connections might be dangerous (rig etc.), it is possible to explicitly attach only image planes.

* OP-6360 - updated plugin labels

* Update openpype/hosts/maya/plugins/create/create_matchmove.py

Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>

* OP-6360 - fixed formatting

---------

Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>
Co-authored-by: Toke Jepsen <tokejepsen@gmail.com>
  • Loading branch information
3 people committed Sep 29, 2023
1 parent d83eb73 commit a66edaf
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 78 deletions.
2 changes: 1 addition & 1 deletion openpype/hosts/maya/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2571,7 +2571,7 @@ def _get_attrs(node):
new_name = "{0}_baked".format(short_name)
new_node = cmds.duplicate(node,
name=new_name,
renameChildren=True)[0]
renameChildren=True)[0] # noqa

# Connect all attributes on the node except for transform
# attributes
Expand Down
32 changes: 32 additions & 0 deletions openpype/hosts/maya/plugins/create/create_matchmove.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from openpype.hosts.maya.api import (
lib,
plugin
)
from openpype.lib import BoolDef


class CreateMatchmove(plugin.MayaCreator):
"""Instance for more complex setup of cameras.
Might contain multiple cameras, geometries etc.
It is expected to be extracted into .abc or .ma
"""

identifier = "io.openpype.creators.maya.matchmove"
label = "Matchmove"
family = "matchmove"
icon = "video-camera"

def get_instance_attr_defs(self):

defs = lib.collect_animation_defs()

defs.extend([
BoolDef("bakeToWorldSpace",
label="Bake Cameras to World-Space",
tooltip="Bake Cameras to World-Space",
default=True),
])

return defs
3 changes: 2 additions & 1 deletion openpype/hosts/maya/plugins/load/load_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"camerarig",
"staticMesh",
"skeletalMesh",
"mvLook"]
"mvLook",
"matchmove"]

representations = ["ma", "abc", "fbx", "mb"]

Expand Down
22 changes: 14 additions & 8 deletions openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
from openpype.hosts.maya.api import lib


class ExtractCameraAlembic(publish.Extractor):
class ExtractCameraAlembic(publish.Extractor,
publish.OptionalPyblishPluginMixin):
"""Extract a Camera as Alembic.
The cameras gets baked to world space by default. Only when the instance's
The camera gets baked to world space by default. Only when the instance's
`bakeToWorldSpace` is set to False it will include its full hierarchy.
'camera' family expects only single camera, if multiple cameras are needed,
'matchmove' is better choice.
"""

label = "Camera (Alembic)"
label = "Extract Camera (Alembic)"
hosts = ["maya"]
families = ["camera"]
families = ["camera", "matchmove"]
bake_attributes = []

def process(self, instance):
Expand All @@ -35,10 +39,11 @@ def process(self, instance):

# validate required settings
assert isinstance(step, float), "Step must be a float value"
camera = cameras[0]

# Define extract output file path
dir_path = self.staging_dir(instance)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
filename = "{0}.abc".format(instance.name)
path = os.path.join(dir_path, filename)

Expand All @@ -64,9 +69,10 @@ def process(self, instance):

# if baked, drop the camera hierarchy to maintain
# clean output and backwards compatibility
camera_root = cmds.listRelatives(
camera, parent=True, fullPath=True)[0]
job_str += ' -root {0}'.format(camera_root)
camera_roots = cmds.listRelatives(
cameras, parent=True, fullPath=True)
for camera_root in camera_roots:
job_str += ' -root {0}'.format(camera_root)

for member in members:
descendants = cmds.listRelatives(member,
Expand Down
144 changes: 110 additions & 34 deletions openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
"""Extract camera as Maya Scene."""
import os
import itertools
import contextlib

from maya import cmds

from openpype.pipeline import publish
from openpype.hosts.maya.api import lib
from openpype.lib import (
BoolDef
)


def massage_ma_file(path):
Expand Down Expand Up @@ -78,7 +82,8 @@ def unlock(plug):
cmds.disconnectAttr(source, destination)


class ExtractCameraMayaScene(publish.Extractor):
class ExtractCameraMayaScene(publish.Extractor,
publish.OptionalPyblishPluginMixin):
"""Extract a Camera as Maya Scene.
This will create a duplicate of the camera that will be baked *with*
Expand All @@ -88,17 +93,22 @@ class ExtractCameraMayaScene(publish.Extractor):
The cameras gets baked to world space by default. Only when the instance's
`bakeToWorldSpace` is set to False it will include its full hierarchy.
'camera' family expects only single camera, if multiple cameras are needed,
'matchmove' is better choice.
Note:
The extracted Maya ascii file gets "massaged" removing the uuid values
so they are valid for older versions of Fusion (e.g. 6.4)
"""

label = "Camera (Maya Scene)"
label = "Extract Camera (Maya Scene)"
hosts = ["maya"]
families = ["camera"]
families = ["camera", "matchmove"]
scene_type = "ma"

keep_image_planes = True

def process(self, instance):
"""Plugin entry point."""
# get settings
Expand Down Expand Up @@ -131,15 +141,15 @@ def process(self, instance):
"bake to world space is ignored...")

# get cameras
members = cmds.ls(instance.data['setMembers'], leaf=True, shapes=True,
long=True, dag=True)
cameras = cmds.ls(members, leaf=True, shapes=True, long=True,
dag=True, type="camera")
members = set(cmds.ls(instance.data['setMembers'], leaf=True,
shapes=True, long=True, dag=True))
cameras = set(cmds.ls(members, leaf=True, shapes=True, long=True,
dag=True, type="camera"))

# validate required settings
assert isinstance(step, float), "Step must be a float value"
camera = cameras[0]
transform = cmds.listRelatives(camera, parent=True, fullPath=True)
transforms = cmds.listRelatives(list(cameras),
parent=True, fullPath=True)

# Define extract output file path
dir_path = self.staging_dir(instance)
Expand All @@ -151,23 +161,21 @@ def process(self, instance):
with lib.evaluation("off"):
with lib.suspended_refresh():
if bake_to_worldspace:
self.log.debug(
"Performing camera bakes: {}".format(transform))
baked = lib.bake_to_world_space(
transform,
transforms,
frame_range=[start, end],
step=step
)
baked_camera_shapes = cmds.ls(baked,
type="camera",
dag=True,
shapes=True,
long=True)

members = members + baked_camera_shapes
members.remove(camera)
baked_camera_shapes = set(cmds.ls(baked,
type="camera",
dag=True,
shapes=True,
long=True))

members.update(baked_camera_shapes)
members.difference_update(cameras)
else:
baked_camera_shapes = cmds.ls(cameras,
baked_camera_shapes = cmds.ls(list(cameras),
type="camera",
dag=True,
shapes=True,
Expand All @@ -186,19 +194,28 @@ def process(self, instance):
unlock(plug)
cmds.setAttr(plug, value)

self.log.debug("Performing extraction..")
cmds.select(cmds.ls(members, dag=True,
shapes=True, long=True), noExpand=True)
cmds.file(path,
force=True,
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
exportSelected=True,
preserveReferences=False,
constructionHistory=False,
channels=True, # allow animation
constraints=False,
shader=False,
expressions=False)
attr_values = self.get_attr_values_from_data(
instance.data)
keep_image_planes = attr_values.get("keep_image_planes")

with transfer_image_planes(sorted(cameras),
sorted(baked_camera_shapes),
keep_image_planes):

self.log.info("Performing extraction..")
cmds.select(cmds.ls(list(members), dag=True,
shapes=True, long=True),
noExpand=True)
cmds.file(path,
force=True,
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
exportSelected=True,
preserveReferences=False,
constructionHistory=False,
channels=True, # allow animation
constraints=False,
shader=False,
expressions=False)

# Delete the baked hierarchy
if bake_to_worldspace:
Expand All @@ -219,3 +236,62 @@ def process(self, instance):

self.log.debug("Extracted instance '{0}' to: {1}".format(
instance.name, path))

@classmethod
def get_attribute_defs(cls):
defs = super(ExtractCameraMayaScene, cls).get_attribute_defs()

defs.extend([
BoolDef("keep_image_planes",
label="Keep Image Planes",
tooltip="Preserving connected image planes on camera",
default=cls.keep_image_planes),

])

return defs


@contextlib.contextmanager
def transfer_image_planes(source_cameras, target_cameras,
keep_input_connections):
"""Reattaches image planes to baked or original cameras.
Baked cameras are duplicates of original ones.
This attaches it to duplicated camera properly and after
export it reattaches it back to original to keep image plane in workfile.
"""
originals = {}
try:
for source_camera, target_camera in zip(source_cameras,
target_cameras):
image_planes = cmds.listConnections(source_camera,
type="imagePlane") or []

# Split of the parent path they are attached - we want
# the image plane node name.
# TODO: Does this still mean the image plane name is unique?
image_planes = [x.split("->", 1)[1] for x in image_planes]

if not image_planes:
continue

originals[source_camera] = []
for image_plane in image_planes:
if keep_input_connections:
if source_camera == target_camera:
continue
_attach_image_plane(target_camera, image_plane)
else: # explicitly dettaching image planes
cmds.imagePlane(image_plane, edit=True, detach=True)
originals[source_camera].append(image_plane)
yield
finally:
for camera, image_planes in originals.items():
for image_plane in image_planes:
_attach_image_plane(camera, image_plane)


def _attach_image_plane(camera, image_plane):
cmds.imagePlane(image_plane, edit=True, detach=True)
cmds.imagePlane(image_plane, edit=True, camera=camera)
6 changes: 6 additions & 0 deletions openpype/settings/defaults/project_settings/maya.json
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,12 @@
"active": true,
"bake_attributes": []
},
"ExtractCameraMayaScene": {
"enabled": true,
"optional": true,
"active": true,
"keep_image_planes": false
},
"ExtractGLB": {
"enabled": true,
"active": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,35 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "ExtractCameraMayaScene",
"label": "Extract camera to Maya scene",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"type": "boolean",
"key": "active",
"label": "Active"
},
{
"type": "boolean",
"key": "keep_image_planes",
"label": "Export Image planes"
}
]
},
{
"type": "dict",
"collapsible": true,
Expand Down
Loading

0 comments on commit a66edaf

Please sign in to comment.