├── .gitignore ├── README.md ├── __init__.py ├── maps ├── _super_map_example.json ├── rs_uber.json ├── v_fbx.json └── v_fbx.py ├── shader.py └── shelf_button.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | _tmp 3 | .idea 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Super Shader Converter 2 | 3 | Скрипт для универсального конвертирования шейдеров между разными типами в Houdini. 4 | 5 | ### Принцип работы 6 | 7 | Работает скрипт следующим образом: 8 | 9 | 1. Существует общая карта параметров, которые всегда встречаются в любом шейдере. 10 | Эти параметры находятся в файле `maps/_super_map.json`. Предполагается что эти параметры в том или ном виде встречаются 11 | в большинстве шейдеров под разными названиями (вы можете составитьс вой список таких параметров). 12 | 13 | 2. Любой шейдер нужно пребразовать к этому списку, написав все соответствия в другом json-файле. 14 | Всегда можно добавлять новые параметры в `super_map` если требуется задействовать новый тип параметров. 15 | Карта для шейдеров служит инструкцией, говорящей о том как и какие параметры считывать\записывать на ноде шейдера когда идёт обращение к параметрам супер шейдера. 16 | 17 | 3. Для преобразования одного типа шейдера в другой мы просто создаём два супер шейдера из разных нод и по списку копируем параметры с одной ноды на другую **как если бы это были одинаковые типы нод**. 18 | 19 | _Пример_ 20 | 21 | ```python 22 | import hou 23 | import super_shader 24 | node1 = hou.node('/shop/arnoldStandart1') 25 | super1 = super_shader.SuperShader(node1) # convert Arnold shader to SuperShader 26 | node2 = hou.node('/shop/RS_Material1') 27 | super2 = super_shader.SuperShader(node2) # convert RedShift shader to SuperShader 28 | 29 | # copy diffuse color 30 | diff = super1.parm('diffuse_color').get() 31 | super2.parm('diffuse_color').set(diff) 32 | 33 | # copy all parameter values 34 | for parm_name in super1.all_super_parm_names(): 35 | parm = super2.parm(parm_name) 36 | if parm: 37 | parm.set(super1.parm(parm_name).get()) 38 | # or shortcut 39 | super1.copy_parms_to(super2) 40 | ``` 41 | 42 | ### Создание карт 43 | 44 | #### Super Map 45 | 46 | Есть главная карта, описывающая все параметры супер шейдера. В любом супер шейдере нельзя прочитать параметры которых нет в этой карте. 47 | Карта находится в файле `maps/_super_map.json`. 48 | 49 | > Если такого файла нет то берется файл поумолчанию `_super_map_example.json`. Для создания своей карты просто скопируйте файл `_super_map_example.json` и переименуйте в `_super_map.json`. В дальнейшем обновление скрипта не затрёт вашу карту. 50 | 51 | В файле находится словарь, ключи которого это имя параметра. Только по этому имени можно обращаться к параметрам супер шейдера. 52 | Значения ключей это словари с двумя полями: 53 | 54 | - `type` - описывает тип данных этого параметра. Используется для проверки типа, считываемого с ноды. Некоторые типы конвертируются в нужные по возможности. Например в `bool`. 55 | 56 | - `default` - значение по умолчанию, используется если в какой-то карте шейдера нет описания данного параметра по разным причинам. 57 | 58 | #### Shader Map 59 | 60 | Для добавления нового типа шейдера нужно сделать для него карту. 61 | Для этого создайте файл `JSON` в папке maps. Внутри нужно создать словарь, описывающий этот шейдер. 62 | Карта содержит обязательные и не обязательные параметры. Соблюдайте соответствие типов параметров! 63 | 64 | Для примера смотрите карту `v_fbx.json` которая работает со стандартным FBX материаом Houdini. 65 | 66 | ##### Обязательные параметры 67 | 68 | - `name` - Используется для отображения данного шейдера в меню. 69 | 70 | - `op_name` - Тип оператора. Используется для создания шейдера под эту карту. 71 | 72 | - `context` - Контекст шейдера (используйте `shop` для SHOP контекста или `vop` для MAT котнекста). 73 | 74 | - `parameters_map` - основной словарь параметров. Данный словарь в ключах содержит имя параметра супер шейдера, а в значении - имя соответствующего параметра на описываемом шейдере. 75 | 76 | Существует несколько вариантов записи значений в `parameters_map`. 77 | 78 | 1. Просто пишем имя параметра с шейдера. В таком случае идёт обычное копирование значения с ноды шейдера в супер шейдер. 79 | 80 | Например: 81 | 82 | `"diffuse_color": "Cd"` 83 | 84 | 2. Есть возможность вызвать функцию для обработки запроса. Для этого в карте должен быть объявлен параметр `remap_module` 85 | из которого берётся эта функция. В таком случае запись будет выглядеть так: 86 | 87 | `"diffuse_map": ":remap_parm"` 88 | 89 | Можно создавать функции с любым именем. Эта функция будет получать всегда 3 аргумента: 90 | 91 | - `node` - Нода шейдера с которого считываем значение 92 | 93 | - `parm_name` - имя параметра супер шейдера которое считываем 94 | 95 | - `get_value` - `True` если считываем значение с ноды через `get()` или `eval()`, и `False` если записываем через `set()`. 96 | 97 | Если во время чтения функция возвращает `None` то это аналогично отсутствию описания данного параметра. Тогда будет использовано значение по умолчанию из `super_map`. 98 | 99 | 3. Можно использовать метод для чтения параметра с ноды но по умолчанию брать значение не из `super_map` а с параметра ноды. 100 | Например, я хочу вызвать функцию для поиска правильного начения параметра, но если оно не найдено то считать значение с конкретного параметра: 101 | 102 | _используем символ `||` (или)_ 103 | 104 | `"diffuse_map": ":remap_parm||map1"` 105 | 106 | В этом случае будет вызван метод `remap_parm`, но если он вернёт `None` то сначала мы попробуем считать значение параметра `Cd`, и если такового не найдется, тогда берём значение по умолчанию из `super_map`. 107 | Поддерживается только один параметр. То есть нельзя написать так `"diffuse_map": ":remap_parm||map1||map2||map3"`. Такая запись игнорируется! 108 | 109 | 4. Если в значение написать `null`, то это аналогично отсутствию данного параметра на ноде. 110 | Например данный шейдер не поддерживает такое свойство. В этом случае можно строку вообще не писать. Значение будет браться из `super_map`. 111 | 112 | ##### Необязательные параметры 113 | 114 | - `remap_module` - нужен если у вас есть вызов функции для чтения параметра. Модуль должен находиться в папке `maps`. Внутри модуля можно создавать любые функции и прописывать их в карте в формате: 115 | 116 | `:{func_name}` 117 | 118 | Например: 119 | 120 | `:get_vray_parameter` 121 | 122 | Эти функции нужны для непрямого преобразования параметра. Когда простого копиования не достаточно и нужны манипуляции с нодами. 123 | 124 | - `allow_creation` - позволяет добавить этот шейдер в меню для создания новых шейдеров. Если false то из такого шейдера можно считать информацию но создать новый не получится. По умолчанию false. 125 | 126 | - `set_value_expr` и `get_value_expr` - экспрешены для быстрого преобразования значений. Это словари в ключах которых находится имя супер параметра а в значении экспрешен в виде строки. 127 | 128 | В самом экспрешене используйте переменную `$value` для подстановки оригинального значения. 129 | 130 | Например, предположим что в супер шейдере есть параметр `specular_roughness` который находится в диапазоне 0-1. Но в нашем шейдере этот диапазон равен 0-100 и означает обратный эффект (не размытость а глянцевость). Задача супершейдера содержать одинаковые, унифицированные значения и преобразовывать их в подходящие для любого шейдера. 131 | Тогда нам потребуются такие преобразования: 132 | 133 | ```json 134 | { 135 | "set_value_expr":{ 136 | "":"complement and multiply by 100", 137 | "specular_roughness": "int((1-$value)*100)" 138 | }, 139 | "get_value_expr": { 140 | "":"divide by 100, complement and round", 141 | "specular_roughness": "round(1-($value*0.01), 3)" 142 | } 143 | } 144 | ``` 145 | 146 | > Проще всего скопировать готовую карту и заменить параметры. 147 | > Главное — не пропустить ни одного параметра и правильно написать строгое соответствие параметров. 148 | 149 | 150 | ### Готовые функции 151 | 152 | В модуле `shader_converter` есть готовые функции. 153 | 154 | - `convert` - преобразует выделеные ноды в новый тип шейдера. Будет создана новая нода и скопированы все параметры. Для выбора типа откроется меню. 155 | 156 | - `convert_and_replace` - аналогична функции `convert`, но после создания нового шейдера все ссылки на на старый шейдер будут заменены ссылками на новый шейдер. 157 | 158 | - `open_new_shader_menu` - открывает меню со списокм всех доступных карт шейдеров для выбора нового типа шейдера во время конверирования. Используется в функции `convert`. 159 | 160 | ### Shelf Button 161 | 162 | Готовый код для быстрого преобразования выделенных нод 163 | 164 | ```python 165 | import super_shader 166 | 167 | """ 168 | LieftClick - create new shader, copy parameters and replace old shader 169 | Ctrl + LeftCLick - just create shader and copy parameters (for tests) 170 | """ 171 | 172 | if kwargs['ctrlclick']: 173 | # just convert 174 | super_shader.convert() 175 | else: 176 | # convert and replace 177 | super_shader.convert_and_replace() 178 | ``` 179 | 180 | Данный код поместите в кнопку на полку. После нажатия на кнопку появится меню с выбором типа в который нужно преобразовать выделенные ноды. 181 | 182 | 183 | ### ToDo 184 | 185 | Идеи для реализации. 186 | 187 | - Копирование коннектов 188 | 189 | - Добавление любых типов шейдерных нод для преобразования всего нетворка 190 | 191 | - Сохранение шейдера в JSON пресет `super_map` и создание шейдера из этого пресета (для аавто переноса переноса между софтами используя meta_data) 192 | 193 | - Вынести весь Houdini-зависимый функционал отдельно чтобы ядро можно было использовать в разных софтах. 194 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | '''menuData 2 | act={name:Convert_shader,action:convert()} 3 | ''' 4 | from __future__ import absolute_import 5 | import hou 6 | from . import shader 7 | reload(shader) 8 | from .shader import SuperShader 9 | 10 | 11 | def convert(replace=False): 12 | """ 13 | Convert selected nodes to a new shaders via super shader 14 | :param replace: replace existing references to new shader 15 | :return: new nodes 16 | """ 17 | nodes = hou.selectedNodes() 18 | if not nodes: 19 | return 20 | errors = [] 21 | new_nodes = [] 22 | new_shader_map = open_new_shader_menu() 23 | if not new_shader_map: 24 | return 25 | for node in nodes: 26 | proxy_node = None 27 | node_name = node.name() 28 | new_shader_type = new_shader_map.get('op_name') 29 | if node.type().name() == new_shader_type: 30 | errors.append('Duplicate node type for %s' % node.name()) 31 | continue 32 | 33 | # get parent 34 | refs = node.parmsReferencingThis() 35 | parent = None 36 | if node.type().name() == 'material': 37 | # it is FBX subnet? 38 | parent = node.parent() 39 | proxy_node = ([x for x in node.allSubChildren() if x.type().name() in ['v_fbx']] or [None])[0] 40 | if not proxy_node: 41 | errors.append('Cant get Material shader in %s' % parent) 42 | continue 43 | if new_shader_map.get('context').lower() == node.type().category().name().lower(): 44 | parent = parent or node.parent() 45 | else: 46 | parent = hou.node('/%s' % new_shader_map.get('context', 'shop').lower().replace('vop', 'mat')) 47 | # super shader 1 48 | try: 49 | super_node = SuperShader(proxy_node or node) 50 | except Exception as e: 51 | errors.append(str(e)) 52 | continue 53 | # create 54 | # super shader 2 55 | new_shader = parent.createNode(new_shader_type, node_name=node_name+'_new') 56 | new_shader.moveToGoodPosition() 57 | try: 58 | super_new_shader = SuperShader(new_shader) 59 | except Exception as e: 60 | errors.append(str(e)) 61 | continue 62 | # copy 63 | super_node.copy_parms_to(super_new_shader) 64 | if replace: 65 | # for r in refs: 66 | # r.set(new_shader.path()) 67 | # node.setName(node_name + '_old') 68 | node.destroy() 69 | new_shader.setName(node_name) 70 | new_shader.setSelected(True, True) 71 | new_nodes.append(super_new_shader) 72 | if errors: 73 | hou.ui.displayMessage('\n'.join(errors), severity=hou.severityType.Warning) 74 | return new_nodes 75 | 76 | 77 | def convert_and_replace(): 78 | return convert(True) 79 | 80 | 81 | def open_new_shader_menu(): 82 | """ 83 | Open menu using QMenu with all maps 84 | :return: 85 | """ 86 | try: 87 | from PySide.QtGui import QMenu, QAction, QCursor 88 | except: 89 | from PySide2.QtGui import QCursor 90 | from PySide2.QtWidgets import QMenu, QAction 91 | maps = SuperShader._get_maps() 92 | if not maps: 93 | return 94 | menu = QMenu(hou.ui.mainQtWindow()) 95 | menu.setStyleSheet(hou.ui.qtStyleSheet()) 96 | for m in maps: 97 | if m.get('allow_creation'): 98 | menu.addAction(QAction(m['name'], menu)) 99 | act = menu.exec_(QCursor.pos()) 100 | if not act: 101 | return 102 | new_shader_map = ([x for x in maps if x['name'] == act.text()] or [None])[0] 103 | return new_shader_map 104 | -------------------------------------------------------------------------------- /maps/_super_map_example.json: -------------------------------------------------------------------------------- 1 | { 2 | // PARAMETER NAME TYPE DEFAULT 3 | 4 | // diffuse 5 | "diffuse_color": {"type": "vec", "default": [1, 1, 1] }, // vector3 6 | "diffuse_map": {"type": "str", "default": "" }, // string 7 | "use_diffuse_map": {"type": "bol", "default": false }, // boolean 8 | "diffuse_weight": {"type": "flt", "default": 1 }, // float or int 9 | "diffuse_weight_map": {"type": "str", "default": "" }, 10 | "use_diffuse_weight_map": {"type": "bol", "default": false }, 11 | "diffuse_roughness": {"type": "flt", "default": 0 }, 12 | "diffuse_roughness_map": {"type": "flt", "default": "" }, 13 | // ambient 14 | "ambient_color": {"type": "vec", "default": [0, 0, 0] }, 15 | "ambient_map": {"type": "str", "default": "" }, 16 | "use_ambient_map": {"type": "bol", "default": false }, 17 | "ambient_weight": {"type": "flt", "default": 1 }, 18 | "ambient_weight_map": {"type": "str", "default": "" }, 19 | "use_ambient_weight_map": {"type": "str", "default": "" }, 20 | // specular 21 | "specular_color": {"type": "vec", "default": [0, 0, 0] }, 22 | "specular_map": {"type": "str", "default": "" }, 23 | "use_specular_map": {"type": "bol", "default": false }, 24 | "specular_weight": {"type": "flt", "default": 0.5 }, 25 | "specular_weight_map": {"type": "str", "default": "" }, 26 | "use_specular_weight_map": {"type": "bol", "default": false }, 27 | "specular_roughness": {"type": "flt", "default": 0 }, 28 | "specular_roughness_map": {"type": "str", "default": "" }, 29 | "use_specular_roughness_map":{"type": "bol", "default": false }, 30 | "specular_ior": {"type": "flt", "default": 1.5 }, 31 | "specular_anisotropy": {"type": "flt", "default": 0 }, 32 | "metallic_weight": {"type": "flt", "default": 0 }, 33 | "metallic_map": {"type": "str", "default": "" }, 34 | "use_metallic_map": {"type": "bol", "default": false }, 35 | // refraction 36 | "refract_color": {"type": "vec", "default": [0, 0, 0] }, 37 | "refract_weight": {"type": "flt", "default": 0 }, 38 | "refract_roughness": {"type": "flt", "default": 0 }, 39 | "refract_ior": {"type": "flt", "default": 1.5 }, 40 | "refract_dispersion": {"type": "flr", "default": 0 }, 41 | "refract_thin_walled": {"type": "bol", "default": false }, 42 | // opacity 43 | "opacity_color": {"type": "vec", "default": [1, 1, 1] }, 44 | "opacity_map": {"type": "str", "default": "" }, 45 | "use_opacity_map": {"type": "bol", "default": false }, 46 | "opacity_weight": {"type": "flt", "default": 0 }, 47 | "opacity_weight_map": {"type": "str", "default": "" }, 48 | "use_opacity_weight_map": {"type": "bol", "default": false }, 49 | // sss 50 | "sss_amount": {"type": "flt", "default": 0 }, 51 | "sss_radius": {"type": "flt", "default": 1 }, 52 | "sss_color1": {"type": "vec", "default": [0, 0, 0] }, 53 | "sss_radius1": {"type": "flt", "default": 0 }, 54 | "sss_weight1": {"type": "flt", "default": 0 }, 55 | "sss_color2": {"type": "vec", "default": [0, 0, 0] }, 56 | "sss_radius2": {"type": "flt", "default": 0 }, 57 | "sss_weight2": {"type": "flt", "default": 0 }, 58 | "sss_color3": {"type": "vec", "default": [0, 0, 0] }, 59 | "sss_radius3": {"type": "flt", "default": 0 }, 60 | "sss_weight3": {"type": "flt", "default": 0 }, 61 | // emission 62 | "emission_color": {"type": "vec", "default": [0, 0, 0] }, 63 | "emission_map": {"type": "str", "default": "" }, 64 | "use_emission_map": {"type": "bol", "default": false }, 65 | "emission_weight": {"type": "flt", "default": 0 }, 66 | "emission_weight_map": {"type": "str", "default": "" }, 67 | "use_emission_weight_map": {"type": "bol", "default": false }, 68 | // bump & displace 69 | "bump_scale": {"type": "flt", "default": 1 }, 70 | "bump_map": {"type": "str", "default": "" }, 71 | "use_bump_map": {"type": "bol", "default": false }, 72 | "normal_map": {"type": "str", "default": "" }, 73 | "use_normal_map": {"type": "bol", "default": false }, 74 | "displace_height": {"type": "flt", "default": 1 }, 75 | "displace_map": {"type": "str", "default": "" }, 76 | "use_displace_map": {"type": "bol", "default": false } 77 | // "some_value" {"type": "int", "default": 0 } int example 78 | } 79 | 80 | -------------------------------------------------------------------------------- /maps/rs_uber.json: -------------------------------------------------------------------------------- 1 | // map for custom asset 2 | { 3 | "name": "RedShift Uber", // menu name 4 | "op_name": "rs_uber", // operator type 5 | "context": "shop", // operator context (shop/vop) 6 | "allow_creation": true, 7 | "parameters_map": { 8 | "diffuse_color": "diffCol", 9 | "diffuse_weight": "diffuse_weight", 10 | "diffuse_roughness": "diffuse_roughness", 11 | "diffuse_map": "diff", 12 | "use_diffuse_map": "useDiff", 13 | "specular_color": "specCol", 14 | "specular_weight": "refl_weight", 15 | "specular_roughness": "refl_roughness", 16 | "specular_map": "spec", 17 | "use_specular_map": "useSpec", 18 | "specular_roughness_map": "roughMap", 19 | "use_specular_roughness_map": "useRough", 20 | "specular_ior": "ior", 21 | "specular_anisotropy": "refl_aniso", 22 | "opacity_weight": "opacity", 23 | "opacity_map": "alphaMap", 24 | "use_opacity_map": "useAlpha", 25 | "refract_color": "refr_color", 26 | "refract_weight": "refr_weight", 27 | "refract_roughness": "refr_roughness", 28 | "refract_ior": "refr_abbe", 29 | "refract_dispersion": "refr_abbe", 30 | "refract_thin_walled": "refr_thin_walled", 31 | "sss_amount": "ms_amount", 32 | "sss_radius": "ms_radius_scale", 33 | "sss_color1": "ms_color0", 34 | "sss_radius1": "ms_radius0", 35 | "sss_weight1": "ms_weight0", 36 | "sss_color2": "ms_color1", 37 | "sss_radius2": "ms_radius1", 38 | "sss_weight2": "ms_weight1", 39 | "sss_color3": "ms_color2", 40 | "sss_radius3": "ms_radius2", 41 | "sss_weight3": "ms_weight2", 42 | "emission_color": "emission_color", 43 | "emission_weight": "emission_weight", 44 | "bump_scale": "scale", 45 | "bump_map": "bump", 46 | "use_bump_map": "useBump", 47 | "normal_map": "normalMap", 48 | "use_normal_map": "useNorm" 49 | }, 50 | "set_value_expr":{ 51 | // "use_diffuse_map": "bool($value)" 52 | }, 53 | "get_value_expr": { 54 | // "use_diffuse_map": "bool($value)" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /maps/v_fbx.json: -------------------------------------------------------------------------------- 1 | { 2 | // map for default FBX material 3 | "name": "FBX Surface", // menu name 4 | "op_name": "v_fbx", // operator type 5 | "context": "shop", // operator context 6 | "remap_module": "v_fbx", // function to dynamically remap 7 | "allow_creation": false, // enable create node this type from super shader. Default false 8 | 9 | "parameters_map": { 10 | // diffuse 11 | "diffuse_color": "Cd", // direct copy value (with expression if exists) 12 | "diffuse_weight": "diffuse_mult", // use function from module OR default value from super_map 13 | "diffuse_map": ":remap_parm||map1", // use function from module OR default value from parameter or super_map 14 | "use_diffuse_map": ":remap_parm||map1", 15 | "diffuse_weight_map": ":remap_parm||map1", 16 | "use_diffuse_weight_map": null, // parameter not supported of shader. You can skip this line. 17 | "diffuse_roughness": null, 18 | "diffuse_roughness_map": null, 19 | "use_diffuse_roughness_map": null, 20 | // ambient 21 | "ambient_color": "Ca", 22 | "ambient_map": ":remap_parm", 23 | "use_ambient_map": ":remap_parm", 24 | "ambient_weight": "ambient_mult", 25 | "ambient_weight_map": ":remap_parm", 26 | "use_ambient_weight_map": ":remap_parm", 27 | // specular 28 | "specular_color": "refl", 29 | "specular_map": ":remap_parm", 30 | "use_specular_map": ":remap_parm", 31 | "specular_weight": "refl_mult", 32 | "specular_weight_map": ":remap_parm", 33 | "specular_roughness": "shininess", 34 | "specular_roughness_map": ":remap_parm", 35 | "use_specular_roughness_map": ":remap_parm", 36 | "specular_ior": null, 37 | "specular_anisotropy": null, 38 | "metallic_weight": null, 39 | "metallic_map": null, 40 | // refraction 41 | "refract_color": null, 42 | "refract_weight": null, 43 | "refract_roughness": null, 44 | "refract_ior": null, 45 | "refract_dispersion": null, 46 | "refract_thin_walled": null, 47 | // opacity 48 | "opacity_color": "opacity", 49 | "opacity_map": ":remap_parm", 50 | "use_opacity_map": ":remap_parm", 51 | "opacity_weight": ":remap_parm||opacity_mult", 52 | "opacity_weight_map": null, 53 | "use_opacity_weight_map": null, 54 | // subsurface scattering 55 | "sss_amount": null, 56 | "sss_radius": null, 57 | "sss_color1": null, 58 | "sss_radius1": null, 59 | "sss_weight1": null, 60 | "sss_color2": null, 61 | "sss_radius2": null, 62 | "sss_weight2": null, 63 | "sss_color3": null, 64 | "sss_radius3": null, 65 | "sss_weight3": null, 66 | // emission 67 | "emission_color": "Ce", 68 | "emission_map": ":remap_parm", 69 | "use_emission_map": ":remap_parm", 70 | "emission_weight": "emission_mult", 71 | "emission_weight_map": ":remap_parm||emission_mult", 72 | // bump and displace 73 | "bump_scale": null, 74 | "bump_map": ":remap_parm", 75 | "use_bump_map": ":remap_parm", 76 | "normal_map": ":remap_parm", 77 | "use_normal_map": ":remap_parm", 78 | "displace_height": null, 79 | "displace_map": null, 80 | "use_displace_map": null 81 | }, 82 | 83 | "set_value_expr":{ 84 | // used when you set parameter value to shader from super shader 85 | "specular_roughness": "int((1-$value)*100)" 86 | }, 87 | "get_value_expr": { 88 | // used when you get parameter value from shader and write to super shader 89 | "specular_roughness": "round(1-($value*0.01), 3)" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /maps/v_fbx.py: -------------------------------------------------------------------------------- 1 | """ 2 | Function for Houdini FBX material. Get layer map on FBX Surface Shader (v_fbx) be parameter name. 3 | """ 4 | 5 | 6 | def remap_parm(node, parm_name, get_value=True): 7 | # find apply# parameter 8 | apply_remap = { 9 | 'diffuse_map': 'd', 10 | 'use_diffuse_map': 'd', 11 | 'diffuse_weight_map': 'df', 12 | 'use_diffuse_weight_map': 'df', 13 | 'ambient_map': 'a', 14 | 'use_ambient_map': 'a', 15 | 'ambient_weight_map': 'af', 16 | 'use_ambient_weight_map': 'af', 17 | 'opacity_map': 'o', 18 | 'use_opacity_map': 'o', 19 | 'opacity_weight': 'of', 20 | 'use_opacity_weight_map': 'of', 21 | 'specular_map': 's', # maybe r? 22 | 'use_specular_map': 's', 23 | 'specular_weight': 'sf', # maybe rf? 24 | 'emission_color': 'ems', 25 | 'emission_map': 'emsf', 26 | 'use_emission_map': 'emsf', 27 | 'specular_roughness': 'shn', 28 | 'specular_roughness_map': 'shn', 29 | 'use_specular_roughness_map': 'shn', 30 | 'bump_map': 'bump', 31 | 'use_bump_map': 'bump', 32 | 'normal_map': 'nml', 33 | 'use_normal_map': 'nml', 34 | } 35 | if get_value: 36 | # get_value 37 | if parm_name not in apply_remap: 38 | return 39 | # search apply in right state 40 | for i in range(1, 17): 41 | p = node.parm('apply%s' % i) 42 | if p: 43 | if p.eval() == apply_remap.get(parm_name): 44 | return node.parm('map%s' % i).eval() 45 | else: 46 | print 'Not implement' 47 | return False 48 | # return True if value is set 49 | -------------------------------------------------------------------------------- /shader.py: -------------------------------------------------------------------------------- 1 | import hou 2 | import os, re, json 3 | import imp 4 | import logging as _logging 5 | logger = _logging.getLogger(__name__) 6 | 7 | 8 | class SuperShader(object): 9 | class Parm(object): 10 | def __init__(self, parm_name, get_callback, set_callback): 11 | self.name = parm_name 12 | self._get_callback = get_callback 13 | self._set_callback = set_callback 14 | 15 | def eval(self): 16 | return self._get_callback(self.name) 17 | 18 | def get(self): 19 | return self.eval() 20 | 21 | def set(self, value): 22 | self._set_callback(self.name, value) 23 | 24 | def __init__(self, node): 25 | self._node = node 26 | if False: 27 | self._node = hou.node('') 28 | self.__maps = self._get_maps() 29 | if not self.__maps: 30 | raise Exception('Map files not found') 31 | self.op_name = self._node.type().name() 32 | self._map = None 33 | for m in self.__maps: 34 | if m.get('op_name') == self.op_name: 35 | self._map = m 36 | break 37 | else: 38 | raise Exception('Node type %s not found in maps' % self.op_name) 39 | self._super_map = self._get_super_map() 40 | 41 | def parm(self, node_parm_name): 42 | """ 43 | Get super parm by name 44 | :param node_parm_name: super name 45 | :return: Parm 46 | """ 47 | if node_parm_name in self._map['parameters_map']: 48 | return self.Parm(node_parm_name, self.__get_parm_value, self.__set_parm_value) 49 | 50 | def __get_parm_value(self, super_name): 51 | """ 52 | Get value callback 53 | :param super_name: super name 54 | :return: value 55 | """ 56 | if super_name in self._map['parameters_map']: 57 | node_parm_name = self._map['parameters_map'].get(super_name) 58 | if not node_parm_name: 59 | return self._apply_expression(self._super_map_default(super_name), super_name, False) 60 | m = re.match(r"^:(\w+)\|\|(\w+)$", node_parm_name) 61 | if m: 62 | return self._apply_expression(self._get_from_method_or_default(m.group(1), super_name, m.group(2)), super_name, False) 63 | m = re.match(r"^:(\w+)$", node_parm_name) 64 | if m: 65 | return self._apply_expression(self._get_from_method_or_default(m.group(1), super_name), super_name, False) 66 | return self._apply_expression(self._get_from_node_parameter(node_parm_name, super_name), super_name, False) 67 | else: 68 | return self._apply_expression(self._super_map_default(super_name), super_name, False) 69 | 70 | def __set_parm_value(self, super_name, value): 71 | """ 72 | Set value callback 73 | :param super_name: super name 74 | :param value: new value 75 | :return: True or False 76 | """ 77 | if super_name in self._map['parameters_map']: 78 | node_parm_name = self._map['parameters_map'].get(super_name) 79 | if not node_parm_name: 80 | return False 81 | m = re.match(r"^:(\w+)\|\|(\w+)$", node_parm_name) 82 | if m: 83 | v = self._get_from_method_or_default(m.group(1), super_name, m.group(2), get_value=False) 84 | return self.__set_node_value(m.group(2), v, super_name) 85 | m = re.match(r"^:(\w+)$", node_parm_name) 86 | if m: 87 | method = self._load_method(m.group(1)) 88 | if method: 89 | return method(self._node, super_name, False) 90 | return self.__set_node_value(node_parm_name, value, super_name) 91 | return False 92 | 93 | def __set_node_value(self, parm_name, value, super_name): 94 | """ 95 | Set value to original node 96 | :param parm_name: node parm name 97 | :param value: new value 98 | :param super_name: super name 99 | :return: success bool 100 | """ 101 | p = self._node.parm(parm_name) 102 | value_type = self._super_map.get(super_name).get('type') 103 | value = self._apply_expression(value, super_name) 104 | value = self.convert_type(value, value_type) 105 | if p: 106 | p.set(self._apply_expression(value, super_name)) 107 | return True 108 | else: 109 | p = self._node.parmTuple(parm_name) 110 | if p: 111 | # value should be list or tuple 112 | p.set(hou.Vector3(*value)) 113 | return True 114 | return False 115 | 116 | def convert_type(self, value, type): 117 | if type == 'vec': 118 | if not isinstance(value, (list, tuple)) or not len(value) == 3: 119 | raise Exception('Wrong value for type %s: %s' % (type, value)) 120 | return list(value) 121 | elif type == 'flt': 122 | try: 123 | return float(value) 124 | except: 125 | raise Exception('Wrong value for type %s: %s' % (type, value)) 126 | elif type == 'int': 127 | try: 128 | return int(value) 129 | except: 130 | raise Exception('Wrong value for type %s: %s' % (type, value)) 131 | elif type == 'bol': 132 | try: 133 | return bool(value) 134 | except: 135 | raise Exception('Wrong value for type %s: %s' % (type, value)) 136 | elif type == 'str': 137 | if isinstance(value, (str, unicode)): 138 | return str(value) 139 | else: 140 | raise Exception('Wrong value for type %s: %s' % (type, value)) 141 | 142 | 143 | def _apply_expression(self, value, super_parm_name, set_value=True): 144 | """ 145 | Apply parameter expression 146 | :param value: value 147 | :param super_parm_name: super name 148 | :param set_value: set value or get value mode 149 | :return: new value 150 | """ 151 | expr = self._map.get('set_value_expr' if set_value else 'get_value_expr', {}).get(super_parm_name) 152 | if expr: 153 | if isinstance(value, (str, unicode)): 154 | value = '"%s"' % value 155 | else: 156 | value = str(value) 157 | value = eval(expr.replace('$value', value)) 158 | return value 159 | 160 | def _get_from_method_or_default(self, method, super_parm, default_parm=None, get_value=True): 161 | """ 162 | Execute method 163 | :param method: method name 164 | :param super_parm: super name 165 | :param default_parm: default node parm name 166 | :param get_value: get value or set value (bool) 167 | :return: 168 | """ 169 | m = self._load_method(method) 170 | res = m(self._node, super_parm, get_value) 171 | if res is None: 172 | if default_parm: 173 | return self._get_from_node_parameter(default_parm, super_parm) 174 | return self._super_map_default(super_parm) 175 | else: 176 | return res 177 | 178 | def _super_map_default(self, super_parm): 179 | """ 180 | Return default value from super map 181 | :param super_parm: super name 182 | :return: 183 | """ 184 | return self._super_map[super_parm]['default'] 185 | 186 | def _get_from_node_parameter(self, parm_name, super_parm_name): 187 | """ 188 | Return value from node 189 | :param parm_name: node parm name 190 | :param super_parm_name: super name (for default value) 191 | :return: 192 | """ 193 | p = self._node.parm(parm_name) or self._node.parmTuple(parm_name) 194 | if not p: 195 | logger.error('Parameter not found %s.%s' % (self.op_name, parm_name)) 196 | return self._super_map_default(super_parm_name) 197 | else: 198 | return p.eval() 199 | 200 | @classmethod 201 | def _get_super_map(cls): 202 | """ 203 | Return super map 204 | :return: dict 205 | """ 206 | map_file = os.path.join(cls.maps_folder(), '_super_map.json') 207 | if not os.path.exists(map_file): 208 | map_file = os.path.join(cls.maps_folder(), '_super_map_example.json') 209 | if not os.path.exists(map_file): 210 | raise Exception('Super Map not found: %s' % map_file) 211 | try: 212 | return cls.read_json(map_file) 213 | except Exception as e: 214 | raise Exception('Cant read Super Map file: %s' % str(e)) 215 | 216 | @classmethod 217 | def _get_maps(cls): 218 | """ 219 | Return list of all existing maps 220 | :return: [{},...] 221 | """ 222 | maps_folder = cls.maps_folder() 223 | json_maps = [] 224 | for f in os.listdir(maps_folder): 225 | if f.startswith('_'): 226 | continue 227 | if os.path.splitext(f)[-1] == '.json': 228 | try: 229 | map = cls.read_json(os.path.join(maps_folder, f)) 230 | json_maps.append(map) 231 | 232 | except: 233 | print 'Error read map file: %s' % f 234 | logger.error('Error read map file: %s' % f) 235 | return json_maps 236 | 237 | def _load_method(self, name): 238 | """ 239 | Load module by name 240 | :param name: module name 241 | :return: object 242 | """ 243 | mod_name = self._map.get('remap_module') 244 | if not mod_name: 245 | raise Exception('Remap module not defined') 246 | try: 247 | mod = imp.load_source('mod_%s' % mod_name, '%s/%s.py' % (self.maps_folder(), mod_name)) 248 | except Exception as e: 249 | raise Exception('Cant import shader module %s: %s' % (mod_name, str(e))) 250 | if hasattr(mod, name): 251 | method = getattr(mod, name) 252 | if hasattr(method, '__call__'): 253 | return method 254 | else: 255 | raise Exception('Object %s.%s not callable' % (mod_name, name)) 256 | else: 257 | raise Exception('Module %s have not method %s' % (mod_name, name)) 258 | 259 | @classmethod 260 | def read_json(cls, path, **kwargs): 261 | """ 262 | Read commented JSON 263 | :param path: file path 264 | :param kwargs: 265 | :return: object 266 | """ 267 | regex = r'\s*(#|\/{2}).*$' 268 | regex_inline = r'(:?(?:\s)*([A-Za-z\d\.{}]*)|((?<=\").*\"),?)(?:\s)*(((#|(\/{2})).*)|)$' 269 | lines = open(path).readlines() 270 | 271 | for index, line in enumerate(lines): 272 | if re.search(regex, line): 273 | if re.search(r'^' + regex, line, re.IGNORECASE): 274 | lines[index] = "" 275 | elif re.search(regex_inline, line): 276 | lines[index] = re.sub(regex_inline, r'\1', line) 277 | return json.loads('\n'.join(lines), **kwargs) 278 | 279 | @staticmethod 280 | def maps_folder(): 281 | """ 282 | Return path to maps folder 283 | :return: str 284 | """ 285 | return os.path.join(os.path.dirname(__file__), 'maps').replace('\\', '/') 286 | 287 | def all_super_parm_names(self): 288 | """ 289 | Return all parameter names from super map 290 | :return: list 291 | """ 292 | return self._super_map.keys() 293 | 294 | def copy_parms_to(self, other): 295 | """ 296 | Copy parameters from current super shader to other 297 | :param other: SuperShader 298 | :return: 299 | """ 300 | if not isinstance(other, SuperShader): 301 | raise Exception('You should pass SuperShader instance only') 302 | for parm_name in self.all_super_parm_names(): 303 | p = other.parm(parm_name) 304 | if p: 305 | src = self.parm(parm_name) 306 | if src: 307 | value = src.get() 308 | else: 309 | value = self._super_map_default(parm_name) 310 | p.set(value) 311 | -------------------------------------------------------------------------------- /shelf_button.py: -------------------------------------------------------------------------------- 1 | import super_shader 2 | reload(super_shader) 3 | 4 | """ 5 | LieftClick - create new shader, copy parameters and replace old shader 6 | Ctrl + LeftCLick - just create shader and copy parameters (for tests) 7 | """ 8 | 9 | if kwargs['ctrlclick']: 10 | # just convert 11 | super_shader.convert() 12 | else: 13 | # convert and replace 14 | super_shader.convert_and_replace() --------------------------------------------------------------------------------