abstractfactory / maya-capture Goto Github PK
View Code? Open in Web Editor NEWPlayblasting in Maya done right
License: MIT License
Playblasting in Maya done right
License: MIT License
To enlighten fellow users of the vast potentials.
The README currently has a few tidbits of examples. Let's expand this with every feature!
Should the default be (when no start and and frame provided as arguments) and a "selection" in the time slider is active that capture()
would take that time range instead of the start and end frame of the time slider bar.
This would make the behavior somewhat closer to Maya's default playblast where you could highlight a small area in your timeline and that playblast only that.
As such behavior would become:
start_frame
and end_frame
provided as arguments use thatI would like to propose another design for our option classes like ViewportOptions
and CameraOptions
.
Streamline how options are passed onto the capture
and snap
functions and how these Option classes are designed.
Currently each Options class needs its own context manager like _applied_camera_options()
and _applied_viewport_options
alongside its options class like CameraOptions
and ViewportOptions
.
We create a base class for Options
that sets some rules on how options are applied and accompany this with our own context manager.
class Options(object):
def set(self, panel, camera):
"""Applies the options for the panel and camera."""
pass
@staticmethod
def current(panel, camera):
"""Return the options for the currently defined user-settings"""
pass
@contextlib.contextmanager
def _options(options, panel, camera):
"""Context-manager to temporarily assign the options and afterwards restore the original"""
original = options.current(panel, camera)
options.set(panel, camera)
yield
original.set(panel, camera)
For example the current viewport options context manager could be removed and the ViewportOptions
class could become:
class ViewportOptions(Options):
"""Viewport options for :func:`capture`"""
useDefaultMaterial = False
wireframeOnShaded = False
displayAppearance = 'smoothShaded'
# Visibility flags
nurbsCurves = False
nurbsSurfaces = False
polymeshes = True
subdivSurfaces = False
cameras = False
lights = False
grid = False
joints = False
ikHandles = False
deformers = False
dynamics = False
fluids = False
hairSystems = False
follicles = False
nCloths = False
nParticles = False
nRigids = False
dynamicConstraints = False
locators = False
manipulators = False
dimensions = False
handles = False
pivots = False
textures = False
strokes = False
def set(panel, camera):
"""Applies the options for the panel and camera."""
options = _parse_options(self)
cmds.modelEditor(panel,
edit=True,
allObjects=False,
grid=False,
manipulators=False)
cmds.modelEditor(panel, edit=True, **options)
@staticmethod
def current(panel, camera):
"""Return the options instance for the currently defined user-settings"""
viewport_options = ViewportOptions()
options = _parse_options(viewport_options)
for key in options:
kwargs = {key: True}
value = cmds.modelEditor(panel, query=True, **kwargs)
setattr(viewport_options, key, value)
return viewport_options
Pros
This way we have:
Options
classesOptions.set(panel, camera)
Cons
The options classes will all have access to panel and camera even though they might not need to.
The CameraOptions is missing some attributes that influence the way an output looks.
For example those that draw a guide/overlay over the viewport:
displayFieldChart
displaySafeAction
displaySafeTitle
displayFilmPivot
displayFilmOrigin
I assume we want to include those settings and have them turned off by default.
What's the preferred expected behavior?
When passing start_frame=0
or end_frame=0
capture won't actually interpret it as those values because of the following lines:
start_frame = start_frame or cmds.playbackOptions(minTime=True, query=True)
end_frame = end_frame or cmds.playbackOptions(maxTime=True, query=True)
As such the value that is zero will instead become respectively the start of the playback range for start_frame or the end of the playback range for end_frame.
EDIT:
The same problem occurs with the frame
parameter.
Change it to:
start_frame = start_frame if start_frame is not None else cmds.playbackOptions(minTime=True, query=True)
end_frame = end_frame if end_frame is not None else cmds.playbackOptions(maxTime=True, query=True)
Or slightly shorter:
start_frame = cmds.playbackOptions(minTime=True, query=True) if start_frame is None else start_frame
end_frame = cmds.playbackOptions(maxTime=True, query=True) if end_frame is None else end_frame
Or:
if start_frame is None:
start_frame = cmds.playbackOptions(minTime=True, query=True)
if end_frame is None:
end_frame = cmds.playbackOptions(maxTime=True, query=True)
I've been doing some captures/snaps and noticed the size wasn't the exact render resolution but slightly smaller.
For example:
It seems to have mismatch of being 4 pixels too small in width and 6 too small in height.
Also did a quick capture of 4096x4096 which ended up being 4092x1441. I thought this height was being cropped because it reaches the limit of the H264 encoding, but the same happened with snap()
(image, png). This made me think it happens because the panel reaches the size of my screen in height.
Fetch frame range and rate, and other scene-related properties used to capture.
Continuing from discussion in #1.
Support saving and loading of presets.
Implement a function to read settings from the currently active view in such a way that the output can be directly fed into the capture()
function.
Example
import json
from maya import cmds
from capture import capture, ViewportOptions, CameraOptions
def parse_active_view():
"""Parse active view for settings"""
panel = cmds.getPanel(withFocus=True)
assert "model" in panel, "No active viewport"
camera = cmds.modelPanel(panel, query=True, camera=True)
camera_shape = cmds.listRelatives(camera, shapes=True)[0]
return {
"camera": camera,
"width": cmds.getAttr("defaultResolution.width"),
"height": cmds.getAttr("defaultResolution.height"),
"camera_options": type("CameraOptions", (object, CameraOptions,), {
"displayFilmGate": cmds.getAttr(camera_shape + ".displayFilmGate"),
"displayResolution": cmds.getAttr(camera_shape + ".displayResolution"),
"displaySafeAction": cmds.getAttr(camera_shape + ".displaySafeAction"),
}),
"viewport_options": type("ViewportOptions", (object, ViewportOptions,), {
"useDefaultMaterial": cmds.modelEditor(panel, query=True, useDefaultMaterial=True),
"wireframeOnShaded": cmds.modelEditor(panel, query=True, wireframeOnShaded=True),
"displayAppearance": cmds.modelEditor(panel, query=True, displayAppearance=True),
})
}
# Create preset
preset = parse_active_view()
# Use preset
capture(**preset)
The default value of h264
of compression does not result (on Windows) in a video stream with H.264 compression. Instead the compression is JPEG
.
For the playblast command the h264
compression is unknown because the compression string should actually be H.264
on Windows. So it seems to fallback on its default compression (since the same happens if you pass it a random compression string value, e.g. aaaaaaa
). I'm unable to test this on Linux/Mac to see if it's also the case for those platforms.
Tested with Maya 2016 Ext 1 SP6 on Windows 7.
Note that H.264 compression suffers from bugs on hardware with more than 16 cores (also described here) resulting in errors randomly stopping midway or errors about disk size full (even when plenty of disk space available). This is the reason we actually spotted it was not compressing to H.264.
Getting this, there's an attempt to use delattr
on a dict.
Traceback (most recent call last):
File "<maya console>", line 2, in <module>
File "/net/homes/mottosso/pythonpath/capture.py", line 137, in capture
_maintained_time()):
File "/opt/autodesk/maya-2015.sp5/lib/python27.zip/contextlib.py", line 17, in __enter__
return self.gen.next()
File "/opt/autodesk/maya-2015.sp5/lib/python27.zip/contextlib.py", line 112, in nested
vars.append(enter())
File "/opt/autodesk/maya-2015.sp5/lib/python27.zip/contextlib.py", line 17, in __enter__
return self.gen.next()
File "/net/homes/mottosso/pythonpath/capture.py", line 566, in _applied_viewport2_options
delattr(options, opt)
AttributeError: 'dict' object has no attribute 'hwFogAlpha' #
# Error: AttributeError: 'dict' object has no attribute 'hwFogAlpha' #
Enable personal, development version of the repository.
Previously, this was hosted in my personal account, making it impossible to keep a personal copy around. In the abstractfactory account, I can maintain a fork, similar to what contributors do.
preset = capture.parse_active_view()
# Error: modelEditor: Object 'scriptEditorPanel1' not found.
# Traceback (most recent call last):
# File "<maya console>", line 3, in <module>
# File "c:\pythonpath\capture.py", line 367, in parse_active_view
# camera = cmds.modelEditor(panel, q=1, camera=1)
# RuntimeError: modelEditor: Object 'scriptEditorPanel1' not found. #
We'll need to throw our own exception, and let the user know that he has to have a modelPanel active.
The offscreen option shouldn't show any window, like cmds.playblast() does.
The camera's overscan is included in the screen capture. This means the output is not the same as it would be when you would perform a render of the shot.
Setting the camera's overscan to 1.0 (could be done through CameraOptions) would fix this.
Also by default CameraOptions
are not used but skipped. To allow the overscan to default to 1.0 this should be used by default.
Provide a flag for using the current scene name for output
import capture
capture.capture(filename=capture.SCENE)
Including simply using it, but also specifying flags such as anti-alias, motion-blur etc.
Why is that? The view is already assigned a camera, why not use and return this?
# Before
parse_view("modelPanel1", "persp")
# {"display_options": {}}
# After
parse_view("modelPanel1")
# {"camera": "persp", "display_options": {}}
The new additions for options in #1 that got merged with #31 introduced a possible viewport message in Maya 2016 (when in-view messages are set as enabled). If disabled the in-view message is not shown.
Note that it's not a show-stopping issue. It's just somewhat odd behavior to see occur, as such it's good to discuss solutions.
This behavior comes from the fact that capture
now also applies default settings to the depth of field (disabling it) whereas before it ignored (or basically didn't know about) those settings.
Enabling/disabling this behavior is a matter of setting some optionVar
settings:
optionVar -iv inViewMessageEnable false;control -edit -enable false prefsInViewMessageAssistEnable;control -edit -enable false prefsInViewMessageStatusEnable;control -edit -enable false prefsInViewMessageDisplayTime;control -edit -enable false prefsInViewMessageFontSize;control -edit -enable false prefsInViewMessageOpacity;
Above is the single line of MEL code Maya runs to disable the in-view messages.
It would be a matter of adding another context manager that ensure the setting is temporarily disabled so these pop-ups do not occur.
Clarify what is actually being parsed and applied. The current use of "view" is synonymous to "panel"; specifically, "modelPanel".
# Before
capture.parse_active_view()
capture.parse_view("modelPanel1")
# After
capture.parse_active_panel()
capture.parse_panel("modelPanel1")
The current method of maintaining the resolution during playblasting is somewhat inaccurate and sometimes results in a slightly (1 pixel) to small a frame.
For example consider a 1920x1080 resolution. This results in a device aspect ratio of 1.7777777777777777777777777777778
which is prone to precision errors. For example in Maya this results in: 1.778
.
When converting the resolution and it tries to maintain the current aspect ratio capture
currently does this:
ratio = cmds.getAttr("defaultResolution.deviceAspectRatio")
height = width / ratio
The height here would result in 1920 / 1.778
which equals 1079.865
(approximately). When passing that value to the maya.cmds.playblast
command instead of rounding to the nearest value it instead floors it to an integer. Resulting in 1079
where we originally wanted 1080
.
Easy fix!
ratio = cmds.getAttr("defaultResolution.deviceAspectRatio")
height = round(width / ratio)
The maya.cmds.playblast
command has a showOrnaments
flag to disable HUD to be rendered with the playblast. This is a simple toggle without needing to change the HUD or any overlay. Without this one needs to control the HUD to always include the exact same information (or exclude those (or all))
To add a show_ornaments
flag to the capture command and also have it parsed with parse_active_scene()
and applyable with its counterpart apply_scene
The maya.cmds.playblast
command seems to suffer from an undo bug, see here. Upon undoing the command it'll revert back to frame 1 as opposed to going back to the frame it was before playblasting.
A workaround/bugfix is to set the current time before calling the playblast command, like so:
maya.cmds.currentTime(maya.cmds.currentTime(q=1))
Automate tests.
Setup continuous integration on Travis with Docker.
tests.py
tests.py
Example
$ docker run --rm -ti -v $(pwd):/root mottosso/maya:2013sp1 mayapy -c "from maya import standalone, cmds;standalone.initialize();cmds.playblast = lambda *args, **kwargs: None;import nose;nose.run()"
cmds.playblast
is mocked, due to platform not having access to graphical hardware. Because of that, tests cannot actually trigger a playblast. But it doesn't matter, we aren't able to validate outputted images anyway by anything other than by eye.
What could happen, is that we have 90% of the tests run headless on Travis, and a separate, optional test_graphics.py
that runs locally.
We've recently been seeing playblasts come out that were looking a bit "different" on the first frame. It seems when the artist is working in Viewport 2.0 and has Anti-aliasing turned off, then we'd start a capture that enables Anti-aliasing the first frame wouldn't get rendered with this setting enabled in the saved playblast.
To reproduce:
# options to enable viewport 2.0 and AA
options = dict()
options['viewport_options'] = dict()
options['viewport2_options'] = dict()
options['viewport_options']['rendererName'] = 'vp2Renderer'
options['viewport2_options']['multiSampleEnable'] = True
options['viewport2_options']['multiSampleCount'] = 8
capture.capture(**options)
The produced playblast will have no anti-aliasing around the cube on the first frame. The rest is correct. (Tested under Windows 7 and 10 with Maya 2016 ext. 1 SP6)
Another Maya 2016 only feature that breaks in 2015.
preset = capture.parse_active_view()
# Error: No object matches name: hardwareRenderingGlobals.hwFogAlpha
# Traceback (most recent call last):
# File "<maya console>", line 1, in <module>
# File "c:\pythonpath\capture.py", line 378, in parse_active_view
# return parse_view(panel, camera)
# File "c:\pythonpath\capture.py", line 406, in parse_view
# ValueError: No object matches name: hardwareRenderingGlobals.hwFogAlpha #
Enable capturing of wedges.
What is a "wedge"? In Houdini, a wedge is defined as running an operation multiple times, each time varying some parameter(s). It can be useful when for example working with simulations and you are interested in the effects of a series of configurations to determine which works best. - Reference
You are tasked with producing playblasts for a combination of properties of nCloth.
In this example, each feature has two states - on or off - resulting in 3**2=9
wedges.
Taking in mind that it may be important to return back to a particular configuration, given a successful review, a traditional workflow would have required an artist to alter the settings of a scene, save this as a uniquely rememberable version, playblast this, and then do the same for the other 8 variations.
With capture.wedge
the same is both more manageable and with better performance, given that each capture can occur simultaneously (see below about multi-processing).
Each wedge is based on an animation layer. Animation layers are capable of storing independent configurations that may be either blended or used in isolation.
Setup an animation layer for each configuration
Run capture.wedge
import capture
capture.wedge(["layer1", "littleSmoke", "moreSmoke", "fastMotion;moreSmoke"])
An animation layer is used per wedge and each wedge is set off as a background process, using a copy of the scene at its current state, including camera and scene settings (using capture.parse_view
).
multiprocessing
An option is provided for running Maya sessions as a background process simultaneously, in addition to the default behaviour of running each capture successively one after the other.
import capture
capture.wedge(["compressible", "nonCompressible"], multiprocess=True)
The benefit of the former is simultaneous capturing, resulting in an n-times shorter capturing duration, assuming capturing consumes a single processor core each. For example, 8 wedges involving nCloth runs 8 times as fast as running 8 captures within the same Maya session.
on_finished
With multi-processing, it's impossible to retrieve the resulting files created during capturing as the process is asynchronous. Therefore, there is an option to pass a callback for when it finishes, which is then passed a list of each newly created capture.
import capture
import subprocess
def run_in_rv(files):
subprocess.Popen(["rv"] + files)
capture.wedge(["compressible", "nonCompressible"],
multiprocess=True,
on_finished=run_in_rv)
The above results in the finished captures automatically opening in RV when finished.
combinations
Animation layers provide a native ability to blend between each other, enabling a combination off effects.
Consider the following three layers.
Layers can then be activated two-and-two.
# Compressible and heavy
stiff: 0
compressible: 1
heavy: 1
# Stiff and heavy
stiff: 1
compressible: 0
heavy: 1
# Stiff and compressible
stiff: 1
compressible: 1
heavy: 0
...
import capture
import subprocess
def run_in_rv(files):
subprocess.Popen(["rv"] + files)
capture.wedge(["stiff;compressible", "stiff;heavy", "compressible;heavy"],
multiprocess=True,
on_finished=run_in_rv)
When performing a capture the current time is changed to wherever the capture ends. It's better to make it capture without changing the state of the scene.
In short, it should preserve the current time.
Whenever a snap()
or capture()
is triggered the independent panel that is created remains in the Maya session and isn't deleted. This usually doesn't trigger any problems since the panels get disabled, but it does show in the UI as panels with no names.
They show in the viewport under panels > panel > ...
It's a minor inconvenience but would be great to get solved.
To make this cleaner I think it's best to create the panel with a name so it's identifiable, something like labeling it capturePanel
. So if it's not deleted for some reason we at least see it show up with that name.
This could also be the problem responsible for the 5 mb RAM cost per capture as noted in the code:
try:
yield panel
finally:
# Ensure window always closes
# .. note:: We hide, rather than delete as deleting
# causes the focus to shift during capture of multiple
# cameras immediately after one another. Altering the
# visibility doesn't seem to have this effect, it does
# however come at a cost to RAM of about 5 mb per capture.
cmds.window(window, edit=True, visible=False)
Looking at this note in the code I'm wondering if it's an issue to delete the panel in between playblasts?
Enable capturing of camera where some parameters are locked.
At the moment, a camera may have connected or otherwise locked channels in which case capture.py fails.
Either do not change these settings and move on, or create a duplicate of the camera with identical settings that we can modify.
With the implementation of snap()
for still images (#7) coming up through #11 it would be great to look into how we can make a quick screengrab of the viewport with snap()
into your system's clipboard.
Simple way of snapping a single image onto your clipboard so it's ready for pasting into other software or explorer.
import capture
capture.snap(clipboard=True)
And then CTRL + V in Photoshop!
Provide a menu-option for capturing the currently selected camera(s), captured using the current range and resolution, using the scene-name for output (if scene is saved).
Upon capture the viewport "show" gets overridden to hide gpuCache and there currently seems to be no way to force enable it.
Make it possible to playblast with gpuCache shapes and allow arguments to enable/disable appropriately.
This is the code to set the gpuCache visibilities. Since it's coming from a plug-in it requires a different method to setting the objects than just a single flag.
import maya.cmds as cmds
panel = 'modelPanel1'
# disable
cmds.modelEditor(panel, e=True, pluginObjects=("gpuCacheDisplayFilter", False))
# enable
cmds.modelEditor(panel, e=True, pluginObjects=("gpuCacheDisplayFilter", True))
In addition to "capture()", also provide "snap()" for single-frame captures, defaulted to compression "png".
Enable use of features in later versions of Maya, with graceful fallback to old versions.
Upon module load, look up current version, and add features starting from Maya 2013 (the lowest supported version) and upwards.
Getting an error on an unavailable option for depthOfFieldPreview
, removing this from the source dict solves the issue.
So that it's useful from batch/mayapy too.
An artist came up to me asking about the positioning of the capture panel on screen since he found it annoying that it would pop up over the timeline. Specifically because that way he was unable to "know" how far the capture is along the timeline.
It should be trivial for an artist to know the progress of the current capture with some sort of visual indicator.
I was thinking of maybe adding a small embedded timeline at the bottom of the capture window so that the progress (the current frame tick) has a visual indicator within the popup panel. Ideas/arguments are welcome!
I'm getting an error when running capture's default settings.
import capture
capture.capture()
This results in an error like:
# Error: Unable to create a movie file. It may be open by another application.
# Unable to copy the video to its final destination. Check if the path is accessible.
# Traceback (most recent call last):
# File "<maya console>", line 6, in <module>
# File "C:\Work\pipeline\dev\git\maya-capture\capture.py", line 159, in capture
# **playblast_kwargs)
# RuntimeError: Unable to create a movie file. It may be open by another application.
# Unable to copy the video to its final destination. Check if the path is accessible. #
I'm assuming this is a permissiong thing. I've seen this happen before when rendering to a Quicktime format from Maya. Some things that were the problems then:
%TMP%
is correctly set up in your environment variables to a folder to which your user (that started Maya) has access and permissions. (Not sure whether Quicktime used %TEMP%
or %TMP%
)Will have a look if one of the above fixes it on my laptop.
Posting this here so it can also be found by others (even if I happen to fix this quickly).
Windows 10
Maya 2016
This bug is present in cmds.playblast
too.
# On Windows, Maya 2015
fname = capture.capture()
print fname
# blast1.mov
fname = capture.capture(viewer=False)
print fname
# blast1
Allow user to use ffmpeg to convert playblasted image sequence instead of using default maya playblast.
We're currently having a problem with a machine that suddenly started to refuse exporting of any quicktimes from maya using cmds.playblast. Feels like old days when 64 bit maya couldn't export quicktime at all because of incompatible codecs.
Long story short. Adding an option to use ffmpeg to create the final video.
Considering this would be 'advanced' feature, I'd assume that ffmpeg is on the path, so we could simply:
Nothing would really change for the user, apart from having more robust solution in case of troubles with maya playblast.
Enable specifying which renderer to capture with.
capture(renderer="viewport2")
capture(renderer="legacy")
capture(renderer="highQuality")
At the moment, the renderer is specified in display_options={"rendererName": "viewport2"}
, with Viewport 2.0 options specified in viewport2_options={}
. That is a bit unintuitive. How, for example, would one specify options for hardwarerenderer
, or a bespoke renderer?
Make all display options part of one dictionary, and separate rendering option into it's own argument.
Having a quick look over the documentation I noticed some visibility options for the viewport to be missing, specifically the imagePlane
, planes
, controlVertices
and hulls
arguments for the modelEditor
command are lacking in the ViewportOptions
dictionary.
I noticed the absence of some of these because image planes happened to be always visible instead of adhering to the settings when combined with a parse_view()
.
The "missing ones" mentioned here have been available since Maya 2014: http://download.autodesk.com/global/docs/maya2014/en_us/CommandsPython/modelEditor.html
Note: In 2017 for example you also have
pluginShapes
argument which is not around back in 2014. I'm not sure how far we want to be backwards compatible, but adding in newer keys might hurt older versions.
Not having these keys in the ViewportOptions
dictionary also means they won't be parsed correctly with parse_view
or alike.
For now I'd propose to at least have all flags that were in Maya since 2014 (or whatever version we choose as oldest official supported) or have the querying of applying happen in a try-except fashion.
By default a playblast ignores the sound active in the timeline and there's also no way to enable it since the sound
argument is not exposed.
To implement the sound
parameter and maybe by default include the active sound in the timeline.
The capture
command is missing the option to customize the amount of frame padding for resulting image sequences. maya.cmds.playblast
supports it with the framePadding argument.
This is only relevant when playblasting to the
image
codec format.
Add it. ๐ We can support framePadding
by adding a frame_padding
argument to capture
Currently there's no way to parse and/or apply any Isolate View options to capture only a subset of what is in the scene.
Add the options related to the modelEditor
in the viewport options or amongst a new Options dictionary. And also implement the "parsing" of the settings from a view/panel.
Supply another struct for HUD.
hud.display = {
'Current Frame': lambda: cmds.currentTime(query=True)
'Currently selected object(s)': lambda: cmds.ls(selection=True)
}
hud.section = hud.TOPLEFT
Currently uses the current background color, but ideally, all playblast within a particular project should have the same color.
Here's some reference for a potential GUI, from Softimage.
The colors and layout is not to be followed, but rather some of it's properties.
Key properties.
Added properties.
Viewport options doesn't seem to work in 2015+, it might be related to Viewport 2 and that it's the new default now.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.