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

Maya: bug fix the playblast without textures #5942

Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
6074876
regenerate UV Tile Preview and reload textures during playblasting
moonyuet Nov 22, 2023
15923dd
move the regenerate uv_tile_preview code right before the capture
moonyuet Nov 22, 2023
2b98ac1
rename regenerateUVTilePreview as reload_textures
moonyuet Nov 22, 2023
ba83d4c
remove unused variables
moonyuet Nov 22, 2023
51f4d8f
make the reload texture being optional and only enabled when the relo…
moonyuet Nov 23, 2023
2d85b5f
code tweaks on capturing playblast and reloadtexture function
moonyuet Nov 24, 2023
950581f
code tweaks on getting texture from the viewport_options dict
moonyuet Nov 24, 2023
39faa70
pop the value of reloadTextures before capture
moonyuet Nov 27, 2023
2dda5c7
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
moonyuet Nov 27, 2023
84f5824
pop the reloadvalues from the preset in thumbnail extractor
moonyuet Nov 27, 2023
69c45c5
make sure reloadtextures is popped when it exists in the preset dict
moonyuet Nov 27, 2023
899bf86
tweak on the preset.pop
moonyuet Nov 28, 2023
cafd02a
preset.pop tweaks in thumbnail extractor
moonyuet Nov 28, 2023
cbc4c67
make sure the texture can be reloaded
moonyuet Dec 7, 2023
7061eab
restore unnecessary tweaks
moonyuet Dec 7, 2023
ccc95f0
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
moonyuet Dec 7, 2023
b929464
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
LiborBatek Dec 11, 2023
cec780a
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
LiborBatek Dec 11, 2023
b73146a
preset pop value should be correct
moonyuet Dec 11, 2023
28a62bf
make sure the material loading mode is parallel
moonyuet Dec 11, 2023
4648895
make sure the contextlib.nested used before material loading while it…
moonyuet Dec 15, 2023
c62862a
hound
moonyuet Dec 15, 2023
9f1ab75
add exitstack.py into maya api folder & code tweaks
moonyuet Dec 15, 2023
b7da570
hound
moonyuet Dec 15, 2023
4e005bf
hound
moonyuet Dec 15, 2023
7dc19ec
hound
moonyuet Dec 15, 2023
008f78e
implement the exitstack inside the capture
moonyuet Dec 19, 2023
f9603bb
refactor the capture function and move it to lib
moonyuet Dec 19, 2023
2f03b61
refactor the capture and playblast functions and put them into lib.py
moonyuet Dec 20, 2023
25e216b
hound
moonyuet Dec 20, 2023
29876a4
hound
moonyuet Dec 20, 2023
c567499
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
moonyuet Dec 20, 2023
0ae3ef0
refactor the capture and capture preset function
moonyuet Dec 20, 2023
04f9d4c
hound
moonyuet Dec 20, 2023
746e34a
hound
moonyuet Dec 20, 2023
8ce8d72
add reloadTextures argument back to cmds.ogs
moonyuet Dec 20, 2023
4b6e5e2
add material_loading_mode into enter_context
moonyuet Dec 20, 2023
0dd4d7b
Code cleanup
BigRoy Dec 20, 2023
5a7079c
Fix calls to refactored function name
BigRoy Dec 20, 2023
9f543f8
Remove `capture` import that's already imported at top
BigRoy Dec 20, 2023
82e5e6b
Clarify log messages
BigRoy Dec 20, 2023
208e3f1
Depth of field is already preserved from camera by `generate_capture_…
BigRoy Dec 20, 2023
4796bca
Cosmetics, + remove unused import
BigRoy Dec 20, 2023
d880cdd
Cosmetics - avoid confusion about what `preset.get("filename")` actua…
BigRoy Dec 20, 2023
fa032d5
Cosmetis + improve comment
BigRoy Dec 20, 2023
f922d3c
Update openpype/hosts/maya/api/lib.py
BigRoy Dec 21, 2023
67a6a11
Update openpype/hosts/maya/api/lib.py
BigRoy Dec 21, 2023
2cfc38b
Merge pull request #6081 from BigRoy/bugfix/OP-7281_Maya-Review---pla…
moonyuet Dec 21, 2023
5f30999
cosmetic tweaks and code clean up
moonyuet Dec 21, 2023
1d4acb7
hound
moonyuet Dec 21, 2023
a3f9379
repharse the preset pop for panel
moonyuet Dec 21, 2023
6f54326
restore unnecessary tweaks
moonyuet Dec 21, 2023
7acbef9
change the args oder in viewport_default_options
moonyuet Dec 21, 2023
3fd9d47
use filename = instance.name
moonyuet Dec 21, 2023
cf29a53
make sure the texture only loaded when the texture is being enabled
moonyuet Jan 2, 2024
d351e5f
renamed reload Textures to Load Textures
moonyuet Jan 2, 2024
379674d
remove the condition with the deprecated environment variable in AYON
moonyuet Jan 3, 2024
f2eef86
Merge branch 'develop' into bugfix/OP-7281_Maya-Review---playblast-re…
moonyuet Jan 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
139 changes: 139 additions & 0 deletions openpype/hosts/maya/api/exitstack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Backwards compatible implementation of ExitStack for Python 2.

ExitStack contextmanager was implemented with Python 3.3.
As long as we supportPython 2 hosts we can use this backwards
compatible implementation to support bothPython 2 and Python 3.

Instead of using ExitStack from contextlib, use it from this module:

>>> from openpype.hosts.maya.api.exitstack import ExitStack

It will provide the appropriate ExitStack implementation for the current
running Python version.

"""
# TODO: Remove the entire script once dropping Python 2 support.
import contextlib
if getattr(contextlib, "nested", None):
from contextlib import ExitStack # noqa
else:
import sys
from collections import deque

class ExitStack(object):

"""Context manager for dynamic management of a stack of exit callbacks

For example:

with ExitStack() as stack:
files = [stack.enter_context(open(fname))
for fname in filenames]
# All opened files will automatically be closed at the end of
# the with statement, even if attempts to open files later
# in the list raise an exception

"""
def __init__(self):
self._exit_callbacks = deque()

def pop_all(self):
"""Preserve the context stack by transferring
it to a new instance"""
new_stack = type(self)()
new_stack._exit_callbacks = self._exit_callbacks
self._exit_callbacks = deque()
return new_stack

def _push_cm_exit(self, cm, cm_exit):
"""Helper to correctly register callbacks
to __exit__ methods"""
def _exit_wrapper(*exc_details):
return cm_exit(cm, *exc_details)
_exit_wrapper.__self__ = cm
self.push(_exit_wrapper)

def push(self, exit):
"""Registers a callback with the standard __exit__ method signature

Can suppress exceptions the same way __exit__ methods can.

Also accepts any object with an __exit__ method (registering a call
to the method instead of the object itself)
"""
# We use an unbound method rather than a bound method to follow
# the standard lookup behaviour for special methods
_cb_type = type(exit)
try:
exit_method = _cb_type.__exit__
except AttributeError:
# Not a context manager, so assume its a callable
self._exit_callbacks.append(exit)
else:
self._push_cm_exit(exit, exit_method)
return exit # Allow use as a decorator

def callback(self, callback, *args, **kwds):
"""Registers an arbitrary callback and arguments.

Cannot suppress exceptions.
"""
def _exit_wrapper(exc_type, exc, tb):
callback(*args, **kwds)
# We changed the signature, so using @wraps is not appropriate, but
# setting __wrapped__ may still help with introspection
_exit_wrapper.__wrapped__ = callback
self.push(_exit_wrapper)
return callback # Allow use as a decorator

def enter_context(self, cm):
"""Enters the supplied context manager

If successful, also pushes its __exit__ method as a callback and
returns the result of the __enter__ method.
"""
# We look up the special methods on the type to
# match the with statement
_cm_type = type(cm)
_exit = _cm_type.__exit__
result = _cm_type.__enter__(cm)
self._push_cm_exit(cm, _exit)
return result

def close(self):
"""Immediately unwind the context stack"""
self.__exit__(None, None, None)

def __enter__(self):
return self

def __exit__(self, *exc_details):
# We manipulate the exception state so it behaves as though
# we were actually nesting multiple with statements
frame_exc = sys.exc_info()[1]

def _fix_exception_context(new_exc, old_exc):
while 1:
exc_context = new_exc.__context__
if exc_context in (None, frame_exc):
break
new_exc = exc_context
new_exc.__context__ = old_exc

# Callbacks are invoked in LIFO order to match the behaviour of
# nested context managers
suppressed_exc = False
while self._exit_callbacks:
cb = self._exit_callbacks.pop()
try:
if cb(*exc_details):
suppressed_exc = True
exc_details = (None, None, None)
except Exception:
new_exc_details = sys.exc_info()
# simulate the stack of exceptions by setting the context
_fix_exception_context(new_exc_details[1], exc_details[1])
if not self._exit_callbacks:
raise
exc_details = new_exc_details
return suppressed_exc
220 changes: 217 additions & 3 deletions openpype/hosts/maya/api/lib.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Standalone helper functions"""

import os
import copy
from pprint import pformat
import sys
import uuid
Expand All @@ -9,6 +10,8 @@
import json
import logging
import contextlib
import capture
from .exitstack import ExitStack
from collections import OrderedDict, defaultdict
from math import ceil
from six import string_types
Expand Down Expand Up @@ -172,6 +175,219 @@ def maintained_selection():
cmds.select(clear=True)


def reload_all_udim_tile_previews():
"""Regenerate all UDIM tile preview in texture file"""
for texture_file in cmds.ls(type="file"):
if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0:
cmds.ogs(regenerateUVTilePreview=texture_file)


@contextlib.contextmanager
def panel_camera(panel, camera):
"""Set modelPanel's camera during the context.

Arguments:
panel (str): modelPanel name.
camera (str): camera name.

"""
original_camera = cmds.modelPanel(panel, query=True, camera=True)
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
try:
cmds.modelPanel(panel, edit=True, camera=camera)
yield
finally:
cmds.modelPanel(panel, edit=True, camera=original_camera)


def render_capture_preset(preset):
"""Capture playblast with a preset.

To generate the preset use `generate_capture_preset`.

Args:
preset (dict): preset options

Returns:
str: Output path of `capture.capture`
"""

# Force a refresh at the start of the timeline
# TODO (Question): Why do we need to do this? What bug does it solve?
# Is this for simulations?
cmds.refresh(force=True)
refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True))
cmds.currentTime(refresh_frame_int - 1, edit=True)
cmds.currentTime(refresh_frame_int, edit=True)

if os.environ.get("OPENPYPE_DEBUG") == "1":
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
log.debug(
"Using preset: {}".format(
json.dumps(preset, indent=4, sort_keys=True)
)
)
preset = copy.deepcopy(preset)
# not supported by `capture` so we pop it off of the preset
reload_textures = preset["viewport_options"].get("loadTextures")
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
panel = preset.pop("panel")
with ExitStack() as stack:
stack.enter_context(maintained_time())
stack.enter_context(panel_camera(panel, preset["camera"]))
stack.enter_context(viewport_default_options(panel, preset))
if reload_textures:
# Force immediate texture loading when to ensure
# all textures have loaded before the playblast starts
stack.enter_context(material_loading_mode(mode="immediate"))
# Regenerate all UDIM tiles previews
reload_all_udim_tile_previews()
preset["viewport_options"].pop("loadTextures")
path = capture.capture(log=self.log, **preset)

return path


def generate_capture_preset(instance, camera, path,
start=None, end=None, capture_preset=None):
"""Function for getting all the data of preset options for
playblast capturing

Args:
instance (pyblish.api.Instance): instance
camera (str): review camera
path (str): filepath
start (int): frameStart
end (int): frameEnd
capture_preset (dict): capture preset

Returns:
dict: Resulting preset
"""
preset = load_capture_preset(data=capture_preset)

preset["camera"] = camera
preset["start_frame"] = start
preset["end_frame"] = end
preset["filename"] = path
preset["overwrite"] = True
preset["panel"] = instance.data["panel"]

# Disable viewer since we use the rendering logic for publishing
# We don't want to open the generated playblast in a viewer directly.
preset["viewer"] = False

# "isolate_view" will already have been applied at creation, so we'll
# ignore it here.
preset.pop("isolate_view")

# Set resolution variables from capture presets
width_preset = capture_preset["Resolution"]["width"]
height_preset = capture_preset["Resolution"]["height"]

# Set resolution variables from asset values
asset_data = instance.data["assetEntity"]["data"]
asset_width = asset_data.get("resolutionWidth")
asset_height = asset_data.get("resolutionHeight")
review_instance_width = instance.data.get("review_width")
review_instance_height = instance.data.get("review_height")

# Use resolution from instance if review width/height is set
# Otherwise use the resolution from preset if it has non-zero values
# Otherwise fall back to asset width x height
# Else define no width, then `capture.capture` will use render resolution
if review_instance_width and review_instance_height:
preset["width"] = review_instance_width
preset["height"] = review_instance_height
elif width_preset and height_preset:
preset["width"] = width_preset
preset["height"] = height_preset
elif asset_width and asset_height:
preset["width"] = asset_width
preset["height"] = asset_height

# Isolate view is requested by having objects in the set besides a
# camera. If there is only 1 member it'll be the camera because we
# validate to have 1 camera only.
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
preset["isolate"] = instance.data["setMembers"]

# Override camera options
# Enforce persisting camera depth of field
camera_options = preset.setdefault("camera_options", {})
camera_options["depthOfField"] = cmds.getAttr(
"{0}.depthOfField".format(camera)
)

# Use Pan/Zoom from instance data instead of from preset
preset.pop("pan_zoom", None)
camera_options["panZoomEnabled"] = instance.data["panZoom"]

# Override viewport options by instance data
viewport_options = preset.setdefault("viewport_options", {})
viewport_options["displayLights"] = instance.data["displayLights"]
viewport_options["imagePlane"] = instance.data.get("imagePlane", True)

# Override transparency if requested.
transparency = instance.data.get("transparency", 0)
if transparency != 0:
preset["viewport2_options"]["transparencyAlgorithm"] = transparency

# Update preset with current panel setting
# if override_viewport_options is turned off
if not capture_preset["Viewport Options"]["override_viewport_options"]:
panel_preset = capture.parse_view(preset["panel"])
panel_preset.pop("camera")
preset.update(panel_preset)

moonyuet marked this conversation as resolved.
Show resolved Hide resolved
return preset


@contextlib.contextmanager
def viewport_default_options(panel, preset):
"""Context manager used by `render_capture_preset`.

We need to explicitly enable some viewport changes so the viewport is
refreshed ahead of playblasting.

"""
# TODO: Clarify in the docstring WHY we need to set it ahead of
# playblasting. What issues does it solve?
viewport_defaults = {}
try:
keys = [
"useDefaultMaterial",
"wireframeOnShaded",
"xray",
"jointXray",
"backfaceCulling",
"textures"
]
for key in keys:
viewport_defaults[key] = cmds.modelEditor(
panel, query=True, **{key: True}
)
if preset["viewport_options"][key]:
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
cmds.modelEditor(
panel, edit=True, **{key: True}
)
yield
finally:
# Restoring viewport options.
if viewport_defaults:
cmds.modelEditor(
panel, edit=True, **viewport_defaults
)


@contextlib.contextmanager
def material_loading_mode(mode="immediate"):
"""Set material loading mode during context"""
original = cmds.displayPref(query=True, materialLoadingMode=True)
cmds.displayPref(materialLoadingMode=mode)
try:
yield
finally:
cmds.displayPref(materialLoadingMode=original)


def get_namespace(node):
"""Return namespace of given node"""
node_name = node.rsplit("|", 1)[-1]
Expand Down Expand Up @@ -2677,7 +2893,7 @@ def _get_attrs(node):
return world_space_nodes


def load_capture_preset(data=None):
def load_capture_preset(data):
"""Convert OpenPype Extract Playblast settings to `capture` arguments

Input data is the settings from:
Expand All @@ -2691,8 +2907,6 @@ def load_capture_preset(data=None):

"""

import capture

options = dict()
viewport_options = dict()
viewport2_options = dict()
Expand Down