├── .gitignore ├── LICENSE.txt ├── README.md ├── RenderMan ├── RendermanButton.qml ├── RendermanPrefsPanel.qml ├── RendermanTextField.qml ├── icons │ ├── PxrDisney_hover.svg │ ├── PxrDisney_idle.svg │ ├── PxrSurface_hover.svg │ ├── PxrSurface_idle.svg │ ├── R_logo.svg │ ├── R_logo_white.svg │ ├── folder.svg │ └── icons.qrc ├── main.qml ├── plugin.json ├── plugintoolbar.qml ├── renderman.js ├── rmanAssetsSubstancePainter.py ├── rules.json └── toolbar.qml ├── img ├── RfSP_v0.gif ├── configure_dialog.jpg ├── open_configure_dialog.jpg └── shelf_buttons.jpg ├── renderman.rcc ├── renderman_for_sp.py ├── renderman_rules.json ├── resources ├── PxrDisney.svg └── PxrSurface.svg └── shaders └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # history dir 101 | .history 102 | 103 | log.txt 104 | rfsp_log.txt 105 | settings.ini 106 | .vscode 107 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Philippe Leprince 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RfSP 2 | 3 | :warning: This repo is obsolete ! Go to https://github.com/prman-pixar/RfSP for the latest version. :warning: 4 | 5 | ## RenderMan for Substance Painter 2.x 6 | 7 | This plugin exports your [Substance Painter](https://www.allegorithmic.com/products/substance-painter) project as one or more RenderManAsset. 8 | 9 | RenderManAsset is the format used by the preset browser that was introduced in [RenderMan For Maya](https://rmanwiki.pixar.com/display/REN/RenderMan+for+Maya) 21.0. It allows for easy material setup interchange and includes dependencies like textures or OSL shaders. 10 | 11 | ![demo](img/RfSP_v0.gif) 12 | 13 | [Full demo video of 0.1.1](https://youtu.be/ZEyT95aPFYk) 14 | 15 | ## Features 16 | 17 | ### ![Alt](RenderMan/icons/PxrDisney.png "PxrDisney") : Export to PxrDisney-based material 18 | 19 | The asset will use the PxrDisney bxdf to re-create the Substance Painter material. There are limitations though: 20 | 21 | * The Substance Painter project MUST use the pbr-metal-rough shader. 22 | * normals MUST be in directx format. 23 | * Opacity is not supported. 24 | 25 | ### ![Alt](RenderMan/icons/PxrSurface.png "PxrSurface") : Export to PxrSurface-based material 26 | 27 | * The Substance Painter project MUST use the pbr-metal-rough shader. 28 | * normals MUST be in directx format. 29 | * Opacity is not supported. 30 | 31 | ## Requirements 32 | 33 | This plugin will NOT work without the following software: 34 | 35 | * Substance Painter 2.3+ 36 | * RenderMan Pro Server 21.0+ 37 | * RenderMan For Maya 21.0+ 38 | * [Python 2.7+](https://www.python.org/downloads/release/python-2712/) (but not Python 3.x) 39 | 40 | ## Install 41 | 42 | * Download a zip archive from the github page 43 | * Un-zip the archive 44 | * Copy the RenderMan folder inside Substance Painter's plugin folder. 45 | > OSX: `/Users/yourlogin/Documents/Substance Painter 2/plugins` 46 | 47 | ## Known Issues 48 | 49 | * No progress indication during export: be patient ! 50 | * It takes time to export the maps and turn them into textures. The plugin will print a message in the log when done. 51 | 52 | ## Usage 53 | 54 | 1. On first use, open the "configure" dialog. 55 | 56 | ![Alt](img/open_configure_dialog.jpg "open config dialog") 57 | 58 | 1. Fill ALL fields of the dialog and click "Save", otherwise the export will fail. 59 | 60 | ![Alt](img/configure_dialog.jpg "open config dialog") 61 | 62 | 1. Once this initial configuration is done, the settings will be remembered even if you close Substance Painter. 63 | 64 | 1. Open a SP project and click one of the pixar buttons in the shelf. 65 | 66 | Hint: _Only the first one (PxrDisney) works for now._ 67 | 68 | ![Alt](img/shelf_buttons.jpg "open config dialog") 69 | 70 | ## Release notes 71 | 72 | ### 0.3.0 73 | 74 | * **new** 75 | * UDIM support 76 | * The export code converts SP images to RenderMan textures 77 | 78 | * **code** 79 | * Refactored javascript json export. 80 | 81 | ### 0.2.2 82 | 83 | * new 84 | * Added metadata to the asset files: user, description, resolution. 85 | * Compatibility chunk holds Substance Painter version. 86 | 87 | * Fixed 88 | * better match with PxrSurface. Updated rules.json to fix a number of issues: 89 | * roughness should not be linearized 90 | * metallicity should not be linearized 91 | * normal map should use directx orientation 92 | * exported normal map contains mesh normals + bump + normal map. 93 | 94 | * code 95 | * Added a new "settings" section to rules.json to set node param values in the shading graph. 96 | * Some refactoring 97 | 98 | ### 0.2.1 99 | 100 | * Fixed 101 | * Fix ui error in Substance Painter 2018 (was ok in SP 2.x) 102 | 103 | ### 0.2.0 104 | 105 | * New 106 | * Added support for RenderMan 22.x. 107 | * REMEMBER to change the SP prefs to point to RenderMan 22.x 108 | * The assets are now named after the SP project's name. 109 | * Fixed 110 | * The specular was incorrect when outputing for PxrSurface. 111 | * The correct renderman version number is saved in the asset file. 112 | * code 113 | * Refactored file path management. 114 | * Used logging module instead of my own. 115 | * Doc strings and PEP-8 fixes 116 | 117 | ### 0.1.4 118 | 119 | * Initial support for PxrSurface 120 | * Simply use the rough metal workflow in Substance Painter and it will be converted to work with PxrSurface. 121 | * Also fixed Issue #2, reported by dayelov. Thanks ! 122 | 123 | ### 0.1.2 124 | 125 | * windows file paths were not correctly serialized to json. 126 | * mention python dependency in README 127 | * Final cleanup was inadvertantly disabled 128 | * Fixed incorrect path on windows. 129 | 130 | ### 0.1.1 131 | 132 | * Added license text to all source files. 133 | 134 | ### 0.1.0 135 | 136 | * Initial Release 137 | * Implemented configure dialog to specify the path to RenderMan Pro Server and RenderMan For Maya. This is necessary because SP uses javascript, a sandboxed language that can not get access to environment variables. 138 | * Export all channels from all textureSets to png files. 139 | * Implemented basic export to PxrDisney-based asset. 140 | * Each TextureSet will be exported as a RenderManAsset directory. 141 | -------------------------------------------------------------------------------- /RenderMan/RendermanButton.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.3 28 | import QtQuick.Controls 1.4 29 | import QtQuick.Controls.Styles 1.4 30 | 31 | Button { 32 | property bool isDefaultButton 33 | isDefaultButton: false 34 | property color rmanblue: "#1e94e6" 35 | property color rmanbluebg: "#303A3F" 36 | 37 | style: ButtonStyle { 38 | background: Rectangle { 39 | implicitWidth: 50 40 | implicitHeight: 24 41 | border.width: isDefaultButton ? 2 : 1 42 | border.color: control.pressed ? rmanblue : isDefaultButton ? "#ccc" : "#222" 43 | radius: 4 44 | color: control.pressed ? "#323232" : control.hovered ? rmanbluebg : "#333" 45 | } 46 | label: Component { 47 | Text { 48 | text: control.text 49 | font.bold: isDefaultButton 50 | clip: true 51 | wrapMode: Text.WordWrap 52 | verticalAlignment: Text.AlignVCenter 53 | horizontalAlignment: Text.AlignHCenter 54 | anchors.fill: parent 55 | color: control.pressed ? "#FFFFFF" : control.hovered ? rmanblue : "#C8C8C8" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RenderMan/RendermanPrefsPanel.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.3 28 | import QtQuick.Window 2.2 29 | import QtQuick.Dialogs 1.2 30 | import QtQuick.Controls 1.4 31 | import QtQuick.Layouts 1.3 32 | import QtQuick.Controls.Styles 1.4 33 | 34 | 35 | Dialog 36 | { 37 | signal accepted() 38 | visible: false 39 | title: "RenderMan Export Preferences" 40 | 41 | function accept() 42 | { 43 | if (rmanPathField.text != "") 44 | { 45 | alg.settings.setValue("RMANTREE", rmanPathField.text) 46 | // alg.log.info("RMANTREE = "+rmanPathField.text) 47 | } 48 | if (rmsPathField.text != "") 49 | { 50 | alg.settings.setValue("RMSTREE", rmsPathField.text) 51 | // alg.log.info("RMSTREE = "+rmsPathField.text) 52 | } 53 | if (exportPathField.text != "") 54 | { 55 | alg.settings.setValue("saveTo", exportPathField.text) 56 | // alg.log.info("saveTo = "+exportPathField.text) 57 | } 58 | 59 | accepted() 60 | close() 61 | } 62 | 63 | function readPrefs() 64 | { 65 | rmanPathField.readPrefs() 66 | rmsPathField.readPrefs() 67 | exportPathField.readPrefs() 68 | } 69 | 70 | FocusScope 71 | { 72 | focus: true 73 | Keys.onPressed: { 74 | if (event.key === Qt.Key_Escape) 75 | { 76 | close() 77 | } 78 | else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) 79 | { 80 | accept() 81 | } 82 | } 83 | } 84 | 85 | contentItem: Rectangle 86 | { 87 | color: "#292929" 88 | implicitWidth: 600 89 | implicitHeight: 180 90 | anchors.fill: parent 91 | property var text_color: "#c0c0c0" 92 | 93 | Text 94 | { 95 | x:10; y:10 96 | height: 30 97 | text: "Please enter the location of RenderMan products." 98 | color: parent.text_color 99 | Layout.fillWidth: true 100 | Layout.fillHeight: true 101 | // anchors.margins: 10 102 | } 103 | 104 | RowLayout 105 | { 106 | id: rmanlayout 107 | x:10; y:40 108 | height: 30 109 | width: parent.width 110 | spacing: 6 111 | Layout.fillWidth: true 112 | 113 | Text 114 | { 115 | id: rmanPathLabel 116 | text: "RenderMan Pro Server:" 117 | horizontalAlignment: Text.AlignRight 118 | color: "#e6e6e6" 119 | Layout.minimumWidth: 150 120 | Layout.maximumWidth: 150 121 | } 122 | RendermanTextField 123 | { 124 | id: rmanPathField 125 | placeholderText: "path to renderman pro server directory" 126 | anchors.left: rmanPathLabel.right 127 | anchors.right: rmanPathButton.left 128 | anchors.leftMargin: 4 129 | anchors.rightMargin: 4 130 | 131 | function readPrefs() 132 | { 133 | text = alg.settings.value("RMANTREE") 134 | } 135 | 136 | Component.onCompleted: { 137 | readPrefs() 138 | } 139 | 140 | } 141 | RendermanButton 142 | { 143 | id:rmanPathButton 144 | text: "pick" 145 | onClicked: { 146 | // alg.log.info("rman: click") 147 | folderPickerDialog.fieldid = rmanPathField 148 | folderPickerDialog.open() 149 | } 150 | width: 40 151 | height: 10 152 | anchors.right: rmanlayout.right 153 | anchors.rightMargin: 20 154 | } 155 | } 156 | 157 | RowLayout { 158 | id: rmslayout 159 | x:10; y:70 160 | height: 30 161 | width: parent.width 162 | spacing: 6 163 | Layout.fillWidth: true 164 | 165 | Text { 166 | id: rmsPathLabel 167 | text: "RenderMan For Maya:" 168 | horizontalAlignment: Text.AlignRight 169 | color: "#e6e6e6" 170 | Layout.minimumWidth: 150 171 | Layout.maximumWidth: 150 172 | } 173 | RendermanTextField { 174 | id: rmsPathField 175 | placeholderText: "path to renderman for maya directory" 176 | anchors.left: rmsPathLabel.right 177 | anchors.right: rmsPathButton.left 178 | anchors.leftMargin: 4 179 | anchors.rightMargin: 4 180 | 181 | function readPrefs() 182 | { 183 | text = alg.settings.value("RMSTREE") 184 | } 185 | 186 | Component.onCompleted: { 187 | readPrefs() 188 | } 189 | } 190 | RendermanButton 191 | { 192 | id: rmsPathButton 193 | text: "pick" 194 | width: 40 195 | height: 10 196 | anchors.right: rmslayout.right 197 | anchors.rightMargin: 20 198 | onClicked: { 199 | folderPickerDialog.fieldid = rmsPathField 200 | folderPickerDialog.setVisible(true) 201 | } 202 | } 203 | } 204 | 205 | RowLayout { 206 | id: exportlayout 207 | x:10; y:100 208 | height: 30 209 | width: parent.width 210 | spacing: 6 211 | Layout.fillWidth: true 212 | 213 | Text { 214 | id: exportPathLabel 215 | text: "Export to:" 216 | horizontalAlignment: Text.AlignRight 217 | color: "#e6e6e6" 218 | Layout.minimumWidth: 150 219 | Layout.maximumWidth: 150 220 | } 221 | RendermanTextField { 222 | id: exportPathField 223 | placeholderText: "where the asset(s) will be saved" 224 | anchors.left: exportPathLabel.right 225 | anchors.right: exportPathButton.left 226 | anchors.leftMargin: 4 227 | anchors.rightMargin: 4 228 | 229 | function readPrefs() 230 | { 231 | text = alg.settings.value("saveTo") 232 | } 233 | 234 | Component.onCompleted: { 235 | readPrefs() 236 | } 237 | } 238 | RendermanButton 239 | { 240 | id: exportPathButton 241 | text: "pick" 242 | width: 40 243 | height: 10 244 | anchors.right: exportlayout.right 245 | anchors.rightMargin: 20 246 | onClicked: { 247 | folderPickerDialog.fieldid = exportPathField 248 | folderPickerDialog.setVisible(true) 249 | } 250 | } 251 | } 252 | 253 | 254 | RendermanButton 255 | { 256 | id: okbutton 257 | text: "Save" 258 | onClicked: accept() 259 | isDefaultButton: true 260 | anchors.bottom: parent.bottom 261 | anchors.bottomMargin: 10 262 | anchors.right: parent.right 263 | anchors.rightMargin: 10 264 | width: 100 265 | } 266 | RendermanButton 267 | { 268 | text: "Cancel" 269 | onClicked: close() 270 | anchors.bottom: parent.bottom 271 | anchors.bottomMargin: 10 272 | anchors.right: okbutton.left 273 | anchors.rightMargin: 10 274 | width: 100 275 | } 276 | 277 | FileDialog 278 | { 279 | id: folderPickerDialog 280 | visible: false 281 | title: "Choose the directory ..." 282 | nameFilters: [ "All files (*)" ] 283 | selectedNameFilter: "Executable files (*)" 284 | selectFolder: true 285 | selectExisting: true 286 | property var fieldid 287 | 288 | onAccepted: 289 | { 290 | fieldid.text = alg.fileIO.urlToLocalFile(fileUrl.toString()) 291 | } 292 | 293 | onVisibleChanged: 294 | { 295 | if (visible == false && parent != null ) 296 | { 297 | // alg.log.info("vis OFF") 298 | parent.active() 299 | } 300 | else 301 | { 302 | // alg.log.info("vis ON") 303 | } 304 | } 305 | } 306 | } 307 | 308 | 309 | } -------------------------------------------------------------------------------- /RenderMan/RendermanTextField.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.3 28 | import QtQuick.Controls 1.4 29 | import QtQuick.Controls.Styles 1.4 30 | 31 | TextField { 32 | style: TextFieldStyle { 33 | textColor: "#c0c0c0" 34 | placeholderTextColor: "#333" 35 | background: Rectangle { 36 | radius: 2 37 | implicitWidth: 100 38 | implicitHeight: 24 39 | border.color: "#333" 40 | border.width: 1 41 | color: "#222" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /RenderMan/icons/PxrDisney_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /RenderMan/icons/PxrDisney_idle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /RenderMan/icons/PxrSurface_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /RenderMan/icons/PxrSurface_idle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /RenderMan/icons/R_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 23 | 28 | 47 | 48 | 53 | 54 | -------------------------------------------------------------------------------- /RenderMan/icons/R_logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 23 | 28 | 47 | 48 | 53 | 54 | -------------------------------------------------------------------------------- /RenderMan/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 60 | 64 | 67 | 71 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /RenderMan/icons/icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PxrDisney_hover.svg 5 | PxrDisney_idle.svg 6 | PxrSurface_hover.svg 7 | PxrSurface_idle.svg 8 | folder.svg 9 | R_logo.svg 10 | R_logo_white.svg 11 | 12 | -------------------------------------------------------------------------------- /RenderMan/main.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.2 28 | import Painter 1.0 29 | 30 | PainterPlugin 31 | { 32 | tickIntervalMS: -1 // Disabled, no need for Tick 33 | jsonServerPort: -1 // Disabled, no need for JSON server 34 | 35 | Component.onCompleted: 36 | { 37 | if(alg.version.painter <= "2017.4.2"){ 38 | // create a toolbar button for releases prior to 2018.1.0 39 | alg.ui.addToolBarWidget("toolbar.qml"); 40 | } 41 | 42 | else{ 43 | // create a plugintoolbar button for releases after 2018.1.0 44 | alg.ui.addWidgetToPluginToolBar("plugintoolbar.qml"); 45 | } 46 | } 47 | 48 | onConfigure: 49 | { 50 | // open the configuration panel 51 | rmanPrefsPanel.open() 52 | } 53 | 54 | RendermanPrefsPanel 55 | { 56 | id: rmanPrefsPanel 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RenderMan/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This plugin exports a painter project to RenderMan Assets.", 3 | "url": "https://github.com/pleprince/RfSP", 4 | "version": "0.3.0", 5 | "license": "MIT" 6 | } -------------------------------------------------------------------------------- /RenderMan/plugintoolbar.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.3 28 | import QtQuick.Window 2.2 29 | import QtQuick.Layouts 1.2 30 | import QtQuick.Controls 1.4 31 | import QtQuick.Controls.Styles 1.4 32 | 33 | import "renderman.js" as Renderman 34 | 35 | Column 36 | { 37 | 38 | Button 39 | { 40 | id: disney 41 | antialiasing: true 42 | tooltip: "Export as a PxrDisney Material Preset" 43 | width: 32 44 | height: 32 45 | 46 | style: ButtonStyle { 47 | background: Rectangle { 48 | implicitWidth: control.width 49 | implicitHeight: control.height 50 | width: control.width; 51 | height: control.height 52 | color: control.hovered ? 53 | "#262626" : 54 | "transparent" 55 | 56 | Image { 57 | anchors.fill: parent 58 | anchors.margins: 7 59 | source: control.hovered && !control.loading ? "icons/PxrDisney_hover.svg" : "icons/PxrDisney_idle.svg" 60 | fillMode: Image.PreserveAspectFit 61 | width: control.width; height: control.height 62 | mipmap: true 63 | opacity: 1.0 64 | } 65 | } 66 | } 67 | 68 | onClicked: 69 | { 70 | try 71 | { 72 | alg.log.info( 'RenderMan: Export PxrDisney-based asset...') 73 | Renderman.exportAssets('PxrDisney') 74 | } 75 | catch(err) 76 | { 77 | alg.log.error( 'rman: ' + err.message ) 78 | } 79 | } 80 | } 81 | 82 | 83 | Button 84 | { 85 | id: surface 86 | antialiasing: true 87 | tooltip: "Export as a PxrSurface Material Preset" 88 | width: 32 89 | height: 32 90 | 91 | style: ButtonStyle { 92 | background: Rectangle { 93 | implicitWidth: control.width 94 | implicitHeight: control.height 95 | width: control.width; 96 | height: control.height 97 | color: control.hovered ? 98 | "#262626" : 99 | "transparent" 100 | 101 | Image { 102 | anchors.fill: parent 103 | anchors.margins: 7 104 | source: control.hovered && !control.loading ? "icons/PxrSurface_hover.svg" : "icons/PxrSurface_idle.svg" 105 | fillMode: Image.PreserveAspectFit 106 | width: control.width; height: control.height 107 | mipmap: true 108 | opacity: 1.0 109 | } 110 | } 111 | } 112 | 113 | onClicked: 114 | { 115 | try 116 | { 117 | Renderman.exportAssets('PxrSurface') 118 | } 119 | catch(err) 120 | { 121 | alg.log.error( 'rman: ' + err.message ) 122 | } 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /RenderMan/renderman.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | function osPath(input_path) { 27 | var platformPath = input_path 28 | if (Qt.platform.os == "windows") { 29 | var tmp = new String(platformPath) 30 | platformPath = tmp.replace(/\//g, "\\") 31 | // alg.log.info("[WIN] platformPath = " + platformPath) 32 | } 33 | return platformPath 34 | } 35 | 36 | 37 | function jsonPath(input_path) { 38 | var jsnPath = input_path 39 | if (Qt.platform.os == "windows") { 40 | var tmp = new String(jsnPath) 41 | jsnPath = tmp.replace(/\\/g, "\\\\") 42 | // alg.log.info("[WIN] jsnPath = " + jsnPath) 43 | } 44 | return jsnPath 45 | } 46 | 47 | 48 | function checkPrefs() { 49 | var all_valid = true 50 | if (alg.settings.value("RMANTREE") == undefined) { 51 | all_valid = false 52 | alg.log.warn("RMANTREE = " + alg.settings.value("RMANTREE")) 53 | } 54 | if (alg.settings.value("RMSTREE") == undefined) { 55 | all_valid = false 56 | alg.log.warn("RMSTREE = " + alg.settings.value("RMSTREE")) 57 | } 58 | if (alg.settings.value("saveTo") == undefined) { 59 | all_valid = false 60 | alg.log.warn("saveTo = " + alg.settings.value("saveTo")) 61 | } 62 | return all_valid 63 | } 64 | 65 | 66 | function isUDIMProject(document) 67 | { 68 | var isUDIM = true 69 | var udimPat = '1[0-9]{3}' 70 | for (var matIdx = 0; matIdx < document.materials.length; matIdx++) 71 | { 72 | if (document.materials[matIdx].name.match(udimPat) == null) 73 | { 74 | isUDIM = false 75 | break 76 | } 77 | } 78 | alg.log.info('UDIM = ' + isUDIM) 79 | return isUDIM 80 | } 81 | 82 | 83 | // FIXME: the bxdf param is currently ignored. 84 | 85 | function exportAssets(bxdf) { 86 | 87 | // bail out if the prefs are not all filled 88 | // 89 | var valid = checkPrefs() 90 | if (!valid) { 91 | alg.log.error("RenderMan: Please open the configure panel and fill all fields !") 92 | return 93 | } 94 | 95 | alg.log.info("\n\n\n\n") 96 | var toks = alg.project.url().split('/') 97 | var scene_name = toks[toks.length - 1] 98 | scene_name = scene_name.split(".")[0] 99 | alg.log.info("Scene name: " + scene_name) 100 | 101 | // Some useful variables 102 | // 103 | var sep = "/" 104 | var pyBin = "python" 105 | var winOS = (Qt.platform.os == "windows") 106 | if (winOS) { 107 | sep = "\\" 108 | pyBin += ".exe" 109 | } 110 | var ext = ".png" 111 | var script = "rmanAssetsSubstancePainter.py" 112 | var exportPath = "" 113 | var jsonFilePath = "" 114 | var tab = " " 115 | var tab2 = tab + tab 116 | var tab3 = tab2 + tab 117 | var tab4 = tab3 + tab 118 | 119 | // Query export path 120 | // 121 | exportPath = osPath(alg.mapexport.exportPath()) 122 | exportPath += sep + "RenderMan" + sep 123 | jsonFilePath = exportPath + "RmanExport.json" 124 | 125 | // Export masks 126 | // 127 | var matIdx = 0 128 | var channelIdx = 0 129 | var document = alg.mapexport.documentStructure() 130 | 131 | // check if this is a UDIM set 132 | var isUDIM = isUDIMProject(document) 133 | 134 | // store env vars 135 | // 136 | var obj = { 137 | scene: scene_name, 138 | sp_version: alg.version.painter, 139 | RMANTREE: jsonPath(alg.settings.value("RMANTREE")), 140 | RMSTREE: jsonPath(alg.settings.value("RMSTREE")), 141 | bxdf: bxdf, 142 | udim: isUDIM, 143 | saveTo: jsonPath(alg.settings.value("saveTo")), 144 | document: [] 145 | } 146 | 147 | // Parse all materials (texture sets) 148 | // 149 | var mobj = null 150 | for (matIdx = 0; matIdx < document.materials.length; matIdx++) 151 | { 152 | var material = document.materials[matIdx].name 153 | if (!isUDIM || matIdx == 0) 154 | { 155 | mobj = { 156 | textureSet: isUDIM ? "UDIM" : material, 157 | resolution: alg.mapexport.textureSetResolution(material), 158 | channels: {} 159 | } 160 | } 161 | 162 | var numChannels = document.materials[matIdx].stacks[0].channels.length 163 | for (channelIdx = 0; channelIdx < numChannels; channelIdx++) 164 | { 165 | var thisChannel = document.materials[matIdx].stacks[0].channels[channelIdx] 166 | // alg.log.info("RenderMan: | " + thisChannel) 167 | 168 | // Skip the height channel: we prefer normal maps. 169 | if (thisChannel == "height") 170 | { 171 | // alg.log.info("RenderMan: |_ skip") 172 | continue 173 | } 174 | 175 | var output = exportPath 176 | if (isUDIM) 177 | output += thisChannel + "." + material + ext 178 | else 179 | output += material + "_" + thisChannel + ext 180 | 181 | var t0 = new Date().getTime() 182 | if (thisChannel == "normal") 183 | { 184 | // Make sure the normals are correctly configured to combine 185 | // mesh + height + normal. 186 | alg.mapexport.saveConvertedMap([material], "normal_directx", output) 187 | } 188 | else 189 | { 190 | // regular map export 191 | alg.mapexport.save([material, thisChannel], output) 192 | } 193 | var t1 = new Date().getTime() 194 | alg.log.info("RenderMan: |_ Exported in " + ((t1-t0)/1000.0).toFixed(2) + " sec.: " + output) 195 | 196 | try { 197 | mobj.channels[thisChannel].push(jsonPath(output)) 198 | } catch (error) { 199 | mobj.channels[thisChannel] = [jsonPath(output)] 200 | } 201 | } 202 | 203 | if (!isUDIM || matIdx == 0) 204 | { 205 | obj.document.push(mobj) 206 | } 207 | } 208 | 209 | // Write json file and export 210 | // 211 | if (obj != null) 212 | { 213 | alg.log.info("RenderMan: Writing " + jsonFilePath + "...") 214 | 215 | // write json file needed by the python script 216 | // 217 | var jsonFile = alg.fileIO.open(jsonFilePath, "w") 218 | jsonFile.write(JSON.stringify(obj, null, 4)) 219 | jsonFile.close() 220 | 221 | // Call python 222 | // FIXME: we should probably just catch exceptions and print the log 223 | // on error. 224 | // 225 | alg.log.info("RenderMan: Launching " + script + "...") 226 | try 227 | { 228 | var fpath = "\"" + jsonFilePath + "\"" 229 | var result = alg.subprocess.check_output([pyBin, script, fpath]) 230 | var lines = result.split(/[\r\n]+/g) 231 | for (var i in lines) 232 | { 233 | alg.log.info("RenderMan: " + lines[i]) 234 | } 235 | } 236 | catch (err) 237 | { 238 | alg.log.error('ERROR: ' + err) 239 | } 240 | } 241 | else { 242 | alg.log.error("RenderMan: Nothing to export !") 243 | } 244 | alg.log.info("RenderMan: Export successful ! :)") 245 | } 246 | 247 | -------------------------------------------------------------------------------- /RenderMan/rmanAssetsSubstancePainter.py: -------------------------------------------------------------------------------- 1 | """python 2.7 plugin for substance painter 2.3+ 2 | Export substance painter maps to a RenderMan Asset package. 3 | """ 4 | # ----------------------------------------------------------------------------- 5 | # MIT License 6 | # 7 | # Copyright (c) 2016 Philippe Leprince 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # ----------------------------------------------------------------------------- 27 | 28 | 29 | # standard imports first 30 | import os 31 | import os.path 32 | import sys 33 | import re 34 | import json 35 | import shutil 36 | import logging 37 | import getpass 38 | import subprocess 39 | 40 | THIS_DIR = os.path.dirname(os.path.realpath(__file__)) 41 | LOGFILE = os.path.join(THIS_DIR, 'rfsp_log.txt') 42 | logging.basicConfig(filename=LOGFILE, 43 | filemode='w', 44 | level=logging.DEBUG, 45 | format='%(levelname)-10s %(message)s') 46 | DBUG = logging.debug 47 | INFO = logging.info 48 | WARN = logging.warning 49 | ERR = logging.error 50 | XCPT = logging.exception 51 | IMG_EXTS = ['.png', '.jpg', '.exr'] 52 | TEX_EXTS = ['.tex', '.tx', '.txr'] 53 | 54 | 55 | class FilePath(unicode): 56 | """A class based on unicode to handle filepaths on various OS platforms. 57 | 58 | Extends: 59 | unicode 60 | """ 61 | 62 | def __new__(cls, path): 63 | """Create new unicode file path in POSIX format. Windows paths will be 64 | converted. 65 | 66 | Arguments: 67 | path {str} -- a file path, in any format. 68 | """ 69 | fpath = path 70 | if os.sep is not '/': 71 | fpath = fpath.replace(os.sep, '/') 72 | return unicode.__new__(cls, fpath) 73 | 74 | def osPath(self): 75 | """return the platform-specif path, i.e. convert to windows format if 76 | need be. 77 | 78 | Returns: 79 | str -- a path formatted for the current OS. 80 | """ 81 | return r'%s' % os.path.normpath(self) 82 | 83 | def exists(self): 84 | """Check is the path actually exists, using os.path. 85 | 86 | Returns: 87 | bool -- True if the path exists. 88 | """ 89 | return os.path.exists(self) 90 | 91 | def join(self, *args): 92 | """Combine the arguments with the current path and return a new 93 | FilePath object. 94 | 95 | Arguments: 96 | *args {list} -- a list of path segments. 97 | 98 | Returns: 99 | FilePath -- A new object containing the joined path. 100 | """ 101 | return FilePath(os.path.join(self, *args)) 102 | 103 | def dirname(self): 104 | """Returns the dirname of the current path (using os.path.dirname) as a 105 | FilePath object. 106 | 107 | Returns: 108 | FilePath -- the path's directory name. 109 | """ 110 | return FilePath(os.path.dirname(self)) 111 | 112 | def basename(self): 113 | """Return the basename, i.e. '/path/to/file.ext' -> 'file.ext' 114 | 115 | Returns: 116 | str -- The final segment of the path. 117 | """ 118 | return os.path.basename(self) 119 | 120 | def isWritable(self): 121 | """Checks if the path is writable. The Write and Execute bits must 122 | be enabled. 123 | 124 | Returns: 125 | bool -- True is writable 126 | """ 127 | return os.access(self, os.W_OK | os.X_OK) 128 | 129 | # functions ------------------------------------------------------------------- 130 | 131 | def readJson(fpath): 132 | """Read a json file without exception handling. 133 | 134 | Arguments: 135 | fpath {str} -- full path to json file 136 | 137 | Returns: 138 | dict -- json data 139 | """ 140 | with open(fpath, 'r') as fhdl: 141 | data = json.load(fhdl) 142 | return data 143 | 144 | 145 | def setup_environment(jsonDict): 146 | """make sure that RMANTREE and RMSTREE are defined in our environment and 147 | we can import our python module. 148 | 149 | Arguments: 150 | jsonDict {dict} -- json data 151 | 152 | Returns: 153 | tuple -- (rmanAssets, rman_version) 154 | """ 155 | rmantree = FilePath(jsonDict['RMANTREE']) 156 | rmstree = FilePath(jsonDict['RMSTREE']) 157 | 158 | if not rmantree in os.environ: 159 | os.environ['RMANTREE'] = rmantree.osPath() 160 | if not rmstree in os.environ: 161 | os.environ['RMSTREE'] = rmstree.osPath() 162 | 163 | rman_version = float(re.search(r'RenderManProServer-(\d+\.\d+)', rmantree).group(1)) 164 | 165 | rmstree_py = rmstree.join(rmstree, "scripts") 166 | if int(rman_version) == 21: 167 | if rmstree_py not in sys.path: 168 | sys.path.append(rmstree_py) 169 | else: 170 | rmantree_py = rmantree.join(rmantree, "bin") 171 | if rmstree_py not in sys.path: 172 | sys.path.insert(0, rmstree_py) 173 | if rmantree_py not in sys.path: 174 | sys.path.insert(0, rmantree_py) 175 | return rman_version 176 | 177 | 178 | def set_params(settings_dict, chan, node_name, asset): 179 | # The bxdf may need specific settings to match Substance Painter 180 | try: 181 | params = settings_dict[chan] 182 | except (KeyError, TypeError): 183 | pass 184 | else: 185 | for pname, pdict in params.iteritems(): 186 | asset.addParam(node_name, pname, pdict) 187 | DBUG(' |_ param: %s %s = %s', pdict['type'], pname, pdict['value']) 188 | 189 | 190 | def add_texture_node(asset, node_name, ntype, filepath): 191 | asset.addNode(node_name, ntype, 'pattern', ntype) 192 | pdict = {'type': 'string', 'value': filepath} 193 | asset.addParam(node_name, 'filename', pdict) 194 | if '_MAPID_' in filepath: 195 | asset.addParam(node_name, 'atlasStyle', {'type': 'int', 'value': 1}) 196 | 197 | 198 | def set_metadata(asset, mat_dict): 199 | meta = asset.stdMetadata() 200 | meta['author'] = getpass.getuser() 201 | meta['description'] = 'Created by RenderMan for Substance Painter 0.3.0' 202 | meta['resolution'] = '%d x %d' % (mat_dict['resolution'][0], 203 | mat_dict['resolution'][1]) 204 | for k, v in meta.iteritems(): 205 | asset.addMetadata(k, v) 206 | 207 | 208 | def startupInfo(): 209 | """Returns a Windows-only object to make sure tasks launched through 210 | subprocess don't open a cmd window. 211 | 212 | Returns: 213 | subprocess.STARTUPINFO -- the properly configured object if we are on 214 | Windows, otherwise None 215 | """ 216 | startupinfo = None 217 | if os.name is 'nt': 218 | startupinfo = subprocess.STARTUPINFO() 219 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 220 | return startupinfo 221 | 222 | 223 | def app(name): 224 | if os.name is 'nt': 225 | return (name + '.exe') 226 | return name 227 | 228 | 229 | def txmake(is_udim, asset_path, fpath_list): 230 | 231 | rmantree = FilePath(os.environ['RMANTREE']) 232 | binary = rmantree.join('bin', app('txmake')).osPath() 233 | cmd = [binary] 234 | if is_udim: 235 | cmd += ['-resize', 'round-', 236 | '-mode', 'clamp', 237 | '-format', 'pixar', 238 | '-compression', 'lossless', 239 | '-newer', 240 | 'src', 'dst'] 241 | else: 242 | cmd += ['-resize', 'round-', 243 | '-mode', 'periodic', 244 | '-format', 'pixar', 245 | '-compression', 'lossless', 246 | '-newer', 247 | 'src', 'dst'] 248 | for img in fpath_list: 249 | cmd[-2] = FilePath(img).osPath() 250 | dirname, filename = os.path.split(img) 251 | texfile = os.path.splitext(filename)[0] + '.tex' 252 | cmd[-1] = asset_path.join(texfile).osPath() 253 | DBUG(' |_ txmake : %s -> %s', cmd[-2], cmd[-1]) 254 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, 255 | stderr=subprocess.PIPE, 256 | startupinfo=startupInfo()) 257 | p.wait() 258 | 259 | # return a local path to the tex file. 260 | dirname, filename = os.path.split(fpath_list[0]) 261 | fname, fext = os.path.splitext(filename) 262 | asset_file_ref = FilePath(dirname).join(fname + '.tex') 263 | if is_udim: 264 | asset_file_ref = re.sub(r'1\d{3}', '_MAPID_', asset_file_ref) 265 | return asset_file_ref 266 | 267 | 268 | def export(): 269 | """Export a RenderManAsset package based on a json file. 270 | """ 271 | INFO('Start !') 272 | 273 | if len(sys.argv) < 2: 274 | ERR('expecting 2 arguments !') 275 | raise Exception 276 | 277 | # get the input json file 278 | jsonFile = FilePath(sys.argv[1].replace('"', '')) 279 | 280 | # import json file 281 | jsonDict = readJson(jsonFile) 282 | DBUG('OK: json read') 283 | 284 | rman_version = setup_environment(jsonDict) 285 | if int(rman_version) >= 22: 286 | import rmanAssets.core as ra # pylint: disable=import-error 287 | else: 288 | import rfm.rmanAssets as ra # pylint: disable=import-error 289 | DBUG('OK: imported rmanAssets') 290 | 291 | # constants 292 | _bump = ('height', 'normal') 293 | slotsFile = FilePath(os.path.dirname(os.path.realpath(__file__))).join('rules.json') 294 | rules = readJson(slotsFile) 295 | DBUG('OK: rules read') 296 | 297 | _bxdf = jsonDict['bxdf'] 298 | bxdf_rules = rules[_bxdf] 299 | mappings = bxdf_rules['mapping'] 300 | graph = bxdf_rules.get('graph', None) 301 | settings = bxdf_rules.get('settings', None) 302 | is_udim = jsonDict['udim'] 303 | 304 | # we save the assets to SP's export directory, because we know it is writable. 305 | # We will move them to the requested location later. 306 | exportPath = jsonFile.dirname() 307 | 308 | # build assets 309 | assetList = [] 310 | scene = jsonDict['scene'] 311 | matArray = jsonDict['document'] 312 | for mat in matArray: 313 | label = scene 314 | if not is_udim: 315 | label = '%s_%s' % (scene, mat['textureSet']) 316 | chans = mat['channels'] 317 | DBUG('+ Exporting %s', label) 318 | 319 | assetPath = exportPath.join(label + '.rma') 320 | DBUG(' + assetPath %s', assetPath) 321 | assetJsonPath = assetPath.join('asset.json') 322 | DBUG(' + assetJsonPath %s', assetJsonPath) 323 | 324 | # create asset directory 325 | if not assetPath.exists(): 326 | try: 327 | os.mkdir(assetPath.osPath()) 328 | except (OSError, IOError): 329 | XCPT('Asset directory could not be created !') 330 | sys.exit(0) 331 | DBUG(' + Created dir: %s', assetPath) 332 | else: 333 | DBUG(' + dir exists: %s', assetPath) 334 | 335 | # create asset 336 | try: 337 | asset = ra.RmanAsset(assetType='nodeGraph', label=label) 338 | except Exception: 339 | XCPT('Asset creation failed') 340 | sys.exit(0) 341 | 342 | # create standard metadata 343 | # 344 | set_metadata(asset, mat) 345 | 346 | # Compatibility data 347 | # This will help other application decide if they can use this asset. 348 | # 349 | prmanVersion = str(rman_version) 350 | asset.setCompatibility(hostName='Substance Painter', 351 | hostVersion=jsonDict['sp_version'], 352 | rendererVersion=prmanVersion) 353 | DBUG(' + compatibility set') 354 | 355 | # create nodes 356 | # start by adding a root node 357 | # 358 | rootNode = label + '_Material' 359 | asset.addNode(rootNode, 'shadingEngine', 'root', 'shadingEngine') 360 | pdict = {'type': 'reference float[]', 'value': None} 361 | asset.addParam(rootNode, 'surfaceShader', pdict) 362 | DBUG(' + Root node: %s', rootNode) 363 | 364 | # add a disney or pixar bxdf 365 | # 366 | bxdfNode = label + "_Srf" 367 | asset.addNode(bxdfNode, _bxdf, 'bxdf', _bxdf) 368 | DBUG(' + BxDF node: %s (%s)', (rootNode, _bxdf)) 369 | 370 | # The bxdf may need specific settings to match Substance Painter 371 | set_params(settings, 'bxdf', bxdfNode, asset) 372 | 373 | # connect surf to root node 374 | # 375 | asset.addConnection('%s.outColor' % bxdfNode, 376 | '%s.surfaceShader' % rootNode) 377 | 378 | # build additional nodes if need be. 379 | # 380 | if graph: 381 | DBUG(' + Create graph nodes...') 382 | for nname, ndict in graph['nodes'].iteritems(): 383 | lname = label + nname 384 | asset.addNode(lname, ndict['nodetype'], 385 | 'pattern', ndict['nodetype']) 386 | DBUG(' |_ %s (%s)' % (lname, ndict['nodetype'])) 387 | if 'params' in ndict: 388 | for pname, pdict in ndict['params'].iteritems(): 389 | asset.addParam(lname, pname, pdict) 390 | DBUG(' |_ param: %s %s = %s' % 391 | (pdict['type'], pname, pdict['value'])) 392 | 393 | # create texture nodes 394 | DBUG(' + Create texture nodes...') 395 | chanNodes = {} 396 | for chan, fpath_list in chans.iteritems(): 397 | nodeName = "%s_%s_tex" % (label, chan) 398 | DBUG(' |_ %s' % nodeName) 399 | chanNodes[chan] = nodeName 400 | fpath = txmake(is_udim, assetPath, fpath_list) 401 | if chan == 'normal': 402 | add_texture_node(asset, nodeName, 'PxrNormalMap', fpath) 403 | elif chan == 'height': 404 | add_texture_node(asset, nodeName, 'PxrBump', fpath) 405 | else: 406 | add_texture_node(asset, nodeName, 'PxrTexture', fpath) 407 | set_params(settings, chan, nodeName, asset) 408 | 409 | # make direct connections 410 | # 411 | DBUG(' + Direct connections...') 412 | for chan in chans: 413 | src = None 414 | dstType = mappings[chan]['type'] 415 | dstParam = mappings[chan]['param'] 416 | if dstType == 'normal': 417 | src = '%s.resultN' % (chanNodes[chan]) 418 | elif dstType == 'color': 419 | src = '%s.resultRGB' % (chanNodes[chan]) 420 | elif dstType == 'float': 421 | src = '%s.resultR' % (chanNodes[chan]) 422 | else: 423 | # don't create a connection 424 | if dstParam != 'graph': 425 | # connections with a graph type will be handled later, so 426 | # we don't warn in that case. 427 | print 'WARNING: Not connecting: %s' % chan 428 | continue 429 | if dstParam == 'graph': 430 | continue 431 | dst = '%s.%s' % (bxdfNode, dstParam) 432 | asset.addConnection(src, dst) 433 | DBUG(' |_ connect: %s -> %s' % (src, dst)) 434 | # also tag the bxdf param as connected 435 | pdict = {'type': 'reference ' + dstType, 'value': None} 436 | asset.addParam(bxdfNode, dstParam, pdict) 437 | DBUG(' |_ param: %s %s -> %s' % (pdict['type'], 438 | dstParam, 439 | pdict['value'])) 440 | 441 | # make graph connections 442 | # 443 | if graph: 444 | if 'connections' in graph: 445 | DBUG(' + Connect graph nodes...') 446 | for con in graph['connections']: 447 | 448 | src_node = con['src']['node'] 449 | src_ch = None 450 | if src_node == _bxdf: 451 | src_node = bxdfNode 452 | elif src_node.startswith('ch:'): 453 | src_ch = src_node[3:] 454 | if src_ch in chanNodes: 455 | src_node = chanNodes[src_ch] 456 | else: 457 | continue 458 | if not src_node.startswith(label): 459 | src_node = label + src_node 460 | src = '%s.%s' % (src_node, con['src']['param']) 461 | 462 | dst_node = con['dst']['node'] 463 | dst_ch = None 464 | if dst_node == _bxdf: 465 | dst_node = bxdfNode 466 | elif dst_node.startswith('ch:'): 467 | dst_ch = dst_node[3:] 468 | if dst_ch in chanNodes: 469 | dst_node = chanNodes[dst_ch] 470 | else: 471 | continue 472 | if not dst_node.startswith(label): 473 | dst_node = label + dst_node 474 | dst = '%s.%s' % (dst_node, con['dst']['param']) 475 | asset.addConnection(src, dst) 476 | DBUG(' |_ connect: %s -> %s' % (src, dst)) 477 | # mark param as a connected 478 | dstType = con['dst']['type'] 479 | pdict = {'type': 'reference %s' % dstType, 'value': None} 480 | asset.addParam(dst_node, con['dst']['param'], pdict) 481 | DBUG(' |_ param: %s %s = %s' % (pdict['type'], 482 | con['dst']['param'], 483 | pdict['value'])) 484 | 485 | # save asset 486 | # 487 | DBUG(' + ready to save: %s' % assetJsonPath) 488 | try: 489 | asset.save(assetJsonPath, False) 490 | except: 491 | XCPT('Saving the asset failed !') 492 | raise 493 | 494 | # mark this asset as ready to be moved 495 | # 496 | assetList.append(assetPath) 497 | 498 | # move assets to the requested location 499 | # 500 | dst = jsonDict['saveTo'] 501 | for item in assetList: 502 | # if the asset already exists in the destination 503 | # location, we need to move it first. 504 | dstAsset = os.path.join(dst, os.path.basename(item)) 505 | if os.path.exists(dstAsset): 506 | try: 507 | os.rename(dstAsset, dstAsset + '_old') 508 | except (OSError, IOError): 509 | XCPT('Could not rename asset to %s_old' % dstAsset) 510 | continue 511 | else: 512 | shutil.rmtree(dstAsset + '_old', ignore_errors=False) 513 | try: 514 | shutil.move(item, dst) 515 | except (OSError, IOError): 516 | XCPT('WARNING: Could not copy asset to %s' % dst) 517 | 518 | 519 | # clean-up intermediate files 520 | for mat in matArray: 521 | for chan, fpath_list in chans.iteritems(): 522 | for fpath in fpath_list: 523 | if not os.path.exists(fpath): 524 | print 'cleanup: file not found: %s' % fpath 525 | continue 526 | try: 527 | os.remove(fpath) 528 | except (OSError, IOError): 529 | XCPT('Cleanup failed: %s' % fpath) 530 | else: 531 | DBUG('Cleanup: %s' % fpath) 532 | 533 | if os.path.exists(jsonFile): 534 | try: 535 | os.remove(jsonFile) 536 | except (OSError, IOError): 537 | XCPT('Cleanup failed: %s' % jsonFile) 538 | else: 539 | DBUG('Cleanup: %s' % jsonFile) 540 | 541 | INFO('RenderMan : Done !') 542 | 543 | 544 | # main 545 | 546 | try: 547 | export() 548 | except Exception: 549 | XCPT('Export failed') 550 | sys.exit(0) 551 | -------------------------------------------------------------------------------- /RenderMan/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "PxrDisney": { 3 | "mapping": { 4 | "basecolor": { 5 | "param": "baseColor", 6 | "type": "color" 7 | }, 8 | "specular": { 9 | "param": "specular", 10 | "type": "float" 11 | }, 12 | "roughness": { 13 | "param": "roughness", 14 | "type": "float" 15 | }, 16 | "metallic": { 17 | "param": "metallic", 18 | "type": "float" 19 | }, 20 | "opacity": { 21 | "param": null, 22 | "type": null 23 | }, 24 | "emissive": { 25 | "param": "emitColor", 26 | "type": "color" 27 | }, 28 | "normal": { 29 | "param": "bumpNormal", 30 | "type": "normal" 31 | } 32 | }, 33 | "settings": { 34 | "basecolor": { 35 | "linearize": { 36 | "type": "int", 37 | "value": 1 38 | } 39 | }, 40 | "specular": { 41 | "linearize": { 42 | "type": "int", 43 | "value": 1 44 | } 45 | }, 46 | "roughness": { 47 | "linearize": { 48 | "type": "int", 49 | "value": 0 50 | } 51 | }, 52 | "metallic": { 53 | "linearize": { 54 | "type": "int", 55 | "value": 0 56 | } 57 | }, 58 | "opacity": { 59 | "linearize": { 60 | "type": "int", 61 | "value": 0 62 | } 63 | }, 64 | "emissive": { 65 | "linearize": { 66 | "type": "int", 67 | "value": 1 68 | } 69 | }, 70 | "normal": { 71 | "orientation": { 72 | "type": "int", 73 | "value": 1 74 | }, 75 | "adjustAmount": { 76 | "type": "float", 77 | "value": 1.0 78 | } 79 | } 80 | } 81 | }, 82 | "PxrSurface": { 83 | "mapping": { 84 | "basecolor": { 85 | "param": "graph", 86 | "type": "color" 87 | }, 88 | "specular": { 89 | "param": "graph", 90 | "type": "color" 91 | }, 92 | "roughness": { 93 | "param": "specularRoughness", 94 | "type": "float" 95 | }, 96 | "metallic": { 97 | "param": "graph", 98 | "type": "float" 99 | }, 100 | "opacity": { 101 | "param": null, 102 | "type": null 103 | }, 104 | "emissive": { 105 | "param": "glowColor", 106 | "type": "color" 107 | }, 108 | "normal": { 109 | "param": "bumpNormal", 110 | "type": "normal" 111 | } 112 | }, 113 | "graph": { 114 | "nodes": { 115 | "_specFaceColor": { 116 | "nodetype": "PxrBlend", 117 | "params": { 118 | "operation": { 119 | "type": "int", 120 | "value": 19 121 | }, 122 | "topA": { 123 | "type": "float", 124 | "value": 0.0 125 | }, 126 | "bottomRGB": { 127 | "type": "color", 128 | "value": [ 129 | 0.04, 130 | 0.04, 131 | 0.04 132 | ] 133 | }, 134 | "bottomA": { 135 | "type": "float", 136 | "value": 1.0 137 | } 138 | } 139 | }, 140 | "_specEdgeColor": { 141 | "nodetype": "PxrBlend", 142 | "params": { 143 | "operation": { 144 | "type": "int", 145 | "value": 19 146 | }, 147 | "topA": { 148 | "type": "float", 149 | "value": 0.0 150 | }, 151 | "bottomRGB": { 152 | "type": "color", 153 | "value": [ 154 | 1, 155 | 1, 156 | 1 157 | ] 158 | }, 159 | "bottomA": { 160 | "type": "float", 161 | "value": 1.0 162 | } 163 | } 164 | }, 165 | "_diffuseAtten": { 166 | "nodetype": "PxrBlend", 167 | "params": { 168 | "operation": { 169 | "type": "int", 170 | "value": 19 171 | }, 172 | "topRGB": { 173 | "type": "color", 174 | "value": [ 175 | 0, 176 | 0, 177 | 0 178 | ] 179 | } 180 | } 181 | } 182 | }, 183 | "connections": [ 184 | { 185 | "src": { 186 | "node": "_specFaceColor", 187 | "param": "resultRGB" 188 | }, 189 | "dst": { 190 | "node": "PxrSurface", 191 | "param": "specularFaceColor", 192 | "type": "color" 193 | } 194 | }, 195 | { 196 | "src": { 197 | "node": "_specEdgeColor", 198 | "param": "resultRGB" 199 | }, 200 | "dst": { 201 | "node": "PxrSurface", 202 | "param": "specularEdgeColor", 203 | "type": "color" 204 | } 205 | }, 206 | { 207 | "src": { 208 | "node": "_diffuseAtten", 209 | "param": "resultRGB" 210 | }, 211 | "dst": { 212 | "node": "PxrSurface", 213 | "param": "diffuseColor", 214 | "type": "color" 215 | } 216 | }, 217 | { 218 | "src": { 219 | "node": "ch:basecolor", 220 | "param": "resultRGB" 221 | }, 222 | "dst": { 223 | "node": "_specEdgeColor", 224 | "param": "topRGB", 225 | "type": "color" 226 | } 227 | }, 228 | { 229 | "src": { 230 | "node": "ch:basecolor", 231 | "param": "resultRGB" 232 | }, 233 | "dst": { 234 | "node": "_specFaceColor", 235 | "param": "topRGB", 236 | "type": "color" 237 | } 238 | }, 239 | { 240 | "src": { 241 | "node": "ch:basecolor", 242 | "param": "resultRGB" 243 | }, 244 | "dst": { 245 | "node": "_diffuseAtten", 246 | "param": "bottomRGB", 247 | "type": "color" 248 | } 249 | }, 250 | { 251 | "src": { 252 | "node": "ch:metallic", 253 | "param": "resultR" 254 | }, 255 | "dst": { 256 | "node": "_specEdgeColor", 257 | "param": "topA", 258 | "type": "float" 259 | } 260 | }, 261 | { 262 | "src": { 263 | "node": "ch:metallic", 264 | "param": "resultR" 265 | }, 266 | "dst": { 267 | "node": "_specFaceColor", 268 | "param": "topA", 269 | "type": "float" 270 | } 271 | }, 272 | { 273 | "src": { 274 | "node": "ch:metallic", 275 | "param": "resultR" 276 | }, 277 | "dst": { 278 | "node": "_diffuseAtten", 279 | "param": "topA", 280 | "type": "float" 281 | } 282 | }, 283 | { 284 | "src": { 285 | "node": "ch:specular", 286 | "param": "resultRGB " 287 | }, 288 | "dst": { 289 | "node": "_specFaceColor", 290 | "param": "bottomRGB", 291 | "type": "color" 292 | } 293 | } 294 | ] 295 | }, 296 | "settings": { 297 | "bxdf": { 298 | "specularModelType": { 299 | "type": "int", 300 | "value": 1 301 | } 302 | }, 303 | "basecolor": { 304 | "linearize": { 305 | "type": "int", 306 | "value": 1 307 | } 308 | }, 309 | "specular": { 310 | "linearize": { 311 | "type": "int", 312 | "value": 1 313 | } 314 | }, 315 | "roughness": { 316 | "linearize": { 317 | "type": "int", 318 | "value": 0 319 | } 320 | }, 321 | "metallic": { 322 | "linearize": { 323 | "type": "int", 324 | "value": 0 325 | } 326 | }, 327 | "opacity": { 328 | "linearize": { 329 | "type": "int", 330 | "value": 0 331 | } 332 | }, 333 | "emissive": { 334 | "linearize": { 335 | "type": "int", 336 | "value": 1 337 | } 338 | }, 339 | "normal": { 340 | "orientation": { 341 | "type": "int", 342 | "value": 1 343 | }, 344 | "adjustAmount": { 345 | "type": "float", 346 | "value": 1.0 347 | } 348 | } 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /RenderMan/toolbar.qml: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // MIT License 3 | // 4 | // Copyright (c) 2016 Philippe Leprince 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // ---------------------------------------------------------------------------- 24 | 25 | 26 | 27 | import QtQuick 2.3 28 | import QtQuick.Window 2.2 29 | import QtQuick.Layouts 1.2 30 | import QtQuick.Controls 1.4 31 | import QtQuick.Controls.Styles 1.4 32 | 33 | import "renderman.js" as Renderman 34 | 35 | Row 36 | { 37 | 38 | Button 39 | { 40 | id: disney 41 | antialiasing: true 42 | tooltip: "Export as a PxrDisney Material Preset" 43 | width: 32 44 | height: 32 45 | 46 | style: ButtonStyle { 47 | background: Rectangle { 48 | implicitWidth: control.width 49 | implicitHeight: control.height 50 | width: control.width; 51 | height: control.height 52 | color: control.hovered ? 53 | "#262626" : 54 | "transparent" 55 | 56 | Image { 57 | anchors.fill: parent 58 | anchors.margins: 4 59 | source: control.hovered && !control.loading ? "icons/PxrDisney_hover.svg" : "icons/PxrDisney_idle.svg" 60 | fillMode: Image.PreserveAspectFit 61 | width: control.width; height: control.height 62 | mipmap: true 63 | opacity: 1.0 64 | } 65 | } 66 | } 67 | 68 | onClicked: 69 | { 70 | try 71 | { 72 | alg.log.info( 'RenderMan: Export PxrDisney-based asset...') 73 | Renderman.exportAssets('PxrDisney') 74 | } 75 | catch(err) 76 | { 77 | alg.log.error( 'rman: ' + err.message ) 78 | } 79 | } 80 | } 81 | 82 | 83 | Button 84 | { 85 | id: surface 86 | antialiasing: true 87 | tooltip: "Export as a PxrSurface Material Preset" 88 | width: 32 89 | height: 32 90 | 91 | style: ButtonStyle { 92 | background: Rectangle { 93 | implicitWidth: control.width 94 | implicitHeight: control.height 95 | width: control.width; 96 | height: control.height 97 | color: control.hovered ? 98 | "#262626" : 99 | "transparent" 100 | 101 | Image { 102 | anchors.fill: parent 103 | anchors.margins: 4 104 | source: control.hovered && !control.loading ? "icons/PxrSurface_hover.svg" : "icons/PxrSurface_idle.svg" 105 | fillMode: Image.PreserveAspectFit 106 | width: control.width; height: control.height 107 | mipmap: true 108 | opacity: 1.0 109 | } 110 | } 111 | } 112 | 113 | onClicked: 114 | { 115 | try 116 | { 117 | Renderman.exportAssets('PxrSurface') 118 | } 119 | catch(err) 120 | { 121 | alg.log.error( 'rman: ' + err.message ) 122 | } 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /img/RfSP_v0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleprince/RfSP/f739ded7b010a5a7b4714d0500f1664f02523d8f/img/RfSP_v0.gif -------------------------------------------------------------------------------- /img/configure_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleprince/RfSP/f739ded7b010a5a7b4714d0500f1664f02523d8f/img/configure_dialog.jpg -------------------------------------------------------------------------------- /img/open_configure_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleprince/RfSP/f739ded7b010a5a7b4714d0500f1664f02523d8f/img/open_configure_dialog.jpg -------------------------------------------------------------------------------- /img/shelf_buttons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleprince/RfSP/f739ded7b010a5a7b4714d0500f1664f02523d8f/img/shelf_buttons.jpg -------------------------------------------------------------------------------- /renderman.rcc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleprince/RfSP/f739ded7b010a5a7b4714d0500f1664f02523d8f/renderman.rcc -------------------------------------------------------------------------------- /renderman_for_sp.py: -------------------------------------------------------------------------------- 1 | 2 | """python plugin for substance painter 2020+. 3 | Export substance painter maps to a RenderMan Asset package. 4 | """ 5 | # ----------------------------------------------------------------------------- 6 | # MIT License 7 | # 8 | # Copyright (c) 2016 Philippe Leprince 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all 18 | # copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | # ----------------------------------------------------------------------------- 28 | 29 | # TODO: Colorspace txmake 30 | # TODO: remove non-exportable channels for def. 31 | # TODO: export progress dialog 32 | # TODO: name textures with color spaces 33 | # pylint: disable=missing-docstring,invalid-name 34 | 35 | import os 36 | import sys 37 | import traceback 38 | import inspect 39 | import json 40 | # import logging 41 | import tempfile 42 | import getpass 43 | import re 44 | import subprocess 45 | import shutil 46 | # from PySide2 import (QtWidgets, QtGui, QtCore) # pylint: disable=import-error 47 | from PySide2.QtCore import (QResource, Qt) # pylint: disable=import-error 48 | from PySide2.QtGui import (QIcon) # pylint: disable=import-error 49 | from PySide2.QtWidgets import ( 50 | QWidget, 51 | QMessageBox, 52 | QFileDialog, 53 | QFormLayout, 54 | QComboBox 55 | ) # pylint: disable=import-error 56 | import substance_painter as sp # pylint: disable=import-error 57 | import substance_painter.ui as spui # pylint: disable=import-error 58 | import substance_painter.logging as spl # pylint: disable=import-error 59 | import substance_painter.project as spp # pylint: disable=import-error 60 | import substance_painter.textureset as spts # pylint: disable=import-error 61 | # import substance_painter.resource as spr # pylint: disable=import-error 62 | import substance_painter.export as spex # pylint: disable=import-error 63 | 64 | 65 | __version__ = '24.1.0' 66 | MIN_RPS = '24.1' 67 | MIN_SP_API = '0.1.0' 68 | 69 | 70 | class Log(object): 71 | def __init__(self, loglevel=spl.ERROR): 72 | self.channel = 'RenderMan %s' % __version__ 73 | self.loglevel = int(loglevel) 74 | self.info('Log Level: %s (%s)', loglevel, self.loglevel) 75 | pyv = sys.version_info 76 | self.info('SP python: %d.%d.%d', *tuple(pyv[0:3])) 77 | 78 | def debug_error(self, msg, *args): 79 | if self.loglevel >= int(spl.DBG_ERROR): # 5 80 | spl.log(spl.ERROR, self.channel, msg % args) 81 | 82 | def debug_warning(self, msg, *args): 83 | if self.loglevel >= int(spl.DBG_WARNING): # 4 84 | spl.log(spl.WARNING, self.channel, msg % args) 85 | 86 | def debug_info(self, msg, *args): 87 | if self.loglevel >= int(spl.DBG_INFO): # 3 88 | spl.log(spl.INFO, self.channel, msg % args) 89 | 90 | def error(self, msg, *args): 91 | if self.loglevel >= int(spl.ERROR): # 2 92 | spl.log(spl.ERROR, self.channel, msg % args) 93 | 94 | def warning(self, msg, *args): 95 | if self.loglevel >= int(spl.WARNING): # 1 96 | spl.log(spl.WARNING, self.channel, msg % args) 97 | 98 | def info(self, msg, *args): 99 | if self.loglevel >= int(spl.INFO): # 0 100 | spl.log(spl.INFO, self.channel, msg % args) 101 | 102 | 103 | LOG = Log(loglevel=spl.ERROR) 104 | 105 | 106 | def root_dir(): 107 | """Returns the path the dir from which this plugin is executed.""" 108 | try: 109 | this_file_path = __file__ 110 | except NameError: 111 | this_file_path = os.path.abspath(inspect.stack()[0][1]) 112 | root = os.path.dirname(this_file_path) 113 | return root 114 | 115 | 116 | def pick_directory(*args): 117 | libpath = QFileDialog.getExistingDirectory( 118 | None, 'Select a directory...') 119 | args[0].setText(libpath) 120 | LOG.info('pick_directory%s', str(args)) 121 | 122 | 123 | class Prefs(object): 124 | 125 | def __init__(self): 126 | self.prefs = {} 127 | self.root = root_dir() 128 | self.file = os.path.join(self.root, 'renderman.prefs') 129 | self.load() 130 | LOG.debug_info('Prefs created') 131 | 132 | def load(self): 133 | if os.path.exists(self.file): 134 | with open(self.file, 'r') as fhdl: 135 | self.prefs = json.load(fhdl) 136 | LOG.debug_info('Loaded: %s', self.file) 137 | else: 138 | LOG.debug_warning('NOT loaded: %s', self.file) 139 | 140 | def save(self): 141 | with open(self.file, mode='w') as fhdl: 142 | json.dump(self.prefs, fhdl, sort_keys=False, indent=4) 143 | LOG.debug_info('PREFS SAVED: %s', self.file) 144 | 145 | def set(self, key, val): 146 | self.prefs[key] = val 147 | 148 | def get(self, key, default): 149 | return self.prefs.get(key, default) 150 | 151 | def __del__(self): 152 | self.save() 153 | 154 | 155 | class RenderManForSP(object): 156 | 157 | def __init__(self): 158 | # find root dir 159 | self.root = root_dir() 160 | LOG.debug_info('root = %r', self.root) 161 | # load resource file 162 | rpath = os.path.join(self.root, 'renderman.rcc') 163 | rloaded = QResource.registerResource(rpath) 164 | if not rloaded: 165 | LOG.error('Invalid Resource: %s', rpath) 166 | # init UI 167 | self.prefs = Prefs() 168 | self.widget, self.dock = self.build_panel() 169 | 170 | def cleanup(self): 171 | LOG.debug_info('cleanup') 172 | self.prefs.save() 173 | spui.delete_ui_element(self.dock) 174 | 175 | def build_panel(self): 176 | """Build the UI""" 177 | LOG.debug_info('build_panel') 178 | # Create a simple text widget 179 | root = QWidget(None, Qt.Window) 180 | root.setWindowTitle("RenderMan") 181 | logo = QIcon(':R_logo.svg') 182 | logo.addFile(':R_logo_white.svg', mode=QIcon.Normal, state=QIcon.On) 183 | root.setWindowIcon(logo) 184 | # Add this widget as a dock to the interface 185 | dock = spui.add_dock_widget(root) 186 | 187 | # preset browser 188 | rman_version_str = env_check(self.prefs) 189 | try: 190 | import rman_utils.rman_assets as ra 191 | import rman_utils.rman_assets.core as rac 192 | import rman_utils.rman_assets.ui as rui 193 | import rman_utils.rman_assets.lib as ral 194 | # from rman_utils.rman_assets.common.ui_utils import createHLayout 195 | from rman_utils.filepath import FilePath 196 | except BaseException as err: 197 | LOG.error('Failed to import: %s', err) 198 | traceback.print_exc(file=sys.stdout) 199 | else: 200 | # ra.setLogLevel(logging.DEBUG) 201 | 202 | class SPrefs(ral.HostPrefs): 203 | saved = { 204 | 'rpbConfigFile': FilePath(''), 'rpbUserLibraries': [], 205 | 'rpbSwatchSize': 64, 'rpbSelectedPreviewEnv': 0, 206 | 'rpbSelectedCategory': 'Materials', 207 | 'rpbSelectedLibrary': FilePath(''), 208 | 'rpbRenderAllHDRs': False, 209 | 'rpbHideFactoryLib': False 210 | } 211 | 212 | def __init__(self, rman_version, pref_obj): 213 | super(SPrefs, self).__init__(rman_version) 214 | self.root_dir = root_dir() 215 | self.prefsobj = pref_obj 216 | self.rules = self._load_rules() 217 | if 'host_prefs' in self.prefsobj.prefs: 218 | hprefs = self.prefsobj.prefs['host_prefs'] 219 | for k in self.saved: 220 | setattr(self, k, hprefs.get(k, self.saved[k])) 221 | if k == 'rpbConfigFile': 222 | self.rpbConfigFile = FilePath( 223 | self.rpbConfigFile) 224 | elif k == 'rpbSelectedLibrary': 225 | self.rpbSelectedLibrary = FilePath( 226 | self.rpbSelectedLibrary) 227 | elif k == 'rpbUserLibraries' and self.rpbUserLibraries: 228 | self.rpbUserLibraries = [ 229 | FilePath(f) for f in self.rpbUserLibraries] 230 | # export vars 231 | self.spx_exported_files = {} 232 | self.opt_bxdf = None 233 | self.opt_ocio = None 234 | self._defaultLabel = 'UNTITLED' 235 | self.ocio_config = {'config': None, 'path': None} 236 | # render previews 237 | self.hostTree = '' 238 | self.rmanTree = self.prefsobj.get('RMANTREE', '') 239 | LOG.debug_info('SPrefs object created') 240 | 241 | def getHostPref(self, pref_name, default_value): 242 | return self.prefsobj.get(pref_name, default_value) 243 | 244 | def setHostPref(self, pref_name, value): 245 | prefs = self.prefsobj.get('host_prefs', {}) 246 | prefs[pref_name] = value 247 | self.prefsobj.set('host_prefs', prefs) 248 | # self._print() 249 | 250 | def saveAllPrefs(self): 251 | for k in self.saved: 252 | self.setHostPref(k, getattr(self, k)) 253 | # self._print() 254 | 255 | def preExportCheck(self, mode, hdr=None): 256 | LOG.debug_info('preExportCheck: %r, hdr=%r', mode, hdr) 257 | if mode == 'material': 258 | try: 259 | self._defaultLabel = spp.name() or 'UNTITLED' 260 | except BaseException as err: 261 | LOG.error('%s', err) 262 | msg_box(str(err), '', QMessageBox.Ok, QMessageBox.Ok) 263 | return False 264 | return True 265 | LOG.warning('Not supported (%s)', mode) 266 | msg_box('This is not supported !', 'Sorry...', 267 | QMessageBox.Ok, QMessageBox.Ok) 268 | return False 269 | 270 | def exportMaterial(self, categorypath, infodict, previewtype): 271 | LOG.debug_info( 272 | 'exportMaterial: %r, %r, %r', categorypath, infodict, 273 | previewtype) 274 | # 275 | _bxdf = self.opt_bxdf.currentText() 276 | self.prefsobj.set('last bxdf', _bxdf) 277 | LOG.debug_info('chosen bxdf: %s', _bxdf) 278 | _ocio = self.opt_ocio.currentText() 279 | self.ocio_config['config'] = _ocio 280 | if _ocio == '$OCIO': 281 | self.ocio_config['path'] = FilePath(os.environ['OCIO']) 282 | elif _ocio != 'Off': 283 | self.ocio_config['path'] = FilePath(self.rmanTree).join( 284 | 'lib', 'ocio', _ocio, 'config.ocio') 285 | self.prefsobj.set('ocio config', _ocio) 286 | LOG.debug_info('chosen ocio config: %s', _ocio) 287 | # setup data 288 | bxdf_rules = self.rules['models'][_bxdf] 289 | mappings = bxdf_rules['mapping'] 290 | graph = bxdf_rules.get('graph', None) 291 | settings = bxdf_rules.get('settings', None) 292 | scene = infodict['label'] 293 | 294 | # we save the assets to SP's export directory, because we 295 | # know it is writable. We will move them to the requested 296 | # location later. 297 | export_path = FilePath(tempfile.mkdtemp(prefix='rfsp_export_')) 298 | 299 | # export project textures 300 | self.sp_export(export_path) 301 | 302 | # list of spts.TextureSet objects 303 | tset_list = spts.all_texture_sets() 304 | 305 | # build assets 306 | asset_list = [] 307 | for mat in tset_list: 308 | label = scene 309 | is_udim = mat.has_uv_tiles() 310 | if not is_udim: 311 | label = '%s_%s' % (scene, mat.name()) 312 | 313 | # chans = mat.get_stack().all_channels() 314 | chans = self.textureset_channels(mat) 315 | LOG.debug_info('+ Exporting %s', label) 316 | 317 | asset_path = export_path.join(label + '.rma') 318 | LOG.debug_info(' + asset_path %s', asset_path) 319 | asset_json_path = asset_path.join('asset.json') 320 | LOG.debug_info(' + asset_json_path %s', asset_json_path) 321 | 322 | # create asset directory 323 | create_directory(asset_path) 324 | 325 | # create asset 326 | try: 327 | asset = rac.RmanAsset(assetType='nodeGraph', label=label) 328 | except Exception: 329 | LOG.error('Asset creation failed') 330 | raise 331 | 332 | asset.ocio = self.ocio_config 333 | 334 | # create standard metadata 335 | # 336 | self.set_metadata(asset, mat) 337 | 338 | # create nodes 339 | # start by adding a root node 340 | # 341 | root_node = label + '_Material' 342 | asset.addNode(root_node, 'shadingEngine', 'root', 'shadingEngine') 343 | pdict = {'type': 'reference float[]', 'value': None} 344 | asset.addParam(root_node, 'surfaceShader', pdict) 345 | LOG.debug_info(' + Root node: %s', root_node) 346 | 347 | # add a disney, pixar or lama bxdf 348 | # 349 | bxdf_node = label + "_Srf" 350 | asset.addNode(bxdf_node, _bxdf, 'bxdf', _bxdf) 351 | LOG.debug_info(' + BxDF node: %s (%s)', root_node, _bxdf) 352 | 353 | # The bxdf may need specific settings to match Substance Painter 354 | set_params(settings, 'bxdf', bxdf_node, asset) 355 | 356 | # connect surf to root node 357 | # 358 | asset.addConnection('%s.outColor' % bxdf_node, 359 | '%s.surfaceShader' % root_node) 360 | 361 | # build additional nodes if need be. 362 | # 363 | if graph: 364 | LOG.debug_info(' + Create graph nodes...') 365 | for nname, ndict in graph['nodes'].items(): 366 | lname = label + nname 367 | asset.addNode(lname, ndict['nodetype'], 368 | ndict.get('category', 'pattern'), 369 | ndict['nodetype']) 370 | LOG.debug_info( 371 | ' |_ %s (%s)', lname, ndict['nodetype']) 372 | if 'params' in ndict: 373 | for pname, pdict in ndict['params'].items(): 374 | asset.addParam(lname, pname, pdict) 375 | LOG.debug_info( 376 | ' |_ param: %s %s = %s', 377 | pdict['type'], pname, pdict['value']) 378 | 379 | # create texture nodes 380 | LOG.debug_info(' + Create texture nodes...') 381 | chan_nodes = {} 382 | for ch_type in chans: 383 | fpath_list = self.spx_exported_files[mat.name()].get(ch_type, None) 384 | if fpath_list is None: 385 | LOG.debug_warning( 386 | ' |_ tex_dict[%r][%r] failed', mat.name(), 387 | ch_type) 388 | continue 389 | node_name = "%s_%s_tex" % (label, ch_type) 390 | LOG.debug_info(' |_ %s', node_name) 391 | chan_nodes[ch_type] = node_name 392 | colorspace = mappings[ch_type]['ocio'] 393 | fpath = self.txmake(is_udim, asset_path, fpath_list, 394 | self.ocio_config, colorspace) 395 | if ch_type == 'Normal': 396 | add_texture_node(asset, node_name, 'PxrNormalMap', fpath) 397 | elif ch_type == 'Height': 398 | add_texture_node(asset, node_name, 'PxrBump', fpath) 399 | else: 400 | add_texture_node(asset, node_name, 'PxrTexture', fpath) 401 | set_params(settings, ch_type, node_name, asset) 402 | 403 | # print_dict(chan_nodes, msg='chan_nodes:\n') 404 | 405 | # make direct connections 406 | # 407 | LOG.debug_info(' + Direct connections...') 408 | for ch_type in chans: 409 | if not ch_type in mappings: 410 | LOG.debug_warning(' |_ skipped %r', ch_type) 411 | continue 412 | src = None 413 | dst_type = mappings[ch_type]['type'] 414 | dst_param = mappings[ch_type]['param'] 415 | if dst_type == 'normal': 416 | src = '%s.resultN' % (chan_nodes[ch_type]) 417 | elif dst_type == 'color': 418 | src = '%s.resultRGB' % (chan_nodes[ch_type]) 419 | elif dst_type == 'float': 420 | src = '%s.resultR' % (chan_nodes[ch_type]) 421 | else: 422 | # don't create a connection 423 | if dst_param != 'graph': 424 | # connections with a graph type will be handled later, so 425 | # we don't warn in that case. 426 | LOG.debug_warning('WARNING: Not connecting: %s', ch_type) 427 | continue 428 | if dst_param == 'graph': 429 | continue 430 | dst = '%s.%s' % (bxdf_node, dst_param) 431 | asset.addConnection(src, dst) 432 | LOG.debug_info(' |_ connect: %s -> %s' % (src, dst)) 433 | # also tag the bxdf param as connected 434 | pdict = {'type': 'reference ' + dst_type, 'value': None} 435 | asset.addParam(bxdf_node, dst_param, pdict) 436 | LOG.debug_info( 437 | ' |_ param: %s %s -> %s', pdict['type'], 438 | dst_param, pdict['value']) 439 | 440 | # make graph connections 441 | # 442 | if graph and 'connections' in graph: 443 | LOG.debug_info(' + Connect graph nodes...') 444 | for con in graph['connections']: 445 | 446 | src_node = con['src']['node'] 447 | src_ch = None 448 | if src_node == _bxdf: 449 | src_node = bxdf_node 450 | elif src_node.startswith('ch:'): 451 | src_ch = src_node[3:] 452 | if src_ch in chan_nodes: 453 | src_node = chan_nodes[src_ch] 454 | else: 455 | continue 456 | if not src_node.startswith(label): 457 | src_node = label + src_node 458 | src = '%s.%s' % (src_node, con['src']['param']) 459 | 460 | dst_node = con['dst']['node'] 461 | dst_ch = None 462 | if dst_node == _bxdf: 463 | dst_node = bxdf_node 464 | elif dst_node.startswith('ch:'): 465 | dst_ch = dst_node[3:] 466 | if dst_ch in chan_nodes: 467 | dst_node = chan_nodes[dst_ch] 468 | else: 469 | continue 470 | if not dst_node.startswith(label): 471 | dst_node = label + dst_node 472 | dst = '%s.%s' % (dst_node, con['dst']['param']) 473 | asset.addConnection(src, dst) 474 | LOG.debug_info(' |_ connect: %s -> %s', src, dst) 475 | # mark param as a connected 476 | dstType = con['dst']['type'] 477 | pdict = {'type': 'reference %s' % dstType, 'value': None} 478 | asset.addParam(dst_node, con['dst']['param'], pdict) 479 | LOG.debug_info( 480 | ' |_ param: %s %s = %s', 481 | pdict['type'], con['dst']['param'], 482 | pdict['value']) 483 | 484 | # save asset 485 | # 486 | LOG.debug_info(' + ready to save: %s' % asset_json_path) 487 | try: 488 | asset.save(asset_json_path, False) 489 | except: 490 | LOG.error('Saving the asset failed !') 491 | raise 492 | 493 | # mark this asset as ready to be moved 494 | # 495 | asset_list.append(asset_path) 496 | 497 | # move assets to the requested location 498 | # 499 | dst = ral.getAbsCategoryPath(self.cfg, categorypath) 500 | for item in asset_list: 501 | # if the asset already exists in the destination 502 | # location, we need to move it first. 503 | dst_asset = os.path.join(dst, os.path.basename(item)) 504 | if os.path.exists(dst_asset): 505 | try: 506 | os.rename(dst_asset, dst_asset + '_old') 507 | except (OSError, IOError): 508 | LOG.error('Could not rename asset to %s_old' % dst_asset) 509 | continue 510 | else: 511 | shutil.rmtree(dst_asset + '_old', ignore_errors=False) 512 | try: 513 | shutil.move(item, dst) 514 | except (OSError, IOError): 515 | LOG.error('WARNING: Could not copy asset to %s' % dst) 516 | 517 | 518 | # clean-up intermediate files 519 | for mat in tset_list: 520 | for chan, fpath_list in chans.items(): 521 | for fpath in fpath_list: 522 | if not os.path.exists(fpath): 523 | LOG.warning('cleanup: file not found: %s', fpath) 524 | continue 525 | try: 526 | os.remove(fpath) 527 | except (OSError, IOError): 528 | LOG.error('Cleanup failed: %s' % fpath) 529 | else: 530 | LOG.debug_info('Cleanup: %s' % fpath) 531 | 532 | LOG.debug_info('RenderMan : Done !') 533 | return True 534 | 535 | def addUiExportOptions(self, top_layout, mode): 536 | if mode == 'material': 537 | lyt = QFormLayout() 538 | lyt.FieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) 539 | lyt.setContentsMargins(5, 5, 5, 5) 540 | lyt.setSpacing(5) 541 | lyt.setLabelAlignment(Qt.AlignRight) 542 | # BXDF 543 | self.opt_bxdf = QComboBox() 544 | self.opt_bxdf.addItems(list(self.rules['models'].keys())) 545 | lyt.addRow('BxDF :', self.opt_bxdf) 546 | # color space 547 | self.opt_ocio = QComboBox() 548 | self.opt_ocio.addItems(['Off', 'ACES-1.2', 549 | 'filmic-blender', '$OCIO']) 550 | lyt.addRow('Color configuration :', self.opt_ocio) 551 | # add to parent layout 552 | top_layout.addLayout(lyt) 553 | # set last used bxdf and ocio config 554 | last_bxdf = self.prefsobj.get('last bxdf', None) 555 | if last_bxdf: 556 | self.opt_bxdf.setCurrentText(last_bxdf) 557 | ocio_config = self.prefsobj.get('ocio config', None) 558 | if ocio_config: 559 | self.opt_ocio.setCurrentText(ocio_config) 560 | 561 | def _print(self): 562 | prefs = self.prefsobj.get('host_prefs', {}) 563 | loaded = ['\t%s: %s\n' % (k, prefs[k]) for k in prefs] 564 | state = ['\t%s: %s\n' % (k, getattr(self, k)) for k in self.saved] 565 | LOG.debug_info( 566 | '%r ------------------------\nLOADED:\n%s\nSTATE:\n%s', self, 567 | ''.join(loaded), ''.join(state)) 568 | 569 | def _load_rules(self): 570 | fpath = FilePath(root_dir()).join('renderman_rules.json') 571 | if fpath.exists(): 572 | with open(fpath, 'r') as hdl: 573 | data = json.load(hdl) 574 | return data 575 | else: 576 | LOG.error('RULES ARE MISSING: can not open %r', fpath) 577 | return {} 578 | 579 | def set_metadata(self, asset, sp_ts): 580 | meta = asset.stdMetadata() 581 | meta['author'] = getpass.getuser() 582 | meta['description'] = ('Created by RenderMan for Substance ' 583 | 'Painter %s' % __version__) 584 | res = sp_ts.get_resolution() 585 | meta['resolution'] = '%d x %d' % (res.width, res.height) 586 | for k, v in meta.items(): 587 | asset.addMetadata(k, v) 588 | # Compatibility data 589 | # This will help other application decide if they can use this asset. 590 | # 591 | asset.setCompatibility( 592 | hostName='Substance Painter', 593 | hostVersion=sp.__version__, 594 | rendererVersion=str(self.rman_version)) 595 | LOG.debug_info(' + compatibility set') 596 | 597 | def sp_export(self, export_path): 598 | tset_names = [s.name() for s in spts.all_texture_sets()] 599 | config = dict(self.rules['export_config']) 600 | tex_path = export_path.join('exported') 601 | create_directory(tex_path) 602 | config['exportPath'] = tex_path.os_path() 603 | # config['defaultExportPreset'] = spr.ResourceID( 604 | # context='allegorithmic', name='Renderman (pxrDisney)').url() 605 | config['exportList'] = [{'rootPath': n} for n in tset_names] 606 | # print_dict(config, msg='config:\n') 607 | result = spex.export_project_textures(config) 608 | if result.status != spex.ExportStatus.Success: 609 | LOG.error(result.message) 610 | raise RuntimeError(result.message) 611 | LOG.debug_info('+ Exported --------------------------------------------') 612 | self.spx_exported_files = {} 613 | for stack, texs in result.textures.items(): 614 | LOG.debug_info(' |_ Stack %s: ', stack) 615 | stck_name = stack[0] 616 | self.spx_exported_files[stck_name] = {} 617 | for t in texs: 618 | LOG.debug_info(' |_ %s', t) 619 | if t: 620 | ch_type = re.search(r'_([A-Za-z]+)(\.\d{4})*\.\w{3}$', t).group(1) 621 | if ch_type in self.spx_exported_files[stck_name]: 622 | self.spx_exported_files[stck_name][ch_type].append(t) 623 | else: 624 | self.spx_exported_files[stck_name][ch_type] = [t] 625 | 626 | def textureset_channels(self, spts_textureset): 627 | result = {} 628 | ts_name = spts_textureset.name() 629 | chans = spts_textureset.get_stack().all_channels() 630 | for chan_type in chans: 631 | if ts_name in self.spx_exported_files: 632 | ch = chan_type_str(chan_type) 633 | result[chan_type_str(chan_type)] = \ 634 | self.spx_exported_files[ts_name].get(ch, []) 635 | return result 636 | 637 | def txmake(self, is_udim, asset_path, fpath_list, ocio_config, 638 | ocio_colorspace): 639 | 640 | rmantree = FilePath(os.environ['RMANTREE']) 641 | binary = rmantree.join('bin', app('txmake')).os_path() 642 | cmd = [binary] 643 | if is_udim: 644 | cmd += ['-resize', 'round-', 645 | '-mode', 'clamp', 646 | '-format', 'openexr', 647 | '-compression', 'pxr24', 648 | '-newer'] 649 | else: 650 | cmd += ['-resize', 'round-', 651 | '-mode', 'periodic', 652 | '-format', 'openexr', 653 | '-compression', 'pxr24', 654 | '-newer'] 655 | if ocio_config['path']: 656 | cmd += ['-ocioconfig', ocio_config['path'], 657 | '-ocioconvert', ocio_colorspace, 'rendering'] 658 | cmd += ['src', 'dst'] 659 | LOG.debug_info(' |_ cmd = %r', ' '.join(cmd)) 660 | for img in fpath_list: 661 | img = FilePath(img) 662 | cmd[-2] = img.os_path() 663 | filename = img.basename() 664 | texfile = os.path.splitext(filename)[0] + '.tex' 665 | cmd[-1] = asset_path.join(texfile).os_path() 666 | LOG.debug_info(' |_ txmake : %s -> %s', 667 | cmd[-2], cmd[-1]) 668 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, 669 | stderr=subprocess.PIPE, 670 | startupinfo=startup_info()) 671 | p.wait() 672 | 673 | # return a local path to the tex file. 674 | filename = FilePath(fpath_list[0]).basename() 675 | fname, _ = os.path.splitext(filename) 676 | asset_file_ref = FilePath(asset_path).join(fname + '.tex') 677 | if is_udim: 678 | asset_file_ref = re.sub(r'1\d{3}', '', asset_file_ref) 679 | return FilePath(asset_file_ref) 680 | 681 | 682 | root.setWindowFlag(Qt.SubWindow, True) 683 | try: 684 | self.aui = rui.Ui(SPrefs(rman_version_str, self.prefs), parent=root) 685 | except BaseException: 686 | traceback.print_exc(file=sys.stdout) 687 | else: 688 | root.setLayout(self.aui.topLayout) 689 | 690 | LOG.debug_info(' |_ done') 691 | return root, dock 692 | 693 | 694 | def pick_rmantree(): 695 | rmantree = QFileDialog.getExistingDirectory( 696 | None, 697 | caption='Select your RenderManProServer %s+ directory' % MIN_RPS) 698 | if not 'RenderManProServer-' in rmantree: 699 | ret = msg_box( 700 | 'This is not a RenderManProServer directory !', 701 | 'This software needs RendermanProServer-%s+ to run.' % MIN_RPS, 702 | QMessageBox.Abort | QMessageBox.Retry, QMessageBox.Retry) 703 | if ret == QMessageBox.Abort: 704 | raise RuntimeError('This is not a RenderMan Pro Server directory') 705 | else: 706 | return pick_rmantree() 707 | return rmantree 708 | 709 | 710 | def env_check(prefs): 711 | rmantree = prefs.get('RMANTREE', None) 712 | if rmantree is None or not os.path.exists(rmantree): 713 | rmantree = pick_rmantree() 714 | prefs.set('RMANTREE', rmantree) 715 | # check the version 716 | rps_version = re.search(r'RenderManProServer-([\d]+)', rmantree).group(1) 717 | if rps_version < MIN_RPS: 718 | ret = msg_box( 719 | 'RenderMan version too old !', 720 | 'This software needs RendermanProServer-%s+ to run.' % MIN_RPS, 721 | QMessageBox.Abort | QMessageBox.Retry, QMessageBox.Retry) 722 | if ret == QMessageBox.Retry: 723 | return env_check(prefs) 724 | raise RuntimeError( 725 | 'This software needs RendermanProServer-%s+ to run.' % MIN_RPS) 726 | 727 | LOG.info('RMANTREE = %r', rmantree) 728 | os.environ['RMANTREE'] = rmantree 729 | rmp_path = os.path.join(rmantree, 'lib', 'python3.7', 'site-packages') 730 | if rmp_path not in sys.path: 731 | sys.path.append(rmp_path) 732 | rmu_path = os.path.join(rmantree, 'bin') 733 | if rmu_path not in sys.path: 734 | sys.path.append(rmu_path) 735 | return rps_version 736 | 737 | 738 | def create_directory(dir_path): 739 | if not dir_path.exists(): 740 | try: 741 | os.mkdir(dir_path.os_path()) 742 | except (OSError, IOError): 743 | LOG.error('Asset directory could not be created !') 744 | raise 745 | LOG.debug_info(' + Created dir: %s', dir_path) 746 | else: 747 | LOG.debug_info(' + dir exists: %s', dir_path) 748 | 749 | 750 | def set_params(settings_dict, chan, node_name, asset): 751 | # The bxdf may need specific settings to match Substance Painter 752 | try: 753 | params = settings_dict[chan] 754 | except (KeyError, TypeError): 755 | pass 756 | else: 757 | for pname, pdict in params.items(): 758 | asset.addParam(node_name, pname, pdict) 759 | LOG.debug_info(' |_ param: %s %s = %s', pdict['type'], 760 | pname, pdict['value']) 761 | 762 | 763 | def add_texture_node(asset, node_name, ntype, filepath): 764 | asset.addNode(node_name, ntype, 'pattern', ntype) 765 | pdict = {'type': 'string', 'value': filepath.basename()} 766 | asset.addParam(node_name, 'filename', pdict) 767 | 768 | 769 | def chan_type_str(channel_type): 770 | return str(channel_type).split('.')[-1] 771 | 772 | 773 | def print_dict(some_dict, msg=''): 774 | LOG.debug_info(msg + json.dumps(some_dict, indent=4)) 775 | 776 | 777 | def app(name): 778 | if os.name is 'nt': 779 | return name + '.exe' 780 | return name 781 | 782 | 783 | def startup_info(): 784 | """Returns a Windows-only object to make sure tasks launched through 785 | subprocess don't open a cmd window. 786 | 787 | Returns: 788 | subprocess.STARTUPINFO -- the properly configured object if we are on 789 | Windows, otherwise None 790 | """ 791 | startupinfo = None 792 | if os.name is 'nt': 793 | startupinfo = subprocess.STARTUPINFO() 794 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 795 | return startupinfo 796 | 797 | 798 | def msg_box(msg, infos, buttons, default_button): 799 | wdgt = QMessageBox() 800 | wdgt.setText(msg) 801 | wdgt.setInformativeText(infos) 802 | wdgt.setStandardButtons(buttons) 803 | wdgt.setDefaultButton(default_button) 804 | return wdgt.exec_() 805 | 806 | 807 | # ----------------------------------------------------------------------------- 808 | 809 | def start_plugin(): 810 | """This method is called when the plugin is started.""" 811 | if sp.__version__ < MIN_SP_API: 812 | raise RuntimeError( 813 | 'RenderMan for Substance Painter requires python API %s+ !' % MIN_SP_API) 814 | setattr(start_plugin, 'obj', RenderManForSP()) 815 | LOG.info('RenderMan started') 816 | 817 | 818 | def close_plugin(): 819 | """This method is called when the plugin is stopped.""" 820 | # We need to remove all added widgets from the UI. 821 | rman_obj = getattr(start_plugin, 'obj') 822 | rman_obj.cleanup() 823 | del rman_obj 824 | setattr(start_plugin, 'obj', None) 825 | LOG.info('RenderMan stopped') 826 | 827 | 828 | if __name__ == "__main__": 829 | start_plugin() 830 | -------------------------------------------------------------------------------- /renderman_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": { 3 | "PxrDisney": { 4 | "mapping": { 5 | "BaseColor": { 6 | "param": "baseColor", 7 | "type": "color", 8 | "ocio": "srgb_texture" 9 | }, 10 | "Specular": { 11 | "param": "specular", 12 | "type": "float", 13 | "ocio": "srgb_texture" 14 | }, 15 | "Roughness": { 16 | "param": "roughness", 17 | "type": "float", 18 | "ocio": "data" 19 | }, 20 | "Metallic": { 21 | "param": "metallic", 22 | "type": "float", 23 | "ocio": "data" 24 | }, 25 | "Opacity": { 26 | "param": null, 27 | "type": null, 28 | "ocio": "data" 29 | }, 30 | "Emissive": { 31 | "param": "emitColor", 32 | "type": "color", 33 | "ocio": "srgb_texture" 34 | }, 35 | "Normal": { 36 | "param": "bumpNormal", 37 | "type": "normal", 38 | "ocio": "data" 39 | }, 40 | "Height": { 41 | "param": null, 42 | "type": null, 43 | "ocio": "data" 44 | } 45 | }, 46 | "settings": { 47 | "BaseColor": { 48 | "linearize": { 49 | "type": "int", 50 | "value": 1 51 | } 52 | }, 53 | "Specular": { 54 | "linearize": { 55 | "type": "int", 56 | "value": 1 57 | } 58 | }, 59 | "Roughness": { 60 | "linearize": { 61 | "type": "int", 62 | "value": 0 63 | } 64 | }, 65 | "Metallic": { 66 | "linearize": { 67 | "type": "int", 68 | "value": 0 69 | } 70 | }, 71 | "Opacity": { 72 | "linearize": { 73 | "type": "int", 74 | "value": 0 75 | } 76 | }, 77 | "Emissive": { 78 | "linearize": { 79 | "type": "int", 80 | "value": 1 81 | } 82 | }, 83 | "Normal": { 84 | "orientation": { 85 | "type": "int", 86 | "value": 1 87 | }, 88 | "adjustAmount": { 89 | "type": "float", 90 | "value": 1.0 91 | } 92 | } 93 | } 94 | }, 95 | "PxrSurface": { 96 | "mapping": { 97 | "BaseColor": { 98 | "param": "graph", 99 | "type": "color", 100 | "ocio": "srgb_texture" 101 | }, 102 | "Specular": { 103 | "param": "graph", 104 | "type": "color", 105 | "ocio": "srgb_texture" 106 | }, 107 | "Roughness": { 108 | "param": "specularRoughness", 109 | "type": "float", 110 | "ocio": "data" 111 | }, 112 | "Metallic": { 113 | "param": "graph", 114 | "type": "float", 115 | "ocio": "data" 116 | }, 117 | "Opacity": { 118 | "param": "presence", 119 | "type": "float", 120 | "ocio": "data" 121 | }, 122 | "Emissive": { 123 | "param": "glowColor", 124 | "type": "color", 125 | "ocio": "srgb_texture" 126 | }, 127 | "Normal": { 128 | "param": "bumpNormal", 129 | "type": "normal", 130 | "ocio": "data" 131 | }, 132 | "Height": { 133 | "param": null, 134 | "type": null, 135 | "ocio": "data" 136 | } 137 | }, 138 | "graph": { 139 | "nodes": { 140 | "_specFaceColor": { 141 | "nodetype": "PxrBlend", 142 | "category": "pattern", 143 | "params": { 144 | "operation": { 145 | "type": "int", 146 | "value": 19 147 | }, 148 | "topA": { 149 | "type": "float", 150 | "value": 0.0 151 | }, 152 | "bottomRGB": { 153 | "type": "color", 154 | "value": [ 155 | 0.04, 156 | 0.04, 157 | 0.04 158 | ] 159 | }, 160 | "bottomA": { 161 | "type": "float", 162 | "value": 1.0 163 | } 164 | } 165 | }, 166 | "_specEdgeColor": { 167 | "nodetype": "PxrBlend", 168 | "category": "pattern", 169 | "params": { 170 | "operation": { 171 | "type": "int", 172 | "value": 19 173 | }, 174 | "topA": { 175 | "type": "float", 176 | "value": 0.0 177 | }, 178 | "bottomRGB": { 179 | "type": "color", 180 | "value": [ 181 | 1, 182 | 1, 183 | 1 184 | ] 185 | }, 186 | "bottomA": { 187 | "type": "float", 188 | "value": 1.0 189 | } 190 | } 191 | }, 192 | "_diffuseAtten": { 193 | "nodetype": "PxrBlend", 194 | "category": "pattern", 195 | "params": { 196 | "operation": { 197 | "type": "int", 198 | "value": 19 199 | }, 200 | "topRGB": { 201 | "type": "color", 202 | "value": [ 203 | 0, 204 | 0, 205 | 0 206 | ] 207 | } 208 | } 209 | } 210 | }, 211 | "connections": [ 212 | { 213 | "src": { 214 | "node": "_specFaceColor", 215 | "param": "resultRGB" 216 | }, 217 | "dst": { 218 | "node": "PxrSurface", 219 | "param": "specularFaceColor", 220 | "type": "color" 221 | } 222 | }, 223 | { 224 | "src": { 225 | "node": "_specEdgeColor", 226 | "param": "resultRGB" 227 | }, 228 | "dst": { 229 | "node": "PxrSurface", 230 | "param": "specularEdgeColor", 231 | "type": "color" 232 | } 233 | }, 234 | { 235 | "src": { 236 | "node": "_diffuseAtten", 237 | "param": "resultRGB" 238 | }, 239 | "dst": { 240 | "node": "PxrSurface", 241 | "param": "diffuseColor", 242 | "type": "color" 243 | } 244 | }, 245 | { 246 | "src": { 247 | "node": "ch:BaseColor", 248 | "param": "resultRGB" 249 | }, 250 | "dst": { 251 | "node": "_specEdgeColor", 252 | "param": "topRGB", 253 | "type": "color" 254 | } 255 | }, 256 | { 257 | "src": { 258 | "node": "ch:BaseColor", 259 | "param": "resultRGB" 260 | }, 261 | "dst": { 262 | "node": "_specFaceColor", 263 | "param": "topRGB", 264 | "type": "color" 265 | } 266 | }, 267 | { 268 | "src": { 269 | "node": "ch:BaseColor", 270 | "param": "resultRGB" 271 | }, 272 | "dst": { 273 | "node": "_diffuseAtten", 274 | "param": "bottomRGB", 275 | "type": "color" 276 | } 277 | }, 278 | { 279 | "src": { 280 | "node": "ch:Metallic", 281 | "param": "resultR" 282 | }, 283 | "dst": { 284 | "node": "_specEdgeColor", 285 | "param": "topA", 286 | "type": "float" 287 | } 288 | }, 289 | { 290 | "src": { 291 | "node": "ch:Metallic", 292 | "param": "resultR" 293 | }, 294 | "dst": { 295 | "node": "_specFaceColor", 296 | "param": "topA", 297 | "type": "float" 298 | } 299 | }, 300 | { 301 | "src": { 302 | "node": "ch:Metallic", 303 | "param": "resultR" 304 | }, 305 | "dst": { 306 | "node": "_diffuseAtten", 307 | "param": "topA", 308 | "type": "float" 309 | } 310 | }, 311 | { 312 | "src": { 313 | "node": "ch:Specular", 314 | "param": "resultRGB " 315 | }, 316 | "dst": { 317 | "node": "_specFaceColor", 318 | "param": "bottomRGB", 319 | "type": "color" 320 | } 321 | } 322 | ] 323 | }, 324 | "settings": { 325 | "bxdf": { 326 | "specularModelType": { 327 | "type": "int", 328 | "value": 1 329 | } 330 | }, 331 | "BaseColor": { 332 | "linearize": { 333 | "type": "int", 334 | "value": 1 335 | } 336 | }, 337 | "Specular": { 338 | "linearize": { 339 | "type": "int", 340 | "value": 1 341 | } 342 | }, 343 | "Roughness": { 344 | "linearize": { 345 | "type": "int", 346 | "value": 0 347 | } 348 | }, 349 | "Metallic": { 350 | "linearize": { 351 | "type": "int", 352 | "value": 0 353 | } 354 | }, 355 | "Opacity": { 356 | "linearize": { 357 | "type": "int", 358 | "value": 0 359 | } 360 | }, 361 | "Emissive": { 362 | "linearize": { 363 | "type": "int", 364 | "value": 1 365 | } 366 | }, 367 | "Normal": { 368 | "orientation": { 369 | "type": "int", 370 | "value": 1 371 | }, 372 | "adjustAmount": { 373 | "type": "float", 374 | "value": 1.0 375 | } 376 | } 377 | } 378 | }, 379 | "LamaSurface": { 380 | "mapping": { 381 | "BaseColor": { 382 | "param": "graph", 383 | "type": "color", 384 | "ocio": "srgb_texture" 385 | }, 386 | "Specular": { 387 | "param": "graph", 388 | "type": "color", 389 | "ocio": "srgb_texture" 390 | }, 391 | "Roughness": { 392 | "param": "graph", 393 | "type": "float", 394 | "ocio": "data" 395 | }, 396 | "Metallic": { 397 | "param": "graph", 398 | "type": "float", 399 | "ocio": "data" 400 | }, 401 | "Opacity": { 402 | "param": "presence", 403 | "type": "float", 404 | "ocio": "data" 405 | }, 406 | "Emissive": { 407 | "param": "graph", 408 | "type": "color", 409 | "ocio": "srgb_texture" 410 | }, 411 | "Normal": { 412 | "param": "graph", 413 | "type": "normal", 414 | "ocio": "data" 415 | }, 416 | "Height": { 417 | "param": null, 418 | "type": null, 419 | "ocio": "data" 420 | } 421 | }, 422 | "graph": { 423 | "nodes": { 424 | "_clearcoatLayer": { 425 | "nodetype": "LamaLayer", 426 | "category": "bxdf", 427 | "params": { 428 | "topMix": { 429 | "type": "float", 430 | "value": 0.0 431 | } 432 | } 433 | }, 434 | "_clearcoat": { 435 | "nodetype": "LamaDielectric", 436 | "category": "bxdf", 437 | "params": {} 438 | }, 439 | "_diffSpec": { 440 | "nodetype": "LamaAdd", 441 | "category": "bxdf", 442 | "params": { 443 | "weight1": { 444 | "type": "float", 445 | "value": 1.0 446 | }, 447 | "weight2": { 448 | "type": "float", 449 | "value": 1.0 450 | } 451 | } 452 | }, 453 | "_diffuse": { 454 | "nodetype": "LamaDiffuse", 455 | "category": "bxdf", 456 | "params": {} 457 | }, 458 | "_specular": { 459 | "nodetype": "LamaConductor", 460 | "category": "bxdf", 461 | "params": { 462 | "fresnelMode": { 463 | "type": "int", 464 | "value": 0 465 | } 466 | } 467 | }, 468 | "_converter": { 469 | "nodetype": "PxrMetallicWorkflow", 470 | "category": "pattern", 471 | "params": {} 472 | } 473 | }, 474 | "connections": [ 475 | { 476 | "src": { 477 | "node": "_clearcoatLayer", 478 | "param": "outColor" 479 | }, 480 | "dst": { 481 | "node": "LamaSurface", 482 | "param": "materialFront", 483 | "type": "bxdf" 484 | } 485 | }, 486 | { 487 | "src": { 488 | "node": "_clearcoat", 489 | "param": "outColor" 490 | }, 491 | "dst": { 492 | "node": "_clearcoatLayer", 493 | "param": "materialTop", 494 | "type": "bxdf" 495 | } 496 | }, 497 | { 498 | "src": { 499 | "node": "_diffSpec", 500 | "param": "outColor" 501 | }, 502 | "dst": { 503 | "node": "_clearcoatLayer", 504 | "param": "materialBase", 505 | "type": "bxdf" 506 | } 507 | }, 508 | { 509 | "src": { 510 | "node": "_diffuse", 511 | "param": "outColor" 512 | }, 513 | "dst": { 514 | "node": "_diffSpec", 515 | "param": "material1", 516 | "type": "bxdf" 517 | } 518 | }, 519 | { 520 | "src": { 521 | "node": "_specular", 522 | "param": "outColor" 523 | }, 524 | "dst": { 525 | "node": "_diffSpec", 526 | "param": "material2", 527 | "type": "bxdf" 528 | } 529 | }, 530 | { 531 | "src": { 532 | "node": "ch:Normal", 533 | "param": "resultN" 534 | }, 535 | "dst": { 536 | "node": "_diffuse", 537 | "param": "normal", 538 | "type": "normal" 539 | } 540 | }, 541 | { 542 | "src": { 543 | "node": "ch:Normal", 544 | "param": "resultN" 545 | }, 546 | "dst": { 547 | "node": "_specular", 548 | "param": "normal", 549 | "type": "normal" 550 | } 551 | }, 552 | { 553 | "src": { 554 | "node": "_converter", 555 | "param": "resultDiffuseRGB" 556 | }, 557 | "dst": { 558 | "node": "_diffuse", 559 | "param": "color", 560 | "type": "color" 561 | } 562 | }, 563 | { 564 | "src": { 565 | "node": "_converter", 566 | "param": "resultSpecularEdgeRGB" 567 | }, 568 | "dst": { 569 | "node": "_specular", 570 | "param": "edgeColor", 571 | "type": "color" 572 | } 573 | }, 574 | { 575 | "src": { 576 | "node": "_converter", 577 | "param": "resultSpecularFaceRGB" 578 | }, 579 | "dst": { 580 | "node": "_specular", 581 | "param": "reflectivity", 582 | "type": "color" 583 | } 584 | }, 585 | { 586 | "src": { 587 | "node": "ch:Roughness", 588 | "param": "resultR" 589 | }, 590 | "dst": { 591 | "node": "_specular", 592 | "param": "roughness", 593 | "type": "float" 594 | } 595 | }, 596 | { 597 | "src": { 598 | "node": "ch:BaseColor", 599 | "param": "resultRGB" 600 | }, 601 | "dst": { 602 | "node": "_converter", 603 | "param": "baseColor", 604 | "type": "color" 605 | } 606 | }, 607 | { 608 | "src": { 609 | "node": "ch:Metallic", 610 | "param": "resultR" 611 | }, 612 | "dst": { 613 | "node": "_converter", 614 | "param": "metallic", 615 | "type": "float" 616 | } 617 | } 618 | ] 619 | }, 620 | "settings": { 621 | "Normal": { 622 | "orientation": { 623 | "type": "int", 624 | "value": 1 625 | }, 626 | "adjustAmount": { 627 | "type": "float", 628 | "value": 1.0 629 | } 630 | } 631 | } 632 | } 633 | }, 634 | "export_config": { 635 | "exportShaderParams": false, 636 | "exportPath": null, 637 | "defaultExportPreset": "rman_pbr", 638 | "exportList": [ 639 | null 640 | ], 641 | "exportPresets": [ 642 | { 643 | "name": "rman_pbr", 644 | "maps": [ 645 | { 646 | "fileName": "$textureSet_BaseColor(.$udim)", 647 | "channels": [ 648 | { 649 | "srcChannel": "R", 650 | "destChannel": "R", 651 | "srcMapType": "documentMap", 652 | "srcMapName": "baseColor" 653 | }, 654 | { 655 | "srcChannel": "G", 656 | "destChannel": "G", 657 | "srcMapType": "documentMap", 658 | "srcMapName": "baseColor" 659 | }, 660 | { 661 | "srcChannel": "B", 662 | "destChannel": "B", 663 | "srcMapType": "documentMap", 664 | "srcMapName": "baseColor" 665 | } 666 | ] 667 | }, 668 | { 669 | "fileName": "$textureSet_Metallic(.$udim)", 670 | "channels": [ 671 | { 672 | "srcChannel": "R", 673 | "destChannel": "R", 674 | "srcMapType": "documentMap", 675 | "srcMapName": "metallic" 676 | } 677 | ] 678 | }, 679 | { 680 | "fileName": "$textureSet_Roughness(.$udim)", 681 | "channels": [ 682 | { 683 | "srcChannel": "R", 684 | "destChannel": "R", 685 | "srcMapType": "documentMap", 686 | "srcMapName": "roughness" 687 | } 688 | ] 689 | }, 690 | { 691 | "fileName": "$textureSet_Normal(.$udim)", 692 | "channels": [ 693 | { 694 | "srcChannel": "R", 695 | "destChannel": "R", 696 | "srcMapType": "virtualMap", 697 | "srcMapName": "Normal_DirectX" 698 | }, 699 | { 700 | "srcChannel": "G", 701 | "destChannel": "G", 702 | "srcMapType": "virtualMap", 703 | "srcMapName": "Normal_DirectX" 704 | }, 705 | { 706 | "srcChannel": "B", 707 | "destChannel": "B", 708 | "srcMapType": "virtualMap", 709 | "srcMapName": "Normal_DirectX" 710 | } 711 | ], 712 | "parameters": { 713 | "fileFormat": "png", 714 | "bitDepth": "16", 715 | "dithering": false, 716 | "paddingAlgorithm": "infinite" 717 | } 718 | }, 719 | { 720 | "fileName": "$textureSet_Height(.$udim)", 721 | "channels": [ 722 | { 723 | "srcChannel": "R", 724 | "destChannel": "R", 725 | "srcMapType": "documentMap", 726 | "srcMapName": "height" 727 | } 728 | ], 729 | "parameters": { 730 | "fileFormat": "png", 731 | "bitDepth": "16", 732 | "dithering": false, 733 | "paddingAlgorithm": "infinite" 734 | } 735 | }, 736 | { 737 | "fileName": "$textureSet_Emission(.$udim)", 738 | "channels": [ 739 | { 740 | "srcChannel": "R", 741 | "destChannel": "R", 742 | "srcMapType": "documentMap", 743 | "srcMapName": "emissive" 744 | }, 745 | { 746 | "srcChannel": "G", 747 | "destChannel": "G", 748 | "srcMapType": "documentMap", 749 | "srcMapName": "emissive" 750 | }, 751 | { 752 | "srcChannel": "B", 753 | "destChannel": "B", 754 | "srcMapType": "documentMap", 755 | "srcMapName": "emissive" 756 | } 757 | ] 758 | }, 759 | { 760 | "fileName": "$textureSet_Opacity(.$udim)", 761 | "channels": [ 762 | { 763 | "srcChannel": "R", 764 | "destChannel": "R", 765 | "srcMapType": "documentMap", 766 | "srcMapName": "opacity" 767 | } 768 | ] 769 | } 770 | ] 771 | } 772 | ], 773 | "exportParameters": [ 774 | { 775 | "parameters": { 776 | "fileFormat": "png", 777 | "bitDepth": "8", 778 | "dithering": true, 779 | "paddingAlgorithm": "infinite", 780 | "filter": { 781 | "outputMaps": [ 782 | "$textureSet_baseColor", 783 | "$textureSet_metallic", 784 | "$textureSet_roughness", 785 | "$textureSet_normal", 786 | "$textureSet_emission", 787 | "$textureSet_height" 788 | ] 789 | } 790 | } 791 | } 792 | ] 793 | } 794 | } -------------------------------------------------------------------------------- /resources/PxrDisney.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | Pixar 87 | D 98 | 99 | 100 | -------------------------------------------------------------------------------- /resources/PxrSurface.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | Pixar 87 | S 98 | 99 | 100 | -------------------------------------------------------------------------------- /shaders/.gitignore: -------------------------------------------------------------------------------- 1 | ref 2 | --------------------------------------------------------------------------------