diff --git a/FireRender.Maya.Src/CMakeLists.txt b/FireRender.Maya.Src/CMakeLists.txt index fe760884..6e7a193a 100644 --- a/FireRender.Maya.Src/CMakeLists.txt +++ b/FireRender.Maya.Src/CMakeLists.txt @@ -38,6 +38,8 @@ set(Commands "RenderCacheWarningDialog.h" "RenderProgressBars.cpp" "RenderProgressBars.h" + "FireRenderImportMaterialX.h" + "FireRenderImportMaterialX.cpp" ) source_group("Commands" FILES ${Commands}) @@ -214,6 +216,8 @@ set(Materials "FireRenderVoronoi.h" "SubsurfaceMaterial.cpp" "SubsurfaceMaterial.h" + "FireRenderMaterialXMaterial.cpp" + "FireRenderMaterialXMaterial.h" ) source_group("Materials" FILES ${Materials}) diff --git a/FireRender.Maya.Src/Context/HybridProContext.cpp b/FireRender.Maya.Src/Context/HybridProContext.cpp index 6ce709d7..3e36ba6d 100644 --- a/FireRender.Maya.Src/Context/HybridProContext.cpp +++ b/FireRender.Maya.Src/Context/HybridProContext.cpp @@ -12,6 +12,7 @@ limitations under the License. ********************************************************************/ #include "HybridProContext.h" #include "RadeonProRender_Baikal.h" +#include "RadeonProRender_MaterialX.h" #include "ContextCreator.h" #include "../VulcanUtils.h" @@ -20,6 +21,7 @@ limitations under the License. #include "../FireRenderDoublesided.h" #include "../FireRenderPBRMaterial.h" #include "../FireRenderShadowCatcherMaterial.h" +#include "../FireRenderMaterialXMaterial.h" rpr_int HybridProContext::m_gHybridProPluginID = INCORRECT_PLUGIN_ID; @@ -142,6 +144,10 @@ void HybridProContext::setupContextPostSceneCreation(const FireRenderGlobalsData #ifdef FORCE_ENABLE_VOLUMES rprContextSetParameterByKey1u(frcontext, (rpr_context_info)RPR_CONTEXT_ENABLE_VOLUMES, RPR_TRUE); #endif + + // set dependency for materialX standard surface (could be missing from *.mtlx files) + frstatus = rprMaterialXAddDependencyMtlx(frcontext, "scripts/standard_surface.mtlx"); + checkStatus(frstatus); } bool HybridProContext::IsAOVSupported(int aov) const @@ -191,6 +197,12 @@ bool HybridProContext::IsShaderNodeSupported(FireMaya::ShaderNode* shaderNode) c return true; } + FireMaya::MaterialXMaterial* pMaterialX = dynamic_cast (shaderNode); + if (pMaterialX != nullptr) + { + return true; + } + return false; } diff --git a/FireRender.Maya.Src/Context/TahoeContext.cpp b/FireRender.Maya.Src/Context/TahoeContext.cpp index 6675ef6a..dd7a1a51 100644 --- a/FireRender.Maya.Src/Context/TahoeContext.cpp +++ b/FireRender.Maya.Src/Context/TahoeContext.cpp @@ -11,6 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. ********************************************************************/ #include "TahoeContext.h" +#include "RadeonProRender_MaterialX.h" #include "maya/MColorManagementUtilities.h" #include "maya/MFileObject.h" @@ -338,6 +339,10 @@ void NorthStarContext::setupContextPostSceneCreation(const FireRenderGlobalsData frstatus = rprContextSetParameterByKeyString(frcontext, RPR_CONTEXT_TEXTURE_CACHE_PATH, fireRenderGlobalsData.textureCachePath.asChar()); checkStatus(frstatus); + // set dependency for materialX standard surface (could be missing from *.mtlx files) + frstatus = rprMaterialXAddDependencyMtlx(frcontext, "scripts/standard_surface.mtlx"); + checkStatus(frstatus); + // SC and RC // we should disable built-in shadow catcher composite frstatus = rprContextSetParameterByKey1u(frcontext, RPR_CONTEXT_SHADOW_CATCHER_BAKING, fireRenderGlobalsData.shadowCatcherEnabled ? 0 : 1); diff --git a/FireRender.Maya.Src/FireMaya.h b/FireRender.Maya.Src/FireMaya.h index 7733abcd..02ce19c4 100644 --- a/FireRender.Maya.Src/FireMaya.h +++ b/FireRender.Maya.Src/FireMaya.h @@ -97,6 +97,7 @@ namespace FireMaya FireRenderRamp, FireRenderDoubleSided, FireRenderBevel, + FireRenderMaterialXMaterial, // ^ always add new ids to end of list (max 128 entries here) FireRenderNodeIdEndCurrent, // <- this value is allowed to change, it marks the end of current list diff --git a/FireRender.Maya.Src/FireRender.vcxproj b/FireRender.Maya.Src/FireRender.vcxproj index 5eb2ee13..bb6b0ace 100644 --- a/FireRender.Maya.Src/FireRender.vcxproj +++ b/FireRender.Maya.Src/FireRender.vcxproj @@ -195,6 +195,7 @@ + @@ -203,6 +204,7 @@ + @@ -358,6 +360,7 @@ + @@ -367,6 +370,7 @@ + diff --git a/FireRender.Maya.Src/FireRender.vcxproj.filters b/FireRender.Maya.Src/FireRender.vcxproj.filters index abce5fc5..babbef4a 100644 --- a/FireRender.Maya.Src/FireRender.vcxproj.filters +++ b/FireRender.Maya.Src/FireRender.vcxproj.filters @@ -704,6 +704,12 @@ Source Files + + Source Files + + + Source Files + @@ -1216,6 +1222,18 @@ Source Files + + Header Files + + + Source Files + + + Header Files + + + Source Files + Utils diff --git a/FireRender.Maya.Src/FireRenderImportMaterialX.cpp b/FireRender.Maya.Src/FireRenderImportMaterialX.cpp new file mode 100644 index 00000000..4084bcf8 --- /dev/null +++ b/FireRender.Maya.Src/FireRenderImportMaterialX.cpp @@ -0,0 +1,145 @@ +/********************************************************************** +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +********************************************************************/ +#include "FireRenderImportMaterialX.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Context/TahoeContext.h" +#include "FireRenderUtils.h" +#include "FileSystemUtils.h" + + +FireRenderMaterialXImportCmd::FireRenderMaterialXImportCmd() +{ +} + +FireRenderMaterialXImportCmd::~FireRenderMaterialXImportCmd() +{ +} + +void* FireRenderMaterialXImportCmd::creator() +{ + return new FireRenderMaterialXImportCmd; +} + +MSyntax FireRenderMaterialXImportCmd::newSyntax() +{ + MSyntax syntax; + + CHECK_MSTATUS(syntax.addFlag(kMaterialFilePathFlag, kMaterialFilePathFlagLong, MSyntax::kString)); + CHECK_MSTATUS(syntax.addFlag(kMaterialNameFlag, kMaterialNameFlagLong, MSyntax::kString)); + + return syntax; +} + +std::tuple GetFilePath(const MArgDatabase& argData) +{ + // back-off + if (!argData.isFlagSet(kMaterialFilePathFlag)) + { + MGlobal::displayError("File path is missing, use -file flag"); + return std::make_tuple(MS::kFailure, MString()); + } + + MString filePath; + argData.getFlagArgument(kMaterialFilePathFlag, 0, filePath); + + return std::make_tuple(MS::kSuccess, filePath); +} + +std::tuple GetName(const MArgDatabase& argData) +{ + // back-off + if (!argData.isFlagSet(kMaterialNameFlag)) + { + MGlobal::displayError("Material name is missing, use -name flag"); + return std::make_tuple(MS::kFailure, MString()); + } + + MString materialName; + argData.getFlagArgument(kMaterialNameFlag, 0, materialName); + + return std::make_tuple(MS::kSuccess, materialName); +} + +MStatus FireRenderMaterialXImportCmd::doIt(const MArgList& args) +{ + MStatus result = MS::kSuccess; + MArgDatabase argData(syntax(), args); + + // Get material name + MString materialName; + std::tie(result, materialName) = GetName(argData); + if (MS::kSuccess != result) + return result; + + // create node + MString executeCommand = "shadingNode -name \"" + materialName + "\" -asShader RPRMaterialXMaterial"; + + MString shaderName = MGlobal::executeCommandStringResult(executeCommand); + MSelectionList sList; + sList.add(shaderName); + MObject shaderNode; + sList.getDependNode(0, shaderNode); + if (shaderNode.isNull()) + { + MGlobal::displayError("Unable to create shader!"); + return MS::kFailure; + } + + // create shading groupand connect it with shader node + MString sgCommand = "sets -renderable true -noSurfaceShader true -empty -name " + shaderName + "SG"; + MString sgName = MGlobal::executeCommandStringResult(sgCommand); + MString connectCommand = "connectAttr -f " + shaderName + ".outColor " + sgName + ".surfaceShader"; + MGlobal::executeCommandStringResult(connectCommand); + + // Get path to file describing material + MString filePath; + std::tie(result, filePath) = GetFilePath(argData); + if (MS::kSuccess != result) + return result; + + // Get directory name + std::string directory = getDirectory(filePath.asChar()); + + // write data into created shader node + MFnDependencyNode nodeFn(shaderNode); + MStatus status; + MPlug filePlug = nodeFn.findPlug("filename", false, &status); + assert(status == MStatus::kSuccess); + if (MS::kSuccess != status) + { + return status; + } + + status = filePlug.setString(filePath); + assert(status == MStatus::kSuccess); + if (MS::kSuccess != status) + { + return status; + } + + // Success! + return MS::kSuccess; +} + diff --git a/FireRender.Maya.Src/FireRenderImportMaterialX.h b/FireRender.Maya.Src/FireRenderImportMaterialX.h new file mode 100644 index 00000000..b720fad3 --- /dev/null +++ b/FireRender.Maya.Src/FireRenderImportMaterialX.h @@ -0,0 +1,40 @@ +/********************************************************************** +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +********************************************************************/ +#pragma once + +#include +#include +#include +#include +#include +#include + +#define kMaterialFilePathFlag "-f" +#define kMaterialFilePathFlagLong "-file" +#define kMaterialNameFlag "-n" +#define kMaterialNameFlagLong "-name" + +class FireRenderMaterialXImportCmd : public MPxCommand +{ +public: + + FireRenderMaterialXImportCmd(); + + virtual ~FireRenderMaterialXImportCmd(); + + static void* creator(); + + static MSyntax newSyntax(); + + MStatus doIt(const MArgList& args); +}; diff --git a/FireRender.Maya.Src/FireRenderMaterialXMaterial.cpp b/FireRender.Maya.Src/FireRenderMaterialXMaterial.cpp new file mode 100644 index 00000000..8010df4b --- /dev/null +++ b/FireRender.Maya.Src/FireRenderMaterialXMaterial.cpp @@ -0,0 +1,131 @@ +/********************************************************************** +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +********************************************************************/ +#include "FireRenderMaterialXMaterial.h" +#include "FireMaya.h" +#include "FireRenderUtils.h" +#include "Context/FireRenderContext.h" +#include "RadeonProRender_MaterialX.h" + +#include +#include +#include +#include + +#include + +namespace +{ + namespace Attribute + { + MObject output; + + // path to materialX file + MObject filename; + + // uv input for materialX textures used + MObject uv; + MObject uvSize; + } +} + +// Register attribute and make it affecting output color and alpha +#define ADD_ATTRIBUTE(attr) \ + CHECK_MSTATUS(addAttribute(attr)); \ + CHECK_MSTATUS(attributeAffects(attr, Attribute::output)); + +MStatus FireMaya::MaterialXMaterial::initialize() +{ + MFnNumericAttribute nAttr; + MFnTypedAttribute tAttr; + + Attribute::output = nAttr.createColor("outColor", "oc"); + MAKE_OUTPUT(nAttr); + + Attribute::filename = tAttr.create("filename", "fnm", MFnData::kString); + tAttr.setUsedAsFilename(true); + MAKE_INPUT_CONST(tAttr); + + Attribute::uv = nAttr.create("uvCoord", "uv", MFnNumericData::k2Float); + MAKE_INPUT(nAttr); + // Note: we are not using this, but we have to have it to support classification of this node as a texture2D in Maya: + Attribute::uvSize = nAttr.create("uvFilterSize", "fs", MFnNumericData::k2Float); + MAKE_INPUT(nAttr); + + // Adding all attributes to the node type + addAttribute(Attribute::output); + + ADD_ATTRIBUTE(Attribute::filename); + ADD_ATTRIBUTE(Attribute::uv); + ADD_ATTRIBUTE(Attribute::uvSize); + + return MS::kSuccess; +} + +void* FireMaya::MaterialXMaterial::creator() +{ + return new MaterialXMaterial; +} + +MStatus FireMaya::MaterialXMaterial::compute(const MPlug& plug, MDataBlock& block) +{ + if ((plug == Attribute::output) || (plug.parent() == Attribute::output)) + { + block.setClean(plug); + + return MS::kSuccess; + } + else + { + return MS::kUnknownParameter; + } +} + +frw::Shader FireMaya::MaterialXMaterial::GetShader(Scope& scope) +{ + MStatus status; + MFnDependencyNode shaderNode(thisMObject()); + + frw::Shader shader(scope.MaterialSystem(), frw::ShaderTypeMaterialX); + + MPlug filePlug = shaderNode.findPlug(Attribute::filename, false, &status); + assert(status == MStatus::kSuccess); + std::string strFilePath = filePlug.asString(&status).asChar(); + assert(status == MStatus::kSuccess); + + if (!std::filesystem::exists(strFilePath)) + { + return shader; + } + + std::filesystem::path filePath = strFilePath; + if (filePath.extension() != ".mtlx") + { + return shader; + } + + // using direct RPR call here because this is a very special case + rpr_int res = rprMaterialXSetFile(shader.Handle(), strFilePath.c_str()); + checkStatus(res); + + return shader; +} + +void FireMaya::MaterialXMaterial::postConstructor() +{ + ShaderNode::postConstructor(); +} + +FireMaya::MaterialXMaterial::~MaterialXMaterial() +{ +} + diff --git a/FireRender.Maya.Src/FireRenderMaterialXMaterial.h b/FireRender.Maya.Src/FireRenderMaterialXMaterial.h new file mode 100644 index 00000000..aad3617c --- /dev/null +++ b/FireRender.Maya.Src/FireRenderMaterialXMaterial.h @@ -0,0 +1,36 @@ +/********************************************************************** +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +********************************************************************/ +#pragma once + +#include "FireMaya.h" + +namespace FireMaya +{ + class MaterialXMaterial : public ShaderNode + { + public: + static void* creator(); + static MStatus initialize(); + static MTypeId FRTypeID() { return FireMaya::TypeId::FireRenderMaterialXMaterial; } + + virtual MStatus compute(const MPlug&, MDataBlock&) override; + virtual frw::Shader GetShader(Scope& scope) override; + + void postConstructor() override; + + ~MaterialXMaterial(); + + private: + }; +} + diff --git a/FireRender.Maya.Src/Shelfs/shelf_Radeon_ProRender.mel b/FireRender.Maya.Src/Shelfs/shelf_Radeon_ProRender.mel index 804b6414..e2c5304c 100644 --- a/FireRender.Maya.Src/Shelfs/shelf_Radeon_ProRender.mel +++ b/FireRender.Maya.Src/Shelfs/shelf_Radeon_ProRender.mel @@ -167,6 +167,42 @@ global proc shelf_Radeon_ProRender () { -enableBackground 0 -horizontal 0 ; + shelfButton + -enableCommandRepeat 1 + -enable 1 + -width 36 + -height 36 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "MaterialX material library" + -enableBackground 0 + -align "center" + // -label "MaterialX Material Library" + -labelOffset 0 + -font "plainLabelFont" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0 + -image "shelf/MaterialX.png" + -image1 "shelf/MaterialX.png" + -style "iconOnly" + -marginWidth 1 + -marginHeight 1 + -command "source shelfCommands.mel; openMaterialLibrary2RPR();" + -sourceType "mel" + -commandRepeatable 1 + -flat 1 + ; + separator + -enable 1 + -width 35 + -height 35 + -manage 1 + -visible 1 + -preventOverride 0 + -enableBackground 0 + -horizontal 0 + ; shelfButton -enableCommandRepeat 1 -enable 1 diff --git a/FireRender.Maya.Src/frWrap.h b/FireRender.Maya.Src/frWrap.h index eb6c710d..860fce05 100644 --- a/FireRender.Maya.Src/frWrap.h +++ b/FireRender.Maya.Src/frWrap.h @@ -200,7 +200,8 @@ namespace frw ShaderTypeFlatColor = RPR_MATERIAL_NODE_PASSTHROUGH, ShaderTypeToon = RPR_MATERIAL_NODE_TOON_CLOSURE, ShaderTypeDoubleSided = RPR_MATERIAL_NODE_TWOSIDED, - ShaderTypeMicrofacetAnisotropicReflection = RPR_MATERIAL_NODE_MICROFACET_ANISOTROPIC_REFLECTION + ShaderTypeMicrofacetAnisotropicReflection = RPR_MATERIAL_NODE_MICROFACET_ANISOTROPIC_REFLECTION, + ShaderTypeMaterialX = RPR_MATERIAL_NODE_MATX }; enum ContextParameterType diff --git a/FireRender.Maya.Src/icons/MaterialXLibraryLogo.png b/FireRender.Maya.Src/icons/MaterialXLibraryLogo.png new file mode 100644 index 00000000..2dd14890 Binary files /dev/null and b/FireRender.Maya.Src/icons/MaterialXLibraryLogo.png differ diff --git a/FireRender.Maya.Src/icons/shelf/MaterialX.png b/FireRender.Maya.Src/icons/shelf/MaterialX.png new file mode 100644 index 00000000..db4db077 Binary files /dev/null and b/FireRender.Maya.Src/icons/shelf/MaterialX.png differ diff --git a/FireRender.Maya.Src/icons/shelf/Update.png b/FireRender.Maya.Src/icons/shelf/Update.png new file mode 100644 index 00000000..e896fd3e Binary files /dev/null and b/FireRender.Maya.Src/icons/shelf/Update.png differ diff --git a/FireRender.Maya.Src/pluginMain.cpp b/FireRender.Maya.Src/pluginMain.cpp index 46e8b680..0b2a78b6 100644 --- a/FireRender.Maya.Src/pluginMain.cpp +++ b/FireRender.Maya.Src/pluginMain.cpp @@ -29,6 +29,7 @@ limitations under the License. #include "FireRenderTransparentMaterial.h" #include "FireRenderMaterialSwatchRender.h" #include "FireRenderToonMaterial.h" +#include "FireRenderMaterialXMaterial.h" #include "FireRenderSwatchInstance.h" #include "FireRenderFresnel.h" @@ -54,6 +55,7 @@ limitations under the License. #include "FireRenderViewportCmd.h" #include "FireRenderExportCmd.h" #include "FireRenderImportCmd.h" +#include "FireRenderImportMaterialX.h" #include "FireRenderConvertVRayCmd.h" #include "Athena/AthenaWrap.h" #include "athenaCmd.h" @@ -705,6 +707,8 @@ MStatus initializePlugin(MObject obj) CHECK_MSTATUS(plugin.registerCommand(namePrefix + "XMLExport", FireRenderXmlExportCmd::creator, FireRenderXmlExportCmd::newSyntax)); CHECK_MSTATUS(plugin.registerCommand(namePrefix + "XMLImport", FireRenderXmlImportCmd::creator, FireRenderXmlImportCmd::newSyntax)); + + CHECK_MSTATUS(plugin.registerCommand(namePrefix + "MaterialXImport", FireRenderMaterialXImportCmd::creator, FireRenderMaterialXImportCmd::newSyntax)); //// CHECK_MSTATUS(plugin.registerCommand(namePrefix + "ImageComparing", FireRenderImageComparing::creator, FireRenderImageComparing::newSyntax)); @@ -865,6 +869,11 @@ MStatus initializePlugin(MObject obj) MPxNode::kDependNode, &UserClassify)); // force load shader UI template to use callback functions MGlobal::executeCommand("source AERPRToonMaterialTemplate"); + + CHECK_MSTATUS(plugin.registerNode(namePrefix + "MaterialXMaterial", FireMaya::MaterialXMaterial::FRTypeID(), + FireMaya::MaterialXMaterial::creator, + FireMaya::MaterialXMaterial::initialize, + MPxNode::kDependNode, &UserClassify)); CHECK_MSTATUS(status); @@ -1092,6 +1101,8 @@ MStatus uninitializePlugin(MObject obj) CHECK_MSTATUS(plugin.deregisterCommand(namePrefix + "ImageComparing")); // + CHECK_MSTATUS(plugin.deregisterCommand(namePrefix + "MaterialXImport")); + if (MGlobal::mayaState() != MGlobal::kBatch) { CHECK_MSTATUS(MSwatchRenderRegister::unregisterSwatchRender("swatchFireRenderMaterial")); diff --git a/FireRender.Maya.Src/python/fireRender/client.py b/FireRender.Maya.Src/python/fireRender/client.py new file mode 100644 index 00000000..fcca5202 --- /dev/null +++ b/FireRender.Maya.Src/python/fireRender/client.py @@ -0,0 +1,199 @@ +import urllib.request +import json +import os +from enum import Enum +from typing import Dict +from urllib.parse import ( + urlencode, unquote, urlparse, parse_qsl, urlunparse, urljoin +) + + +class MatlibSession: + def __init__(self, *args, **kwargs): + pass + + @staticmethod + def add_url_params(url: str, params: Dict): + """ Add GET params to provided URL being aware of existing. + + :param url: string of target URL + :param params: dict containing requested params to be added + :return: string with updated URL + """ + parsed_url = urlparse(unquote(url)) + query_dict = dict(parse_qsl(parsed_url.query)) + # convert bool and dict to json strings + query_dict = { + k: json.dumps(v) if isinstance(v, (bool, dict)) else v + for k, v in query_dict.items() + } + query_dict.update(params) + parsed_url = parsed_url._replace(query=urlencode(query_dict, doseq=True)) + return urlunparse(parsed_url) + + @staticmethod + def get_last_url_path(url: str): + return urlparse(url).path.rsplit('/', 1)[-1] + + @staticmethod + def decode_json_response(response: str): + return json.loads(response.decode("utf-8")) + + +class MatlibEndpoint(Enum): + PREFIX = 'api' + REGISTRATION = 'registration' + AUTH = 'auth' + AUTH_LOGIN = 'auth/login' + AUTH_SOCIAL = 'auth/social' + MATERIALS = 'materials' + CATEGORIES = 'categories' + COLLECTIONS = 'collections' + TAGS = 'tags' + RENDERS = 'renders' + PACKAGES = 'packages' + + +class MatlibEntityClient: + endpoint = None + session = None + base = '' + + def __init__( + self, + session: MatlibSession, + base: str, + endpoint: MatlibEndpoint, + ): + self.session = session + self.base = base + self.endpoint = endpoint + + @property + def base_url(self): + return urljoin(self.base, '/{}/{}/'.format(MatlibEndpoint.PREFIX.value, self.endpoint.value)) + + def _get_list(self, url: str = None, limit: int = None, offset: int = None, params: dict = None): + url = urljoin(base=self.base_url, url=url) + if params is not None: + url = self.session.add_url_params(url, params) + url = self.session.add_url_params(url, {'limit': limit, 'offset': offset}) + request = urllib.request.Request(url) + response = urllib.request.urlopen(request) + response_content = json.loads(response.read().decode("utf-8")) + return response_content['results'] + + def _get_by_id(self, item_id: str, url: str = None): + url = urljoin(base=self.base_url, url=url) + url = urljoin(base=url, url='{}/'.format(item_id)) + request = urllib.request.Request(url) + response = urllib.request.urlopen(request) + response_content = json.loads(response.read().decode("utf-8")) + return response_content + + def _download(self, url: str, callback = None, target_dir: str = None, filename: str = None): + response = urllib.request.urlopen(url) + length = response.getheader('content-length') + if length: + length = int(length) + blocksize = max(0x1000, length//100) + else: + blocksize = 1000000 # just made something up + + if not filename: + filename = self.session.get_last_url_path(response.url) or 'file' + if not target_dir: + target_dir = '.' + full_filename = os.path.abspath(os.path.join(target_dir, filename)) + with open(full_filename, 'wb') as file: + size = 0 + while True: + buf = response.read(blocksize) + if not buf: + break + file.write(buf) + size += len(buf) + if callback: + if not callback(size, length): + break + if length and size != length and os.path.exists(full_filename): + os.remove(full_filename) + raise EOFError + + +class MatlibEntityListClient(MatlibEntityClient): + def __init__(self, *args, **kwargs): + super(MatlibEntityListClient, self).__init__(*args, **kwargs) + + def get_list(self, limit: int, offset: int, params: dict = None): + return self._get_list(limit=limit, offset=offset, params=params) + + def get(self, item_id: str): + return self._get_by_id(item_id=item_id) + + +class MatlibMaterialsClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibMaterialsClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.MATERIALS) + + +class MatlibCategoriesClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibCategoriesClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.CATEGORIES) + + +class MatlibCollectionsClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibCollectionsClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.COLLECTIONS) + + +class MatlibTagsClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibTagsClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.TAGS) + + +class MatlibRendersClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibRendersClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.RENDERS) + self._imageurl = self.base.replace('https://api.', 'https://image.') + + def download(self, item_id: str, callback = None, target_dir: str = None, filename: str = None): + self._download( + url=urljoin(self.base_url, '{}/download/'.format(item_id)), callback=callback, + target_dir=target_dir, filename=filename + ) + + def download_thumbnail(self, item_id: str, callback = None, target_dir: str = None, filename: str = None): + self._download( + url=urljoin(self._imageurl, '{}_thumbnail.jpeg'.format(item_id)), callback=callback, + target_dir=target_dir, filename=filename + ) + + +class MatlibPackagesClient(MatlibEntityListClient): + def __init__(self, *args, **kwargs): + super(MatlibPackagesClient, self).__init__(*args, **kwargs, endpoint=MatlibEndpoint.PACKAGES) + + def download(self, item_id: str, callback = None, target_dir: str = None, filename: str = None): + self._download( + url=urljoin(self.base_url, '{}/download/'.format(item_id)), callback=callback, + target_dir=target_dir, filename=filename + ) + + +class MatlibClient: + + def __init__(self, host: str): + """ + Web Material Library API Client + :param host (str): Web Material Library host (example: https://web.material.library.com + """ + self.host = host + self.session = MatlibSession() + + self.materials = MatlibMaterialsClient(session=self.session, base=self.host) + self.collections = MatlibCollectionsClient(session=self.session, base=self.host) + self.categories = MatlibCategoriesClient(session=self.session, base=self.host) + self.tags = MatlibTagsClient(session=self.session, base=self.host) + self.renders = MatlibRendersClient(session=self.session, base=self.host) + self.packages = MatlibPackagesClient(session=self.session, base=self.host) diff --git a/FireRender.Maya.Src/python/fireRender/fireRenderMenu.py b/FireRender.Maya.Src/python/fireRender/fireRenderMenu.py index 8da27a58..55e21201 100644 --- a/FireRender.Maya.Src/python/fireRender/fireRenderMenu.py +++ b/FireRender.Maya.Src/python/fireRender/fireRenderMenu.py @@ -64,6 +64,9 @@ def importRPRMaterialsXml(data): def showRPRMaterialLibrary(value): mel.eval('source shelfCommands.mel; openMaterialLibraryRPR();') +def showRPRMaterialXLibrary(value): + mel.eval('source shelfCommands.mel; openMaterialLibrary2RPR();') + def enableDebugTracing(value): maya.cmds.fireRender(d=value) @@ -120,6 +123,7 @@ def createFireRenderMenu(): maya.cmds.menuItem("FrImportMaterialXML", label="Import XML Material", p=frImportMenu, c=importRPRMaterialsXml) maya.cmds.menuItem("FrRPRMaterialLibrary", label="Radeon ProRender Material Library", p=showFireRenderMenuCtrl, c=showRPRMaterialLibrary) + maya.cmds.menuItem("FrMaterialXMaterialLibrary", label="AMD MaterialX Library", p=showFireRenderMenuCtrl, c=showRPRMaterialXLibrary) maya.cmds.menuItem( divider=True, p=showFireRenderMenuCtrl ) diff --git a/FireRender.Maya.Src/python/fireRender/rprMaterialXBrowser.py b/FireRender.Maya.Src/python/fireRender/rprMaterialXBrowser.py new file mode 100644 index 00000000..b1a57dcb --- /dev/null +++ b/FireRender.Maya.Src/python/fireRender/rprMaterialXBrowser.py @@ -0,0 +1,759 @@ +# +# Copyright 2020 Advanced Micro Devices, Inc +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import maya.cmds as cmds +import maya.mel as mel +import os +import json +import math +from functools import partial +from sys import platform +from fireRender.client import MatlibClient +import zipfile + + +# Show the material browser window. +# ----------------------------------------------------------------------------- +def show() : + RPRMaterialBrowser().show() + +# A material browser window for Radeon Pro Render materials. +# ----------------------------------------------------------------------------- +class RPRMaterialBrowser(object) : + + # Constructor. + # ----------------------------------------------------------------------------- + def __init__(self, *args) : + + # Character lengths used to calculate label widths. + self.charLengths = [3, 3, 4, 7, 6, 9, 9, 3, 3, 3, 5, 8, 3, 4, 3, 4, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 3, 3, 8, 8, 8, 5, 11, 7, 7, 7, 8, 6, 6, 8, 8, 3, 4, 6, 5, 10, + 8, 9, 6, 9, 7, 6, 5, 8, 7, 11, 6, 5, 6, 3, 4, 3, 8, 5, 3, 6, 7, 5, 7, + 6, 4, 7, 7, 3, 3, 6, 3, 9, 7, 7, 7, 7, 4, 5, 4, 7, 5, 9, 5, 5, 5, 3, + 3, 3, 8, 6, 6, 0, 3, 6, 4, 9, 4, 4, 4, 13, 6, 3, 10, 0, 6, 0, 0, 3, 3, + 4, 4, 4, 6, 11, 4, 9, 5, 3, 10, 0, 5, 5, 3, 3, 6, 6, 7, 6, 3, 5, 5, + 10, 4, 6, 8, 0, 10, 5, 4, 8, 4, 4, 3, 7, 5, 3, 2, 4, 5, 6, 10, 10, 10, + 5, 7, 7, 7, 7, 7, 7, 9, 7, 6, 6, 6, 6, 3, 3, 3, 3, 8, 8, 9, 9, 9, 9, + 9, 8, 9, 8, 8, 8, 8, 5, 6, 6, 6, 6, 6, 6, 6, 6, 9, 5, 6, 6, 6, 6, 3, + 3, 3, 3, 7, 7, 7, 7, 7, 7, 7, 8, 7, 7, 7, 7, 7, 5, 7, 5] + + # Panel background color. + self.backgroundColor = [0.16862745098039217, 0.16862745098039217, 0.16862745098039217] + + # Set the default material icon size. + self.setMaterialIconSize(self.getDefaultMaterialIconSize()) + + # Show the material browser. + # ----------------------------------------------------------------------------- + def show(self) : + + #load all material at one time. It's possible to rework to load in chunks if needs + maxElementCount = 10000 + + self.matlibClient = MatlibClient("https://api.matlib.gpuopen.com") + self.categoryListData = self.matlibClient.categories.get_list(maxElementCount, 0) + self.pathRootThumbnail = os.environ["USERPROFILE"] + "/Documents/Maya/RprUsd/WebMatlibCache" + os.makedirs(self.pathRootThumbnail, exist_ok=True) + + if len(self.categoryListData) <= 0 : + print("ML Log: ERROR: We couldn't load categories from the Web") + return + + self.categoryDict = dict() + + for category in self.categoryListData : + self.categoryDict[category["id"]] = category + + self.tags = self.matlibClient.tags.get_list(maxElementCount, 0) + + self.tagDict = dict() + for tag in self.tags : + self.tagDict[tag["id"]] = tag["title"] + + self.materialListData = self.matlibClient.materials.get_list(maxElementCount, 0) + self.materialDict = dict() + + self.materialByCategory = dict() + + for material in self.materialListData : + self.materialDict[material["id"]] = material + categoryId = material["category"] + + if categoryId not in self.materialByCategory : + self.materialByCategory[categoryId] = list() + self.materialByCategory[categoryId].append(material) + + self.createLayout() + + # Create the browser layout. + # ----------------------------------------------------------------------------- + def createLayout(self) : + + # Delete any existing window. + if (cmds.window("RPRMaterialBrowserWindow", exists = True)) : + cmds.deleteUI("RPRMaterialBrowserWindow") + + # Create a new window. + self.window = cmds.window("RPRMaterialBrowserWindow", + widthHeight=(1200, 700), + title="Radeon ProRender MaterialX Browser") + + # Place UI sections in a horizontal 3 pane layout. + paneLayout = cmds.paneLayout(configuration='vertical3', staticWidthPane=3, + separatorMovedCommand=self.updateMaterialsLayout) + + cmds.paneLayout(paneLayout, edit=True, paneSize=(1, 20, 100)) + cmds.paneLayout(paneLayout, edit=True, paneSize=(2, 52, 100)) + cmds.paneLayout(paneLayout, edit=True, paneSize=(3, 28, 100)) + + # Create UI sections. + self.createCategoriesLayout() + self.createMaterialsLayout() + self.createSelectedLayout() + + cmds.setParent('..') + + # Select the first material of the first category. + self.selectCategory(0) + self.selectMaterial(0) + + # Show the material browser window. + cmds.showWindow(self.window) + + # Initialize the layout. + self.initializeLayout(); + + # Create the material categories layout. + # ----------------------------------------------------------------------------- + def createCategoriesLayout(self) : + + # Create tab, form and scroll layouts. + tabLayout = cmds.tabLayout(innerMarginWidth=5, innerMarginHeight=15, borderStyle="full") + formLayout = cmds.formLayout(numberOfDivisions=100) + scrollLayout = cmds.scrollLayout(backgroundColor=self.backgroundColor, + horizontalScrollBarThickness=0, + verticalScrollBarThickness=16, + childResizable=True) + + # Lay out categories vertically. + catergories = cmds.columnLayout() + + # Add layouts with index based names for easy lookup. + index = 0 + + for category in self.categoryListData : + cmds.iconTextButton("RPRCategory" + str(index), style='iconAndTextHorizontal', + image='material_browser/folder_closed.png', + label=category["title"], height=20, + command=partial(self.selectCategory, index)) + index += 1 + + # Assign the form to the tab. + cmds.tabLayout(tabLayout, edit=True, tabLabel=((formLayout, 'Categories'))) + + # Lay out components within the form. + cmds.formLayout(formLayout, edit=True, + attachForm=[(scrollLayout, 'top', 5), (scrollLayout, 'left', 5), + (scrollLayout, 'bottom', 5), (scrollLayout, 'right', 5)]) + + cmds.setParent('..') + cmds.setParent('..') + cmds.setParent('..') + cmds.setParent('..') + + def getMaterialFileName(self, material) : + render_id = material["renders_order"][0] + return render_id + ".png" + + def getMaterialFullPath(self, fileName) : + return os.path.join(self.pathRootThumbnail, fileName) + + def onSortModeChanged(self, modeName) : + mode = cmds.optionMenu(self.sortDropdown, q=True, select=True) + self.sortMaterials(mode) + self.populateMaterialsInternal() + + # 1 - No Sort, 2 - Ascending, 3 - Descending + def sortMaterials(self, mode) : + if mode == 1 : + self.materials = self.nonSortedMaterials.copy() + else : + self.materials = sorted(self.nonSortedMaterials, key=lambda material: material["title"], reverse = (mode == 3) ) + # Create the materials layout. + # ----------------------------------------------------------------------------- + def createMaterialsLayout(self) : + + # Create the tab and form layouts. + self.materialsTab = cmds.tabLayout(borderStyle="full") + self.materialsForm = cmds.formLayout(numberOfDivisions=100) + + # Add the icon size slider. + iconSizeRow = cmds.rowLayout("RPRIconSize", numberOfColumns=2, columnWidth=(1, 22)) + cmds.image(image='material_browser/thumbnails.png') + self.iconSizeSlider = cmds.intSlider(width=120, step=1, minValue=1, maxValue=4, + value=self.getDefaultMaterialIconSize(), + dragCommand=self.updateMaterialIconSize) + cmds.setParent('..') + + # Add the search field. + searchRow = cmds.rowLayout(numberOfColumns=4) + + helpText = cmds.text(label="Sort Mode: ") + self.sortDropdown = cmds.optionMenu(cc=self.onSortModeChanged) + cmds.menuItem(p=self.sortDropdown, l="No Sort") + cmds.menuItem(p=self.sortDropdown, l="Sort Ascending") + cmds.menuItem(p=self.sortDropdown, l="Sort Descending") + + cmds.image(image='material_browser/search.png') + self.searchField = cmds.textField(placeholderText="Search...", width=250, height=22, + textChangedCommand=self.searchMaterials) + + cmds.setParent('..') + + # Add help text. + helpText = cmds.text(label="Select material, choose package you wish and click the Download button.") + + # Add the background canvas. + bg = cmds.canvas(rgbValue=self.backgroundColor) + + # Add the scroll layout that will contain the material icons. + self.materialsContainer = cmds.scrollLayout(backgroundColor=self.backgroundColor, childResizable=True, + resizeCommand=self.updateMaterialsLayout) + cmds.setParent('..') + + # Assign the form to the tab. + cmds.tabLayout(self.materialsTab, edit=True, tabLabel=((self.materialsForm, 'Materials'))) + + # Lay out components within the form. + cmds.formLayout(self.materialsForm, edit=True, + attachForm=[(searchRow, 'top', 5), (searchRow, 'right', 5), + (iconSizeRow, 'top', 10), (iconSizeRow, 'left', 6), + (self.materialsContainer, 'left', 10), (self.materialsContainer, 'bottom', 10), + (self.materialsContainer, 'right', 5), + (bg, 'left', 5), (bg, 'bottom', 5), (bg, 'right', 5), + (helpText, 'left', 10), (helpText, 'bottom', 8)], + attachNone=[(searchRow, 'bottom'), (searchRow, 'left'), + (iconSizeRow, 'bottom'), (iconSizeRow, 'right'), + (helpText, 'right'), (helpText, 'top')], + attachControl=[(self.materialsContainer, 'top', 10, searchRow), + (self.materialsContainer, 'bottom', 10, helpText), + (bg, 'top', 5, searchRow), + (bg, 'bottom', 5, helpText)]) + + cmds.setParent('..') + cmds.setParent('..') + + # Initialize the currently selected category index. + self.selectedCategoryIndex = 0 + + + # Create the selected material layout. + # ----------------------------------------------------------------------------- + def createSelectedLayout(self) : + + # Create a pane layout to contain material info and preview. + paneLayout = cmds.paneLayout("RPRSelectedPane", configuration='horizontal2', staticHeightPane=2, + separatorMovedCommand=self.updatePreviewLayout) + + self.createInfoLayout() + self.createPreviewLayout() + + + # Perform initial layout tasks. + # ----------------------------------------------------------------------------- + def initializeLayout(self) : + + # Configure the selected pane. + cmds.paneLayout("RPRSelectedPane", e=True, paneSize=(1, 100, 50)) + cmds.paneLayout("RPRSelectedPane", e=True, paneSize=(2, 100, 50)) + + # Perform an initial preview layout update. + self.updatePreviewLayout() + + + # Create the info layout. + # ----------------------------------------------------------------------------- + def createInfoLayout(self) : + + # Create tab and form layouts. + tabLayout = cmds.tabLayout(innerMarginWidth=8, innerMarginHeight=8, borderStyle="full") + formLayout = cmds.formLayout(numberOfDivisions=100) + + # Add the RPR logo. + logoBackground = cmds.canvas("RPRLogoBackground", rgbValue=[0, 0, 0]) + logo = cmds.iconTextStaticLabel("RPRLogo", style='iconOnly', + image="MaterialXLibraryLogo.png", width=1, height=1) + + # Add material info text. + columnLayout = cmds.columnLayout(rowSpacing=5) + + cmds.text(label="Category:", font="boldLabelFont") + cmds.text("RPRCategoryText", recomputeSize=False) + cmds.canvas(height=10) + cmds.text(label="Name:", font="boldLabelFont") + cmds.text("RPRNameText", recomputeSize=False) + cmds.canvas(height=10) + cmds.text(label="File name:", font="boldLabelFont") + cmds.text("RPRFileNameText", recomputeSize=False) + cmds.canvas(height=10) + cmds.text(label="Type:", font="boldLabelFont") + cmds.text("RPRMaterialType", recomputeSize=False) + cmds.canvas(height=10) + cmds.text(label="License:", font="boldLabelFont") + cmds.text("RPRMaterialLicense", recomputeSize=False) + cmds.canvas(height=10) + + self.downloadButtonLayout = cmds.columnLayout(rowSpacing=5, parent=formLayout) + + self.downloadPackageDropdown = cmds.optionMenu(parent = self.downloadButtonLayout) + downloadButton = cmds.button(label="Download", parent = self.downloadButtonLayout, w=100, h=30, command=self.downloadMaterial) + + cmds.setParent('..') + cmds.setParent('..') + cmds.setParent('..') + + # Assign the form to the tab. + cmds.tabLayout(tabLayout, edit=True, tabLabel=((formLayout, 'Info'))) + + cmds.formLayout(formLayout, edit=True, + attachControl=[(columnLayout, 'top', 10, logo)], + attachForm=[(logo, 'left', 8), (logo, 'right', 8), (logo, 'top', 8), + (logoBackground, 'left', 8), (logoBackground, 'right', 8), + (logoBackground, 'top', 8), + (columnLayout, 'left', 10), (columnLayout, 'right', 10), + (self.downloadButtonLayout, 'left', 10), + (self.downloadButtonLayout, 'bottom', 10), + (self.downloadButtonLayout, 'right', 10)]) + + + # Create the preview layout. + # ----------------------------------------------------------------------------- + def createPreviewLayout(self) : + + # Create tab layout. + tabLayout = cmds.tabLayout(borderStyle="full") + + # Add horizontal and vertical flow layouts and + # spacers to center the image in the preview area. + flowLayout = cmds.flowLayout("RPRPreviewArea") + cmds.canvas("RPRHSpacer", width=1, height=1) + + cmds.flowLayout("RPRPreviewFlow", vertical=True) + cmds.canvas("RPRVSpacer", width=1, height=1) + + cmds.iconTextStaticLabel("RPRPreviewImage", style='iconOnly', width=1, height=1) + cmds.setParent('..') + + cmds.setParent('..') + cmds.setParent('..') + cmds.setParent('..') + + # Assign the top flow layout to the tab. + cmds.tabLayout(tabLayout, edit=True, tabLabel=((flowLayout, 'Preview'))) + + + # Select a material category by index. + # ----------------------------------------------------------------------------- + def selectCategory(self, index) : + + # Populate the materials view from the selected category. + self.materials = self.materialByCategory[self.categoryListData[index]["id"]] + self.populateMaterials() + + # Update the folder open / closed state on the category list. + cmds.iconTextButton("RPRCategory" + str(self.selectedCategoryIndex), + edit=True, image='material_browser/folder_closed.png') + + cmds.iconTextButton("RPRCategory" + str(index), + edit=True, image='material_browser/folder_open.png') + + self.selectedCategoryIndex = index + + # Clear the search field. + cmds.textField(self.searchField, edit=True, text="") + + def downloadPackageCallback(self, size, length) : + + percent = int(100 * size / length) + cmds.progressWindow( edit=True, progress=percent, status=('Downloading: ' + str(percent) + '%' ) ) + + return True + + def downloadMaterial(self, *args) : + menuItems = cmds.optionMenu(self.downloadPackageDropdown, q=True, itemListLong=True) # itemListLong returns the children + index = cmds.optionMenu(self.downloadPackageDropdown, q=True, select=True) - 1 + + package = self.packageDataList[index] + packageId = package["id"] + + optionVarNameRecentDirectory = "RprUsd_DownloadPackageRecentDirectory" + previousDirectoryUsed = "" + if cmds.optionVar(exists=optionVarNameRecentDirectory) : + previousDirectoryUsed = cmds.optionVar(query=optionVarNameRecentDirectory) + + path = cmds.fileDialog2(cap="Select A Directory", startingDirectory=previousDirectoryUsed, fm=3) + cmds.progressWindow( title='Downloading Package',progress=0,status='downloading: 0%',isInterruptable=False) + if path is not None : + print("ML Log: start downloading packageId=" + packageId) + self.matlibClient.packages.download(packageId, self.downloadPackageCallback, path[0], package["file"]) + cmds.optionVar(sv=(optionVarNameRecentDirectory, path[0])) + + zipFileName = os.path.join(path[0], package["file"]) + + fullPathToExtract = os.path.join(path[0], os.path.splitext(package["file"])[0]) + os.makedirs(fullPathToExtract, exist_ok=True) + + #unzip + with zipfile.ZipFile(zipFileName, 'r') as zip_ref: + zip_ref.extractall(fullPathToExtract) + # remove zip-archive + os.remove(zipFileName) + + #load downloaded material into Maya + for mtlxfile in os.listdir(fullPathToExtract): + if mtlxfile.endswith(".mtlx"): + pathToMaterialJoined = os.path.join(fullPathToExtract, mtlxfile) + self.importSelectedMaterial(pathToMaterialJoined) + + cmds.progressWindow(endProgress=1) + + def updateSelectedMaterialPanel(self, fileName, categoryName, materialName, materialType, license) : + + def sortAccordingPackageSize(package) : + numberDirtyString = package["size"] + return float(''.join(c for c in numberDirtyString if (c.isdigit() or c =='.'))) + + imageFileName = self.getMaterialFullPath(fileName) + + cmds.iconTextStaticLabel("RPRPreviewImage", edit=True, image=imageFileName) + cmds.text("RPRCategoryText", edit=True, label=categoryName) + cmds.text("RPRNameText", edit=True, label=materialName) + cmds.text("RPRFileNameText", edit=True, label=fileName) + cmds.text("RPRMaterialType", edit=True, label=materialType) + cmds.text("RPRMaterialLicense", edit=True, label=license) + + params = dict() + params["material"] = self.selectedMaterial["id"] + self.packageDataList = self.matlibClient.packages.get_list(limit=100, offset=0, params = params) + + self.packageDataList.sort(key=sortAccordingPackageSize) + + menuItems = cmds.optionMenu(self.downloadPackageDropdown, q=True, itemListLong=True) # itemListLong returns the children + if menuItems: + cmds.deleteUI(menuItems) + + index = 0 + for package in self.packageDataList: + menuItemName = "Package: " + package["label"] + " ( " + package["size"] + " )" + cmd = partial(self.downloadMaterial, package) + + cmds.menuItem(p=self.downloadPackageDropdown, l=menuItemName, data=index) + + index += 1 + + def selectMaterial(self, materialIndex) : + + self.selectedMaterial = self.materials[materialIndex] + material = self.selectedMaterial + fileName = self.getMaterialFileName(self.selectedMaterial) + + self.updateSelectedMaterialPanel(fileName, self.categoryDict[material["category"]]["title"], material["title"], material["material_type"], material["license"]) + + self.updatePreviewLayout() + + # Update the height of the materials flow layout + # based on the width of its container and the + # number of children. This is required so + # a scrollable flow layout works properly. + # ----------------------------------------------------------------------------- + def updateMaterialsLayout(self) : + + # Determine the width of the material view + # and the total number of materials to display. + width = cmds.scrollLayout(self.materialsContainer, query=True, width=True) + count = cmds.flowLayout("RPRMaterialsFlow", query=True, numberOfChildren=True) + + if (count <= 0) : + return + + # Calculate the number of materials that can fit on + # a row and the total required height of the container. + perRow = max(1, math.floor((width - 18) / self.cellWidth)) + height = math.ceil(count / perRow) * self.cellHeight + + cmds.flowLayout("RPRMaterialsFlow", edit=True, height=height) + + # Adjust the form to be narrower than the tab that + # contains it. This is required so the form doesn't + # prevent the tab layout shrinking. + materialsWidth = cmds.tabLayout(self.materialsTab, query=True, width=True) + newMaterialsWidth = max(1, materialsWidth - 10) + cmds.formLayout(self.materialsForm, edit=True, width=newMaterialsWidth) + + # Hide the icon size slider if there isn't enough room for it. + cmds.rowLayout("RPRIconSize", edit=True, visible=(materialsWidth > 440)) + + # Update the preview layout. + self.updatePreviewLayout() + + + # Update the size and position of the preview image. + # This is required because the script-able Maya UI + # doesn't provide enough control over layout. + # ----------------------------------------------------------------------------- + def updatePreviewLayout(self) : + + # Determine the size of the preview area. + width = cmds.flowLayout("RPRPreviewArea", query=True, width=True) + height = cmds.flowLayout("RPRPreviewArea", query=True, height=True) + + # Choose the smallest dimension and shrink + # slightly so the enclosing layouts can shrink. + size = min(width, height) - 10 + + # Calculate the horizontal and vertical + # offsets required to center the preview image. + hOffset = max(0, (width - size) / 2.0) + vOffset = max(0, (height - size) / 2.0) + + # Clamp the size to a minimum of 64. + newWidth = max(64, width - 10) + newHeight = max(64, height - 10) + size = max(64, size) + + # Update the layout and image sizes. + cmds.flowLayout("RPRPreviewFlow", edit=True, width=newWidth, height=newHeight) + cmds.iconTextStaticLabel("RPRPreviewImage", edit=True, width=size, height=size) + cmds.canvas("RPRHSpacer", edit=True, width=hOffset, height=1) + cmds.canvas("RPRVSpacer", edit=True, width=1, height=vOffset) + + # Update the RPR logo size. + logoWidth = cmds.iconTextStaticLabel("RPRLogo", query=True, width=True) + logoHeight = max(min(logoWidth * 0.16, 60), 1) + cmds.iconTextStaticLabel("RPRLogo", edit=True, height=logoHeight) + cmds.canvas("RPRLogoBackground", edit=True, height=logoHeight) + + + # Update the material icon size from the slider. + # ----------------------------------------------------------------------------- + def updateMaterialIconSize(self, *args) : + + # Calculate the new icon size and repopulate the view. + value = cmds.intSlider(self.iconSizeSlider, query=True, value=True) + self.setMaterialIconSize(value) + self.populateMaterials() + + # Save the size setting to user preferences. + cmds.optionVar(intValue=['RPRIconSize', value]) + + + # Set the size of the material icons. + # ----------------------------------------------------------------------------- + def setMaterialIconSize(self, value) : + + # Set the size to a power of two. + size = pow(2, value + 4) + self.iconSize = size + + # Use horizontal cells for small icons + # and vertical cells for large icons. + if (size < 64) : + self.cellWidth = size + 200 + self.cellHeight = size + 10 + else : + self.cellWidth = size + 10 + self.cellHeight = size + 30 + + + # Get the default icon size value. + # ----------------------------------------------------------------------------- + def getDefaultMaterialIconSize(self) : + + # Query user preferences. + if (cmds.optionVar(exists="RPRIconSize")) : + return cmds.optionVar(query="RPRIconSize") + + # Use a default value if no preference found. + return 3 + + + # Search materials for the specified string. + # ----------------------------------------------------------------------------- + def searchMaterials(self, *args) : + + # Convert the search string to lower + # case so the search is not case sensitive. + searchString = cmds.textField(self.searchField, query=True, text=True).lower() + + # Check that the string is long enough + # to search and not whitespace. + if (len(searchString) < 2 or searchString.isspace()) : + return + + # Set current materials to the search result. + self.materials = [] + + for material in self.materialListData: + if (searchString in material["title"].lower()) : + self.materials.append(material) + else : + for tagId in material["tags"] : + if (searchString == self.tagDict[tagId].lower()) : + self.materials.append(material) + break; + + # Repopulate the material view. + self.populateMaterials() + + # Populate the materials view with a list of materials. + # ----------------------------------------------------------------------------- + + def populateMaterials(self) : + self.nonSortedMaterials = self.materials.copy() + self.sortMaterials(cmds.optionMenu(self.sortDropdown, q=True, select=True)) + self.populateMaterialsInternal() + + + def populateMaterialsInternal(self) : + # Remove any existing materials. + if (cmds.layout("RPRMaterialsFlow", exists=True)) : + cmds.deleteUI("RPRMaterialsFlow", layout=True) + + # Ensure that the material container is the current parent. + cmds.setParent(self.materialsContainer) + + # Create the new flow layout. + cmds.flowLayout("RPRMaterialsFlow", columnSpacing=0, wrap=True) + + materialIndex = 0 + progressBarShown = False + + # Add materials for the selected category. + for material in self.materials : + fileName = self.getMaterialFileName(material) + cmd = partial(self.selectMaterial, materialIndex) + imageFileName = self.getMaterialFullPath(fileName) + + # Checks if end condition has been reached + render_id = material["renders_order"][0] + + if (not os.path.isfile(imageFileName)) : + if (not progressBarShown) : + cmds.progressWindow( title='Opening category',progress=0,status='opening: 0%',isInterruptable=False ) + progressBarShown = True + self.matlibClient.renders.download_thumbnail(render_id, None, self.pathRootThumbnail, fileName) + + if (progressBarShown) : + percent = int(100 * materialIndex / len(self.materials)) + cmds.progressWindow( edit=True, progress=percent, status=('opening: ' + str(percent) + '%' ) ) + + + materialName = material["title"] + + # Horizontal layout for small icons. + if (self.iconSize < 64) : + iconWidth = self.iconSize + 5 + cmds.rowLayout(width=self.cellWidth, height=self.cellHeight, numberOfColumns=2, + columnWidth2=(self.iconSize, self.cellWidth - iconWidth - 5)) + + cmds.iconTextButton(style='iconOnly', image=imageFileName, width=self.iconSize, + height=self.iconSize, command=cmd) + + cmds.iconTextButton(style='textOnly', height=self.iconSize, + label=self.getTruncatedText(materialName, self.cellWidth - iconWidth - 5), + align="left", command=cmd) + + # Vertical layout for large icons. + else : + cmds.columnLayout(width=self.cellWidth, height=self.cellHeight) + cmds.iconTextButton(style='iconOnly', image=imageFileName, width=self.iconSize, + height=self.iconSize, command=cmd) + cmds.text(label=self.getTruncatedText(materialName, self.iconSize), + align="center", width=self.iconSize) + + cmds.setParent('..') + materialIndex += 1 + + if (progressBarShown) : + cmds.progressWindow( endProgress=1 ) + # Perform an initial layout update. + self.updateMaterialsLayout() + + + # Import the currently selected material into Maya. + # ----------------------------------------------------------------------------- + def importSelectedMaterial(self, filepath) : + print("ML Log: importSelectedMaterial") + self.importMaterial(self.selectedMaterial, filepath) + + def importMaterial(self, material, filepath) : + + print("ML Log: importMaterial "+filepath) + print("ML Log: material name = "+material["title"]) + cmds.RPRMaterialXImport(file=filepath, name=material["title"]) + + + # Return the width of a text UI element given it's label string. + # ----------------------------------------------------------------------------- + def getTextWidth(self, text) : + + # Iterate over the string characters adding character widths. + width = 0 + charCount = len(self.charLengths) + + for c in text : + i = ord(c) - 32 + + if (i < 0 or i >= charCount) : + continue + + width += self.charLengths[i] + + return width + + + # Return a string truncated to fit in the required width if necessary. + # ----------------------------------------------------------------------------- + def getTruncatedText(self, text, requiredWidth) : + + # Return the string immediately if it fits within the required width. + if (self.getTextWidth(text) < requiredWidth) : + return text + + # Reserve enough space for the ellipsis (...). + requiredWidth -= (self.getTextWidth(".") * 3) + + # Iterate over the string characters. + width = 0 + charCount = len(self.charLengths) + truncated = "" + + for c in text : + + # Get the index of the character in the char lengths array. + i = ord(c) - 32 + if (i < 0 or i >= charCount) : + continue + + width += self.charLengths[i] + + # Add characters until the required width is reached. + if (width < requiredWidth) : + truncated += c + else : + break + + # Add the ellipsis. + if (len(truncated) < len(text)) : + truncated += "..." + + return truncated diff --git a/FireRender.Maya.Src/scripts/shelfCommands.mel b/FireRender.Maya.Src/scripts/shelfCommands.mel index 64dd85f5..d92aa76d 100644 --- a/FireRender.Maya.Src/scripts/shelfCommands.mel +++ b/FireRender.Maya.Src/scripts/shelfCommands.mel @@ -378,6 +378,16 @@ global proc openMaterialLibraryRPR() python("import fireRender.rpr_material_browser\nfireRender.rpr_material_browser.show()"); } +global proc openMaterialLibrary2RPR() +{ + print("openMaterialLibrary2RPR called from shelf!\n"); + + if (!checkPluginLoadedRPR()) + return; + + python("import fireRender.rprMaterialXBrowser\nfireRender.rprMaterialXBrowser.show()"); +} + global proc createSkyRPR() { if (!checkPluginLoadedRPR()) diff --git a/FireRender.Maya.Src/scripts/standard_surface.mtlx b/FireRender.Maya.Src/scripts/standard_surface.mtlx new file mode 100644 index 00000000..dedc6ca4 --- /dev/null +++ b/FireRender.Maya.Src/scripts/standard_surface.mtlx @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +