diff --git a/pype/hosts/maya/api/lib.py b/pype/hosts/maya/api/lib.py index 3a820af814..dc802b6a37 100644 --- a/pype/hosts/maya/api/lib.py +++ b/pype/hosts/maya/api/lib.py @@ -43,17 +43,17 @@ SHAPE_ATTRS = {"castsShadows", "opposite"} RENDER_ATTRS = {"vray": { - "node": "vraySettings", - "prefix": "fileNamePrefix", - "padding": "fileNamePadding", - "ext": "imageFormatStr" - }, - "default": { - "node": "defaultRenderGlobals", - "prefix": "imageFilePrefix", - "padding": "extensionPadding" - } - } + "node": "vraySettings", + "prefix": "fileNamePrefix", + "padding": "fileNamePadding", + "ext": "imageFormatStr" +}, + "default": { + "node": "defaultRenderGlobals", + "prefix": "imageFilePrefix", + "padding": "extensionPadding" +} +} DEFAULT_MATRIX = [1.0, 0.0, 0.0, 0.0, @@ -95,6 +95,8 @@ _alembic_options = { INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000} FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} +RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] + def _get_mel_global(name): """Return the value of a mel global variable""" @@ -114,7 +116,9 @@ def matrix_equals(a, b, tolerance=1e-10): bool : True or False """ - return all(abs(x - y) < tolerance for x, y in zip(a, b)) + if not all(abs(x - y) < tolerance for x, y in zip(a, b)): + return False + return True def float_round(num, places=0, direction=ceil): @@ -2466,12 +2470,21 @@ class shelf(): cmds.shelfLayout(self.name, p="ShelfLayout") -def _get_render_instance(): +def _get_render_instances(): + """Return all 'render-like' instances. + + This returns list of instance sets that needs to receive informations + about render layer changes. + + Returns: + list: list of instances + + """ objectset = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) + instances = [] for objset in objectset: - if not cmds.attributeQuery("id", node=objset, exists=True): continue @@ -2485,16 +2498,18 @@ def _get_render_instance(): if not has_family: continue - if cmds.getAttr("{}.family".format(objset)) == 'rendering': - return objset + if cmds.getAttr( + "{}.family".format(objset)) in RENDERLIKE_INSTANCE_FAMILIES: + instances.append(objset) - return None + return instances renderItemObserverList = [] class RenderSetupListObserver: + """Observer to catch changes in render setup layers.""" def listItemAdded(self, item): print("--- adding ...") @@ -2505,56 +2520,95 @@ class RenderSetupListObserver: self._remove_render_layer(item.name()) def _add_render_layer(self, item): - render_set = _get_render_instance() + render_sets = _get_render_instances() layer_name = item.name() - if not render_set: - return + for render_set in render_sets: + members = cmds.sets(render_set, query=True) or [] - members = cmds.sets(render_set, query=True) or [] - if not "LAYER_{}".format(layer_name) in members: + namespace_name = "_{}".format(render_set) + if not cmds.namespace(exists=namespace_name): + index = 1 + namespace_name = "_{}".format(render_set) + try: + cmds.namespace(rm=namespace_name) + except RuntimeError: + # namespace is not empty, so we leave it untouched + pass + orignal_namespace_name = namespace_name + while(cmds.namespace(exists=namespace_name)): + namespace_name = "{}{}".format( + orignal_namespace_name, index) + index += 1 + + namespace = cmds.namespace(add=namespace_name) + + if members: + # if set already have namespaced members, use the same + # namespace as others. + namespace = members[0].rpartition(":")[0] + else: + namespace = namespace_name + + render_layer_set_name = "{}:{}".format(namespace, layer_name) + if render_layer_set_name in members: + continue print(" - creating set for {}".format(layer_name)) - set = cmds.sets(n="LAYER_{}".format(layer_name), empty=True) - cmds.sets(set, forceElement=render_set) + maya_set = cmds.sets(n=render_layer_set_name, empty=True) + cmds.sets(maya_set, forceElement=render_set) rio = RenderSetupItemObserver(item) print("- adding observer for {}".format(item.name())) item.addItemObserver(rio.itemChanged) renderItemObserverList.append(rio) def _remove_render_layer(self, layer_name): - render_set = _get_render_instance() + render_sets = _get_render_instances() - if not render_set: - return + for render_set in render_sets: + members = cmds.sets(render_set, query=True) + if not members: + continue - members = cmds.sets(render_set, query=True) - if "LAYER_{}".format(layer_name) in members: - print(" - removing set for {}".format(layer_name)) - cmds.delete("LAYER_{}".format(layer_name)) + # all sets under set should have the same namespace + namespace = members[0].rpartition(":")[0] + render_layer_set_name = "{}:{}".format(namespace, layer_name) + + if render_layer_set_name in members: + print(" - removing set for {}".format(layer_name)) + cmds.delete(render_layer_set_name) class RenderSetupItemObserver(): + """Handle changes in render setup items.""" def __init__(self, item): self.item = item self.original_name = item.name() def itemChanged(self, *args, **kwargs): + """Item changed callback.""" if self.item.name() == self.original_name: return - render_set = _get_render_instance() + render_sets = _get_render_instances() - if not render_set: - return + for render_set in render_sets: + members = cmds.sets(render_set, query=True) + if not members: + continue - members = cmds.sets(render_set, query=True) - if "LAYER_{}".format(self.original_name) in members: - print(" <> renaming {} to {}".format(self.original_name, - self.item.name())) - cmds.rename("LAYER_{}".format(self.original_name), - "LAYER_{}".format(self.item.name())) - self.original_name = self.item.name() + # all sets under set should have the same namespace + namespace = members[0].rpartition(":")[0] + render_layer_set_name = "{}:{}".format( + namespace, self.original_name) + + if render_layer_set_name in members: + print(" <> renaming {} to {}".format(self.original_name, + self.item.name())) + cmds.rename(render_layer_set_name, + "{}:{}".format( + namespace, self.item.name())) + self.original_name = self.item.name() renderListObserver = RenderSetupListObserver() @@ -2564,14 +2618,19 @@ def add_render_layer_change_observer(): import maya.app.renderSetup.model.renderSetup as renderSetup rs = renderSetup.instance() - render_set = _get_render_instance() - if not render_set: - return + render_sets = _get_render_instances() - members = cmds.sets(render_set, query=True) layers = rs.getRenderLayers() - for layer in layers: - if "LAYER_{}".format(layer.name()) in members: + for render_set in render_sets: + members = cmds.sets(render_set, query=True) + if not members: + continue + # all sets under set should have the same namespace + namespace = members[0].rpartition(":")[0] + for layer in layers: + render_layer_set_name = "{}:{}".format(namespace, layer.name()) + if render_layer_set_name not in members: + continue rio = RenderSetupItemObserver(layer) print("- adding observer for {}".format(layer.name())) layer.addItemObserver(rio.itemChanged) diff --git a/pype/hosts/maya/api/render_setup_tools.py b/pype/hosts/maya/api/render_setup_tools.py new file mode 100644 index 0000000000..9ba48310d6 --- /dev/null +++ b/pype/hosts/maya/api/render_setup_tools.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""Export stuff in render setup layer context. + +Export Maya nodes from Render Setup layer as if flattened in that layer instead +of exporting the defaultRenderLayer as Maya forces by default + +Credits: Roy Nieterau (BigRoy) / Colorbleed +Modified for use in Pype + +""" + +import os +import contextlib + +from maya import cmds +from maya.app.renderSetup.model import renderSetup + +# from colorbleed.maya import lib +from .lib import pairwise + + +@contextlib.contextmanager +def _allow_export_from_render_setup_layer(): + """Context manager to override Maya settings to allow RS layer export""" + try: + + rs = renderSetup.instance() + + # Exclude Render Setup nodes from the export + rs._setAllRSNodesDoNotWrite(True) + + # Disable Render Setup forcing the switch to master layer + os.environ["MAYA_BATCH_RENDER_EXPORT"] = "1" + + yield + + finally: + # Reset original state + rs._setAllRSNodesDoNotWrite(False) + os.environ.pop("MAYA_BATCH_RENDER_EXPORT", None) + + +def export_in_rs_layer(path, nodes, export=None): + """Export nodes from Render Setup layer. + + When exporting from Render Setup layer Maya by default + forces a switch to the defaultRenderLayer as such making + it impossible to export the contents of a Render Setup + layer. Maya presents this warning message: + # Warning: Exporting Render Setup master layer content # + + This function however avoids the renderlayer switch and + exports from the Render Setup layer as if the edits were + 'flattened' in the master layer. + + It does so by: + - Allowing export from Render Setup Layer + - Enforce Render Setup nodes to NOT be written on export + - Disconnect connections from any `applyOverride` nodes + to flatten the values (so they are written correctly)* + *Connection overrides like Shader Override and Material + Overrides export correctly out of the box since they don't + create an intermediate connection to an 'applyOverride' node. + However, any scalar override (absolute or relative override) + will get input connections in the layer so we'll break those + to 'store' the values on the attribute itself and write value + out instead. + + Args: + path (str): File path to export to. + nodes (list): Maya nodes to export. + export (callable, optional): Callback to be used for exporting. If + not specified, default export to `.ma` will be called. + + Returns: + None + + Raises: + AssertionError: When not in a Render Setup layer an + AssertionError is raised. This command assumes + you are currently in a Render Setup layer. + + """ + rs = renderSetup.instance() + assert rs.getVisibleRenderLayer().name() != "defaultRenderLayer", \ + ("Export in Render Setup layer is only supported when in " + "Render Setup layer") + + # Break connection to any value overrides + history = cmds.listHistory(nodes) or [] + nodes_all = list( + set(cmds.ls(nodes + history, long=True, objectsOnly=True))) + overrides = cmds.listConnections(nodes_all, + source=True, + destination=False, + type="applyOverride", + plugs=True, + connections=True) or [] + for dest, src in pairwise(overrides): + # Even after disconnecting the values + # should be preserved as they were + # Note: animated overrides would be lost for export + cmds.disconnectAttr(src, dest) + + # Export Selected + with _allow_export_from_render_setup_layer(): + cmds.select(nodes, noExpand=True) + if export: + export() + else: + cmds.file(path, + force=True, + typ="mayaAscii", + exportSelected=True, + preserveReferences=False, + channels=True, + constraints=True, + expressions=True, + constructionHistory=True) + + if overrides: + # If we have broken override connections then Maya + # is unaware that the Render Setup layer is in an + # invalid state. So let's 'hard reset' the state + # by going to default render layer and switching back + layer = rs.getVisibleRenderLayer() + rs.switchToLayer(None) + rs.switchToLayer(layer) diff --git a/pype/hosts/maya/plugins/create/create_render.py b/pype/hosts/maya/plugins/create/create_render.py index 2fd9972721..51655ef175 100644 --- a/pype/hosts/maya/plugins/create/create_render.py +++ b/pype/hosts/maya/plugins/create/create_render.py @@ -10,6 +10,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup from pype.hosts.maya.api import lib from pype.api import get_system_settings + import avalon.maya @@ -86,12 +87,28 @@ class CreateRender(avalon.maya.Creator): """Entry point.""" exists = cmds.ls(self.name) if exists: - return cmds.warning("%s already exists." % exists[0]) + cmds.warning("%s already exists." % exists[0]) + return use_selection = self.options.get("useSelection") with lib.undo_chunk(): self._create_render_settings() instance = super(CreateRender, self).process() + # create namespace with instance + index = 1 + namespace_name = "_{}".format(str(instance)) + try: + cmds.namespace(rm=namespace_name) + except RuntimeError: + # namespace is not empty, so we leave it untouched + pass + + while(cmds.namespace(exists=namespace_name)): + namespace_name = "_{}{}".format(str(instance), index) + index += 1 + + namespace = cmds.namespace(add=namespace_name) + cmds.setAttr("{}.machineList".format(instance), lock=True) self._rs = renderSetup.instance() layers = self._rs.getRenderLayers() @@ -99,17 +116,19 @@ class CreateRender(avalon.maya.Creator): print(">>> processing existing layers") sets = [] for layer in layers: - print(" - creating set for {}".format(layer.name())) - render_set = cmds.sets(n="LAYER_{}".format(layer.name())) + print(" - creating set for {}:{}".format( + namespace, layer.name())) + render_set = cmds.sets( + n="{}:{}".format(namespace, layer.name())) sets.append(render_set) cmds.sets(sets, forceElement=instance) # if no render layers are present, create default one with # asterix selector if not layers: - rl = self._rs.createRenderLayer('Main') - cl = rl.createCollection("defaultCollection") - cl.getSelector().setPattern('*') + render_layer = self._rs.createRenderLayer('Main') + collection = render_layer.createCollection("defaultCollection") + collection.getSelector().setPattern('*') renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() diff --git a/pype/hosts/maya/plugins/create/create_vrayscene.py b/pype/hosts/maya/plugins/create/create_vrayscene.py index df1c232858..b2c3317540 100644 --- a/pype/hosts/maya/plugins/create/create_vrayscene.py +++ b/pype/hosts/maya/plugins/create/create_vrayscene.py @@ -1,27 +1,236 @@ +# -*- coding: utf-8 -*- +"""Create instance of vrayscene.""" +import os +import json +import appdirs +import requests + +from maya import cmds +import maya.app.renderSetup.model.renderSetup as renderSetup + +from pype.hosts.maya.api import lib +from pype.api import get_system_settings + import avalon.maya class CreateVRayScene(avalon.maya.Creator): + """Create Vray Scene.""" label = "VRay Scene" family = "vrayscene" icon = "cubes" def __init__(self, *args, **kwargs): + """Entry.""" super(CreateVRayScene, self).__init__(*args, **kwargs) + self._rs = renderSetup.instance() + self.data["exportOnFarm"] = False - # We don't need subset or asset attributes - self.data.pop("subset", None) - self.data.pop("asset", None) - self.data.pop("active", None) + def process(self): + """Entry point.""" + exists = cmds.ls(self.name) + if exists: + return cmds.warning("%s already exists." % exists[0]) - self.data.update({ - "id": "avalon.vrayscene", # We won't be publishing this one - "suspendRenderJob": False, - "suspendPublishJob": False, - "extendFrames": False, - "pools": "", - "framesPerTask": 1 - }) + use_selection = self.options.get("useSelection") + with lib.undo_chunk(): + self._create_vray_instance_settings() + instance = super(CreateVRayScene, self).process() + index = 1 + namespace_name = "_{}".format(str(instance)) + try: + cmds.namespace(rm=namespace_name) + except RuntimeError: + # namespace is not empty, so we leave it untouched + pass + + while(cmds.namespace(exists=namespace_name)): + namespace_name = "_{}{}".format(str(instance), index) + index += 1 + + namespace = cmds.namespace(add=namespace_name) + # create namespace with instance + layers = self._rs.getRenderLayers() + if use_selection: + print(">>> processing existing layers") + sets = [] + for layer in layers: + print(" - creating set for {}".format(layer.name())) + render_set = cmds.sets( + n="{}:{}".format(namespace, layer.name())) + sets.append(render_set) + cmds.sets(sets, forceElement=instance) + + # if no render layers are present, create default one with + # asterix selector + if not layers: + render_layer = self._rs.createRenderLayer('Main') + collection = render_layer.createCollection("defaultCollection") + collection.getSelector().setPattern('*') + + def _create_vray_instance_settings(self): + # get pools + pools = [] + + system_settings = get_system_settings()["modules"] + + deadline_enabled = system_settings["deadline"]["enabled"] + muster_enabled = system_settings["muster"]["enabled"] + deadline_url = system_settings["deadline"]["DEADLINE_REST_URL"] + muster_url = system_settings["muster"]["MUSTER_REST_URL"] + + if deadline_enabled and muster_enabled: + self.log.error( + "Both Deadline and Muster are enabled. " "Cannot support both." + ) + raise RuntimeError("Both Deadline and Muster are enabled") + + if deadline_enabled: + argument = "{}/api/pools?NamesOnly=true".format(deadline_url) + try: + response = self._requests_get(argument) + except requests.exceptions.ConnectionError as e: + msg = 'Cannot connect to deadline web service' + self.log.error(msg) + raise RuntimeError('{} - {}'.format(msg, e)) + if not response.ok: + self.log.warning("No pools retrieved") + else: + pools = response.json() + self.data["primaryPool"] = pools + # We add a string "-" to allow the user to not + # set any secondary pools + self.data["secondaryPool"] = ["-"] + pools + + if muster_enabled: + self.log.info(">>> Loading Muster credentials ...") + self._load_credentials() + self.log.info(">>> Getting pools ...") + try: + pools = self._get_muster_pools() + except requests.exceptions.HTTPError as e: + if e.startswith("401"): + self.log.warning("access token expired") + self._show_login() + raise RuntimeError("Access token expired") + except requests.exceptions.ConnectionError: + self.log.error("Cannot connect to Muster API endpoint.") + raise RuntimeError("Cannot connect to {}".format(muster_url)) + pool_names = [] + for pool in pools: + self.log.info(" - pool: {}".format(pool["name"])) + pool_names.append(pool["name"]) + + self.data["primaryPool"] = pool_names + + self.data["suspendPublishJob"] = False + self.data["priority"] = 50 + self.data["whitelist"] = False + self.data["machineList"] = "" + self.data["vraySceneMultipleFiles"] = False self.options = {"useSelection": False} # Force no content + + def _load_credentials(self): + """Load Muster credentials. + + Load Muster credentials from file and set ``MUSTER_USER``, + ``MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from presets. + + Raises: + RuntimeError: If loaded credentials are invalid. + AttributeError: If ``MUSTER_REST_URL`` is not set. + + """ + app_dir = os.path.normpath(appdirs.user_data_dir("pype-app", "pype")) + file_name = "muster_cred.json" + fpath = os.path.join(app_dir, file_name) + file = open(fpath, "r") + muster_json = json.load(file) + self._token = muster_json.get("token", None) + if not self._token: + self._show_login() + raise RuntimeError("Invalid access token for Muster") + file.close() + self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") + if not self.MUSTER_REST_URL: + raise AttributeError("Muster REST API url not set") + + def _get_muster_pools(self): + """Get render pools from Muster. + + Raises: + Exception: If pool list cannot be obtained from Muster. + + """ + params = {"authToken": self._token} + api_entry = "/api/pools/list" + response = self._requests_get(self.MUSTER_REST_URL + api_entry, + params=params) + if response.status_code != 200: + if response.status_code == 401: + self.log.warning("Authentication token expired.") + self._show_login() + else: + self.log.error( + ("Cannot get pools from " + "Muster: {}").format(response.status_code) + ) + raise Exception("Cannot get pools from Muster") + try: + pools = response.json()["ResponseData"]["pools"] + except ValueError as e: + self.log.error("Invalid response from Muster server {}".format(e)) + raise Exception("Invalid response from Muster server") + + return pools + + def _show_login(self): + # authentication token expired so we need to login to Muster + # again to get it. We use Pype API call to show login window. + api_url = "{}/muster/show_login".format( + os.environ["PYPE_REST_API_URL"]) + self.log.debug(api_url) + login_response = self._requests_post(api_url, timeout=1) + if login_response.status_code != 200: + self.log.error("Cannot show login form to Muster") + raise Exception("Cannot show login form to Muster") + + def _requests_post(self, *args, **kwargs): + """Wrap request post method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if "verify" not in kwargs: + kwargs["verify"] = ( + False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True + ) # noqa + return requests.post(*args, **kwargs) + + def _requests_get(self, *args, **kwargs): + """Wrap request get method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if "verify" not in kwargs: + kwargs["verify"] = ( + False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True + ) # noqa + return requests.get(*args, **kwargs) diff --git a/pype/hosts/maya/plugins/load/load_vrayscene.py b/pype/hosts/maya/plugins/load/load_vrayscene.py new file mode 100644 index 0000000000..b258119a1a --- /dev/null +++ b/pype/hosts/maya/plugins/load/load_vrayscene.py @@ -0,0 +1,145 @@ +from avalon.maya import lib +from avalon import api +from pype.api import config +import os +import maya.cmds as cmds + + +class VRaySceneLoader(api.Loader): + """Load Vray scene""" + + families = ["vrayscene_layer"] + representations = ["vrscene"] + + label = "Import VRay Scene" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + + from avalon.maya.pipeline import containerise + from pype.hosts.maya.lib import namespaced + + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vrayscene_layer" + + asset_name = context['asset']["name"] + namespace = namespace or lib.unique_namespace( + asset_name + "_", + prefix="_" if asset_name[0].isdigit() else "", + suffix="_", + ) + + # Ensure V-Ray for Maya is loaded. + cmds.loadPlugin("vrayformaya", quiet=True) + + with lib.maintained_selection(): + cmds.namespace(addNamespace=namespace) + with namespaced(namespace, new=False): + nodes, group_node = self.create_vray_scene(name, + filename=self.fname) + + self[:] = nodes + if not nodes: + return + + # colour the group node + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] + c = colors.get(family) + if c is not None: + cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) + cmds.setAttr("{0}.outlinerColor".format(group_node), + c[0], c[1], c[2]) + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + + node = container['objectName'] + assert cmds.objExists(node), "Missing container" + + members = cmds.sets(node, query=True) or [] + vraymeshes = cmds.ls(members, type="VRayScene") + assert vraymeshes, "Cannot find VRayScene in container" + + filename = api.get_representation_path(representation) + + for vray_mesh in vraymeshes: + cmds.setAttr("{}.FilePath".format(vray_mesh), + filename, + type="string") + + # Update metadata + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + + def remove(self, container): + + # Delete container and its contents + if cmds.objExists(container['objectName']): + members = cmds.sets(container['objectName'], query=True) or [] + cmds.delete([container['objectName']] + members) + + # Remove the namespace, if empty + namespace = container['namespace'] + if cmds.namespace(exists=namespace): + members = cmds.namespaceInfo(namespace, listNamespace=True) + if not members: + cmds.namespace(removeNamespace=namespace) + else: + self.log.warning("Namespace not deleted because it " + "still has members: %s", namespace) + + def switch(self, container, representation): + self.update(container, representation) + + def create_vray_scene(self, name, filename): + """Re-create the structure created by VRay to support vrscenes + + Args: + name(str): name of the asset + + Returns: + nodes(list) + """ + + # Create nodes + mesh_node_name = "VRayScene_{}".format(name) + + trans = cmds.createNode( + "transform", name="{}".format(mesh_node_name)) + mesh = cmds.createNode( + "mesh", name="{}_Shape".format(mesh_node_name), parent=trans) + vray_scene = cmds.createNode( + "VRayScene", name="{}_VRSCN".format(mesh_node_name), parent=trans) + + cmds.connectAttr( + "{}.outMesh".format(vray_scene), "{}.inMesh".format(mesh)) + + cmds.setAttr("{}.FilePath".format(vray_scene), filename, type="string") + + # Create important connections + cmds.connectAttr("time1.outTime", + "{0}.inputTime".format(trans)) + + # Connect mesh to initialShadingGroup + cmds.sets([mesh], forceElement="initialShadingGroup") + + group_node = cmds.group(empty=True, name="{}_GRP".format(name)) + cmds.parent(trans, group_node) + nodes = [trans, vray_scene, mesh, group_node] + + # Fix: Force refresh so the mesh shows correctly after creation + cmds.refresh() + + return nodes, group_node diff --git a/pype/hosts/maya/plugins/publish/collect_render.py b/pype/hosts/maya/plugins/publish/collect_render.py index fdd77815cc..c24cf1dfef 100644 --- a/pype/hosts/maya/plugins/publish/collect_render.py +++ b/pype/hosts/maya/plugins/publish/collect_render.py @@ -95,9 +95,17 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.maya_layers = maya_render_layers for layer in collected_render_layers: - # every layer in set should start with `LAYER_` prefix try: - expected_layer_name = re.search(r"^LAYER_(.*)", layer).group(1) + if layer.startswith("LAYER_"): + # this is support for legacy mode where render layers + # started with `LAYER_` prefix. + expected_layer_name = re.search( + r"^LAYER_(.*)", layer).group(1) + else: + # new way is to prefix render layer name with instance + # namespace. + expected_layer_name = re.search( + r"^.+:(.*)", layer).group(1) except IndexError: msg = "Invalid layer name in set [ {} ]".format(layer) self.log.warnig(msg) @@ -277,10 +285,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # handle standalone renderers if render_instance.data.get("vrayScene") is True: - data["families"].append("vrayscene") + data["families"].append("vrayscene_render") if render_instance.data.get("assScene") is True: - data["families"].append("assscene") + data["families"].append("assscene_render") # Include (optional) global settings # Get global overrides and translate to Deadline values diff --git a/pype/hosts/maya/plugins/publish/collect_renderable_camera.py b/pype/hosts/maya/plugins/publish/collect_renderable_camera.py index 893a2cab61..b90b85e7ec 100644 --- a/pype/hosts/maya/plugins/publish/collect_renderable_camera.py +++ b/pype/hosts/maya/plugins/publish/collect_renderable_camera.py @@ -12,11 +12,15 @@ class CollectRenderableCamera(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.02 label = "Collect Renderable Camera(s)" hosts = ["maya"] - families = ["vrayscene", + families = ["vrayscene_layer", "renderlayer"] def process(self, instance): - layer = instance.data["setMembers"] + if "vrayscene_layer" in instance.data.get("families", []): + layer = instance.data.get("layer") + else: + layer = instance.data["setMembers"] + self.log.info("layer: {}".format(layer)) cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if diff --git a/pype/hosts/maya/plugins/publish/collect_vrayscene.py b/pype/hosts/maya/plugins/publish/collect_vrayscene.py new file mode 100644 index 0000000000..7960bb7937 --- /dev/null +++ b/pype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +"""Collect Vray Scene and prepare it for extraction and publishing.""" +import re + +import maya.app.renderSetup.model.renderSetup as renderSetup +from maya import cmds + +import pyblish.api +from avalon import api +from pype.hosts.maya import lib + + +class CollectVrayScene(pyblish.api.InstancePlugin): + """Collect Vray Scene. + + If export on farm is checked, job is created to export it. + """ + + order = pyblish.api.CollectorOrder + 0.01 + label = "Collect Vray Scene" + families = ["vrayscene"] + + def process(self, instance): + """Collector entry point.""" + collected_render_layers = instance.data["setMembers"] + instance.data["remove"] = True + context = instance.context + + _rs = renderSetup.instance() + # current_layer = _rs.getVisibleRenderLayer() + + # collect all frames we are expecting to be rendered + renderer = cmds.getAttr( + "defaultRenderGlobals.currentRenderer" + ).lower() + + if renderer != "vray": + raise AssertionError("Vray is not enabled.") + + maya_render_layers = { + layer.name(): layer for layer in _rs.getRenderLayers() + } + + layer_list = [] + for layer in collected_render_layers: + # every layer in set should start with `LAYER_` prefix + try: + expected_layer_name = re.search(r"^.+:(.*)", layer).group(1) + except IndexError: + msg = "Invalid layer name in set [ {} ]".format(layer) + self.log.warnig(msg) + continue + + self.log.info("processing %s" % layer) + # check if layer is part of renderSetup + if expected_layer_name not in maya_render_layers: + msg = "Render layer [ {} ] is not in " "Render Setup".format( + expected_layer_name + ) + self.log.warning(msg) + continue + + # check if layer is renderable + if not maya_render_layers[expected_layer_name].isRenderable(): + msg = "Render layer [ {} ] is not " "renderable".format( + expected_layer_name + ) + self.log.warning(msg) + continue + + layer_name = "rs_{}".format(expected_layer_name) + + self.log.debug(expected_layer_name) + layer_list.append(expected_layer_name) + + frame_start_render = int(self.get_render_attribute( + "startFrame", layer=layer_name)) + frame_end_render = int(self.get_render_attribute( + "endFrame", layer=layer_name)) + + if (int(context.data['frameStartHandle']) == frame_start_render + and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 + + handle_start = context.data['handleStart'] + handle_end = context.data['handleEnd'] + frame_start = context.data['frameStart'] + frame_end = context.data['frameEnd'] + frame_start_handle = context.data['frameStartHandle'] + frame_end_handle = context.data['frameEndHandle'] + else: + handle_start = 0 + handle_end = 0 + frame_start = frame_start_render + frame_end = frame_end_render + frame_start_handle = frame_start_render + frame_end_handle = frame_end_render + + # Get layer specific settings, might be overrides + data = { + "subset": expected_layer_name, + "layer": layer_name, + "setMembers": cmds.sets(layer, q=True) or ["*"], + "review": False, + "publish": True, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, + "byFrameStep": int( + self.get_render_attribute("byFrameStep", + layer=layer_name)), + "renderer": self.get_render_attribute("currentRenderer", + layer=layer_name), + # instance subset + "family": "vrayscene_layer", + "families": ["vrayscene_layer"], + "asset": api.Session["AVALON_ASSET"], + "time": api.time(), + "author": context.data["user"], + # Add source to allow tracing back to the scene from + # which was submitted originally + "source": context.data["currentFile"].replace("\\", "/"), + "resolutionWidth": cmds.getAttr("defaultResolution.width"), + "resolutionHeight": cmds.getAttr("defaultResolution.height"), + "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "priority": instance.data.get("priority"), + "useMultipleSceneFiles": instance.data.get( + "vraySceneMultipleFiles") + } + + # Define nice label + label = "{0} ({1})".format(expected_layer_name, data["asset"]) + label += " [{0}-{1}]".format( + int(data["frameStartHandle"]), int(data["frameEndHandle"]) + ) + + instance = context.create_instance(expected_layer_name) + instance.data["label"] = label + instance.data.update(data) + + def get_render_attribute(self, attr, layer): + """Get attribute from render options. + + Args: + attr (str): name of attribute to be looked up. + + Returns: + Attribute value + + """ + return lib.get_attr_in_layer( + "defaultRenderGlobals.{}".format(attr), layer=layer + ) diff --git a/pype/hosts/maya/plugins/publish/extract_vrayscene.py b/pype/hosts/maya/plugins/publish/extract_vrayscene.py new file mode 100644 index 0000000000..a217332d8e --- /dev/null +++ b/pype/hosts/maya/plugins/publish/extract_vrayscene.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Extract vrayscene from specified families.""" +import os +import re + +import avalon.maya +import pype.api +from pype.hosts.maya.render_setup_tools import export_in_rs_layer + +from maya import cmds + + +class ExtractVrayscene(pype.api.Extractor): + """Extractor for vrscene.""" + + label = "VRay Scene (.vrscene)" + hosts = ["maya"] + families = ["vrayscene_layer"] + + def process(self, instance): + """Plugin entry point.""" + if instance.data.get("exportOnFarm"): + self.log.info("vrayscenes will be exported on farm.") + raise NotImplementedError( + "exporting vrayscenes is not implemented") + + # handle sequence + if instance.data.get("vraySceneMultipleFiles"): + self.log.info("vrayscenes will be exported on farm.") + raise NotImplementedError( + "exporting vrayscene sequences not implemented yet") + + vray_settings = cmds.ls(type="VRaySettingsNode") + if not vray_settings: + node = cmds.createNode("VRaySettingsNode") + else: + node = vray_settings[0] + + # setMembers on vrayscene_layer shoudl contain layer name. + layer_name = instance.data.get("layer") + + staging_dir = self.staging_dir(instance) + self.log.info("staging: {}".format(staging_dir)) + template = cmds.getAttr("{}.vrscene_filename".format(node)) + start_frame = instance.data.get( + "frameStartHandle") if instance.data.get( + "vraySceneMultipleFiles") else None + formatted_name = self.format_vray_output_filename( + os.path.basename(instance.data.get("source")), + layer_name, + template, + start_frame + ) + + file_path = os.path.join( + staging_dir, "vrayscene", *formatted_name.split("/")) + + # Write out vrscene file + self.log.info("Writing: '%s'" % file_path) + with avalon.maya.maintained_selection(): + if "*" not in instance.data["setMembers"]: + self.log.info( + "Exporting: {}".format(instance.data["setMembers"])) + set_members = instance.data["setMembers"] + cmds.select(set_members, noExpand=True) + else: + self.log.info("Exporting all ...") + set_members = cmds.ls( + long=True, objectsOnly=True, + geometry=True, lights=True, cameras=True) + cmds.select(set_members, noExpand=True) + + self.log.info("Appending layer name {}".format(layer_name)) + set_members.append(layer_name) + + export_in_rs_layer( + file_path, + set_members, + export=lambda: cmds.file( + file_path, type="V-Ray Scene", + pr=True, es=True, force=True)) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + files = file_path + + representation = { + 'name': 'vrscene', + 'ext': 'vrscene', + 'files': os.path.basename(files), + "stagingDir": os.path.dirname(files), + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s" + % (instance.name, staging_dir)) + + @staticmethod + def format_vray_output_filename( + filename, layer, template, start_frame=None): + """Format the expected output file of the Export job. + + Example: + filename: /mnt/projects/foo/shot010_v006.mb + template: // + result: "shot010_v006/CHARS/CHARS.vrscene" + + Args: + filename (str): path to scene file. + layer (str): layer name. + template (str): token template. + start_frame (int, optional): start frame - if set we use + mutliple files export mode. + + Returns: + str: formatted path. + + """ + # format template to match pythons format specs + template = re.sub(r"<(\w+?)>", r"{\1}", template.lower()) + + # Ensure filename has no extension + file_name, _ = os.path.splitext(filename) + mapping = { + "scene": file_name, + "layer": layer + } + + output_path = template.format(**mapping) + + if start_frame: + filename_zero = "{}_{:04d}.vrscene".format( + output_path, start_frame) + else: + filename_zero = "{}.vrscene".format(output_path) + + result = filename_zero.replace("\\", "/") + + return result diff --git a/pype/hosts/maya/plugins/publish/validate_vray_translator_settings.py b/pype/hosts/maya/plugins/publish/validate_vray_translator_settings.py index 592f24e36f..fb290a2d5d 100644 --- a/pype/hosts/maya/plugins/publish/validate_vray_translator_settings.py +++ b/pype/hosts/maya/plugins/publish/validate_vray_translator_settings.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Validate VRay Translator settings.""" import pyblish.api import pype.api from pype.plugin import contextplugin_should_run @@ -6,14 +8,15 @@ from maya import cmds class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): + """Validate VRay Translator settings for extracting vrscenes.""" order = pype.api.ValidateContentsOrder label = "VRay Translator Settings" - families = ["vrayscene"] + families = ["vrayscene_layer"] actions = [pype.api.RepairContextAction] def process(self, context): - + """Plugin entry point.""" # Workaround bug pyblish-base#250 if not contextplugin_should_run(self, context): return @@ -24,7 +27,7 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): @classmethod def get_invalid(cls, context): - + """Get invalid instances.""" invalid = False # Get vraySettings node @@ -34,16 +37,26 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): node = vray_settings[0] if cmds.setAttr("{}.vrscene_render_on".format(node)): - cls.log.error("Render is enabled, this should be disabled") + cls.log.error( + "Render is enabled, for export it should be disabled") invalid = True if not cmds.getAttr("{}.vrscene_on".format(node)): cls.log.error("Export vrscene not enabled") invalid = True - if not cmds.getAttr("{}.misc_eachFrameInFile".format(node)): - cls.log.error("Each Frame in File not enabled") - invalid = True + for instance in context: + if "vrayscene_layer" not in instance.data.get("families"): + continue + + if instance.data.get("vraySceneMultipleFiles"): + if not cmds.getAttr("{}.misc_eachFrameInFile".format(node)): + cls.log.error("Each Frame in File not enabled") + invalid = True + else: + if cmds.getAttr("{}.misc_eachFrameInFile".format(node)): + cls.log.error("Each Frame in File is enabled") + invalid = True vrscene_filename = cmds.getAttr("{}.vrscene_filename".format(node)) if vrscene_filename != "vrayscene///": @@ -54,7 +67,7 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): @classmethod def repair(cls, context): - + """Repair invalid settings.""" vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") @@ -63,7 +76,14 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): cmds.setAttr("{}.vrscene_render_on".format(node), False) cmds.setAttr("{}.vrscene_on".format(node), True) - cmds.setAttr("{}.misc_eachFrameInFile".format(node), True) + for instance in context: + if "vrayscene" not in instance.data.get("families"): + continue + + if instance.data.get("vraySceneMultipleFiles"): + cmds.setAttr("{}.misc_eachFrameInFile".format(node), True) + else: + cmds.setAttr("{}.misc_eachFrameInFile".format(node), False) cmds.setAttr("{}.vrscene_filename".format(node), "vrayscene///", type="string") diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index cf267a84cf..14b25b9c46 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -66,6 +66,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "vdbcache", "scene", "vrayproxy", + "vrayscene_layer", "render", "prerender", "imagesequence", @@ -701,7 +702,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'type': 'subset', '_id': io.ObjectId(subset["_id"]) }, {'$set': {'data.subsetGroup': - instance.data.get('subsetGroup')}} + instance.data.get('subsetGroup')}} ) # Update families on subset. @@ -878,9 +879,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path = rootless_path else: self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(path)) + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) return path def get_files_info(self, instance, integrated_file_sizes):