Skip to content

Commit

Permalink
Fix Load VDB to V-Ray for Maya
Browse files Browse the repository at this point in the history
  • Loading branch information
BigRoy committed Jan 14, 2022
1 parent 1e23bc0 commit d53db6c
Showing 1 changed file with 223 additions and 16 deletions.
239 changes: 223 additions & 16 deletions openpype/hosts/maya/plugins/load/load_vdb_to_vray.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,72 @@
from openpype.api import get_project_settings
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):

Expand All @@ -14,29 +80,36 @@ class LoadVDBtoVRay(api.Loader):

def load(self, context, name, namespace, data):

from maya import cmds
import avalon.maya.lib as lib
from avalon.maya.pipeline import containerise

assert os.path.exists(self.fname), (
"Path does not exist: %s" % self.fname
)

try:
family = context["representation"]["context"]["family"]
except ValueError:
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)
render_engine = None
compatible = "OpenGLCoreProfileCompat"
if cmds.optionVar(exists="vp2RenderingEngine"):
render_engine = cmds.optionVar(query="vp2RenderingEngine")

if not render_engine or render_engine != compatible:
raise RuntimeError("Current scene's settings are incompatible."
"See Preferences > Display > Viewport 2.0 to "
"set the render engine to '%s'" % compatible)
self.log.warning("Current scene's settings are incompatible."
"See Preferences > Display > Viewport 2.0 to "
"set the render engine to '%s'" % compatible)

asset = context['asset']
version = context["version"]

asset_name = asset["name"]
namespace = namespace or lib.unique_namespace(
asset_name + "_",
Expand All @@ -45,7 +118,7 @@ def load(self, context, name, namespace, data):
)

# Root group
label = "{}:{}".format(namespace, name)
label = "{}:{}_VDB".format(namespace, name)
root = cmds.group(name=label, empty=True)

settings = get_project_settings(os.environ['AVALON_PROJECT'])
Expand All @@ -55,20 +128,25 @@ def load(self, context, name, namespace, data):
if c is not None:
cmds.setAttr(root + ".useOutlinerColor", 1)
cmds.setAttr(root + ".outlinerColor",
(float(c[0])/255),
(float(c[1])/255),
(float(c[2])/255)
float(c[0])/255,
float(c[1])/255,
float(c[2])/255
)

# Create VR
# Create VRayVolumeGrid
grid_node = cmds.createNode("VRayVolumeGrid",
name="{}VVGShape".format(label),
name="{}Shape".format(label),
parent=root)

# Set attributes
cmds.setAttr("{}.inFile".format(grid_node), self.fname, type="string")
cmds.setAttr("{}.inReadOffset".format(grid_node),
version["startFrames"])
# Ensure .currentTime is connected to time1.outTime
cmds.connectAttr("time1.outTime", grid_node + ".currentTime")

# 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]
self[:] = nodes
Expand All @@ -79,3 +157,132 @@ def load(self, context, name, namespace, data):
nodes=nodes,
context=context,
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

0 comments on commit d53db6c

Please sign in to comment.