mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge pull request #2533 from BigRoy/fix_maya_vdb_loader_for_vray
Maya: Fix Load VDB to V-Ray
This commit is contained in:
commit
64b985b158
1 changed files with 223 additions and 17 deletions
|
|
@ -2,6 +2,72 @@ from avalon import api
|
||||||
from openpype.api import get_project_settings
|
from openpype.api import get_project_settings
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from maya import cmds
|
||||||
|
|
||||||
|
# List of 3rd Party Channels Mapping names for VRayVolumeGrid
|
||||||
|
# See: https://docs.chaosgroup.com/display/VRAY4MAYA/Input
|
||||||
|
# #Input-3rdPartyChannelsMapping
|
||||||
|
THIRD_PARTY_CHANNELS = {
|
||||||
|
2: "Smoke",
|
||||||
|
1: "Temperature",
|
||||||
|
10: "Fuel",
|
||||||
|
4: "Velocity.x",
|
||||||
|
5: "Velocity.y",
|
||||||
|
6: "Velocity.z",
|
||||||
|
7: "Red",
|
||||||
|
8: "Green",
|
||||||
|
9: "Blue",
|
||||||
|
14: "Wavelet Energy",
|
||||||
|
19: "Wavelet.u",
|
||||||
|
20: "Wavelet.v",
|
||||||
|
21: "Wavelet.w",
|
||||||
|
# These are not in UI or documentation but V-Ray does seem to set these.
|
||||||
|
15: "AdvectionOrigin.x",
|
||||||
|
16: "AdvectionOrigin.y",
|
||||||
|
17: "AdvectionOrigin.z",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fix_duplicate_vvg_callbacks():
|
||||||
|
"""Workaround to kill duplicate VRayVolumeGrids attribute callbacks.
|
||||||
|
|
||||||
|
This fixes a huge lag in Maya on switching 3rd Party Channels Mappings
|
||||||
|
or to different .vdb file paths because it spams an attribute changed
|
||||||
|
callback: `vvgUserChannelMappingsUpdateUI`.
|
||||||
|
|
||||||
|
ChaosGroup bug ticket: 154-008-9890
|
||||||
|
|
||||||
|
Found with:
|
||||||
|
- Maya 2019.2 on Windows 10
|
||||||
|
- V-Ray: V-Ray Next for Maya, update 1 version 4.12.01.00001
|
||||||
|
|
||||||
|
Bug still present in:
|
||||||
|
- Maya 2022.1 on Windows 10
|
||||||
|
- V-Ray 5 for Maya, Update 2.1 (v5.20.01 from Dec 16 2021)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# todo(roy): Remove when new V-Ray release fixes duplicate calls
|
||||||
|
|
||||||
|
jobs = cmds.scriptJob(listJobs=True)
|
||||||
|
|
||||||
|
matched = set()
|
||||||
|
for entry in jobs:
|
||||||
|
# Remove the number
|
||||||
|
index, callback = entry.split(":", 1)
|
||||||
|
callback = callback.strip()
|
||||||
|
|
||||||
|
# Detect whether it is a `vvgUserChannelMappingsUpdateUI`
|
||||||
|
# attribute change callback
|
||||||
|
if callback.startswith('"-runOnce" 1 "-attributeChange" "'):
|
||||||
|
if '"vvgUserChannelMappingsUpdateUI(' in callback:
|
||||||
|
if callback in matched:
|
||||||
|
# If we've seen this callback before then
|
||||||
|
# delete the duplicate callback
|
||||||
|
cmds.scriptJob(kill=int(index))
|
||||||
|
else:
|
||||||
|
matched.add(callback)
|
||||||
|
|
||||||
|
|
||||||
class LoadVDBtoVRay(api.Loader):
|
class LoadVDBtoVRay(api.Loader):
|
||||||
|
|
||||||
|
|
@ -14,15 +80,24 @@ class LoadVDBtoVRay(api.Loader):
|
||||||
|
|
||||||
def load(self, context, name, namespace, data):
|
def load(self, context, name, namespace, data):
|
||||||
|
|
||||||
from maya import cmds
|
|
||||||
import avalon.maya.lib as lib
|
import avalon.maya.lib as lib
|
||||||
from avalon.maya.pipeline import containerise
|
from avalon.maya.pipeline import containerise
|
||||||
|
|
||||||
|
assert os.path.exists(self.fname), (
|
||||||
|
"Path does not exist: %s" % self.fname
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
family = context["representation"]["context"]["family"]
|
family = context["representation"]["context"]["family"]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
family = "vdbcache"
|
family = "vdbcache"
|
||||||
|
|
||||||
|
# Ensure V-ray is loaded with the vrayvolumegrid
|
||||||
|
if not cmds.pluginInfo("vrayformaya", query=True, loaded=True):
|
||||||
|
cmds.loadPlugin("vrayformaya")
|
||||||
|
if not cmds.pluginInfo("vrayvolumegrid", query=True, loaded=True):
|
||||||
|
cmds.loadPlugin("vrayvolumegrid")
|
||||||
|
|
||||||
# Check if viewport drawing engine is Open GL Core (compat)
|
# Check if viewport drawing engine is Open GL Core (compat)
|
||||||
render_engine = None
|
render_engine = None
|
||||||
compatible = "OpenGLCoreProfileCompat"
|
compatible = "OpenGLCoreProfileCompat"
|
||||||
|
|
@ -30,13 +105,11 @@ class LoadVDBtoVRay(api.Loader):
|
||||||
render_engine = cmds.optionVar(query="vp2RenderingEngine")
|
render_engine = cmds.optionVar(query="vp2RenderingEngine")
|
||||||
|
|
||||||
if not render_engine or render_engine != compatible:
|
if not render_engine or render_engine != compatible:
|
||||||
raise RuntimeError("Current scene's settings are incompatible."
|
self.log.warning("Current scene's settings are incompatible."
|
||||||
"See Preferences > Display > Viewport 2.0 to "
|
"See Preferences > Display > Viewport 2.0 to "
|
||||||
"set the render engine to '%s'" % compatible)
|
"set the render engine to '%s'" % compatible)
|
||||||
|
|
||||||
asset = context['asset']
|
asset = context['asset']
|
||||||
version = context["version"]
|
|
||||||
|
|
||||||
asset_name = asset["name"]
|
asset_name = asset["name"]
|
||||||
namespace = namespace or lib.unique_namespace(
|
namespace = namespace or lib.unique_namespace(
|
||||||
asset_name + "_",
|
asset_name + "_",
|
||||||
|
|
@ -45,7 +118,7 @@ class LoadVDBtoVRay(api.Loader):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Root group
|
# Root group
|
||||||
label = "{}:{}".format(namespace, name)
|
label = "{}:{}_VDB".format(namespace, name)
|
||||||
root = cmds.group(name=label, empty=True)
|
root = cmds.group(name=label, empty=True)
|
||||||
|
|
||||||
settings = get_project_settings(os.environ['AVALON_PROJECT'])
|
settings = get_project_settings(os.environ['AVALON_PROJECT'])
|
||||||
|
|
@ -55,20 +128,24 @@ class LoadVDBtoVRay(api.Loader):
|
||||||
if c is not None:
|
if c is not None:
|
||||||
cmds.setAttr(root + ".useOutlinerColor", 1)
|
cmds.setAttr(root + ".useOutlinerColor", 1)
|
||||||
cmds.setAttr(root + ".outlinerColor",
|
cmds.setAttr(root + ".outlinerColor",
|
||||||
(float(c[0])/255),
|
float(c[0]) / 255,
|
||||||
(float(c[1])/255),
|
float(c[1]) / 255,
|
||||||
(float(c[2])/255)
|
float(c[2]) / 255)
|
||||||
)
|
|
||||||
|
|
||||||
# Create VR
|
# Create VRayVolumeGrid
|
||||||
grid_node = cmds.createNode("VRayVolumeGrid",
|
grid_node = cmds.createNode("VRayVolumeGrid",
|
||||||
name="{}VVGShape".format(label),
|
name="{}Shape".format(label),
|
||||||
parent=root)
|
parent=root)
|
||||||
|
|
||||||
# Set attributes
|
# Ensure .currentTime is connected to time1.outTime
|
||||||
cmds.setAttr("{}.inFile".format(grid_node), self.fname, type="string")
|
cmds.connectAttr("time1.outTime", grid_node + ".currentTime")
|
||||||
cmds.setAttr("{}.inReadOffset".format(grid_node),
|
|
||||||
version["startFrames"])
|
# Set path
|
||||||
|
self._set_path(grid_node, self.fname, show_preset_popup=True)
|
||||||
|
|
||||||
|
# Lock the shape node so the user can't delete the transform/shape
|
||||||
|
# as if it was referenced
|
||||||
|
cmds.lockNode(grid_node, lock=True)
|
||||||
|
|
||||||
nodes = [root, grid_node]
|
nodes = [root, grid_node]
|
||||||
self[:] = nodes
|
self[:] = nodes
|
||||||
|
|
@ -79,3 +156,132 @@ class LoadVDBtoVRay(api.Loader):
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
context=context,
|
context=context,
|
||||||
loader=self.__class__.__name__)
|
loader=self.__class__.__name__)
|
||||||
|
|
||||||
|
def _set_path(self, grid_node, path, show_preset_popup=True):
|
||||||
|
|
||||||
|
from openpype.hosts.maya.api.lib import attribute_values
|
||||||
|
from maya import cmds
|
||||||
|
|
||||||
|
def _get_filename_from_folder(path):
|
||||||
|
# Using the sequence of .vdb files we check the frame range, etc.
|
||||||
|
# to set the filename with #### padding.
|
||||||
|
files = sorted(x for x in os.listdir(path) if x.endswith(".vdb"))
|
||||||
|
if not files:
|
||||||
|
raise RuntimeError("Couldn't find .vdb files in: %s" % path)
|
||||||
|
|
||||||
|
if len(files) == 1:
|
||||||
|
# Ensure check for single file is also done in folder
|
||||||
|
fname = files[0]
|
||||||
|
else:
|
||||||
|
# Sequence
|
||||||
|
from avalon.vendor import clique
|
||||||
|
# todo: check support for negative frames as input
|
||||||
|
collections, remainder = clique.assemble(files)
|
||||||
|
assert len(collections) == 1, (
|
||||||
|
"Must find a single image sequence, "
|
||||||
|
"found: %s" % (collections,)
|
||||||
|
)
|
||||||
|
collection = collections[0]
|
||||||
|
|
||||||
|
fname = collection.format('{head}{{padding}}{tail}')
|
||||||
|
padding = collection.padding
|
||||||
|
if padding == 0:
|
||||||
|
# Clique doesn't provide padding if the frame number never
|
||||||
|
# starts with a zero and thus has never any visual padding.
|
||||||
|
# So we fall back to the smallest frame number as padding.
|
||||||
|
padding = min(len(str(i)) for i in collection.indexes)
|
||||||
|
|
||||||
|
# Supply frame/padding with # signs
|
||||||
|
padding_str = "#" * padding
|
||||||
|
fname = fname.format(padding=padding_str)
|
||||||
|
|
||||||
|
return os.path.join(path, fname)
|
||||||
|
|
||||||
|
# The path is either a single file or sequence in a folder so
|
||||||
|
# we do a quick lookup for our files
|
||||||
|
if os.path.isfile(path):
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
path = _get_filename_from_folder(path)
|
||||||
|
|
||||||
|
# Even when not applying a preset V-Ray will reset the 3rd Party
|
||||||
|
# Channels Mapping of the VRayVolumeGrid when setting the .inPath
|
||||||
|
# value. As such we try and preserve the values ourselves.
|
||||||
|
# Reported as ChaosGroup bug ticket: 154-011-2909
|
||||||
|
# todo(roy): Remove when new V-Ray release preserves values
|
||||||
|
original_user_mapping = cmds.getAttr(grid_node + ".usrchmap") or ""
|
||||||
|
|
||||||
|
# Workaround for V-Ray bug: fix lag on path change, see function
|
||||||
|
_fix_duplicate_vvg_callbacks()
|
||||||
|
|
||||||
|
# Suppress preset pop-up if we want.
|
||||||
|
popup_attr = "{0}.inDontOfferPresets".format(grid_node)
|
||||||
|
popup = {popup_attr: not show_preset_popup}
|
||||||
|
with attribute_values(popup):
|
||||||
|
cmds.setAttr(grid_node + ".inPath", path, type="string")
|
||||||
|
|
||||||
|
# Reapply the 3rd Party channels user mapping when no preset popup
|
||||||
|
# was shown to the user
|
||||||
|
if not show_preset_popup:
|
||||||
|
channels = cmds.getAttr(grid_node + ".usrchmapallch").split(";")
|
||||||
|
channels = set(channels) # optimize lookup
|
||||||
|
restored_mapping = ""
|
||||||
|
for entry in original_user_mapping.split(";"):
|
||||||
|
if not entry:
|
||||||
|
# Ignore empty entries
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If 3rd Party Channels selection channel still exists then
|
||||||
|
# add it again.
|
||||||
|
index, channel = entry.split(",")
|
||||||
|
attr = THIRD_PARTY_CHANNELS.get(int(index),
|
||||||
|
# Fallback for when a mapping
|
||||||
|
# was set that is not in the
|
||||||
|
# documentation
|
||||||
|
"???")
|
||||||
|
if channel in channels:
|
||||||
|
restored_mapping += entry + ";"
|
||||||
|
else:
|
||||||
|
self.log.warning("Can't preserve '%s' mapping due to "
|
||||||
|
"missing channel '%s' on node: "
|
||||||
|
"%s" % (attr, channel, grid_node))
|
||||||
|
|
||||||
|
if restored_mapping:
|
||||||
|
cmds.setAttr(grid_node + ".usrchmap",
|
||||||
|
restored_mapping,
|
||||||
|
type="string")
|
||||||
|
|
||||||
|
def update(self, container, representation):
|
||||||
|
|
||||||
|
path = api.get_representation_path(representation)
|
||||||
|
|
||||||
|
# Find VRayVolumeGrid
|
||||||
|
members = cmds.sets(container['objectName'], query=True)
|
||||||
|
grid_nodes = cmds.ls(members, type="VRayVolumeGrid", long=True)
|
||||||
|
assert len(grid_nodes) > 0, "This is a bug"
|
||||||
|
|
||||||
|
# Update the VRayVolumeGrid
|
||||||
|
for grid_node in grid_nodes:
|
||||||
|
self._set_path(grid_node, path=path, show_preset_popup=False)
|
||||||
|
|
||||||
|
# Update container representation
|
||||||
|
cmds.setAttr(container["objectName"] + ".representation",
|
||||||
|
str(representation["_id"]),
|
||||||
|
type="string")
|
||||||
|
|
||||||
|
def switch(self, container, representation):
|
||||||
|
self.update(container, representation)
|
||||||
|
|
||||||
|
def remove(self, container):
|
||||||
|
|
||||||
|
# Get all members of the avalon container, ensure they are unlocked
|
||||||
|
# and delete everything
|
||||||
|
members = cmds.sets(container['objectName'], query=True)
|
||||||
|
cmds.lockNode(members, lock=False)
|
||||||
|
cmds.delete([container['objectName']] + members)
|
||||||
|
|
||||||
|
# Clean up the namespace
|
||||||
|
try:
|
||||||
|
cmds.namespace(removeNamespace=container['namespace'],
|
||||||
|
deleteNamespaceContent=True)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue