Integrating up through commit 90f050496
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
its licensors.
|
||||
|
||||
For complete copyright and license terms please see the LICENSE at the root of this
|
||||
distribution (the "License"). All use of this software is governed by the License,
|
||||
or, if provided, by the license below or the license accompanying this file. Do not
|
||||
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# from maya_mapping import MayaMapping
|
||||
# from max_mapping import MaxMapping
|
||||
# from blender_mapping import BlenderMapping
|
||||
|
||||
# def get_dcc_mapping(app):
|
||||
# app = app.lower()
|
||||
# if app == 'maya':
|
||||
# return MayaMapping()
|
||||
# elif app == '3dsmax':
|
||||
# return MaxMapping()
|
||||
# elif app == 'blender':
|
||||
# return BlenderMapping()
|
||||
# else:
|
||||
# return NullMapping(app)
|
||||
|
||||
__all__ = ['blender_materials',
|
||||
'dcc_material_mapping',
|
||||
'drag_and_drop',
|
||||
'main',
|
||||
'materials_export',
|
||||
'max_materials',
|
||||
'maya_materials',
|
||||
'model']
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
class BlenderMaterials(object):
|
||||
def __init__(self, files_list, materials_count):
|
||||
super(BlenderMaterials, self).__init__()
|
||||
|
||||
self.files_list = files_list
|
||||
self.current_scene = None
|
||||
self.materials_dictionary = {}
|
||||
self.materials_count = materials_count
|
||||
self.get_material_information()
|
||||
|
||||
def get_material_information(self):
|
||||
"""
|
||||
Main entry point for the material information extraction. Because this class is run
|
||||
in Standalone mode as a subprocess, the list is passed as a string- some parsing/measures
|
||||
need to be taken in order to separate values that originated as a list before passed.
|
||||
|
||||
:return: A dictionary of all of the materials gathered. Sent back to main UI through stdout
|
||||
"""
|
||||
for file in file_list.split(','):
|
||||
self.current_scene = file.replace('\'', '')
|
||||
print('File in BlenderMaterials to be processed: {}'.format(self.current_scene))
|
||||
|
||||
# pm.openFile(self.current_scene, force=True)
|
||||
# self.set_material_descriptions()
|
||||
# json.dump(self.materials_dictionary, sys.stdout)
|
||||
|
||||
@staticmethod
|
||||
def get_materials(target_mesh):
|
||||
"""
|
||||
Gathers a list of all materials attached to each mesh's shader
|
||||
|
||||
:param target_mesh: The target mesh to pull attached material information from.
|
||||
:return: List of unique material values attached to the mesh passed as an argument.
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_shader(material_name):
|
||||
"""
|
||||
Convenience function for obtaining the shader that the specified material (as an argument)
|
||||
is attached to.
|
||||
|
||||
:param material_name: Takes the material name as an argument to get associated shader object
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_shader_information(shader):
|
||||
"""
|
||||
Helper function for extracting shader/material attributes used to form the DCC specific dictionary
|
||||
of found material values for conversion.
|
||||
|
||||
:param shader: The target shader object to analyze
|
||||
:return: Complete set (in the form of two dictionaries) of file connections and material attribute values
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_material_dictionary(self, material_name, material_type, material_mesh):
|
||||
"""
|
||||
When a unique material has been found, this creates a dictionary entry with all relevant material values. This
|
||||
includes material attributes as well as attached file textures. Later in the process this information is
|
||||
leveraged when creating the Lumberyard material definition.
|
||||
|
||||
:param material_name: The name attached to the material
|
||||
:param material_type: Specific type of material (Arnold, Stingray, etc.)
|
||||
:param material_mesh: Mesh that the material is applied to
|
||||
:return:
|
||||
"""
|
||||
self.materials_count += 1
|
||||
pass
|
||||
# shader = self.get_shader(material_name)
|
||||
# shader_file_connections, shader_attributes = self.get_shader_information(shader)
|
||||
# material_dictionary = {'MaterialName': material_name, 'MaterialType': material_type, 'DccApplication': 'Maya',
|
||||
# 'AppliedMesh': material_mesh, 'FileConnections': shader_file_connections,
|
||||
# 'SceneName': str(self.current_scene), 'MaterialAttributes': shader_attributes}
|
||||
# material_name = 'Material_{}'.format(self.materials_count)
|
||||
# self.materials_dictionary[material_name] = material_dictionary
|
||||
|
||||
def set_material_descriptions(self):
|
||||
"""
|
||||
This function serves as the clearinghouse for all analyzed materials passing through the system.
|
||||
It will determine whether or not the found material has already been processed, or if it needs to
|
||||
be added to the final material dictionary. In the event that an encountered material has already
|
||||
been processed, this function creates a register of all meshes it is applied to in the 'AppliedMesh'
|
||||
attribute.
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
# scene_geo = pm.ls(v=True, geometry=True)
|
||||
# for target_mesh in scene_geo:
|
||||
# material_list = self.get_materials(target_mesh)
|
||||
# for material_name in material_list:
|
||||
# material_type = pm.nodeType(material_name, api=True)
|
||||
# material_listed = [x for x in self.materials_dictionary
|
||||
# if self.materials_dictionary[x]['MaterialName'] == material_name]
|
||||
#
|
||||
# if not material_listed:
|
||||
# self.set_material_dictionary(str(material_name), str(material_type), str(target_mesh))
|
||||
# else:
|
||||
# mesh_list = self.materials_dictionary[material_name].get('AppliedMesh')
|
||||
# if not isinstance(mesh_list, list):
|
||||
# self.materials_dictionary[material_name]['AppliedMesh'] = [mesh_list, target_mesh]
|
||||
# else:
|
||||
# mesh_list.append(target_mesh)
|
||||
|
||||
# def get_material_information():
|
||||
# material_attributes = {}
|
||||
# for target_object in bpy.data.objects:
|
||||
# if target_object.type == 'MESH':
|
||||
# print('Object: {} Material: {}'.format(target_object, target_object.active_material))
|
||||
# assigned_material = target_object.active_material
|
||||
# material_nodes = assigned_material.node_tree.nodes
|
||||
#
|
||||
# for node in material_nodes:
|
||||
# for material_input in node.inputs:
|
||||
# try:
|
||||
# attribute_name = material_input.name
|
||||
# attribute_value = node.inputs[attribute_name].default_value
|
||||
# material_attributes[attribute_name] = str(attribute_value)
|
||||
# except Exception as e:
|
||||
# print ('[{}] Exception encountered: {}'.format(material_input.name, e))
|
||||
#
|
||||
# print(json.dumps(material_attributes, sort_keys=True, indent=4))
|
||||
|
||||
|
||||
|
||||
|
||||
# get_material_information()
|
||||
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# Maya Specific Shader Mapping #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(len(sys.argv))
|
||||
print(sys.argv[5])
|
||||
# arg_values = sys.argv[5].split(',')
|
||||
# scene_information = []
|
||||
# for value in arg_values:
|
||||
# print(value)
|
||||
# scene_information.append(value)
|
||||
# initialize_scene(scene_information)
|
||||
# instance = BlenderMaterials(file_list, total_material_count)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
its licensors.
|
||||
|
||||
For complete copyright and license terms please see the LICENSE at the root of this
|
||||
distribution (the "License"). All use of this software is governed by the License,
|
||||
or, if provided, by the license below or the license accompanying this file. Do not
|
||||
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
def get_maya_material_mapping(name, material_type, file_connections):
|
||||
material_properties = {}
|
||||
if material_type == 'StingrayPBS':
|
||||
print('Mapping StingrayPBS')
|
||||
maps = 'color, metallic, roughness, normal, emissive, ao, opacity'.split(', ')
|
||||
naming_exceptions = {'color': 'baseColor', 'ao': 'ambientOcclusion'}
|
||||
for m in maps:
|
||||
texture_attribute = 'TEX_{}_map'.format(m)
|
||||
for tex in file_connections.keys():
|
||||
if tex.find(texture_attribute) != -1:
|
||||
key = m if m not in naming_exceptions else naming_exceptions.get(m)
|
||||
logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute))
|
||||
material_properties[key] = {'useTexture': 'true',
|
||||
'textureMap': file_connections.get(
|
||||
'{}.{}'.format(name, texture_attribute))}
|
||||
elif material_type == 'aiStandardSurface':
|
||||
print('Mapping AiStandardSurface')
|
||||
# TODO- Occlusion is based on a more difficult setup- there is no standard channel. Set this up as time permits
|
||||
maps = 'baseColor, metalness, specularRoughness, normal, emissionColor, opacity'.split(', ')
|
||||
naming_exceptions = {'metalness': 'metallic', 'specularRoughness': 'roughness', 'emissionColor': 'emissive'}
|
||||
for m in maps:
|
||||
key = m if m not in naming_exceptions.keys() else naming_exceptions.get(m)
|
||||
texture_attribute = m
|
||||
for tex in file_connections.keys():
|
||||
if tex.find(texture_attribute) != -1:
|
||||
logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute))
|
||||
material_properties[key] = {'useTexture': 'true',
|
||||
'textureMap': file_connections.get(
|
||||
'{}.{}'.format(name, texture_attribute))}
|
||||
else:
|
||||
pass
|
||||
|
||||
return material_properties
|
||||
|
||||
|
||||
def get_blender_material_mapping(name, material_type, file_connections):
|
||||
pass
|
||||
|
||||
|
||||
def get_max_material_mapping(name, material_type, file_connections):
|
||||
pass
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
its licensors.
|
||||
|
||||
For complete copyright and license terms please see the LICENSE at the root of this
|
||||
distribution (the "License"). All use of this software is governed by the License,
|
||||
or, if provided, by the license below or the license accompanying this file. Do not
|
||||
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
from PySide2 import QtWidgets, QtCore
|
||||
from PySide2.QtCore import Signal
|
||||
|
||||
|
||||
class DragAndDrop(QtWidgets.QWidget):
|
||||
drop_update = QtCore.Signal(list)
|
||||
drop_over = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, frame_color=None, highlight=None, parent=None):
|
||||
super(DragAndDrop, self).__init__(parent)
|
||||
|
||||
self.urls = []
|
||||
self.frame_color = frame_color
|
||||
self.frame_highlight = highlight
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
self.drag_and_drop_frame = QtWidgets.QFrame(self)
|
||||
self.drag_and_drop_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color))
|
||||
|
||||
def dragEnterEvent(self, e):
|
||||
if e.mimeData().hasUrls:
|
||||
e.accept()
|
||||
self.drop_over.emit(True)
|
||||
if self.frame_highlight:
|
||||
self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_highlight))
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
def dragLeaveEvent(self, e):
|
||||
self.drop_over.emit(False)
|
||||
|
||||
if self.frame_highlight:
|
||||
self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color))
|
||||
|
||||
|
||||
def dragMoveEvent(self, e):
|
||||
if e.mimeData().hasUrls:
|
||||
e.accept()
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
def dropEvent(self, e):
|
||||
if e.mimeData().hasUrls:
|
||||
e.setDropAction(QtCore.Qt.CopyAction)
|
||||
e.accept()
|
||||
|
||||
for url in e.mimeData().urls():
|
||||
file_name = str(url.toLocalFile())
|
||||
self.urls.append(file_name)
|
||||
self.drop_update.emit(self.urls)
|
||||
if self.frame_highlight:
|
||||
self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color))
|
||||
else:
|
||||
e.ignore()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a78856a5dddee1466183f56dad21148dcf2f093fb316f4aa3af6842912fff61
|
||||
size 8087
|
||||
+957
@@ -0,0 +1,957 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
"""
|
||||
Usage
|
||||
=====
|
||||
Put usage instructions here.
|
||||
|
||||
Output
|
||||
======
|
||||
Put output information here.
|
||||
|
||||
Notes:
|
||||
In order to run this, you'll need to verify that the "mayapy_path" class attribute corresponds to the location on
|
||||
your machine. Currently I've just included mapping instructions for Maya StingrayPBS materials, although most of
|
||||
the needed elements are in place to carry out additional materials inside of Maya pretty quickly moving forward.
|
||||
I've marked areas that still need refinement (or to be added altogether) with TODO comments
|
||||
|
||||
TODO- Add command line access (Be sure to use pathlib and box libraries)
|
||||
TODO- Docstrings need work... wanted to get descriptions in but they need to be set for Sphinx
|
||||
TODO- Add Blender and 3ds Max interoperability
|
||||
Links:
|
||||
https://blender.stackexchange.com/questions/100497/use-blenders-bpy-in-projects-outside-blender
|
||||
https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/3DSMax-Batch/files/GUID-0968FF0A-5ADD-454D-B8F6-1983E76A4AF9-htm.html
|
||||
|
||||
TODO- Look at dynaconf and wire in a solid means for configuration settings
|
||||
TODO- This hasn't been "designed"- might be worth it to consider the visual design to ensure the most effective and
|
||||
attractive UI
|
||||
TODO- Allow revisions to Model
|
||||
TODO- Create several test files from different DCC Applications with different materials
|
||||
|
||||
Reading FBX file information (might come in handy later)
|
||||
-- Materials information can be extracted from ASCII fbx pretty easily, binary is possible but more difficult
|
||||
-- FBX files could be exported as ASCII files and I could use regex there to extract material information
|
||||
-- I couldn't get pyfbx_i42 to work, but purportedly it can extract information from binary files. You may just have
|
||||
to use the specified python versions
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from PySide2 import QtWidgets, QtCore, QtGui
|
||||
from PySide2.QtCore import Slot
|
||||
from PySide2.QtWidgets import QApplication
|
||||
|
||||
from dcc_materials.model import MaterialsModel
|
||||
from dcc_materials.drag_and_drop import DragAndDrop
|
||||
import dcc_materials.dcc_material_mapping as mat_map
|
||||
|
||||
|
||||
class MaterialsToLumberyard(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(MaterialsToLumberyard, self).__init__(parent)
|
||||
|
||||
self.app = QtWidgets.QApplication.instance()
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setGeometry(50, 50, 800, 520)
|
||||
self.setObjectName('MaterialsToLumberyard')
|
||||
self.setWindowTitle(' ')
|
||||
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowMinMaxButtonsHint)
|
||||
self.isTopLevel()
|
||||
|
||||
self.desktop_location = os.path.join(os.path.expanduser('~'), 'Desktop')
|
||||
self.directory_path = os.path.dirname(os.path.abspath(__file__))
|
||||
self.lumberyard_materials_directory = os.path.join(self.desktop_location, 'LumberyardMaterials')
|
||||
self.mayapy_path = os.path.abspath("C:/Program Files/Autodesk/Maya2020/bin/mayapy.exe")
|
||||
self.blender_path = self.get_blender_path()
|
||||
self.bold_font_large = QtGui.QFont('Helvetica', 7, QtGui.QFont.Bold)
|
||||
self.medium_font = QtGui.QFont('Helvetica', 7, QtGui.QFont.Normal)
|
||||
self.blessed_file_extensions = 'ma mb fbx max blend'.split(' ')
|
||||
|
||||
self.dcc_materials_dictionary = {}
|
||||
self.lumberyard_materials_dictionary = {}
|
||||
self.lumberyard_material_nodes = []
|
||||
self.target_file_list = []
|
||||
self.current_scene = None
|
||||
self.model = None
|
||||
self.total_materials = 0
|
||||
|
||||
self.main_container = QtWidgets.QVBoxLayout(self)
|
||||
self.main_container.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_container.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.setLayout(self.main_container)
|
||||
self.content_layout = QtWidgets.QVBoxLayout()
|
||||
self.content_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.content_layout.setContentsMargins(10, 3, 10, 5)
|
||||
self.main_container.addLayout(self.content_layout)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Header Bar
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.header_bar_layout = QtWidgets.QHBoxLayout()
|
||||
self.lumberyard_logo_layout = QtWidgets.QHBoxLayout()
|
||||
self.lumberyard_logo_layout.setAlignment(QtCore.Qt.AlignLeft)
|
||||
logo_path = os.path.join(self.directory_path, 'img/lumberyard_logo.png')
|
||||
logo_pixmap = QtGui.QPixmap(logo_path)
|
||||
self.lumberyard_logo = QtWidgets.QLabel()
|
||||
self.lumberyard_logo.setPixmap(logo_pixmap)
|
||||
self.lumberyard_logo_layout.addWidget(self.lumberyard_logo)
|
||||
self.header_bar_layout.addLayout(self.lumberyard_logo_layout)
|
||||
|
||||
self.switch_combobox_layout = QtWidgets.QHBoxLayout()
|
||||
self.switch_combobox_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.switch_layout_combobox = QtWidgets.QComboBox()
|
||||
self.set_combobox_items_accessibility()
|
||||
self.switch_layout_combobox.setFixedSize(250, 30)
|
||||
self.combobox_items = ['Add Source Files', 'Source File List', 'DCC Material Values', 'Export Materials']
|
||||
self.switch_layout_combobox.setStyleSheet('QComboBox {padding-left:6px;}')
|
||||
self.switch_layout_combobox.addItems(self.combobox_items)
|
||||
self.switch_combobox_layout.addWidget(self.switch_layout_combobox)
|
||||
self.header_bar_layout.addLayout(self.switch_combobox_layout)
|
||||
|
||||
self.content_layout.addSpacing(5)
|
||||
self.content_layout.addLayout(self.header_bar_layout)
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# File Source Table / Attributes (Stacked Layout) #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
|
||||
self.content_stacked_layout = QtWidgets.QStackedLayout()
|
||||
self.content_layout.addLayout(self.content_stacked_layout)
|
||||
self.switch_layout_combobox.currentIndexChanged.connect(self.layout_combobox_changed)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Add Source Files
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
frame_color_value = '75,75,75'
|
||||
highlight_color_value = '20,106,30'
|
||||
self.drag_and_drop_widget = DragAndDrop(frame_color_value, highlight_color_value)
|
||||
self.drag_and_drop_widget.drop_update.connect(self.drag_and_drop_file_update)
|
||||
self.drag_and_drop_widget.drop_over.connect(self.drag_and_drop_over)
|
||||
self.drag_and_drop_layout = QtWidgets.QVBoxLayout()
|
||||
self.drag_and_drop_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.drag_and_drop_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.drag_and_drop_widget.setLayout(self.drag_and_drop_layout)
|
||||
|
||||
start_message = 'Drag source files here, or use file browser button below to get started.'
|
||||
self.drag_and_drop_label = QtWidgets.QLabel(start_message)
|
||||
self.drag_and_drop_label.setStyleSheet('color: white;')
|
||||
self.drag_and_drop_layout.addWidget(self.drag_and_drop_label)
|
||||
self.drag_and_drop_layout.addSpacing(10)
|
||||
|
||||
self.select_files_button_layout = QtWidgets.QHBoxLayout()
|
||||
self.select_files_button_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.select_files_button = QtWidgets.QPushButton('Select Files')
|
||||
self.select_files_button_layout.addWidget(self.select_files_button)
|
||||
self.select_files_button.clicked.connect(self.select_files_button_clicked)
|
||||
self.select_files_button.setFixedSize(80, 35)
|
||||
self.drag_and_drop_layout.addLayout(self.select_files_button_layout)
|
||||
self.content_stacked_layout.addWidget(self.drag_and_drop_widget)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Files Table
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.target_files_table = QtWidgets.QTableWidget()
|
||||
self.target_files_table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.target_files_table.setColumnCount(2)
|
||||
self.target_files_table.setAlternatingRowColors(True)
|
||||
self.target_files_table.setHorizontalHeaderLabels(['File List', ''])
|
||||
self.target_files_table.horizontalHeader().setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220); padding-top:7px; padding-left:5px;}')
|
||||
self.target_files_table.verticalHeader().hide()
|
||||
files_header = self.target_files_table.horizontalHeader()
|
||||
files_header.setFixedHeight(30)
|
||||
files_header.setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
files_header.setContentsMargins(10, 10, 0, 0)
|
||||
files_header.setDefaultSectionSize(60)
|
||||
files_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
files_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
self.target_files_table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.content_stacked_layout.addWidget(self.target_files_table)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Scene Information Table
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.material_tree_view = QtWidgets.QTreeView()
|
||||
self.headers = ['Key', 'Value']
|
||||
self.material_tree_view.setStyleSheet('QTreeView::item {height:25px;} QHeaderView::section {background-color: rgb(220, 220, 220); height:30px; padding-left:10px}')
|
||||
self.material_tree_view.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.material_tree_view.setAlternatingRowColors(True)
|
||||
self.material_tree_view.setUniformRowHeights(True)
|
||||
self.content_stacked_layout.addWidget(self.material_tree_view)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> LY Material Definitions
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.lumberyard_material_definitions_widget = QtWidgets.QWidget()
|
||||
self.lumberyard_material_definitions_layout = QtWidgets.QHBoxLayout(self.lumberyard_material_definitions_widget)
|
||||
self.lumberyard_material_definitions_layout.setSpacing(0)
|
||||
self.lumberyard_material_definitions_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.lumberyard_material_definitions_frame = QtWidgets.QFrame(self.lumberyard_material_definitions_widget)
|
||||
self.lumberyard_material_definitions_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.lumberyard_material_definitions_frame.setStyleSheet('background-color:rgb(75,75,75);')
|
||||
self.lumberyard_material_definitions_scroller = QtWidgets.QScrollArea()
|
||||
self.scroller_widget = QtWidgets.QWidget()
|
||||
self.scroller_layout = QtWidgets.QVBoxLayout()
|
||||
self.scroller_widget.setLayout(self.scroller_layout)
|
||||
self.lumberyard_material_definitions_scroller.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
||||
self.lumberyard_material_definitions_scroller.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.lumberyard_material_definitions_scroller.setWidgetResizable(True)
|
||||
self.lumberyard_material_definitions_scroller.setWidget(self.scroller_widget)
|
||||
self.lumberyard_material_definitions_layout.addWidget(self.lumberyard_material_definitions_scroller)
|
||||
self.content_stacked_layout.addWidget(self.lumberyard_material_definitions_widget)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> File processing buttons
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.process_files_layout = QtWidgets.QHBoxLayout()
|
||||
self.content_layout.addLayout(self.process_files_layout)
|
||||
self.process_files_button = QtWidgets.QPushButton('Process Added Files')
|
||||
self.process_files_button.setFixedHeight(50)
|
||||
self.process_files_button.clicked.connect(self.process_listed_files_clicked)
|
||||
self.process_files_layout.addWidget(self.process_files_button)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Status bar / Loader
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
# TODO- Move all processing of files to another thread and display progress with loader
|
||||
|
||||
self.status_bar = QtWidgets.QStatusBar()
|
||||
self.status_bar.setStyleSheet('background-color: rgb(220, 220, 220);')
|
||||
self.status_bar.setContentsMargins(0, 0, 0, 0)
|
||||
self.status_bar.setSizeGripEnabled(False)
|
||||
self.message_readout_label = QtWidgets.QLabel('Ready.')
|
||||
self.message_readout_label.setStyleSheet('padding-left: 10px')
|
||||
self.status_bar.addWidget(self.message_readout_label)
|
||||
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar_widget = QtWidgets.QWidget()
|
||||
self.progress_bar_widget_layout = QtWidgets.QHBoxLayout()
|
||||
self.progress_bar_widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.progress_bar_widget_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.progress_bar_widget.setLayout(self.progress_bar_widget_layout)
|
||||
self.status_bar.addPermanentWidget(self.progress_bar_widget)
|
||||
self.progress_bar_widget_layout.addWidget(self.progress_bar)
|
||||
self.progress_bar.setFixedSize(180, 20)
|
||||
self.main_container.addWidget(self.status_bar)
|
||||
|
||||
############################
|
||||
# UI Display Layers ########
|
||||
############################
|
||||
|
||||
def populate_source_files_table(self):
|
||||
"""
|
||||
Adds selected files from the 'Source Files' section of the UI. This creates each item listing in the table
|
||||
as well as adds a 'Remove' button that will clear corresponding item from the table. Processed files will
|
||||
get color coded, based on whether or not the materials in the file could be successfully processed. Subsequent
|
||||
searches will not clear items from the table currently, as each item acts as a register of materials that have
|
||||
and have not yet been processed.
|
||||
:return:
|
||||
"""
|
||||
self.target_files_table.setRowCount(0)
|
||||
for index, entry in enumerate(self.target_file_list):
|
||||
entry = entry[1] if type(entry) == list else entry
|
||||
self.target_files_table.insertRow(index)
|
||||
item = QtWidgets.QTableWidgetItem(' {}'.format(entry))
|
||||
self.target_files_table.setRowHeight(index, 45)
|
||||
remove_button = QtWidgets.QPushButton('Remove')
|
||||
remove_button.setFixedWidth(60)
|
||||
remove_button.clicked.connect(self.remove_source_file_clicked)
|
||||
self.target_files_table.setItem(index, 0, item)
|
||||
self.target_files_table.setCellWidget(index, 1, remove_button)
|
||||
|
||||
def populate_dcc_material_values_tree(self):
|
||||
"""
|
||||
Sets the materials model class to the file attribute tree.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Create mechanism for collapsing previously gathered materials, and or pushing them further down the list
|
||||
|
||||
self.material_tree_view.setModel(self.model)
|
||||
self.material_tree_view.expandAll()
|
||||
self.material_tree_view.resizeColumnToContents(0)
|
||||
|
||||
def populate_export_materials_list(self):
|
||||
"""
|
||||
Once all materials have been analyzed inside of DCC applications, the 'Export Materials' view lists all
|
||||
materials presented as their Lumberyard counterparts. Each listing displays a representation of the material
|
||||
file based on its corresponding DCC material values and file connections.
|
||||
:return:
|
||||
"""
|
||||
self.reset_export_materials_description()
|
||||
for count, value in enumerate(self.lumberyard_materials_dictionary):
|
||||
material_definition_node = MaterialNode([value, self.lumberyard_materials_dictionary[value]], count)
|
||||
self.lumberyard_material_nodes.append(material_definition_node)
|
||||
self.scroller_layout.addWidget(material_definition_node)
|
||||
self.scroller_layout.addLayout(self.create_separator_line())
|
||||
|
||||
############################
|
||||
# TBD ########
|
||||
############################
|
||||
|
||||
def process_file_list(self):
|
||||
"""
|
||||
The entry point for reading DCC files and extracting values. Files are filtered and separated
|
||||
by DCC app (based on file extensions) before processing is done.
|
||||
|
||||
Supported DCC applications:
|
||||
Maya (.ma, .mb, .fbx), 3dsMax(.max), Blender(.blend)
|
||||
:return:
|
||||
"""
|
||||
files_dict = {'maya': [], 'max': [], 'blender': [], 'na': []}
|
||||
for file_location in self.target_file_list:
|
||||
file_name = os.path.basename(str(file_location))
|
||||
file_extension = os.path.splitext(file_name)[1]
|
||||
target_application = self.get_target_application(file_extension)
|
||||
if target_application in files_dict.keys():
|
||||
files_dict[target_application].append(file_location)
|
||||
|
||||
for key, values in files_dict.items():
|
||||
try:
|
||||
if key == 'maya' and len(values):
|
||||
self.get_maya_material_values(values)
|
||||
elif key == 'max' and len(values):
|
||||
self.get_max_material_values(values)
|
||||
elif key == 'blender' and len(values):
|
||||
self.get_blender_material_values(values)
|
||||
else:
|
||||
pass
|
||||
except Exception as e:
|
||||
# TODO- Allow corrective actions or some display of errors if this fails?
|
||||
logging.warning('Could not process files. Error: {}'.format(e))
|
||||
|
||||
if self.dcc_materials_dictionary:
|
||||
self.set_transfer_status(self.dcc_materials_dictionary)
|
||||
# Create Model with extracted values from file list
|
||||
self.set_material_model()
|
||||
# Setup Lumberyard Material File Values
|
||||
self.set_export_materials_description()
|
||||
# Update UI Layout
|
||||
self.populate_export_materials_list()
|
||||
self.switch_layout_combobox.setCurrentIndex(3)
|
||||
self.set_ui_buttons()
|
||||
self.message_readout_label.setText('Ready.')
|
||||
|
||||
def reset_export_materials_description(self):
|
||||
pass
|
||||
|
||||
def reset_all_values(self):
|
||||
pass
|
||||
|
||||
def create_separator_line(self):
|
||||
""" Convenience function for adding separation line to the UI. """
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
line = QtWidgets.QLabel()
|
||||
line.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken)
|
||||
line.setLineWidth(1)
|
||||
line.setFixedHeight(10)
|
||||
layout.addWidget(line)
|
||||
layout.setContentsMargins(8, 0, 8, 0)
|
||||
return layout
|
||||
|
||||
def export_selected_materials(self):
|
||||
"""
|
||||
This will eventually be revised to save material definitions in the proper place in the user's project folder,
|
||||
but for now material definitions will be saved to the desktop.
|
||||
:return:
|
||||
"""
|
||||
if not os.path.exists(self.lumberyard_materials_directory):
|
||||
os.makedirs(self.lumberyard_materials_directory)
|
||||
|
||||
for node in self.lumberyard_material_nodes:
|
||||
if node.material_name_checkbox.isChecked():
|
||||
output = os.path.join(self.lumberyard_materials_directory, '{}.material'.format(node.material_name))
|
||||
with open(output, 'w', encoding='utf-8') as material_file:
|
||||
json.dump(node.material_info, material_file, ensure_ascii=False, indent=4)
|
||||
|
||||
############################
|
||||
# Getters/Setters ##########
|
||||
############################
|
||||
|
||||
@staticmethod
|
||||
def get_target_application(file_extension):
|
||||
"""
|
||||
Searches compatible file extensions and returns one of three Application names- Maya, 3dsMax, or Blender.
|
||||
:param file_extension: Passed file extension used to determine DCC Application it originated from.
|
||||
:return: Returns the application corresponding to the extension if found- otherwise returns a Boolean None
|
||||
"""
|
||||
app_extensions = {'maya': ['.ma', '.mb', '.fbx'], 'max': ['.max'], 'blender': ['.blend']}
|
||||
target_dcc_application = [key for key, values in app_extensions.items() if file_extension in values]
|
||||
if target_dcc_application:
|
||||
return target_dcc_application[0]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_lumberyard_material_template(shader_type):
|
||||
"""
|
||||
Loads material descriptions from the Lumberyard installation, providing a template to compare and convert DCC
|
||||
shaders to Lumberyard material definitions. This is the first step in the comparison. The second step is to
|
||||
compare these values with specific mapping instructions for DCC Application and DCC material type to arrive at
|
||||
a converted material.
|
||||
:param shader_type: The type of Lumberyard shader to pair material attributes to (i.e. PBR Shader)
|
||||
:return: File dictionary of the available boilerplate Lumberyard shader settings.
|
||||
"""
|
||||
definitions = os.path.join(os.path.dirname(os.path.abspath(__file__)), '{}.material'.format(shader_type))
|
||||
if os.path.exists(definitions):
|
||||
with open(definitions) as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def get_lumberyard_material_properties(name, dcc_app, material_type, file_connections):
|
||||
"""
|
||||
This system will probably need rethinking if DCCs and compatible materials grow. I've tried to keep this
|
||||
flexible so that it can be expanded with more apps and materials.
|
||||
|
||||
:param name: Material name from within the DCC application
|
||||
:param dcc_app: The application that the material was sourced from
|
||||
:param material_type: DCC material type
|
||||
:param file_connections: Texture files found attached to the materials
|
||||
"""
|
||||
|
||||
material_properties = {}
|
||||
if dcc_app == 'Maya':
|
||||
material_properties = mat_map.get_maya_material_mapping(name, material_type, file_connections)
|
||||
elif dcc_app == 'Blender':
|
||||
material_properties = mat_map.get_blender_material_mapping(name, material_type, file_connections)
|
||||
elif dcc_app == '3dsMax':
|
||||
material_properties = mat_map.get_max_material_mapping(name, material_type, file_connections)
|
||||
else:
|
||||
pass
|
||||
return material_properties
|
||||
|
||||
@staticmethod
|
||||
def get_filename_increment(name):
|
||||
"""
|
||||
Convenience function that assists in ensuring that if any materials are encountered with the same name, an
|
||||
underscore and number is appended to it to prevent overwrites.
|
||||
:param name: The name of the material. The function searches the string for increment numbers, and either adds
|
||||
one to any encountered, or adds an "_1" if passed name is the first duplicate encountered.
|
||||
:return: The adjusted name with a unique incremental value.
|
||||
"""
|
||||
last_number = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')
|
||||
number_found = last_number.search(name)
|
||||
if number_found:
|
||||
next_number = str(int(number_found.group(1)) + 1)
|
||||
start, end = number_found.span(1)
|
||||
name = name[:max(end - len(next_number), start)] + next_number + name[end:]
|
||||
return name
|
||||
|
||||
def get_maya_material_values(self, target_files):
|
||||
"""
|
||||
Launches Maya Standalone and processes list of materials for each scene passed to the 'target_files' argument.
|
||||
Also sets the environment paths needed for an instance of Maya's Python distribution. After files are processed
|
||||
a single dictionary of scene materials is returned, and added to the "materials_dictionary" scene attribute.
|
||||
:param target_files: List of files filtered from total list of files requested for processing that have a
|
||||
Maya file extension
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Set load process to a separate thread and wire load progress bar up
|
||||
|
||||
try:
|
||||
script_path = str(os.path.join(self.directory_path, 'maya_materials.py'))
|
||||
target_files.append(self.total_materials)
|
||||
runtime_env = os.environ.copy()
|
||||
runtime_env['MAYA_LOCATION'] = os.path.dirname(self.mayapy_path)
|
||||
runtime_env['PYTHONPATH'] = os.path.dirname(self.mayapy_path)
|
||||
command = f'{self.mayapy_path} "{script_path}"'
|
||||
for file in target_files:
|
||||
command += f' "{file}"'
|
||||
p = subprocess.Popen(command, shell=False, env=runtime_env, stdout=subprocess.PIPE)
|
||||
output = p.communicate()[0]
|
||||
self.set_material_dictionary(json.loads(output))
|
||||
except Exception as e:
|
||||
logging.warning('maya error: {}'.format(e))
|
||||
|
||||
def get_max_material_values(self, target_files):
|
||||
logging.debug('Max Target file: {}'.format(target_files))
|
||||
|
||||
def get_blender_material_values(self, target_files):
|
||||
logging.debug('Blender Target file: {}'.format(target_files))
|
||||
|
||||
script_path = str(os.path.join(self.directory_path, 'blender_materials.py'))
|
||||
# command = f'{self.blender_path} "{script_path}" "{target_files}" "{self.total_materials}"'
|
||||
# p = subprocess.Popen(command, shell=True, env=runtime_env, stdout=subprocess.PIPE)
|
||||
|
||||
target_files.append(self.total_materials)
|
||||
|
||||
p = subprocess.Popen([self.blender_path, '--background', '--python', script_path, '--', target_files])
|
||||
output = p.communicate()[0]
|
||||
self.set_material_dictionary(json.loads(output))
|
||||
|
||||
def get_blender_path(self):
|
||||
blender_base_directory = os.path.join(os.path.join('C:\\', 'Program Files', 'Blender Foundation'))
|
||||
blender_versions_found = []
|
||||
for (dirpath, dirnames, filenames) in os.walk(blender_base_directory):
|
||||
for filename in filenames:
|
||||
if filename == 'blender.exe':
|
||||
blender_versions_found.append(os.path.join(dirpath, filename))
|
||||
|
||||
if blender_versions_found:
|
||||
return max(blender_versions_found, key=os.path.getctime)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_combobox_items_accessibility(self):
|
||||
"""
|
||||
Locks items from within the combobox until the sections they connect to have content
|
||||
:return:
|
||||
"""
|
||||
# TODO- Add this functionality
|
||||
pass
|
||||
|
||||
def set_transfer_status(self, transfer_info):
|
||||
"""
|
||||
Colorizes listings in the 'Source Files' view of the UI after processing to green or red, indicating whether or
|
||||
not scene analysis successfully returned compatible materials and their values.
|
||||
:param transfer_info: Each file the scripts attempt to process return a receipt of the success or failure of
|
||||
the analysis.
|
||||
:return:
|
||||
"""
|
||||
# TODO- Include some way to get error information if analysis fails, and potentially offer the means to
|
||||
# effectively repair values as they mat_map to intended Lumberyard shader type
|
||||
|
||||
for row in range(self.target_files_table.rowCount()):
|
||||
for key, values in transfer_info.items():
|
||||
row_path = self.target_files_table.item(row, 0).text().strip()
|
||||
scene_processed = {x for x in transfer_info if values['SceneName'] == row_path}
|
||||
if scene_processed:
|
||||
self.target_files_table.item(row, 0).setBackground(QtGui.QColor(192, 255, 171))
|
||||
break
|
||||
else:
|
||||
self.target_files_table.item(row, 0).setBackground(QtGui.QColor(255, 177, 171))
|
||||
|
||||
def set_export_materials_description(self):
|
||||
root = self.model.rootItem
|
||||
for row in range(self.model.rowCount()):
|
||||
source_file = self.model.get_attribute_value('SceneName', root.child(row))
|
||||
name = self.model.get_attribute_value('MaterialName', root.child(row))
|
||||
material_type = self.model.get_attribute_value('MaterialType', root.child(row))
|
||||
dcc_app = self.model.get_attribute_value('DccApplication', root.child(row))
|
||||
file_connections = {}
|
||||
shader_attributes = {}
|
||||
|
||||
for childIndex in range(root.child(row).childCount()):
|
||||
child_item = root.child(row).child(childIndex)
|
||||
child_value = child_item.itemData
|
||||
if child_item.childCount():
|
||||
target_dict = file_connections if child_value[0] == 'FileConnections' else shader_attributes
|
||||
for subChildIndex in range(child_item.childCount()):
|
||||
sub_child_data = child_item.child(subChildIndex).itemData
|
||||
target_dict[sub_child_data[0]] = sub_child_data[1]
|
||||
self.set_material_description(source_file, name, dcc_app, material_type, file_connections)
|
||||
|
||||
def set_material_dictionary(self, dcc_dictionary):
|
||||
"""
|
||||
Adds all material descriptions pulled from each DCC file analyzed to the "materials_dictionary" class attribute.
|
||||
This function runs each time a subprocess is launched to gather DCC application material values.
|
||||
:param dcc_dictionary: The dictionary of values for each material analyzed by each specific DCC file list
|
||||
return analyzed values
|
||||
:return:
|
||||
"""
|
||||
logging.debug('DCC Dictionary: {}'.format(json.dumps(dcc_dictionary, indent=4)))
|
||||
self.total_materials += len(dcc_dictionary)
|
||||
self.dcc_materials_dictionary.update(dcc_dictionary)
|
||||
|
||||
def set_material_model(self, initialize=True):
|
||||
"""
|
||||
Once all materials have been gathered across a selected file set query, this organizes the values into a
|
||||
QT Model Class
|
||||
:param initialize: Default is set to boolean True. If a model has already been established in the current
|
||||
session, the initialize parameter would be set to false, and the values added to the Model. All changes to
|
||||
the model would then be redistributed to other informational views in the UI.
|
||||
:return:
|
||||
"""
|
||||
if initialize:
|
||||
self.model = MaterialsModel(self.headers, self.dcc_materials_dictionary)
|
||||
else:
|
||||
self.model.update()
|
||||
self.dcc_materials_dictionary.clear()
|
||||
self.populate_dcc_material_values_tree()
|
||||
|
||||
def set_ui_buttons(self):
|
||||
"""
|
||||
Handles UI buttons for each of the three stacked layout views (Source Files, DCC Material Values,
|
||||
Export Materials)
|
||||
:return:
|
||||
"""
|
||||
display_index = self.content_stacked_layout.currentIndex()
|
||||
self.switch_layout_combobox.setEnabled(True)
|
||||
self.process_files_button.setText('Process Listed Files')
|
||||
# Add Source Files Layout ------------------------------->>
|
||||
if display_index == 0:
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
# Source File List -------------------------------------->>
|
||||
elif display_index == 1:
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
# DCC Material Values Layout ---------------------------->>
|
||||
elif display_index == 2:
|
||||
self.process_files_button.setEnabled(False)
|
||||
|
||||
# Export Materials Layout ------------------------------->>
|
||||
else:
|
||||
self.process_files_button.setText('Export Selected Materials')
|
||||
if self.lumberyard_materials_dictionary:
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
def set_material_description(self, source_file, name, dcc_app, material_type, file_connections):
|
||||
"""
|
||||
Build dictionary for material description based on extracted values
|
||||
|
||||
:param source_file: The file that the material was extracted from
|
||||
:param name: Name of material
|
||||
:param dcc_app: Source file type of material (Maya, Blender or 3ds Max)
|
||||
:param material_type: Material type within app (i.e. Stingray PBS)
|
||||
:param file_connections: Texture files found connected to the shader
|
||||
:return:
|
||||
"""
|
||||
|
||||
default_settings = self.get_lumberyard_material_template('pbr')
|
||||
material = {'sourceFile': source_file,
|
||||
'description': name,
|
||||
'materialType': default_settings.get('materialType'),
|
||||
'parentMaterial': default_settings.get('parentMaterial'),
|
||||
'propertyLayoutVersion': default_settings.get('propertyLayoutVersion'),
|
||||
'properties': self.get_lumberyard_material_properties(name, dcc_app, material_type, file_connections)}
|
||||
self.lumberyard_materials_dictionary[name if name not in self.lumberyard_materials_dictionary.keys() else
|
||||
self.get_filename_increment(name)] = material
|
||||
|
||||
############################
|
||||
# Button Actions ###########
|
||||
############################
|
||||
|
||||
def remove_source_file_clicked(self):
|
||||
"""
|
||||
In the Source File view of the UI layout, this will remove the listed file in its respective row. If files
|
||||
have not been processed yet, it prevents that file from being analyzed. If the files have already been
|
||||
analyzed, this will remove the materials from stored values.
|
||||
:return:
|
||||
"""
|
||||
file_index = self.target_files_table.indexAt(self.sender().pos())
|
||||
del self.target_file_list[file_index.row()]
|
||||
self.populate_files_table()
|
||||
|
||||
def process_listed_files_clicked(self):
|
||||
"""
|
||||
The button serves a dual purpose, depending on the current layout of the window. 'Process listed files'
|
||||
initiates the DCC file analysis that extracts material information. In the "Export Materials" layout, this
|
||||
button (for now) will export material files corresponding to each analyzed material.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Need to decide how the materials are going to be routed. At this stage they will just be saved to the
|
||||
# desktop, but I assume that we want these files to be saved to an associated project folder
|
||||
|
||||
if self.sender().text() == 'Process Added Files':
|
||||
self.message_readout_label.setText('Gathering Material Information...')
|
||||
self.app.processEvents()
|
||||
self.process_file_list()
|
||||
else:
|
||||
self.export_selected_materials()
|
||||
|
||||
def select_files_button_clicked(self):
|
||||
"""
|
||||
This dialog allows user to select DCC files to be processed for the materials present for conversion.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Eventually it might be worth it to allow files from multiple locations to be selected. Currently
|
||||
# this only allows single/multiple files from a single directory to be selected.
|
||||
|
||||
dialog = QtWidgets.QFileDialog(self, 'Shift-Select Target Files', self.desktop_location)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
|
||||
dialog.setNameFilter('Compatible Files (*.ma *.mb *.fbx *.max *.blend)')
|
||||
dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
||||
file_view = dialog.findChild(QtWidgets.QListView, 'listView')
|
||||
|
||||
# Workaround for selecting multiple files with File Dialog
|
||||
if file_view:
|
||||
file_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
f_tree_view = dialog.findChild(QtWidgets.QTreeView)
|
||||
if f_tree_view:
|
||||
f_tree_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
|
||||
if dialog.exec_() == QtWidgets.QDialog.Accepted:
|
||||
self.target_file_list += dialog.selectedFiles()
|
||||
if self.target_file_list:
|
||||
self.populate_source_files_table()
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
def layout_combobox_changed(self):
|
||||
"""
|
||||
Handles main window layout combobox index change.
|
||||
:return:
|
||||
"""
|
||||
self.content_stacked_layout.setCurrentIndex(self.switch_layout_combobox.currentIndex())
|
||||
self.set_ui_buttons()
|
||||
|
||||
def reset_clicked(self):
|
||||
"""
|
||||
Brings the application and all variables back to their initial state.
|
||||
:return:
|
||||
"""
|
||||
self.reset_all_values()
|
||||
|
||||
############################
|
||||
# Slots ####################
|
||||
############################
|
||||
|
||||
@Slot(list)
|
||||
def drag_and_drop_file_update(self, file_list):
|
||||
for file in file_list:
|
||||
if os.path.basename(file).split('.')[-1] in self.blessed_file_extensions:
|
||||
self.target_file_list.append(file)
|
||||
self.drag_and_drop_widget.urls.clear()
|
||||
self.populate_source_files_table()
|
||||
self.message_readout_label.setText('Source files added: {}'.format(len(self.target_file_list)))
|
||||
self.drag_and_drop_label.setStyleSheet('color: white;')
|
||||
|
||||
@Slot(bool)
|
||||
def drag_and_drop_over(self, is_over):
|
||||
if is_over:
|
||||
self.drag_and_drop_label.setStyleSheet('color: rgb(0, 255, 0);')
|
||||
else:
|
||||
self.drag_and_drop_label.setStyleSheet('color: white;')
|
||||
|
||||
|
||||
class MaterialNode(QtWidgets.QWidget):
|
||||
def __init__(self, material_info, current_position, parent=None):
|
||||
super(MaterialNode, self).__init__(parent)
|
||||
|
||||
self.material_name = material_info[0]
|
||||
self.material_info = material_info[1]
|
||||
self.current_position = current_position
|
||||
self.property_settings = {}
|
||||
|
||||
self.small_font = QtGui.QFont("Helvetica", 7, QtGui.QFont.Bold)
|
||||
self.bold_font = QtGui.QFont("Helvetica", 8, QtGui.QFont.Bold)
|
||||
self.main_layout = QtWidgets.QVBoxLayout()
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
self.background_frame = QtWidgets.QFrame(self)
|
||||
self.background_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.background_frame.setStyleSheet('background-color:rgb(220, 220, 220);')
|
||||
|
||||
# ########################
|
||||
# Title Bar
|
||||
# ########################
|
||||
|
||||
self.title_bar_widget = QtWidgets.QWidget()
|
||||
self.title_bar_layout = QtWidgets.QHBoxLayout(self.title_bar_widget)
|
||||
self.title_bar_layout.setContentsMargins(10, 0, 10, 0)
|
||||
self.title_bar_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.title_bar_frame = QtWidgets.QFrame(self.title_bar_widget)
|
||||
self.title_bar_frame.setGeometry(0, 0, 5000, 40)
|
||||
self.title_bar_frame.setStyleSheet('background-color:rgb(193,154,255);')
|
||||
self.main_layout.addWidget(self.title_bar_widget)
|
||||
self.material_name_checkbox = QtWidgets.QCheckBox(self.material_name)
|
||||
self.material_name_checkbox.setFixedHeight(35)
|
||||
self.material_name_checkbox.setStyleSheet('spacing:10px; color:white')
|
||||
self.material_name_checkbox.setFont(self.bold_font)
|
||||
self.material_name_checkbox.setChecked(True)
|
||||
self.title_bar_layout.addWidget(self.material_name_checkbox)
|
||||
|
||||
self.material_file_layout = QtWidgets.QHBoxLayout()
|
||||
self.material_file_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.source_file = QtWidgets.QLabel(os.path.basename(self.material_info['sourceFile']))
|
||||
self.source_file.setStyleSheet('color:white;')
|
||||
self.source_file.setFont(self.small_font)
|
||||
self.material_file_layout.addWidget(self.source_file)
|
||||
self.material_file_layout.addSpacing(10)
|
||||
|
||||
self.edit_button = QtWidgets.QPushButton('Edit')
|
||||
self.edit_button.clicked.connect(self.edit_button_clicked)
|
||||
self.edit_button.setFixedWidth(55)
|
||||
self.material_file_layout.addWidget(self.edit_button)
|
||||
self.title_bar_layout.addLayout(self.material_file_layout)
|
||||
|
||||
self.information_layout = QtWidgets.QHBoxLayout()
|
||||
self.information_layout.setContentsMargins(10, 0, 10, 10)
|
||||
self.main_layout.addLayout(self.information_layout)
|
||||
|
||||
# ########################
|
||||
# Details layout
|
||||
# ########################
|
||||
|
||||
self.details_layout = QtWidgets.QVBoxLayout()
|
||||
self.details_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.details_groupbox = QtWidgets.QGroupBox("Details")
|
||||
self.details_groupbox.setFixedWidth(200)
|
||||
self.details_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.details_layout.addSpacing(15)
|
||||
self.material_type_label = QtWidgets.QLabel('Material Type')
|
||||
self.material_type_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);')
|
||||
self.material_type_label.setFixedHeight(25)
|
||||
self.material_type_label.setFont(self.bold_font)
|
||||
self.details_layout.addWidget(self.material_type_label)
|
||||
|
||||
self.material_type_combobox = QtWidgets.QComboBox()
|
||||
self.material_type_combobox.setFixedHeight(30)
|
||||
self.material_type_combobox.setStyleSheet('QCombobox QAbstractItemView { padding-left: 15px; }')
|
||||
material_type_items = [' Standard PBR']
|
||||
self.material_type_combobox.addItems(material_type_items)
|
||||
self.details_layout.addWidget(self.material_type_combobox)
|
||||
self.details_layout.addSpacing(10)
|
||||
|
||||
self.description_label = QtWidgets.QLabel('Description')
|
||||
self.description_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);')
|
||||
self.description_label.setFixedHeight(25)
|
||||
self.description_label.setFont(self.bold_font)
|
||||
self.details_layout.addWidget(self.description_label)
|
||||
|
||||
self.description_box = QtWidgets.QTextEdit('This space is reserved for additional information.')
|
||||
self.details_layout.addWidget(self.description_box)
|
||||
self.information_layout.addWidget(self.details_groupbox)
|
||||
self.details_groupbox.setLayout(self.details_layout)
|
||||
|
||||
# ########################
|
||||
# Properties layout
|
||||
# ########################
|
||||
|
||||
self.properties_layout = QtWidgets.QVBoxLayout()
|
||||
self.properties_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.properties_groupbox = QtWidgets.QGroupBox("Properties")
|
||||
self.properties_groupbox.setFixedWidth(150)
|
||||
self.properties_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.properties_list_widget = QtWidgets.QListWidget()
|
||||
self.material_properties = ['ambientOcclusion', 'baseColor', 'emissive', 'metallic', 'roughness', 'specularF0',
|
||||
'normal', 'opacity']
|
||||
self.properties_list_widget.addItems(self.material_properties)
|
||||
self.properties_list_widget.itemSelectionChanged.connect(self.property_selection_changed)
|
||||
self.properties_layout.addSpacing(15)
|
||||
self.properties_layout.addWidget(self.properties_list_widget)
|
||||
self.information_layout.addWidget(self.properties_groupbox)
|
||||
self.properties_groupbox.setLayout(self.properties_layout)
|
||||
|
||||
# ########################
|
||||
# Attributes layout
|
||||
# ########################
|
||||
|
||||
self.attributes_layout = QtWidgets.QVBoxLayout()
|
||||
self.attributes_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.attributes_groupbox = QtWidgets.QGroupBox("Attributes")
|
||||
self.attributes_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.information_layout.addWidget(self.attributes_groupbox)
|
||||
self.attributes_layout.addSpacing(15)
|
||||
self.attributes_table = QtWidgets.QTableWidget()
|
||||
self.attributes_table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.attributes_table.setColumnCount(2)
|
||||
self.attributes_table.setAlternatingRowColors(True)
|
||||
self.attributes_table.setHorizontalHeaderLabels(['Attribute', 'Value'])
|
||||
self.attributes_table.verticalHeader().hide()
|
||||
attributes_table_header = self.attributes_table.horizontalHeader()
|
||||
attributes_table_header.setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220);}')
|
||||
attributes_table_header.setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
attributes_table_header.setContentsMargins(10, 10, 0, 0)
|
||||
attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
attributes_table_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
||||
attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Interactive)
|
||||
self.attributes_layout.addWidget(self.attributes_table)
|
||||
self.attributes_groupbox.setLayout(self.attributes_layout)
|
||||
|
||||
self.initialize_display_values()
|
||||
|
||||
def initialize_display_values(self):
|
||||
"""
|
||||
Initializes all of the widget item information for material based on the DCC application info the class has
|
||||
been passed.
|
||||
:return:
|
||||
"""
|
||||
for material_property in self.material_properties:
|
||||
if material_property in self.material_info.get('properties'):
|
||||
self.property_settings[material_property] = self.material_info['properties'].get(material_property)
|
||||
current_row = self.material_properties.index(material_property)
|
||||
current_item = self.properties_list_widget.takeItem(current_row)
|
||||
self.properties_list_widget.insertItem(0, current_item)
|
||||
else:
|
||||
self.property_settings[material_property] = 'inactive'
|
||||
current_row = self.material_properties.index(material_property)
|
||||
item = self.properties_list_widget.item(current_row)
|
||||
item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEnabled)
|
||||
item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
|
||||
self.properties_list_widget.setCurrentRow(0)
|
||||
self.set_attributes_table(self.get_selected_property())
|
||||
|
||||
def set_attributes_table(self, selected_property):
|
||||
"""
|
||||
Displays the key, value pairs for the item selected in the Properties list widget
|
||||
:param selected_property: The item in the Properties list widget that is currently selected. Only active
|
||||
values are displayed.
|
||||
:return:
|
||||
"""
|
||||
self.attributes_table.setRowCount(0)
|
||||
row_count = 0
|
||||
for key, value in self.property_settings[selected_property].items():
|
||||
self.attributes_table.insertRow(row_count)
|
||||
key_item = QtWidgets.QTableWidgetItem(key)
|
||||
self.attributes_table.setItem(row_count, 0, key_item)
|
||||
value_item = QtWidgets.QTableWidgetItem(value)
|
||||
self.attributes_table.setItem(row_count, 1, value_item)
|
||||
row_count += 1
|
||||
|
||||
def get_selected_property(self):
|
||||
"""
|
||||
Convenience function to get current value selected in the Properties list widget.
|
||||
:return:
|
||||
"""
|
||||
return self.properties_list_widget.currentItem().text()
|
||||
|
||||
def update_model(self):
|
||||
"""
|
||||
Not sure if this will go away, but if desired, I could make attribute values able to be revised after
|
||||
materials have been scraped from the DCC materials
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def edit_button_clicked(self):
|
||||
"""
|
||||
This is in place in the event that we want to allow material revisions for properties to be made after
|
||||
DCC processing step has already been executed. The idea would basically be to surface an editable
|
||||
table where values can be added, removed or changed within the final material definition.
|
||||
:return:
|
||||
"""
|
||||
logging.debug('Edit button clicked')
|
||||
|
||||
def property_selection_changed(self):
|
||||
"""
|
||||
Fired when index of list view selected property selection has changed.
|
||||
:return:
|
||||
"""
|
||||
self.set_attributes_table(self.get_selected_property())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
materials_to_lumberyard = MaterialsToLumberyard()
|
||||
materials_to_lumberyard.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
+852
@@ -0,0 +1,852 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
"""
|
||||
Usage
|
||||
=====
|
||||
Put usage instructions here.
|
||||
|
||||
Output
|
||||
======
|
||||
Put output information here.
|
||||
|
||||
Notes:
|
||||
In order to run this, you'll need to verify that the "mayapy_path" class attribute corresponds to the location on
|
||||
your machine. Currently I've just included mapping instructions for Maya StingrayPBS materials, although most of
|
||||
the needed elements are in place to carry out additional materials inside of Maya pretty quickly moving forward.
|
||||
I've marked areas that still need refinement (or to be added altogether) with TODO comments
|
||||
|
||||
TODO- Add command line access
|
||||
TODO- Docstrings need work... wanted to get descriptions in but they need to be set for Sphinx
|
||||
TODO- Add Blender and 3ds Max interoperablity
|
||||
Links:
|
||||
https://blender.stackexchange.com/questions/100497/use-blenders-bpy-in-projects-outside-blender
|
||||
https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/3DSMax-Batch/files/GUID-0968FF0A-5ADD-454D-B8F6-1983E76A4AF9-htm.html
|
||||
|
||||
TODO- Look at dynaconf and wire in a solid means for configuration settings
|
||||
TODO- This hasn't been "designed"- might be worth it to consider the visual design to ensure the most effective and
|
||||
attractive UI
|
||||
|
||||
Reading FBX file information (might come in handy later)
|
||||
-- Materials information can be extracted from ASCII fbx pretty easily, binary is possible but more difficult
|
||||
-- FBX files could be exported as ASCII files and I could use regex there to extract material information
|
||||
-- I couldn't get pyfbx_i42 to work, but purportedly it can extract information from binary files. You may just have
|
||||
to use the specified python versions
|
||||
|
||||
-- Jonny wants me to use pathlib wherever possible for OO pathing, as well as python-box (aka Box) for dict access
|
||||
|
||||
# Things to do:
|
||||
--> Create the cube demo for Jonny with 4 attached materials
|
||||
--> Allow export of JSON file for demo
|
||||
--> Need to figure out how to clear pointers for stored data properly
|
||||
--> Allow for command line control
|
||||
|
||||
"""
|
||||
|
||||
from PySide2 import QtWidgets, QtCore, QtGui
|
||||
from PySide2.QtWidgets import QApplication
|
||||
from model import MaterialsModel, TreeNode
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class MaterialsToLumberyard(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(MaterialsToLumberyard, self).__init__(parent)
|
||||
|
||||
self.app = QtWidgets.QApplication.instance()
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setGeometry(50, 50, 800, 520)
|
||||
self.setObjectName('MaterialsToLumberyard')
|
||||
self.setWindowTitle('Materials To Lumberyard')
|
||||
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowMinMaxButtonsHint)
|
||||
self.isTopLevel()
|
||||
|
||||
self.desktop_location = os.path.join(os.path.expanduser('~'), 'Desktop')
|
||||
self.lumberyard_materials_directory = os.path.join(self.desktop_location, 'LumberyardMaterials')
|
||||
self.mayapy_path = os.path.abspath('C:/"Program Files"/Autodesk/Maya2020/bin/mayapy.exe')
|
||||
self.bold_font_large = QtGui.QFont('Helvetica', 7, QtGui.QFont.Bold)
|
||||
self.medium_font = QtGui.QFont('Helvetica', 7, QtGui.QFont.Normal)
|
||||
|
||||
self.materials_dictionary = {}
|
||||
self.material_definitions = {}
|
||||
self.material_definitions_nodes = []
|
||||
self.processed_materials = []
|
||||
self.target_file_list = []
|
||||
self.current_scene = None
|
||||
self.model = None
|
||||
self.total_materials = 0
|
||||
|
||||
self.main_container = QtWidgets.QVBoxLayout(self)
|
||||
self.main_container.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_container.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.setLayout(self.main_container)
|
||||
self.content_layout = QtWidgets.QVBoxLayout()
|
||||
self.content_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.content_layout.setContentsMargins(10, 3, 10, 5)
|
||||
self.main_container.addLayout(self.content_layout)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Header Bar
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.header_bar_layout = QtWidgets.QHBoxLayout()
|
||||
self.select_files_layout = QtWidgets.QHBoxLayout()
|
||||
self.select_files_layout.setAlignment(QtCore.Qt.AlignLeft)
|
||||
self.select_files_button = QtWidgets.QPushButton('Select Files')
|
||||
self.select_files_button.clicked.connect(self.select_files_button_clicked)
|
||||
self.select_files_button.setFixedSize(90, 35)
|
||||
self.select_files_layout.addWidget(self.select_files_button)
|
||||
self.header_bar_layout.addLayout(self.select_files_layout)
|
||||
self.switch_combobox_layout = QtWidgets.QHBoxLayout()
|
||||
self.switch_combobox_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.switch_layout_combobox = QtWidgets.QComboBox()
|
||||
self.switch_layout_combobox.setEnabled(False)
|
||||
self.switch_layout_combobox.setFixedSize(250, 30)
|
||||
self.combobox_items = ['Source Files', 'DCC Material Values', 'Export Materials']
|
||||
self.switch_layout_combobox.setStyleSheet('QComboBox {padding-left:6px;}')
|
||||
self.switch_layout_combobox.addItems(self.combobox_items)
|
||||
self.header_bar_layout.addWidget(self.switch_layout_combobox)
|
||||
self.header_bar_layout.addLayout(self.switch_combobox_layout)
|
||||
self.content_layout.addSpacing(5)
|
||||
self.content_layout.addLayout(self.header_bar_layout)
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# File Source Table / Attributes (Stacked Layout) #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
|
||||
self.content_stacked_layout = QtWidgets.QStackedLayout()
|
||||
self.content_layout.addLayout(self.content_stacked_layout)
|
||||
self.switch_layout_combobox.currentIndexChanged.connect(self.layout_combobox_changed)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Files Table
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.target_files_table = QtWidgets.QTableWidget()
|
||||
self.target_files_table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.target_files_table.setColumnCount(2)
|
||||
self.target_files_table.setAlternatingRowColors(True)
|
||||
self.target_files_table.setHorizontalHeaderLabels(['File List', ''])
|
||||
self.target_files_table.horizontalHeader().setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220); padding-top:7px; padding-left:5px;}')
|
||||
self.target_files_table.verticalHeader().hide()
|
||||
files_header = self.target_files_table.horizontalHeader()
|
||||
files_header.setFixedHeight(30)
|
||||
files_header.setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
files_header.setContentsMargins(10, 10, 0, 0)
|
||||
files_header.setDefaultSectionSize(60)
|
||||
files_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
files_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
self.target_files_table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.content_stacked_layout.addWidget(self.target_files_table)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Scene Information Table
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.material_tree_view = QtWidgets.QTreeView()
|
||||
self.headers = ['Key', 'Value']
|
||||
self.material_tree_view.setStyleSheet('QTreeView::item {height:25px;} QHeaderView::section {background-color: rgb(220, 220, 220); height:30px; padding-left:10px}')
|
||||
|
||||
self.material_tree_view.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.material_tree_view.setAlternatingRowColors(True)
|
||||
self.material_tree_view.setUniformRowHeights(True)
|
||||
self.content_stacked_layout.addWidget(self.material_tree_view)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> LY Material Definitions
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.material_definitions_widget = QtWidgets.QWidget()
|
||||
self.material_definitions_layout = QtWidgets.QHBoxLayout(self.material_definitions_widget)
|
||||
self.material_definitions_layout.setSpacing(0)
|
||||
self.material_definitions_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.material_definitions_frame = QtWidgets.QFrame(self.material_definitions_widget)
|
||||
self.material_definitions_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.material_definitions_frame.setStyleSheet('background-color:rgb(75,75,75);')
|
||||
self.material_definitions_scroller = QtWidgets.QScrollArea()
|
||||
self.scroller_widget = QtWidgets.QWidget()
|
||||
self.scroller_layout = QtWidgets.QVBoxLayout()
|
||||
self.scroller_widget.setLayout(self.scroller_layout)
|
||||
self.material_definitions_scroller.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
||||
self.material_definitions_scroller.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.material_definitions_scroller.setWidgetResizable(True)
|
||||
self.material_definitions_scroller.setWidget(self.scroller_widget)
|
||||
self.material_definitions_layout.addWidget(self.material_definitions_scroller)
|
||||
self.content_stacked_layout.addWidget(self.material_definitions_widget)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> File processing buttons
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
self.process_files_layout = QtWidgets.QHBoxLayout()
|
||||
self.content_layout.addLayout(self.process_files_layout)
|
||||
self.process_files_button = QtWidgets.QPushButton('Process Listed Files')
|
||||
self.process_files_button.setFixedHeight(50)
|
||||
self.process_files_button.clicked.connect(self.process_listed_files_clicked)
|
||||
self.process_files_layout.addWidget(self.process_files_button)
|
||||
self.reset_button = QtWidgets.QPushButton('Reset')
|
||||
self.reset_button.setFixedSize(50, 50)
|
||||
self.reset_button.clicked.connect(self.reset_clicked)
|
||||
self.reset_button.setEnabled(False)
|
||||
self.process_files_layout.addWidget(self.reset_button)
|
||||
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# ---->> Status bar / Loader
|
||||
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
# TODO- Move all processing of files to another thread and display progress with loader
|
||||
|
||||
self.status_bar = QtWidgets.QStatusBar()
|
||||
self.status_bar.setStyleSheet('background-color: rgb(220, 220, 220);')
|
||||
self.status_bar.setContentsMargins(0, 0, 0, 0)
|
||||
self.status_bar.setSizeGripEnabled(False)
|
||||
self.message_readout_label = QtWidgets.QLabel('Ready.')
|
||||
self.message_readout_label.setStyleSheet('padding-left: 10px')
|
||||
self.status_bar.addWidget(self.message_readout_label)
|
||||
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar_widget = QtWidgets.QWidget()
|
||||
self.progress_bar_widget_layout = QtWidgets.QHBoxLayout()
|
||||
self.progress_bar_widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.progress_bar_widget_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.progress_bar_widget.setLayout(self.progress_bar_widget_layout)
|
||||
self.status_bar.addPermanentWidget(self.progress_bar_widget)
|
||||
self.progress_bar_widget_layout.addWidget(self.progress_bar)
|
||||
self.progress_bar.setFixedSize(180, 20)
|
||||
self.main_container.addWidget(self.status_bar)
|
||||
|
||||
############################
|
||||
# UI Display Layers ########
|
||||
############################
|
||||
|
||||
def populate_source_files_table(self):
|
||||
"""
|
||||
Adds selected files from the 'Source Files' section of the UI. This creates each item listing in the table
|
||||
as well as adds a 'Remove' button that will clear corresponding item from the table. Processed files will
|
||||
get color coded, based on whether or not the materials in the file could be successfully processed. Subsequent
|
||||
searches will not clear items from the table currently, as each item acts as a register of materials that have
|
||||
and have not yet been processed.
|
||||
:return:
|
||||
"""
|
||||
self.target_files_table.setRowCount(0)
|
||||
for index, entry in enumerate(self.target_file_list):
|
||||
entry = entry[1] if type(entry) == list else entry
|
||||
self.target_files_table.insertRow(index)
|
||||
item = QtWidgets.QTableWidgetItem(' {}'.format(entry))
|
||||
self.target_files_table.setRowHeight(index, 45)
|
||||
remove_button = QtWidgets.QPushButton('Remove')
|
||||
remove_button.setFixedWidth(60)
|
||||
remove_button.clicked.connect(self.remove_source_file_clicked)
|
||||
self.target_files_table.setItem(index, 0, item)
|
||||
self.target_files_table.setCellWidget(index, 1, remove_button)
|
||||
|
||||
def populate_dcc_material_values_tree(self):
|
||||
"""
|
||||
Sets the materials model class to the file attribute tree.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Create mechanism for collapsing previously gathered materials, and or pushing them further down the list
|
||||
|
||||
self.material_tree_view.setModel(self.model)
|
||||
self.material_tree_view.expandAll()
|
||||
self.material_tree_view.resizeColumnToContents(0)
|
||||
|
||||
def populate_export_materials_list(self):
|
||||
"""
|
||||
Once all materials have been analyzed inside of DCC applications, the 'Export Materials' view lists all
|
||||
materials presented as their Lumberyard counterparts. Each listing displays a representation of the material
|
||||
file based on its corresponding DCC material values and file connections.
|
||||
:return:
|
||||
"""
|
||||
self.reset_export_materials_description()
|
||||
for count, value in enumerate(self.material_definitions):
|
||||
material_definition_node = MaterialNode([value, self.material_definitions[value]], count)
|
||||
self.material_definitions_nodes.append(material_definition_node)
|
||||
self.scroller_layout.addWidget(material_definition_node)
|
||||
self.scroller_layout.addLayout(self.create_separator_line())
|
||||
|
||||
############################
|
||||
# TBD ########
|
||||
############################
|
||||
|
||||
def process_file_list(self):
|
||||
"""
|
||||
The entry point for reading DCC files and extracting values. Files are filtered and separated
|
||||
by DCC app (based on file extensions) before processing is done.
|
||||
|
||||
Supported DCC applications:
|
||||
Maya (.ma, .mb, .fbx), 3dsMax(.max), Blender(.blend)
|
||||
:return:
|
||||
"""
|
||||
files_dict = {'maya': [], 'max': [], 'blender': [], 'na': []}
|
||||
for file_location in self.target_file_list:
|
||||
file_name = os.path.basename(str(file_location))
|
||||
file_extension = os.path.splitext(file_name)[1]
|
||||
target_application = self.get_target_application(file_extension)
|
||||
if target_application in files_dict.keys():
|
||||
files_dict[target_application].append(file_location)
|
||||
|
||||
for key, values in files_dict.items():
|
||||
for value in values:
|
||||
transfer_info = {'file_location': value, 'success': True}
|
||||
try:
|
||||
if key == 'maya' and len(value):
|
||||
self.get_maya_material_values(value)
|
||||
elif key == 'max' and len(value):
|
||||
self.get_max_material_values(value)
|
||||
elif key == 'blender' and len(value):
|
||||
self.get_blender_material_values(value)
|
||||
else:
|
||||
pass
|
||||
except Exception as e:
|
||||
# TODO- Allow corrective actions or some display of errors if this fails?
|
||||
transfer_info['success'] = [value, e]
|
||||
self.set_transfer_status(transfer_info)
|
||||
|
||||
if self.materials_dictionary:
|
||||
# Create Model with extracted values from file list
|
||||
self.set_material_model()
|
||||
# Setup Lumberyard Material File Values
|
||||
self.set_export_materials_description()
|
||||
# Update UI Layout
|
||||
self.populate_export_materials_list()
|
||||
self.switch_layout_combobox.setCurrentIndex(2)
|
||||
self.set_ui_buttons()
|
||||
self.message_readout_label.setText('Ready.')
|
||||
|
||||
def get_maya_material_values(self, target_files):
|
||||
"""
|
||||
Launches Maya Standalone and processes list of materials for each scene passed to the 'target_files' argument.
|
||||
Also sets the environment paths needed for an instance of Maya's Python distribution. After files are processed
|
||||
a single dictionary of scene materials is returned, and added to the "materials_dictionary" scene attribute.
|
||||
:param target_files: List of files filtered from total list of files requested for processing that have a
|
||||
Maya file extension
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Set load process to a separate thread and wire load progress bar up
|
||||
|
||||
script_path = str(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'maya_materials.py'))
|
||||
runtime_env = os.environ.copy()
|
||||
runtime_env['MAYA_LOCATION'] = os.path.dirname(self.mayapy_path)
|
||||
runtime_env['PYMEL_SKIP_MEL_INIT'] = '1'
|
||||
runtime_env['PYTHONPATH'] = os.path.dirname(self.mayapy_path)
|
||||
command = f'{self.mayapy_path} "{script_path}" "{target_files}" "{self.total_materials}"'
|
||||
p = subprocess.Popen(command, shell=True, env=runtime_env, stdout=subprocess.PIPE)
|
||||
output = p.communicate()[0]
|
||||
self.set_material_dictionary(json.loads(output))
|
||||
|
||||
def get_max_material_values(self, target_file):
|
||||
print('Max Target file: {}'.format(target_file))
|
||||
|
||||
def get_blender_material_values(self, target_file):
|
||||
print('Blender Target file: {}'.format(target_file))
|
||||
|
||||
def reset_export_materials_description(self):
|
||||
pass
|
||||
|
||||
def reset_all_values(self):
|
||||
pass
|
||||
|
||||
def create_separator_line(self):
|
||||
""" Convenience function for adding separation line to the UI. """
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
line = QtWidgets.QLabel()
|
||||
line.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken)
|
||||
line.setLineWidth(1)
|
||||
line.setFixedHeight(10)
|
||||
layout.addWidget(line)
|
||||
layout.setContentsMargins(8, 0, 8, 0)
|
||||
return layout
|
||||
|
||||
def export_selected_materials(self):
|
||||
"""
|
||||
This will eventually be revised to save material definitions in the proper place in the user's project folder,
|
||||
but for now material definitions will be saved to the desktop.
|
||||
:return:
|
||||
"""
|
||||
if not os.path.exists(self.lumberyard_materials_directory):
|
||||
os.makedirs(self.lumberyard_materials_directory)
|
||||
|
||||
for node in self.material_definitions_nodes:
|
||||
if node.material_name_checkbox.isChecked():
|
||||
output = os.path.join(self.lumberyard_materials_directory, '{}.material'.format(node.material_name))
|
||||
with open(output, 'w', encoding='utf-8') as material_file:
|
||||
json.dump(node.material_info, material_file, ensure_ascii=False, indent=4)
|
||||
|
||||
############################
|
||||
# Getters/Setters ##########
|
||||
############################
|
||||
|
||||
@staticmethod
|
||||
def get_target_application(file_extension):
|
||||
"""
|
||||
Searches compatible file extensions and returns one of three Application names- Maya, 3dsMax, or Blender.
|
||||
:param file_extension: Passed file extension used to determine DCC Application it originated from.
|
||||
:return: Returns the application corresponding to the extension if found- otherwise returns a Boolean None
|
||||
"""
|
||||
app_extensions = {'maya': ['.ma', '.mb', '.fbx'], 'max': ['.max'], 'blender': ['.blend']}
|
||||
target_dcc_application = [key for key, values in app_extensions.items() if file_extension in values]
|
||||
if target_dcc_application:
|
||||
return target_dcc_application[0]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_lumberyard_material_template(shader_type):
|
||||
"""
|
||||
Loads material descriptions from the Lumberyard installation, providing a template to compare and convert DCC
|
||||
shaders to Lumberyard material definitions. This is the first step in the comparison. The second step is to
|
||||
compare these values with specific mapping instructions for DCC Application and DCC material type to arrive at
|
||||
a converted material.
|
||||
:param shader_type: The type of Lumberyard shader to pair material attributes to (i.e. PBR Shader)
|
||||
:return: File dictionary of the available boilerplate Lumberyard shader settings.
|
||||
"""
|
||||
definitions = os.path.join(os.path.dirname(os.path.abspath(__file__)), '{}.material'.format(shader_type))
|
||||
if os.path.exists(definitions):
|
||||
with open(definitions) as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def get_lumberyard_material_properties(name, material_type, file_connections):
|
||||
""" This system will probably need rethinking if DCCs and compatible materials grow """
|
||||
material_properties = {}
|
||||
if material_type == 'StingrayPBS':
|
||||
naming_exceptions = {'color': 'baseColor', 'ao': 'ambientOcclusion'}
|
||||
maps = 'color, metallic, roughness, normal, emissive, ao, opacity'.split(', ')
|
||||
for m in maps:
|
||||
texture_attribute = 'TEX_{}_map'.format(m)
|
||||
for tex in file_connections.keys():
|
||||
if tex.find(texture_attribute) != -1:
|
||||
key = m if m not in naming_exceptions else naming_exceptions.get(m)
|
||||
material_properties[key] = {'useTexture': 'true',
|
||||
'textureMap': file_connections.get('{}.{}'.format(name, texture_attribute))}
|
||||
return material_properties
|
||||
|
||||
@staticmethod
|
||||
def get_filename_increment(name):
|
||||
"""
|
||||
Convenience function that assists in ensuring that if any materials are encountered with the same name, an
|
||||
underscore and number is appended to it to prevent overwrites.
|
||||
:param name: The name of the material. The function searches the string for increment numbers, and either adds
|
||||
one to any encountered, or adds an "_1" if passed name is the first duplicate encountered.
|
||||
:return: The adjusted name with a unique incremental value.
|
||||
"""
|
||||
last_number = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')
|
||||
number_found = last_number.search(name)
|
||||
if number_found:
|
||||
next_number = str(int(number_found.group(1)) + 1)
|
||||
start, end = number_found.span(1)
|
||||
name = name[:max(end - len(next_number), start)] + next_number + name[end:]
|
||||
return name
|
||||
|
||||
def set_transfer_status(self, transfer_info):
|
||||
"""
|
||||
Colorizes listings in the 'Source Files' view of the UI after processing to green or red, indicating whether or
|
||||
not scene analysis successfully returned compatible materials and their values.
|
||||
:param transfer_info: Each file the scripts attempt to process return a receipt of the success or failure of
|
||||
the analysis.
|
||||
:return:
|
||||
"""
|
||||
# TODO- Include some way to get error information if analysis fails, and potentially offer the means to
|
||||
# effectively repair values as they map to intended Lumberyard shader type
|
||||
|
||||
for row in range(self.target_files_table.rowCount()):
|
||||
if self.target_files_table.item(row, 0).text().strip() == transfer_info['file_location']:
|
||||
if transfer_info['success']:
|
||||
self.target_files_table.item(row, 0).setBackground(QtGui.QColor(192, 255, 171))
|
||||
else:
|
||||
self.target_files_table.item(row, 0).setBackground(QtGui.QColor(255, 177, 171))
|
||||
|
||||
def set_export_materials_description(self):
|
||||
root = self.model.rootItem
|
||||
for row in range(self.model.rowCount()):
|
||||
source_file = self.model.get_attribute_value('SceneName', root.child(row))
|
||||
name = self.model.get_attribute_value('MaterialName', root.child(row))
|
||||
material_type = self.model.get_attribute_value('MaterialType', root.child(row))
|
||||
file_connections = {}
|
||||
shader_attributes = {}
|
||||
|
||||
for childIndex in range(root.child(row).childCount()):
|
||||
child_item = root.child(row).child(childIndex)
|
||||
child_value = child_item.itemData
|
||||
if child_item.childCount():
|
||||
target_dict = file_connections if child_value[0] == 'FileConnections' else shader_attributes
|
||||
for subChildIndex in range(child_item.childCount()):
|
||||
sub_child_data = child_item.child(subChildIndex).itemData
|
||||
target_dict[sub_child_data[0]] = sub_child_data[1]
|
||||
self.set_pbr_material_description(source_file, name, material_type, file_connections)
|
||||
|
||||
def set_material_dictionary(self, dcc_dictionary):
|
||||
"""
|
||||
Adds all material descriptions pulled from each DCC file analyzed to the "materials_dictionary" class attribute.
|
||||
This function runs each time a subprocess is launced to gather DCC application material values.
|
||||
:param dcc_dictionary: The dictionary of values for each material analyzed by each specific DCC file list
|
||||
return analyzed values
|
||||
:return:
|
||||
"""
|
||||
self.total_materials += len(dcc_dictionary)
|
||||
self.materials_dictionary.update(dcc_dictionary)
|
||||
|
||||
def set_material_model(self, initialize=True):
|
||||
"""
|
||||
Once all materials have been gathered across a selected file set query, this organizes the values into a
|
||||
QT Model Class
|
||||
:param initialize: Default is set to boolean True. If a model has already been established in the current
|
||||
session, the initialize parameter would be set to false, and the values added to the Model. All changes to
|
||||
the model would then be redistributed to other informational views in the UI.
|
||||
:return:
|
||||
"""
|
||||
if initialize:
|
||||
self.model = MaterialsModel(self.headers, self.materials_dictionary)
|
||||
else:
|
||||
self.model.update()
|
||||
self.materials_dictionary.clear()
|
||||
self.populate_dcc_material_values_tree()
|
||||
|
||||
def set_ui_buttons(self):
|
||||
"""
|
||||
Handles UI buttons for each of the three stacked layout views (Source Files, DCC Material Values,
|
||||
Export Materials)
|
||||
:return:
|
||||
"""
|
||||
display_index = self.content_stacked_layout.currentIndex()
|
||||
self.switch_layout_combobox.setEnabled(True)
|
||||
# Source Files Layout ----------------------------------->>
|
||||
if display_index == 0:
|
||||
self.select_files_button.setEnabled(True)
|
||||
self.reset_button.setEnabled(True)
|
||||
self.process_files_button.setText('Process Listed Files')
|
||||
|
||||
# DCC Material Values Layout ---------------------------->>
|
||||
elif display_index == 1:
|
||||
self.reset_button.setEnabled(True)
|
||||
self.process_files_button.setEnabled(False)
|
||||
self.select_files_button.setEnabled(False)
|
||||
|
||||
# Export Materials Layout ------------------------------->>
|
||||
else:
|
||||
self.select_files_button.setEnabled(False)
|
||||
self.process_files_button.setText('Export Selected Materials')
|
||||
if self.material_definitions:
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
def set_pbr_material_description(self, source_file, name, material_type, file_connections):
|
||||
# Build dictionary for material description based on extracted values
|
||||
default_settings = self.get_lumberyard_material_template('pbr')
|
||||
material = {'sourceFile': source_file,
|
||||
'description': name,
|
||||
'materialType': default_settings.get('materialType'),
|
||||
'parentMaterial': default_settings.get('parentMaterial'),
|
||||
'propertyLayoutVersion': default_settings.get('propertyLayoutVersion'),
|
||||
'properties': self.get_lumberyard_material_properties(name, material_type, file_connections)}
|
||||
self.material_definitions[name if name not in self.material_definitions.keys() else
|
||||
self.get_filename_increment(name)] = material
|
||||
|
||||
############################
|
||||
# Button Actions ###########
|
||||
############################
|
||||
|
||||
def remove_source_file_clicked(self):
|
||||
"""
|
||||
In the Source File view of the UI layout, this will remove the listed file in its respective row. If files
|
||||
have not been processed yet, it prevents that file from being analyzed. If the files have already been
|
||||
analyzed, this will remove the materials from stored values.
|
||||
:return:
|
||||
"""
|
||||
file_index = self.target_files_table.indexAt(self.sender().pos())
|
||||
del self.target_file_list[file_index.row()]
|
||||
self.populate_files_table()
|
||||
|
||||
def process_listed_files_clicked(self):
|
||||
"""
|
||||
The button serves a dual purpose, depending on the current layout of the window. 'Process listed files'
|
||||
initiates the DCC file analysis that extracts material information. In the "Export Materials" layout, this
|
||||
button (for now) will export material files corresponding to each analyzed material.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Need to decide how the materials are going to be routed. At this stage they will just be saved to the
|
||||
# desktop, but I assume that we want these files to be saved to an associated project folder
|
||||
|
||||
if self.sender().text() == 'Process Listed Files':
|
||||
self.message_readout_label.setText('Gathering Material Information...')
|
||||
self.app.processEvents()
|
||||
self.process_file_list()
|
||||
else:
|
||||
self.export_selected_materials()
|
||||
|
||||
def select_files_button_clicked(self):
|
||||
"""
|
||||
This dialog allows user to select DCC files to be processed for the materials present for conversion.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO- Eventually it might be worth it to allow files from multiple locations to be selected. Currently
|
||||
# this only allows single/multiple files from a single directory to be selected.
|
||||
|
||||
dialog = QtWidgets.QFileDialog(self, 'Shift-Select Target Files', self.desktop_location)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
|
||||
dialog.setNameFilter('Compatible Files (*.ma *.mb *.fbx *.max *.blend)')
|
||||
dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
||||
file_view = dialog.findChild(QtWidgets.QListView, 'listView')
|
||||
|
||||
# Workaround for selecting multiple files with File Dialog
|
||||
if file_view:
|
||||
file_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
f_tree_view = dialog.findChild(QtWidgets.QTreeView)
|
||||
if f_tree_view:
|
||||
f_tree_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
|
||||
if dialog.exec_() == QtWidgets.QDialog.Accepted:
|
||||
self.target_file_list = dialog.selectedFiles()
|
||||
if self.target_file_list:
|
||||
self.populate_source_files_table()
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
def layout_combobox_changed(self):
|
||||
"""
|
||||
Handles main window layout combobox index change.
|
||||
:return:
|
||||
"""
|
||||
self.content_stacked_layout.setCurrentIndex(self.switch_layout_combobox.currentIndex())
|
||||
self.set_ui_buttons()
|
||||
|
||||
def reset_clicked(self):
|
||||
"""
|
||||
Brings the application and all variables back to their initial state.
|
||||
:return:
|
||||
"""
|
||||
self.reset_all_values()
|
||||
|
||||
|
||||
class MaterialNode(QtWidgets.QWidget):
|
||||
def __init__(self, material_info, current_position, parent=None):
|
||||
super(MaterialNode, self).__init__(parent)
|
||||
|
||||
self.material_name = material_info[0]
|
||||
self.material_info = material_info[1]
|
||||
self.current_position = current_position
|
||||
self.property_settings = {}
|
||||
|
||||
self.small_font = QtGui.QFont("Helvetica", 7, QtGui.QFont.Bold)
|
||||
self.bold_font = QtGui.QFont("Helvetica", 8, QtGui.QFont.Bold)
|
||||
self.main_layout = QtWidgets.QVBoxLayout()
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
self.background_frame = QtWidgets.QFrame(self)
|
||||
self.background_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.background_frame.setStyleSheet('background-color:rgb(220, 220, 220);')
|
||||
|
||||
# ########################
|
||||
# Title Bar
|
||||
# ########################
|
||||
|
||||
self.title_bar_widget = QtWidgets.QWidget()
|
||||
self.title_bar_layout = QtWidgets.QHBoxLayout(self.title_bar_widget)
|
||||
self.title_bar_layout.setContentsMargins(10, 0, 10, 0)
|
||||
self.title_bar_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.title_bar_frame = QtWidgets.QFrame(self.title_bar_widget)
|
||||
self.title_bar_frame.setGeometry(0, 0, 5000, 40)
|
||||
self.title_bar_frame.setStyleSheet('background-color:rgb(193,154,255);')
|
||||
self.main_layout.addWidget(self.title_bar_widget)
|
||||
self.material_name_checkbox = QtWidgets.QCheckBox(self.material_name)
|
||||
self.material_name_checkbox.setFixedHeight(35)
|
||||
self.material_name_checkbox.setStyleSheet('spacing:10px; color:white')
|
||||
self.material_name_checkbox.setFont(self.bold_font)
|
||||
self.material_name_checkbox.setChecked(True)
|
||||
self.title_bar_layout.addWidget(self.material_name_checkbox)
|
||||
|
||||
self.material_file_layout = QtWidgets.QHBoxLayout()
|
||||
self.material_file_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.source_file = QtWidgets.QLabel(os.path.basename(self.material_info['sourceFile']))
|
||||
self.source_file.setStyleSheet('color:white;')
|
||||
self.source_file.setFont(self.small_font)
|
||||
self.material_file_layout.addWidget(self.source_file)
|
||||
self.material_file_layout.addSpacing(10)
|
||||
|
||||
self.edit_button = QtWidgets.QPushButton('Edit')
|
||||
self.edit_button.clicked.connect(self.edit_button_clicked)
|
||||
self.edit_button.setFixedWidth(55)
|
||||
self.material_file_layout.addWidget(self.edit_button)
|
||||
self.title_bar_layout.addLayout(self.material_file_layout)
|
||||
|
||||
self.information_layout = QtWidgets.QHBoxLayout()
|
||||
self.information_layout.setContentsMargins(10, 0, 10, 10)
|
||||
self.main_layout.addLayout(self.information_layout)
|
||||
|
||||
# ########################
|
||||
# Details layout
|
||||
# ########################
|
||||
|
||||
self.details_layout = QtWidgets.QVBoxLayout()
|
||||
self.details_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.details_groupbox = QtWidgets.QGroupBox("Details")
|
||||
self.details_groupbox.setFixedWidth(200)
|
||||
self.details_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.details_layout.addSpacing(15)
|
||||
self.material_type_label = QtWidgets.QLabel('Material Type')
|
||||
self.material_type_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);')
|
||||
self.material_type_label.setFixedHeight(25)
|
||||
self.material_type_label.setFont(self.bold_font)
|
||||
self.details_layout.addWidget(self.material_type_label)
|
||||
|
||||
self.material_type_combobox = QtWidgets.QComboBox()
|
||||
self.material_type_combobox.setFixedHeight(30)
|
||||
self.material_type_combobox.setStyleSheet('QCombobox QAbstractItemView { padding-left: 15px; }')
|
||||
material_type_items = [' Standard PBR']
|
||||
self.material_type_combobox.addItems(material_type_items)
|
||||
self.details_layout.addWidget(self.material_type_combobox)
|
||||
self.details_layout.addSpacing(10)
|
||||
|
||||
self.description_label = QtWidgets.QLabel('Description')
|
||||
self.description_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);')
|
||||
self.description_label.setFixedHeight(25)
|
||||
self.description_label.setFont(self.bold_font)
|
||||
self.details_layout.addWidget(self.description_label)
|
||||
|
||||
self.description_box = QtWidgets.QTextEdit('This space is reserved for additional information.')
|
||||
self.details_layout.addWidget(self.description_box)
|
||||
self.information_layout.addWidget(self.details_groupbox)
|
||||
self.details_groupbox.setLayout(self.details_layout)
|
||||
|
||||
# ########################
|
||||
# Properties layout
|
||||
# ########################
|
||||
|
||||
self.properties_layout = QtWidgets.QVBoxLayout()
|
||||
self.properties_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.properties_groupbox = QtWidgets.QGroupBox("Properties")
|
||||
self.properties_groupbox.setFixedWidth(150)
|
||||
self.properties_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.properties_list_widget = QtWidgets.QListWidget()
|
||||
self.material_properties = ['ambientOcclusion', 'baseColor', 'emissive', 'metallic', 'roughness', 'specularF0',
|
||||
'normal', 'opacity']
|
||||
self.properties_list_widget.addItems(self.material_properties)
|
||||
self.properties_list_widget.itemSelectionChanged.connect(self.property_selection_changed)
|
||||
self.properties_layout.addSpacing(15)
|
||||
self.properties_layout.addWidget(self.properties_list_widget)
|
||||
self.information_layout.addWidget(self.properties_groupbox)
|
||||
self.properties_groupbox.setLayout(self.properties_layout)
|
||||
|
||||
# ########################
|
||||
# Attributes layout
|
||||
# ########################
|
||||
|
||||
self.attributes_layout = QtWidgets.QVBoxLayout()
|
||||
self.attributes_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.attributes_groupbox = QtWidgets.QGroupBox("Attributes")
|
||||
self.attributes_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; "
|
||||
"margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); "
|
||||
"subcontrol-position: top left;}")
|
||||
self.information_layout.addWidget(self.attributes_groupbox)
|
||||
self.attributes_layout.addSpacing(15)
|
||||
self.attributes_table = QtWidgets.QTableWidget()
|
||||
self.attributes_table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.attributes_table.setColumnCount(2)
|
||||
self.attributes_table.setAlternatingRowColors(True)
|
||||
self.attributes_table.setHorizontalHeaderLabels(['Attribute', 'Value'])
|
||||
attributes_table_header = self.attributes_table.horizontalHeader()
|
||||
attributes_table_header.setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220);}')
|
||||
attributes_table_header.setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
attributes_table_header.setContentsMargins(10, 10, 0, 0)
|
||||
attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
attributes_table_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
||||
self.attributes_layout.addWidget(self.attributes_table)
|
||||
self.attributes_groupbox.setLayout(self.attributes_layout)
|
||||
|
||||
self.initialize_display_values()
|
||||
|
||||
def initialize_display_values(self):
|
||||
"""
|
||||
Initializes all of the widget item information for material based on the DCC application info the class has
|
||||
been passed.
|
||||
:return:
|
||||
"""
|
||||
for material_property in self.material_properties:
|
||||
if material_property in self.material_info.get('properties'):
|
||||
self.property_settings[material_property] = self.material_info['properties'].get(material_property)
|
||||
current_row = self.material_properties.index(material_property)
|
||||
current_item = self.properties_list_widget.takeItem(current_row)
|
||||
self.properties_list_widget.insertItem(0, current_item)
|
||||
else:
|
||||
self.property_settings[material_property] = 'inactive'
|
||||
current_row = self.material_properties.index(material_property)
|
||||
item = self.properties_list_widget.item(current_row)
|
||||
item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEnabled)
|
||||
item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
|
||||
self.properties_list_widget.setCurrentRow(0)
|
||||
self.set_attributes_table(self.get_selected_property())
|
||||
|
||||
def set_attributes_table(self, selected_property):
|
||||
"""
|
||||
Displays the key, value pairs for the item selected in the Properties list widget
|
||||
:param selected_property: The item in the Properties list widget that is currently selected. Only active
|
||||
values are displayed.
|
||||
:return:
|
||||
"""
|
||||
self.attributes_table.setRowCount(0)
|
||||
row_count = 0
|
||||
for key, value in self.property_settings[selected_property].items():
|
||||
self.attributes_table.insertRow(row_count)
|
||||
key_item = QtWidgets.QTableWidgetItem(key)
|
||||
self.attributes_table.setItem(row_count, 0, key_item)
|
||||
value_item = QtWidgets.QTableWidgetItem(value)
|
||||
self.attributes_table.setItem(row_count, 1, value_item)
|
||||
row_count += 1
|
||||
|
||||
def get_selected_property(self):
|
||||
"""
|
||||
Convenience function to get current value selected in the Properties list widget.
|
||||
:return:
|
||||
"""
|
||||
return self.properties_list_widget.currentItem().text()
|
||||
|
||||
def update_model(self):
|
||||
"""
|
||||
Not sure if this will go away, but if desired, I could make attribute values able to be revised after
|
||||
materials have been scraped from the DCC materials
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def edit_button_clicked(self):
|
||||
"""
|
||||
This is in place in the event that we want to allow material revisions for properties to be made after
|
||||
DCC processing step has already been executed. The idea would basically be to surface an editable
|
||||
table where values can be added, removed or changed within the final material definition.
|
||||
:return:
|
||||
"""
|
||||
print('Edit button clicked')
|
||||
|
||||
def property_selection_changed(self):
|
||||
"""
|
||||
Fired when index of list view selected property selection has changed.
|
||||
:return:
|
||||
"""
|
||||
self.set_attributes_table(self.get_selected_property())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
materials_to_lumberyard = MaterialsToLumberyard()
|
||||
materials_to_lumberyard.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
# import pymxs
|
||||
|
||||
def get_material_information(file_location):
|
||||
print('[Max] Get Material Information: {}'.format(file_location))
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
from PySide2 import QtCore
|
||||
import maya.standalone
|
||||
maya.standalone.initialize(name='python')
|
||||
import pymel.core as pm
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
|
||||
class MayaMaterials(QtCore.QObject):
|
||||
def __init__(self, files_list, materials_count, parent=None):
|
||||
super(MayaMaterials, self).__init__(parent)
|
||||
|
||||
self.files_list = files_list
|
||||
self.current_scene = None
|
||||
self.materials_dictionary = {}
|
||||
self.materials_count = int(materials_count)
|
||||
self.get_material_information()
|
||||
|
||||
def get_material_information(self):
|
||||
"""
|
||||
Main entry point for the material information extraction. Because this class is run
|
||||
in Standalone mode as a subprocess, the list is passed as a string- some parsing/measures
|
||||
need to be taken in order to separate values that originated as a list before passed.
|
||||
|
||||
:return: A dictionary of all of the materials gathered. Sent back to main UI through stdout
|
||||
"""
|
||||
for file in file_list:
|
||||
self.current_scene = file.replace('\'', '')
|
||||
pm.openFile(self.current_scene, force=True)
|
||||
self.set_material_descriptions()
|
||||
json.dump(self.materials_dictionary, sys.stdout)
|
||||
|
||||
@staticmethod
|
||||
def get_materials(target_mesh):
|
||||
"""
|
||||
Gathers a list of all materials attached to each mesh's shader
|
||||
|
||||
:param target_mesh: The target mesh to pull attached material information from.
|
||||
:return: List of unique material values attached to the mesh passed as an argument.
|
||||
"""
|
||||
shading_group = pm.listConnections(pm.PyNode(target_mesh), type='shadingEngine')
|
||||
materials = pm.ls(pm.listConnections(shading_group), materials=1)
|
||||
return list(set(materials))
|
||||
|
||||
@staticmethod
|
||||
def get_shader(material_name):
|
||||
"""
|
||||
Convenience function for obtaining the shader that the specified material (as an argument)
|
||||
is attached to.
|
||||
|
||||
:param material_name: Takes the material name as an argument to get associated shader object
|
||||
:return:
|
||||
"""
|
||||
connections = pm.listConnections(material_name, type='shadingEngine')[0]
|
||||
shader_name = '{}.surfaceShader'.format(connections)
|
||||
shader = pm.listConnections(shader_name)[0]
|
||||
return shader
|
||||
|
||||
@staticmethod
|
||||
def get_shader_information(shader, material_mesh):
|
||||
"""
|
||||
Helper function for extracting shader/material attributes used to form the DCC specific dictionary
|
||||
of found material values for conversion.
|
||||
|
||||
:param shader: The target shader object to analyze
|
||||
:param material_mesh: The material mesh needs to be passed to search for textures attached to it.
|
||||
:return: Complete set (in the form of two dictionaries) of file connections and material attribute values
|
||||
"""
|
||||
shader_file_connections = {}
|
||||
shading_group = pm.listConnections(material_mesh, type='shadingEngine')
|
||||
material = pm.ls(pm.listConnections(shading_group), materials=1)[0]
|
||||
used_texture_nodes = set(pm.listHistory(material, type='file'))
|
||||
for node in used_texture_nodes:
|
||||
for attr in pm.listConnections(node, plugs=1):
|
||||
try:
|
||||
if attr.split('.')[0] == material:
|
||||
shader_file_connections[str(attr)] = pm.getAttr(node.fileTextureName)
|
||||
break
|
||||
except pm.MayaAttributeError:
|
||||
pass
|
||||
|
||||
shader_attributes = {}
|
||||
for shader_attribute in pm.listAttr(shader, s=True, iu=True):
|
||||
try:
|
||||
shader_attributes[str(shader_attribute)] = str(pm.getAttr('{}.{}'.format(shader, shader_attribute)))
|
||||
except pm.MayaAttributeError as e:
|
||||
logging.error('MayaAttributeError: {}'.format(e))
|
||||
|
||||
return shader_file_connections, shader_attributes
|
||||
|
||||
def set_material_dictionary(self, material_name, material_type, material_mesh):
|
||||
"""
|
||||
When a unique material has been found, this creates a dictionary entry with all relevant material values. This
|
||||
includes material attributes as well as attached file textures. Later in the process this information is
|
||||
leveraged when creating the Lumberyard material definition.
|
||||
|
||||
:param material_name: The name attached to the material
|
||||
:param material_type: Specific type of material (Arnold, Stingray, etc.)
|
||||
:param material_mesh: Mesh that the material is applied to
|
||||
:return:
|
||||
"""
|
||||
self.materials_count += 1
|
||||
shader = self.get_shader(material_name)
|
||||
shader_file_connections, shader_attributes = self.get_shader_information(shader, material_mesh)
|
||||
material_dictionary = {'MaterialName': material_name, 'MaterialType': material_type, 'DccApplication': 'Maya',
|
||||
'AppliedMesh': material_mesh, 'FileConnections': shader_file_connections,
|
||||
'SceneName': str(self.current_scene), 'MaterialAttributes': shader_attributes}
|
||||
material_name = 'Material_{}'.format(self.materials_count)
|
||||
self.materials_dictionary[material_name] = material_dictionary
|
||||
|
||||
def set_material_descriptions(self):
|
||||
"""
|
||||
This function serves as the clearinghouse for all analyzed materials passing through the system.
|
||||
It will determine whether or not the found material has already been processed, or if it needs to
|
||||
be added to the final material dictionary. In the event that an encountered material has already
|
||||
been processed, this function creates a register of all meshes it is applied to in the 'AppliedMesh'
|
||||
attribute.
|
||||
:return:
|
||||
"""
|
||||
scene_geo = pm.ls(v=True, geometry=True)
|
||||
for target_mesh in scene_geo:
|
||||
material_list = self.get_materials(target_mesh)
|
||||
for material_name in material_list:
|
||||
material_type = pm.nodeType(material_name, api=True)
|
||||
material_listed = [x for x in self.materials_dictionary
|
||||
if self.materials_dictionary[x]['MaterialName'] == material_name]
|
||||
|
||||
if not material_listed:
|
||||
self.set_material_dictionary(str(material_name), str(material_type), str(target_mesh))
|
||||
else:
|
||||
mesh_list = self.materials_dictionary[material_name].get('AppliedMesh')
|
||||
if not isinstance(mesh_list, list):
|
||||
self.materials_dictionary[material_name]['AppliedMesh'] = [mesh_list, target_mesh]
|
||||
else:
|
||||
mesh_list.append(target_mesh)
|
||||
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# Maya Specific Shader Mapping #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
|
||||
file_list = sys.argv[1:-1]
|
||||
count = sys.argv[-1]
|
||||
instance = MayaMaterials(file_list, count)
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
from PySide2.QtCore import QAbstractItemModel, QModelIndex, Qt
|
||||
|
||||
|
||||
class MaterialsModel(QAbstractItemModel):
|
||||
def __init__(self, headers, data, parent=None):
|
||||
super(MaterialsModel, self).__init__(parent)
|
||||
|
||||
self.rootItem = TreeNode(headers)
|
||||
self.parents = [self.rootItem]
|
||||
self.indentations = [0]
|
||||
self.create_data(data)
|
||||
|
||||
def create_data(self, data, indent=-1):
|
||||
"""
|
||||
Recursive loop that structures Model data into tree form.
|
||||
:param data: Row information.
|
||||
:param indent: Column information. This helps to facilitate the creation of nested rows.
|
||||
:return:
|
||||
"""
|
||||
if type(data) == dict:
|
||||
indent += 1
|
||||
position = 4 * indent
|
||||
for key, value in data.items():
|
||||
if position > self.indentations[-1]:
|
||||
if self.parents[-1].childCount() > 0:
|
||||
self.parents.append(self.parents[-1].child(self.parents[-1].childCount() - 1))
|
||||
self.indentations.append(position)
|
||||
else:
|
||||
while position < self.indentations[-1] and len(self.parents) > 0:
|
||||
self.parents.pop()
|
||||
self.indentations.pop()
|
||||
parent = self.parents[-1]
|
||||
parent.insertChildren(parent.childCount(), 1, parent.columnCount())
|
||||
parent.child(parent.childCount() - 1).setData(0, key)
|
||||
value_string = str(value) if type(value) != dict else str('')
|
||||
parent.child(parent.childCount() - 1).setData(1, value_string)
|
||||
try:
|
||||
self.create_data(value, indent)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_attribute_value(search_string, search_column):
|
||||
""" Convenience function for quickly accessing row information based on attribute keys. """
|
||||
for childIndex in range(search_column.childCount()):
|
||||
child_item = search_column.child(childIndex)
|
||||
child_value = child_item.itemData
|
||||
if child_value[0] == search_string:
|
||||
return child_value[1]
|
||||
return None
|
||||
|
||||
def index(self, row, column, index=QModelIndex()):
|
||||
""" Returns the index of the item in the model specified by the given row, column and parent index """
|
||||
if not self.hasIndex(row, column, index):
|
||||
return QModelIndex()
|
||||
if not index.isValid():
|
||||
item = self.rootItem
|
||||
else:
|
||||
item = index.internalPointer()
|
||||
|
||||
child = item.child(row)
|
||||
if child:
|
||||
return self.createIndex(row, column, child)
|
||||
return QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
"""
|
||||
Returns the parent of the model item with the given index If the item has no parent,
|
||||
an invalid QModelIndex is returned
|
||||
"""
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
item = index.internalPointer()
|
||||
if not item:
|
||||
return QModelIndex()
|
||||
|
||||
parent = item.parentItem
|
||||
if parent == self.rootItem:
|
||||
return QModelIndex()
|
||||
else:
|
||||
return self.createIndex(parent.childNumber(), 0, parent)
|
||||
|
||||
def rowCount(self, index=QModelIndex()):
|
||||
"""
|
||||
Returns the number of rows under the given parent. When the parent is valid it means that
|
||||
rowCount is returning the number of children of parent
|
||||
"""
|
||||
if index.isValid():
|
||||
parent = index.internalPointer()
|
||||
else:
|
||||
parent = self.rootItem
|
||||
return parent.childCount()
|
||||
|
||||
def columnCount(self, index=QModelIndex()):
|
||||
""" Returns the number of columns for the children of the given parent """
|
||||
return self.rootItem.columnCount()
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
""" Returns the data stored under the given role for the item referred to by the index """
|
||||
if index.isValid() and role == Qt.DisplayRole:
|
||||
return index.internalPointer().data(index.column())
|
||||
elif not index.isValid():
|
||||
return self.rootItem.data(index.column())
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
""" Returns the data for the given role and section in the header with the specified orientation """
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.rootItem.data(section)
|
||||
|
||||
|
||||
class TreeNode(object):
|
||||
def __init__(self, data, parent=None):
|
||||
self.parentItem = parent
|
||||
self.itemData = data
|
||||
self.children = []
|
||||
|
||||
def child(self, row):
|
||||
return self.children[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self.children)
|
||||
|
||||
def childNumber(self):
|
||||
if self.parentItem is not None:
|
||||
return self.parentItem.children.index(self)
|
||||
|
||||
def columnCount(self):
|
||||
return len(self.itemData)
|
||||
|
||||
def data(self, column):
|
||||
return self.itemData[column]
|
||||
|
||||
def insertChildren(self, position, count, columns):
|
||||
if position < 0 or position > len(self.children):
|
||||
return False
|
||||
for row in range(count):
|
||||
data = [v for v in range(columns)]
|
||||
item = TreeNode(data, self)
|
||||
self.children.insert(position, item)
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def setData(self, column, value):
|
||||
if column < 0 or column >= len(self.itemData):
|
||||
return False
|
||||
self.itemData[column] = value
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"description": "",
|
||||
"materialType": "Materials/Types/StandardPBR.materialtype",
|
||||
"parentMaterial": "",
|
||||
"propertyLayoutVersion": 3,
|
||||
"properties": {
|
||||
"ambientOcclusion": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"baseColor": {
|
||||
"color": [ 1.0, 1.0, 1.0 ],
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"emissive": {
|
||||
"color": [ 1.0, 1.0, 1.0 ],
|
||||
"intensity": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"metallic": {
|
||||
"factor": 0.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"roughness": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif"
|
||||
},
|
||||
"specularF0": {
|
||||
"factor": 0.5,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"normal": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif"
|
||||
},
|
||||
"opacity": {
|
||||
"doubleSided": false,
|
||||
"factor": 1.0,
|
||||
"cutoutAlpha": false,
|
||||
"cutoutThreshold": 0.5,
|
||||
"useBaseColorTextureAlpha": false,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
@echo off
|
||||
|
||||
REM
|
||||
REM All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
REM its licensors.
|
||||
REM
|
||||
REM For complete copyright and license terms please see the LICENSE at the root of this
|
||||
REM distribution (the "License"). All use of this software is governed by the License,
|
||||
REM or, if provided, by the license below or the license accompanying this file. Do not
|
||||
REM remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
REM
|
||||
|
||||
:: Set up and run LY Python CMD prompt
|
||||
:: Sets up the DccScriptingInterface_Env,
|
||||
:: Puts you in the CMD within the dev environment
|
||||
|
||||
:: Set up window
|
||||
TITLE Lumberyard DCC Scripting Interface Cmd
|
||||
:: Use obvious color to prevent confusion (Grey with Yellow Text)
|
||||
COLOR 8E
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
|
||||
:: Keep changes local
|
||||
SETLOCAL enableDelayedExpansion
|
||||
|
||||
:: This maps up to the \Dev folder
|
||||
IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..\..\..\..\..\..)
|
||||
|
||||
:: Change to root Lumberyard dev dir
|
||||
:: Don't use the LY_DEV so we can test that ENVAR!!!
|
||||
CD /d %DEV_REL_PATH%
|
||||
set Rel_Dev=%CD%
|
||||
echo Rel_Dev = %Rel_Dev%
|
||||
:: Restore original directory
|
||||
popd
|
||||
|
||||
set DCCSI_PYTHON_INSTALL=%Rel_Dev%\Tools\Python\3.7.5\windows
|
||||
|
||||
:: add to the PATH
|
||||
SET PATH=%DCCSI_PYTHON_INSTALL%;%PATH%
|
||||
|
||||
:: dcc scripting interface gem path
|
||||
set DCCSIG_PATH=%Rel_Dev%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
|
||||
echo DCCSIG_PATH = %DCCSIG_PATH%
|
||||
|
||||
:: add to the PATH
|
||||
SET PATH=%DCCSIG_PATH%;%PATH%
|
||||
|
||||
:: Constant Vars (Global)
|
||||
:: global debug (propogates)
|
||||
IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=false)
|
||||
echo DCCSI_GDEBUG = %DCCSI_GDEBUG%
|
||||
:: initiates debugger connection
|
||||
IF "%DCCSI_DEV_MODE%"=="" (set DCCSI_DEV_MODE=false)
|
||||
echo DCCSI_DEV_MODE = %DCCSI_DEV_MODE%
|
||||
:: sets debugger, options: WING, PYCHARM
|
||||
IF "%DCCSI_GDEBUGGER%"=="" (set DCCSI_GDEBUGGER=WING)
|
||||
echo DCCSI_GDEBUGGER = %DCCSI_GDEBUGGER%
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ LY DCCsi, DCC Material Converter
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
:: Change to root dir
|
||||
CD /D %DCCSIG_PATH%
|
||||
|
||||
:: add to the PATH
|
||||
SET PATH=%DCCSIG_PATH%;%PATH%
|
||||
|
||||
set PYTHONPATH=%DCCSIG_PATH%;%PYTHONPATH%
|
||||
|
||||
CALL %DCCSI_PYTHON_INSTALL%\python.exe "%DCCSIG_PATH%\SDK\Maya\Scripts\Python\kitbash_converter\standalone.py"
|
||||
|
||||
|
||||
ENDLOCAL
|
||||
|
||||
:: Return to starting directory
|
||||
POPD
|
||||
|
||||
:END_OF_FILE
|
||||
|
||||
exit /b 0
|
||||
+1076
File diff suppressed because it is too large
Load Diff
+372
@@ -0,0 +1,372 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# This file is designed to run in Maya Standalone- it imports existing FBX files and reformats materials as Stingray
|
||||
# PBS materials, and attempts to attach PBR Metal/Rough texture sets generated for each asset. There is currently a
|
||||
# bug in Maya that prevents this from being possible, although it has been officially logged with Autodesk so hopefully
|
||||
# a fix will exist in an updated version
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
# maya imports
|
||||
from PySide2 import QtCore
|
||||
import maya.cmds as mc
|
||||
import maya.standalone
|
||||
maya.standalone.initialize(name='python')
|
||||
mc.loadPlugin("fbxmaya")
|
||||
import maya.mel as mel
|
||||
mel.eval('loadPlugin fbxmaya')
|
||||
|
||||
# built-ins
|
||||
import logging as _logging
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
module_name = 'kitbash_converter.process_fbx_file'
|
||||
_LOGGER = _logging.getLogger(module_name)
|
||||
|
||||
|
||||
SUPPORTED_MATERIAL_PROPERTIES = ['baseColor', 'emissive', 'metallic', 'roughness', 'normal', 'opacity']
|
||||
|
||||
|
||||
class ProcessFbxFile(QtCore.QObject):
|
||||
def __init__(self, fbx_file, base_directory, relative_destination_path, parent=None):
|
||||
super(ProcessFbxFile, self).__init__(parent)
|
||||
self.fbx_file = fbx_file
|
||||
self.base_directory = base_directory
|
||||
self.textures_directory = self.get_textures_directory()
|
||||
self.relative_destination_path = relative_destination_path
|
||||
self.default_material_definition = 'standardPBR.template.material'
|
||||
self.transfer_data = {}
|
||||
self.process_file()
|
||||
|
||||
def process_file(self):
|
||||
"""
|
||||
This makes a copy of the Standard PBR material for use as a template when processing a Kitbash3d
|
||||
asset. Then it imports the FBX file into Maya and begins the separation and material conversion
|
||||
process.
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('MAYA STANDALONE PROCESS STARTED ---------------------------------------------')
|
||||
_LOGGER.info('FBX File Passed: {}'.format(self.fbx_file))
|
||||
_LOGGER.info('Base Directory: {}'.format(self.base_directory))
|
||||
_LOGGER.info('Destination Directory: {}'.format(self.relative_destination_path))
|
||||
_LOGGER.info('-----------------------------------------------------------------------------\n')
|
||||
|
||||
if self.textures_directory and os.path.exists(self.fbx_file):
|
||||
# Get Material Definition Template
|
||||
with open(self.default_material_definition) as json_file:
|
||||
self.default_material_definition = json.load(json_file)
|
||||
|
||||
mc.file(self.fbx_file, i=True, type="FBX")
|
||||
return_dictionary = self.process_groups()
|
||||
_LOGGER.info('ReturnDictionary: {}'.format(return_dictionary))
|
||||
json.dump(return_dictionary, sys.stdout)
|
||||
_LOGGER.info('Process Complete.')
|
||||
|
||||
def process_groups(self):
|
||||
"""
|
||||
This is the main function of the module and orchestrates the complete set of steps for extracting
|
||||
each subobject from the asset FBX file and generating the .material files for use in Lumberyard.
|
||||
:return:
|
||||
"""
|
||||
asset_dictionary = {}
|
||||
ignore_list = ['persp', 'top', 'front', 'side']
|
||||
group_list = []
|
||||
for item in mc.ls(assemblies=True):
|
||||
if item not in ignore_list:
|
||||
group_list.append(item)
|
||||
|
||||
for grp in group_list:
|
||||
_LOGGER.info('\n_\n////////////////////////////////\n--> {}\n////////////////////////////////'.format(grp))
|
||||
asset_material_dictionary = self.get_asset_materials(grp)
|
||||
material_dictionary = []
|
||||
for material_name, texture_set in asset_material_dictionary.items():
|
||||
_LOGGER.info('+++++ MaterialName: {} TextureSet: {}'.format(material_name, texture_set))
|
||||
fbx_parts_list = grp.split('_')[1:-1]
|
||||
new_fbx_name = ('_').join(fbx_parts_list)
|
||||
_LOGGER.info('NewFBXName: {}'.format(new_fbx_name))
|
||||
material_definition_name = '{}_{}.material'.format(new_fbx_name, material_name)
|
||||
_LOGGER.info('Material File: {}'.format(material_definition_name))
|
||||
|
||||
# Export FBX
|
||||
output_directory = os.path.join(base_directory, self.relative_destination_path, new_fbx_name)
|
||||
_LOGGER.info('FBX Output Directory: {}'.format(output_directory))
|
||||
if not os.path.exists(output_directory):
|
||||
os.makedirs(output_directory)
|
||||
self.export_fbx(grp, os.path.join(output_directory, '{}.fbx'.format(new_fbx_name)))
|
||||
material_definition = self.create_object_material(output_directory, material_definition_name,
|
||||
texture_set, material_name)
|
||||
asset_dictionary[new_fbx_name] = {'asset_location': output_directory}
|
||||
return asset_dictionary
|
||||
|
||||
def find_textures(self, material_name):
|
||||
"""
|
||||
This is run if there were no files attached to materials. There appears to be material sets for all materials,
|
||||
and in the case of glass textures they just are not assigned.
|
||||
:param material_name: The material name inside of the FBX- if there are textures they will begin with this.
|
||||
"""
|
||||
_LOGGER.info('Searching for textures for the following material: {}'.format(material_name))
|
||||
texture_set = {}
|
||||
for file in os.listdir(self.textures_directory):
|
||||
if file.startswith(material_name):
|
||||
try:
|
||||
texture_type = self.get_texture_type(file)
|
||||
texture_set[texture_type] = os.path.join(self.textures_directory, file)
|
||||
except Exception as e:
|
||||
_LOGGER.info('Could not create texture record for file [{}]... Exception: {}'.format(file, e))
|
||||
return texture_set
|
||||
|
||||
def create_object_material(self, output_directory, material_definition_name, texture_dictionary, material_name):
|
||||
"""
|
||||
Using the file textures present, this function takes materials and associated texture sets
|
||||
and forms the .material file based on those values for each material/fbx found
|
||||
:param output_directory: The destination directory for the exported assets
|
||||
:param material_definition_name: The name for the exported .material file
|
||||
:param texture_dictionary: All the file textures associated with a material
|
||||
:return:
|
||||
"""
|
||||
output_path = os.path.join(output_directory, material_definition_name)
|
||||
_LOGGER.info('Creating Material: {} OutputPath: {}'.format(material_definition_name, output_path))
|
||||
|
||||
if not os.path.exists(output_path):
|
||||
material_definition = collections.OrderedDict()
|
||||
material_template = self.default_material_definition.copy()
|
||||
try:
|
||||
for key, values in material_template.items():
|
||||
# These values remain constant
|
||||
if key != 'properties':
|
||||
material_definition[key] = values
|
||||
else:
|
||||
modified_items = {}
|
||||
for material_property in SUPPORTED_MATERIAL_PROPERTIES:
|
||||
for k, v in values.items():
|
||||
if k.lower() in texture_dictionary.keys():
|
||||
temp_dict = self.get_texture_attributes(k.lower(), texture_dictionary, material_name)
|
||||
modified_items[k] = temp_dict
|
||||
|
||||
if modified_items:
|
||||
material_definition[key] = modified_items
|
||||
|
||||
if len(material_definition) > 4:
|
||||
_LOGGER.info('\n++++++++++++++\n+++++++++++++++\nFinal Material: {}\n++++++++++++++\n'
|
||||
'+++++++++++++++\n_\n'.format(json.dumps(material_definition, indent=4, sort_keys=False)))
|
||||
self.export_lumberyard_material(output_path, material_definition)
|
||||
except Exception as e:
|
||||
_LOGGER.info('Problem creating Material Definition: {}'.format(e))
|
||||
|
||||
def remediate_empty_material(self, material_name):
|
||||
_LOGGER.info('Resolving empty material::::::: {}'.format(material_name))
|
||||
|
||||
##############################
|
||||
# Getters/Setters ############
|
||||
##############################
|
||||
|
||||
def get_asset_materials(self, target_group):
|
||||
"""
|
||||
Pulls information about assigned materials from materials in order to coordinate file textures present
|
||||
for each material being set up for Lumberyard.
|
||||
:param target_group: Kitbash 3d assets contain several subobjects grouped under locators. "Target group"
|
||||
signifies a group from the total set of groups present.
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Getting Asset Materials... Target Group: {}'.format(target_group))
|
||||
materials_dictionary = {}
|
||||
mc.select(target_group, hierarchy=True)
|
||||
|
||||
target_nodes = mc.ls(sl=True, dag=True, s=True)
|
||||
sg = mc.listConnections(target_nodes, type= 'shadingEngine')
|
||||
materials = list(set(mc.ls(mc.listConnections(sg), materials=True)))
|
||||
|
||||
for material_name in materials:
|
||||
found_textures = []
|
||||
texture_files = [x for x in mc.listConnections(material_name, plugs=1, source=1) if x.endswith('outColor')]
|
||||
for file_name in texture_files:
|
||||
try:
|
||||
found_textures.append(mc.getAttr('{}.fileTextureName'.format(file_name.split('.')[0])))
|
||||
except Exception as e:
|
||||
_LOGGER.info('Error occured: {}'.format(e))
|
||||
|
||||
texture_set = self.get_texture_set(found_textures) if found_textures else self.find_textures(material_name)
|
||||
if texture_set:
|
||||
materials_dictionary[material_name] = texture_set
|
||||
else:
|
||||
attribute_list = self.remediate_empty_material(material_name)
|
||||
if attribute_list:
|
||||
materials_dictionary[material_name] = attribute_list
|
||||
return materials_dictionary
|
||||
|
||||
def get_textures_directory(self):
|
||||
for (root, dirs, files) in os.walk(self.base_directory, topdown=True):
|
||||
for dir in dirs:
|
||||
if dir.lower() == 'textures':
|
||||
return os.path.join(root, dir)
|
||||
return None
|
||||
|
||||
def get_texture_set(self, texture_files):
|
||||
"""
|
||||
Gets the entire set of textures associated with a material using a single texture.
|
||||
:param texture_files:
|
||||
:return:
|
||||
"""
|
||||
texture_set = {}
|
||||
for texture_file in texture_files:
|
||||
base_texture_name = self.get_base_texture_name(texture_file)
|
||||
for file in os.listdir(self.textures_directory):
|
||||
if file.startswith(base_texture_name):
|
||||
try:
|
||||
texture_type = self.get_texture_type(file)
|
||||
texture_set[texture_type] = os.path.join(self.textures_directory, file)
|
||||
_LOGGER.info('{} texture added. Path::: {}'.format(texture_type, os.path.join(self.textures_directory, file)))
|
||||
except Exception:
|
||||
pass
|
||||
return texture_set
|
||||
|
||||
def get_texture_type(self, file):
|
||||
"""
|
||||
Tries to extract the texture type from a texture filename (ie "basecolor", "roughness")
|
||||
|
||||
:param file: The file name to get the texture type from
|
||||
"""
|
||||
file_stem = file.split('.')[0]
|
||||
file_parts = file_stem.split('_')
|
||||
if len(file_parts) > 1:
|
||||
return file_parts[-1]
|
||||
|
||||
def get_texture_attributes(self, texture_type, texture_dictionary, material_name):
|
||||
target_path = os.path.normpath(texture_dictionary[texture_type])
|
||||
if texture_type == 'opacity':
|
||||
temp_dict = self.get_opacity_settings(material_name, target_path)
|
||||
elif texture_type == 'subsurfacescattering':
|
||||
temp_dict = self.get_sss_settings(material_name, target_path)
|
||||
elif texture_type == 'emissive':
|
||||
temp_dict = self.get_emissive_settings(material_name, target_path)
|
||||
elif texture_type == 'basecolor':
|
||||
temp_dict = self.get_basecolor_settings(material_name, target_path)
|
||||
else:
|
||||
temp_dict = {'textureMap': self.get_relative_path(target_path)}
|
||||
|
||||
return temp_dict
|
||||
|
||||
def get_opacity_settings(self, material_name, target_path):
|
||||
"""
|
||||
Reads Maya material to construct material opacity attributes
|
||||
|
||||
:param material_name: The Maya Material name being translated for Lumberyard
|
||||
:param target_path: The path to the texture file
|
||||
:return: Attribute values as a dictionary
|
||||
"""
|
||||
_LOGGER.info('Getting opacity settings...')
|
||||
return {'alphaSource': 'Split', 'mode': 'Cutout', 'textureMap': self.get_relative_path(target_path)}
|
||||
|
||||
def get_sss_settings(self, material_name, target_path):
|
||||
"""
|
||||
Reads Maya material to construct material subsurface scattering attributes
|
||||
|
||||
:param material_name: The Maya material name being translated for Lumberyard
|
||||
:param target_path: The path to the texture file
|
||||
:return: Attribute values as a dictionary
|
||||
"""
|
||||
_LOGGER.info('Getting Subsurface Scattering settings...')
|
||||
return {'enableSubsurfaceScattering': True, 'influenceMap': self.get_relative_path(target_path)}
|
||||
|
||||
def get_emissive_settings(self, material_name, target_path):
|
||||
"""
|
||||
Reads Maya material to construct material emissive attributes
|
||||
|
||||
:param material_name: The Maya material name being translated for Lumberyard
|
||||
:param target_path: The path to the texture file
|
||||
:return: Attribute values as a dictionary
|
||||
"""
|
||||
_LOGGER.info('Getting Emissive settings...')
|
||||
return {'enable': True, 'intensity': 4, 'textureMap': self.get_relative_path(target_path)}
|
||||
|
||||
def get_basecolor_settings(self, material_name, target_path):
|
||||
"""
|
||||
Reads Maya material to construct material BaseColor settings
|
||||
|
||||
:param material_name: The Maya material name being translated for Lumberyard
|
||||
:param target_path: The path to the texture file
|
||||
:return: Attribute values as a dictionary
|
||||
"""
|
||||
_LOGGER.info('Getting BaseColor settings...')
|
||||
# Get shader color values
|
||||
return {'textureMap': self.get_relative_path(target_path)}
|
||||
|
||||
def get_base_texture_name(self, texture_path):
|
||||
"""
|
||||
Tries to extract texture name without the added texture type (ie "basecolor", "roughness", etc)
|
||||
|
||||
:param texture_path: The path to the texture from which to extract the base texture name
|
||||
"""
|
||||
path_list = texture_path.split('\\')
|
||||
file_name = path_list[-1]
|
||||
texture_list = file_name.split('_')
|
||||
base_texture_name = ('_').join(texture_list[:-1])
|
||||
return base_texture_name
|
||||
|
||||
def get_relative_path(self, full_path):
|
||||
"""
|
||||
Material definitions use relative paths for file textures- this function takes the full paths of assets and
|
||||
converts to the abbreviated relative path format needed for Lumberyard to source the files
|
||||
|
||||
:param full_path: Full path to the asset
|
||||
:return:
|
||||
"""
|
||||
truncated_path = self.textures_directory.split('AtomContent\\')[-1]
|
||||
start_directory = truncated_path.split('\\')[0]
|
||||
path_list = full_path.split('\\')
|
||||
return_path = ('/').join(path_list[path_list.index(start_directory):])
|
||||
return return_path
|
||||
|
||||
##############################
|
||||
# Export Files ###############
|
||||
##############################
|
||||
|
||||
def export_fbx(self, group_name, output_path):
|
||||
"""
|
||||
Exports sub-objects from Kitbash3d assets, groups of assets are contained in a single FBX for delivery.
|
||||
|
||||
:param group_name: Group name inside of FBX- subobjects are grouped under locators
|
||||
:param output_path: The destination path for the exported FBX object
|
||||
:return:
|
||||
"""
|
||||
if not os.path.exists(output_path):
|
||||
mc.move(0, 0, 0, group_name, absolute=True)
|
||||
mc.select(group_name, hierarchy=True)
|
||||
mc.FBXExport('-file', output_path, '-s')
|
||||
mc.select(clear=True)
|
||||
|
||||
def export_lumberyard_material(self, output_path, material_description):
|
||||
"""
|
||||
Takes one final dictionary with information gathered in the process and saves with JSON formatting into a
|
||||
.material file
|
||||
|
||||
:param output:
|
||||
:param material_description:
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Output .material++++++>> {}'.format(output_path))
|
||||
with open(output_path, 'w') as material_file:
|
||||
json.dump(dict(material_description), material_file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
fbx_file = sys.argv[1]
|
||||
_LOGGER.info('FBX file: {}'.format(fbx_file))
|
||||
base_directory = sys.argv[-2]
|
||||
_LOGGER.info('Base Directory: {}'.format(base_directory))
|
||||
relative_destination_path = sys.argv[-1].replace('/', '\\')
|
||||
_LOGGER.info('Relative Destination Path: {}'.format(relative_destination_path))
|
||||
ProcessFbxFile(fbx_file, base_directory, relative_destination_path)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
"""Boostraps and Starts Standalone DCC Material Converter utility"""
|
||||
|
||||
# built in's
|
||||
import os
|
||||
import site
|
||||
|
||||
_MODULE_PATH = os.path.abspath(__file__)
|
||||
|
||||
_DCCSIG_REL_PATH = "../../../.."
|
||||
_DCCSIG_PATH = os.path.join(_MODULE_PATH, _DCCSIG_REL_PATH)
|
||||
_DCCSIG_PATH = os.path.normpath(_DCCSIG_PATH)
|
||||
|
||||
_DCCSIG_PATH = os.getenv('DCCSIG_PATH',
|
||||
os.path.abspath(_DCCSIG_PATH))
|
||||
|
||||
# we don't have access yet to the DCCsi Lib\site-packages
|
||||
site.addsitedir(_DCCSIG_PATH) # PYTHONPATH
|
||||
|
||||
# azpy bootstrapping and extensions
|
||||
import azpy.config_utils
|
||||
_config = azpy.config_utils.get_dccsi_config()
|
||||
settings = _config.get_config_settings(setup_ly_pyside=True)
|
||||
|
||||
from main import launch_kitbash_converter
|
||||
|
||||
launch_kitbash_converter()
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"description": "",
|
||||
"materialType": "Materials/Types/StandardPBR.materialtype",
|
||||
"parentMaterial": "",
|
||||
"propertyLayoutVersion": 3,
|
||||
"properties": {
|
||||
"general": {
|
||||
"texcoord": 0
|
||||
},
|
||||
"ambientOcclusion": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"baseColor": {
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"emissive": {
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"enable": false,
|
||||
"intensity": -5.0,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"metallic": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"roughness": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif"
|
||||
},
|
||||
"specularF0": {
|
||||
"factor": 0.5,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"normal": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif"
|
||||
},
|
||||
"subsurfaceScattering": {
|
||||
"enableSubsurfaceScattering": true,
|
||||
"influenceMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif",
|
||||
"quality": 0.4,
|
||||
"scatterDistance": 8,
|
||||
"subsurfaceScatterFactor": 1
|
||||
},
|
||||
"opacity": {
|
||||
"mode": "Opaque",
|
||||
"alphaSource": "Packed",
|
||||
"doubleSided": false,
|
||||
"factor": 1.0,
|
||||
"cutoutAlpha": false,
|
||||
"cutoutThreshold": 0.5,
|
||||
"useBaseColorTextureAlpha": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"uv": {
|
||||
"center": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsetU": 0,
|
||||
"offsetV": 0,
|
||||
"rotateDegrees": 0,
|
||||
"scale": 1.0,
|
||||
"tileU": 1.0,
|
||||
"tileV": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.temp
|
||||
materialsdb.dat
|
||||
materialsdb.dir
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
:: coding:utf-8
|
||||
:: !/usr/bin/python
|
||||
::
|
||||
:: All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
:: its licensors.
|
||||
::
|
||||
:: For complete copyright and license terms please see the LICENSE at the root of this
|
||||
:: distribution (the "License"). All use of this software is governed by the License,
|
||||
:: or, if provided, by the license below or the license accompanying this file. Do not
|
||||
:: remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
::
|
||||
|
||||
@echo off
|
||||
:: Set up and run LY Python CMD prompt
|
||||
:: Sets up the DccScriptingInterface_Env,
|
||||
:: Puts you in the CMD within the dev environment
|
||||
|
||||
:: Set up window
|
||||
TITLE Lumberyard DCC Scripting Interface Cmd
|
||||
:: Use obvious color to prevent confusion (Grey with Yellow Text)
|
||||
COLOR 8E
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
PUSHD %~dp0
|
||||
|
||||
:: Keep changes local
|
||||
SETLOCAL enableDelayedExpansion
|
||||
|
||||
CALL %~dp0\Project_Env.bat
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ LY DCC Scripting Interface CMD ...
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
:: Create command prompt with environment
|
||||
CALL %windir%\system32\cmd.exe
|
||||
|
||||
ENDLOCAL
|
||||
|
||||
:: Return to starting directory
|
||||
POPD
|
||||
|
||||
:END_OF_FILE
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
:: coding:utf-8
|
||||
:: !/usr/bin/python
|
||||
::
|
||||
:: All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
:: its licensors.
|
||||
::
|
||||
:: For complete copyright and license terms please see the LICENSE at the root of this
|
||||
:: distribution (the "License"). All use of this software is governed by the License,
|
||||
:: or, if provided, by the license below or the license accompanying this file. Do not
|
||||
:: remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
::
|
||||
|
||||
@echo off
|
||||
:: Launches maya with a bunch of local hooks for Lumberyard
|
||||
:: ToDo: move all of this to a .json data driven boostrapping system
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
PUSHD %~dp0
|
||||
|
||||
echo ________________________________
|
||||
echo ~ calling PROJ_Env.bat
|
||||
|
||||
:: Keep changes local
|
||||
SETLOCAL enableDelayedExpansion
|
||||
|
||||
:: PY version Major
|
||||
set DCCSI_PY_VERSION_MAJOR=2
|
||||
echo DCCSI_PY_VERSION_MAJOR = %DCCSI_PY_VERSION_MAJOR%
|
||||
|
||||
:: PY version Major
|
||||
set DCCSI_PY_VERSION_MINOR=7
|
||||
echo DCCSI_PY_VERSION_MINOR = %DCCSI_PY_VERSION_MINOR%
|
||||
|
||||
:: Maya Version
|
||||
set MAYA_VERSION=2020
|
||||
echo MAYA_VERSION = %MAYA_VERSION%
|
||||
|
||||
:: if a local customEnv.bat exists, run it
|
||||
IF EXIST "%~dp0Project_Env.bat" CALL %~dp0Project_Env.bat
|
||||
|
||||
echo ________________________________
|
||||
echo Launching Maya %MAYA_VERSION% for Lumberyard...
|
||||
|
||||
:::: Set Maya native project acess to this project
|
||||
::set MAYA_PROJECT=%LY_PROJECT%
|
||||
::echo MAYA_PROJECT = %MAYA_PROJECT%
|
||||
|
||||
:: DX11 Viewport
|
||||
Set MAYA_VP2_DEVICE_OVERRIDE = VirtualDeviceDx11
|
||||
|
||||
:: Default to the right version of Maya if we can detect it... and launch
|
||||
IF EXIST "%MAYA_LOCATION%\bin\Maya.exe" (
|
||||
start "" "%MAYA_LOCATION%\bin\Maya.exe" %*
|
||||
) ELSE (
|
||||
Where maya.exe 2> NUL
|
||||
IF ERRORLEVEL 1 (
|
||||
echo Maya.exe could not be found
|
||||
pause
|
||||
) ELSE (
|
||||
start "" Maya.exe %*
|
||||
)
|
||||
)
|
||||
|
||||
:: Return to starting directory
|
||||
POPD
|
||||
|
||||
:END_OF_FILE
|
||||
|
||||
exit /b 0
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
:: coding:utf-8
|
||||
:: !/usr/bin/python
|
||||
::
|
||||
:: All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
:: its licensors.
|
||||
::
|
||||
:: For complete copyright and license terms please see the LICENSE at the root of this
|
||||
:: distribution (the "License"). All use of this software is governed by the License,
|
||||
:: or, if provided, by the license below or the license accompanying this file. Do not
|
||||
:: remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
::
|
||||
|
||||
@echo off
|
||||
:: Launches Wing IDE and the DccScriptingInterface Project Files
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ Setting up LY DCCsi WingIDE Dev Env...
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
:: Store current dir
|
||||
%~d0
|
||||
cd %~dp0
|
||||
PUSHD %~dp0
|
||||
|
||||
:: Keep changes local
|
||||
SETLOCAL enableDelayedExpansion
|
||||
|
||||
SET ABS_PATH=%~dp0
|
||||
echo Current Dir, %ABS_PATH%
|
||||
|
||||
:: WingIDE version Major
|
||||
SET WING_VERSION_MAJOR=7
|
||||
echo WING_VERSION_MAJOR = %WING_VERSION_MAJOR%
|
||||
|
||||
:: WingIDE version Major
|
||||
SET WING_VERSION_MINOR=1
|
||||
echo WING_VERSION_MINOR = %WING_VERSION_MINOR%
|
||||
|
||||
:: note the changed path from IDE to Pro
|
||||
set WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %WING_VERSION_MAJOR%.%WING_VERSION_MINOR%
|
||||
echo WINGHOME = %WINGHOME%
|
||||
|
||||
CALL %~dp0\Project_Env.bat
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ WingIDE Version %WING_VERSION_MAJOR%.%WING_VERSION_MINOR%
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
SET WING_PROJ=%DCCSIG_PATH%\Solutions\.wing\DCCsi_%WING_VERSION_MAJOR%x.wpr
|
||||
echo WING_PROJ = %WING_PROJ%
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ Launching %LY_PROJECT% project in WingIDE %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% ...
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
|
||||
IF EXIST "%WINGHOME%\bin\wing.exe" (
|
||||
start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%"
|
||||
) ELSE (
|
||||
Where wing.exe 2> NUL
|
||||
IF ERRORLEVEL 1 (
|
||||
echo wing.exe could not be found
|
||||
pause
|
||||
) ELSE (
|
||||
start "" wing.exe "%WING_PROJ%"
|
||||
)
|
||||
)
|
||||
|
||||
ENDLOCAL
|
||||
|
||||
:: Return to starting directory
|
||||
POPD
|
||||
|
||||
:END_OF_FILE
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
:: coding:utf-8
|
||||
:: !/usr/bin/python
|
||||
::
|
||||
:: All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
:: its licensors.
|
||||
::
|
||||
:: For complete copyright and license terms please see the LICENSE at the root of this
|
||||
:: distribution (the "License"). All use of this software is governed by the License,
|
||||
:: or, if provided, by the license below or the license accompanying this file. Do not
|
||||
:: remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
::
|
||||
|
||||
@echo off
|
||||
:: Sets up environment for Lumberyard DCC tools and code access
|
||||
|
||||
:: Store current dir
|
||||
%~d0
|
||||
cd %~dp0
|
||||
PUSHD %~dp0
|
||||
|
||||
for %%a in (.) do set LY_PROJECT=%%~na
|
||||
|
||||
echo.
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
echo ~ Setting up LY DSI PROJECT Environment ...
|
||||
echo _____________________________________________________________________
|
||||
echo.
|
||||
|
||||
echo LY_PROJECT = %LY_PROJECT%
|
||||
|
||||
:: Put you project env vars and overrides here
|
||||
|
||||
:: chanhe the relative path up to dev
|
||||
set DEV_REL_PATH=../../..
|
||||
set ABS_PATH=%~dp0
|
||||
|
||||
:: Override the default maya version
|
||||
set MAYA_VERSION=2020
|
||||
echo MAYA_VERSION = %MAYA_VERSION%
|
||||
|
||||
set LY_PROJECT_PATH=%ABS_PATH%
|
||||
echo LY_PROJECT_PATH = %LY_PROJECT_PATH%
|
||||
|
||||
:: Change to root Lumberyard dev dir
|
||||
CD /d %LY_PROJECT_PATH%\%DEV_REL_PATH%
|
||||
set LY_DEV=%CD%
|
||||
echo LY_DEV = %LY_DEV%
|
||||
|
||||
CALL %LY_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Launchers\Windows\Env.bat
|
||||
|
||||
rem :: Constant Vars (Global)
|
||||
rem SET LYPY_GDEBUG=0
|
||||
rem echo LYPY_GDEBUG = %LYPY_GDEBUG%
|
||||
rem SET LYPY_DEV_MODE=0
|
||||
rem echo LYPY_DEV_MODE = %LYPY_DEV_MODE%
|
||||
rem SET LYPY_DEBUGGER=WING
|
||||
rem echo LYPY_DEBUGGER = %LYPY_DEBUGGER%
|
||||
|
||||
:: Restore original directory
|
||||
popd
|
||||
|
||||
:: Change to root dir
|
||||
CD /D %ABS_PATH%
|
||||
|
||||
:: if the user has set up a custom env call it
|
||||
IF EXIST "%~dp0User_Env.bat" CALL %~dp0User_Env.bat
|
||||
|
||||
GOTO END_OF_FILE
|
||||
|
||||
:: Return to starting directory
|
||||
POPD
|
||||
|
||||
:END_OF_FILE
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
__all__ = [
|
||||
'create_maya_files',
|
||||
'constants'
|
||||
'image_conversion',
|
||||
'isolate_and_assign',
|
||||
'lumberyard_data',
|
||||
'cli_control',
|
||||
'utilities',
|
||||
'main'
|
||||
]
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
its licensors.
|
||||
|
||||
For complete copyright and license terms please see the LICENSE at the root of this
|
||||
distribution (the "License"). All use of this software is governed by the License,
|
||||
or, if provided, by the license below or the license accompanying this file. Do not
|
||||
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
import click
|
||||
import os
|
||||
import main as app_main
|
||||
|
||||
|
||||
@click.version_option('1.0.0')
|
||||
@click.option('--output', default='PBR', help='Lumberyard material type. Current options: [pbr_basic]')
|
||||
@click.argument('operands', type=click.STRING, nargs=-1)
|
||||
@click.command(context_settings=dict(ignore_unknown_options=True))
|
||||
def main(output, operands):
|
||||
target_files = []
|
||||
for index, operand in enumerate(operands):
|
||||
entry_path = os.path.abspath(str(operand))
|
||||
if os.path.isdir(entry_path):
|
||||
for directory_path, directory_names, file_names in os.walk(entry_path):
|
||||
for file_name in file_names:
|
||||
if is_valid_file(file_name):
|
||||
target_files.append(os.path.join(entry_path, file_name))
|
||||
else:
|
||||
if is_valid_file(operand):
|
||||
target_files.append(operand)
|
||||
|
||||
if len(target_files):
|
||||
app_main.launch_cli(output, target_files)
|
||||
|
||||
|
||||
def is_valid_file(file_name):
|
||||
target_extensions = 'ma mb fbx blend max'.split(' ')
|
||||
if file_name.split('.')[-1] in target_extensions:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
# File Description:
|
||||
# This is an area to make customized changes in how the script looks for and processes files
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
IMAGE_TYPES = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.tga']
|
||||
|
||||
DIRECTORY_EXCLUSION_LIST = ['.mayaSwatches']
|
||||
|
||||
LUMBERYARD_DATA_FILES = ['.mtl', '.assetinfo', '.material']
|
||||
|
||||
EXPORT_MATERIAL_TYPES = ['Illum']
|
||||
|
||||
PREFERRED_IMAGE_FORMAT = '.tif'
|
||||
|
||||
TRANSFER_EXTENSIONS = ['.fbx', '.assetinfo']
|
||||
|
||||
IMAGE_KEYS = {
|
||||
'ddn': ['ddn'],
|
||||
'ddna': ['ddna'],
|
||||
'diffuse': ['diffuse', 'diff', 'dif', 'd'],
|
||||
'emissive': ['emis', 'emissive', 'e', 'emiss'],
|
||||
'specular': ['spec', 'specular'],
|
||||
'scattering': ['scattering', 'sss'],
|
||||
'normal': ['normal'],
|
||||
'basecolor': ['basecolor', 'albedo']
|
||||
}
|
||||
|
||||
LOAD_WEIGHTS = {
|
||||
'maya': 4,
|
||||
'fbx': 2,
|
||||
'material': 1
|
||||
}
|
||||
|
||||
SUPPORTED_MATERIAL_PROPERTIES = ['general', 'ambientOcclusion', 'baseColor', 'emissive',
|
||||
'metallic', 'roughness', 'specularF0', 'normal', 'opacity', 'uv']
|
||||
|
||||
MATERIAL_SUFFIX_LIST = ['_mat', '_m']
|
||||
|
||||
DIELECTRIC_METALLIC_COLOR = (.04, .04, .04)
|
||||
|
||||
# For more information see OpenImageIO docs
|
||||
# https://openimageio.readthedocs.io/en/latest/pythonbindings.html
|
||||
# Recommended weight values for:
|
||||
# ImageBufAlgo.channel_sum
|
||||
WEIGHTED_RGB_VALUES = (.2126, .7152, .0722)
|
||||
|
||||
# MATERIAL DB
|
||||
FBX_DIRECTORY_PATH = 'directorypath'
|
||||
FBX_DIRECTORY_NAME = 'directoryname'
|
||||
FBX_FILES = 'fbxfiles'
|
||||
FBX_MATERIALS = 'materials'
|
||||
FBX_TEXTURES = 'textures'
|
||||
FBX_MAYA_FILE = 'mayafile'
|
||||
FBX_DESIGNER_FILE = 'designerfile'
|
||||
FBX_TEXTURE_MODIFICATIONS = 'modifications'
|
||||
FBX_NUMERICAL_SETTINGS = 'numericalsettings'
|
||||
FBX_MATERIAL_FILE = 'materialfile'
|
||||
FBX_ASSIGNED_GEO = 'assigned'
|
||||
|
||||
# Threshold values for baked vertex color
|
||||
# id mask images when no UVs present
|
||||
EMPTY_IMAGE_LOW = 260000
|
||||
EMPTY_IMAGE_LOW = 270000
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
# coding:utf-8
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# This file is designed to run in Maya Standalone- it imports existing FBX files and reformats materials as Stingray
|
||||
# PBS materials, and attempts to attach PBR Metal/Rough texture sets generated for each asset. There is currently a
|
||||
# bug in Maya that prevents this from being possible, although it has been officially logged with Autodesk so hopefully
|
||||
# a fix will exist in an updated version
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
# maya imports
|
||||
from PySide2 import QtCore
|
||||
import maya.cmds as mc
|
||||
import maya.standalone
|
||||
maya.standalone.initialize(name='python')
|
||||
mc.loadPlugin("fbxmaya")
|
||||
import maya.mel as mel
|
||||
mel.eval('loadPlugin fbxmaya')
|
||||
|
||||
# built-ins
|
||||
import logging as _logging
|
||||
import shelve
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
# local tool imports
|
||||
import constants
|
||||
|
||||
|
||||
module_name = 'legacy_asset_converter.create_maya_files'
|
||||
_LOGGER = _logging.getLogger(module_name)
|
||||
|
||||
|
||||
class CreateMayaFiles(QtCore.QObject):
|
||||
def __init__(self, files_list, base_directory, destination_directory, modify_naming, parent=None):
|
||||
super(CreateMayaFiles, self).__init__(parent)
|
||||
self.files_list = files_list
|
||||
self.base_directory = base_directory
|
||||
self.destination_directory = destination_directory
|
||||
self.modify_naming = modify_naming
|
||||
self.scene_shader_info = None
|
||||
self.materials_db = shelve.open('materialsdb', protocol=2)
|
||||
self.new_file = True
|
||||
self.file_name = None
|
||||
self.target_database_listing = None
|
||||
self.material_list = {}
|
||||
self.transfer_data = {}
|
||||
self.create_maya_files()
|
||||
|
||||
def create_maya_files(self):
|
||||
"""
|
||||
The main function of the script- this runs when the standalone session begins, and creates a Maya file
|
||||
based on each FBX file the script is passed through the subprocess that launches it. Due to the bug mentioned
|
||||
in the description above, a template file ('stingray_helper.ma') is loaded into the file so Maya at least
|
||||
knows what a Stingray material is- the root of the bug is that when it loads up it is unaware of the Stingray
|
||||
material node as well as its extended attributes. This workaround informs Maya of what a Stingray material is,
|
||||
although it is limited to this information only- it still doesn't know what the attributes of the material are
|
||||
that are necessary for making texture connections. This function processes each file, makes a note of it, and
|
||||
continues to move through the passed list until all FBX files have had companion Maya files created, and then
|
||||
returns a receipt of converted files.
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Base Directory: {}'.format(self.base_directory))
|
||||
_LOGGER.info('Destination Directory: {}'.format(self.destination_directory))
|
||||
_LOGGER.info('Files List: {}'.format(self.files_list))
|
||||
|
||||
for count, fbx_file in enumerate(self.files_list):
|
||||
target_file_path = os.path.join(self.base_directory, fbx_file)
|
||||
target_file_path = target_file_path.replace('\\', '/')
|
||||
self.file_name = os.path.basename(fbx_file)
|
||||
maya_file_name = os.path.join(self.destination_directory,'{}.ma'.format(os.path.splitext(self.file_name)[0]))
|
||||
sys.stdout.write('maya_{}'.format(maya_file_name))
|
||||
self.flush_then_wait()
|
||||
_LOGGER.info('\n\n++++++++++\n++++++++++>>>>\n {}\n++++++++++>>>>\n++++++++++\n'.format(self.file_name))
|
||||
|
||||
if not os.path.isfile(maya_file_name):
|
||||
try:
|
||||
self.reset_transfer_values()
|
||||
mc.file('stingray_helper.ma', o=True, force=True)
|
||||
mc.listAttr('StingrayTemplate')
|
||||
mc.listAttr('StingrayTemplate')
|
||||
mc.file(target_file_path, i=True, type="FBX")
|
||||
self.get_file_information(self.file_name, True)
|
||||
self.replace_materials()
|
||||
mc.delete('stingrayHelper')
|
||||
# mel.eval('cleanUpScene 3')
|
||||
mc.file(rename=maya_file_name)
|
||||
mc.file(save=True, type='mayaAscii', force=True)
|
||||
_LOGGER.info('Maya File Processing Complete... Transferring:')
|
||||
_LOGGER.info('FileName: {}'.format(self.file_name))
|
||||
_LOGGER.info('MayaFileName: {}'.format(maya_file_name))
|
||||
self.transfer_data[self.file_name] = {'maya_file': maya_file_name}
|
||||
except Exception as e:
|
||||
_LOGGER.info('Error Processing File: {} -- {}'.format(e, target_file_path))
|
||||
else:
|
||||
mc.file(maya_file_name, force=True, o=True)
|
||||
self.get_file_information(fbx_file, false)
|
||||
try:
|
||||
return_dictionary = {self.target_database_listing: self.transfer_data}
|
||||
json.dump(return_dictionary, sys.stdout)
|
||||
self.materials_db.close()
|
||||
except Exception as e:
|
||||
_LOGGER.info('Error: {} -- {}'.format(e, self.transfer_data))
|
||||
|
||||
def flush_then_wait(self):
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
time.sleep(0.5)
|
||||
|
||||
def get_file_information(self, fbx_file, new_file):
|
||||
"""
|
||||
Draws information relating to the target directory of the shelve database to gather texture assignments
|
||||
:param fbx_file:
|
||||
:param new_file:
|
||||
:return:
|
||||
"""
|
||||
self.new_file = new_file
|
||||
for key, values in self.materials_db.items():
|
||||
if os.path.normpath(values['directorypath']) == self.base_directory:
|
||||
self.target_database_listing = key
|
||||
self.material_list = values['fbxfiles'][fbx_file]
|
||||
break
|
||||
|
||||
def reset_transfer_values(self):
|
||||
"""
|
||||
Refreshes variables in between each FBX file processed
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.new_file = True
|
||||
self.material_list.clear()
|
||||
|
||||
def replace_materials(self):
|
||||
"""
|
||||
Finds the currently assigned Phong and Lambert shaders customary with Legacy material setups in FBX files
|
||||
and replaces with Stingray PBS materials.
|
||||
:return:
|
||||
"""
|
||||
for key, values in self.material_list['materials'].items():
|
||||
_LOGGER.info('\n_\n---------|| Processing material: {}'.format(key))
|
||||
if self.new_file:
|
||||
try:
|
||||
material_name = str(self.resolve_legacy_material(key))
|
||||
_LOGGER.info('MaterialName: {}'.format(material_name))
|
||||
_LOGGER.info('MaterialInfo: {}'.format(values))
|
||||
if values['assigned']:
|
||||
target_assignments = values['assigned']
|
||||
_LOGGER.info('TargetAssignments: {}'.format(target_assignments))
|
||||
sha, sg = self.get_material(material_name)
|
||||
_LOGGER.info('ShaderInfo: {}, {}'.format(sha, sg))
|
||||
if target_assignments:
|
||||
_LOGGER.info('Target Assignments exist- Assigning Shader...')
|
||||
self.set_material(sha, sg, target_assignments)
|
||||
# Disabled until Autodesk fixes bug
|
||||
# self.set_texture_maps(material_name, values['textures'])
|
||||
except Exception as e:
|
||||
_LOGGER.info('Cannot replace materials... Error: {}'.format(e))
|
||||
|
||||
def resolve_legacy_material(self, material_name):
|
||||
"""
|
||||
Deletes the legacy Phong and Lambert materials so it can name newly generated Stingray materials with the
|
||||
same name.
|
||||
:param material_name: Legacy material name to be re-applied to new material
|
||||
:return:
|
||||
"""
|
||||
mc.delete(material_name)
|
||||
for listing in constants.MATERIAL_SUFFIX_LIST:
|
||||
if material_name.lower().endswith(listing) and self.modify_naming == 'True':
|
||||
string_length = len(listing) * -1
|
||||
new_material_name = material_name[:string_length]
|
||||
self.material_list[new_material_name] = self.material_list[material_name]
|
||||
del self.material_list[material_name]
|
||||
return new_material_name
|
||||
return material_name
|
||||
|
||||
def get_texture_modifications(self, material_name):
|
||||
"""
|
||||
Checks for tiling information drawn from the corresponding mtl file. Because the attributes are currently
|
||||
not able to be accessed, this function has been tabled for now
|
||||
|
||||
:param material_name: Target material name
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Scanning for texture modifications...')
|
||||
for material in self.textures_modifications:
|
||||
_LOGGER.info('Modification listing:::: {}'.format(material))
|
||||
|
||||
def get_material(self, material_name):
|
||||
"""
|
||||
Gets material assignments and swaps legacy formatted materials with Stingray PBS materials
|
||||
:param material_name: Target material name
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Shaders in scene: {}'.format(self.get_materials_in_scene()))
|
||||
sha = None
|
||||
sg = None
|
||||
if material_name in self.get_materials_in_scene():
|
||||
_LOGGER.info('Deleting material: {}'.format(material_name))
|
||||
mc.delete(material_name)
|
||||
|
||||
_LOGGER.info('Creating Stingray material: {}'.format(material_name))
|
||||
try:
|
||||
sha = mc.shadingNode('StingrayPBS', asShader=True, name=material_name)
|
||||
sg = mc.sets(renderable=True, noSurfaceShader=True, empty=True)
|
||||
mc.connectAttr(sha + '.outColor', sg + '.surfaceShader', force=True)
|
||||
except Exception as e:
|
||||
_LOGGER.info('Shader creation failed: {}'.format(e))
|
||||
return sha, sg
|
||||
|
||||
def get_scene_shader_info(self):
|
||||
"""
|
||||
Audits scene to gather all materials present in scene geometry (that is to be converted)
|
||||
:return:
|
||||
"""
|
||||
self.scene_shader_info = {}
|
||||
scene_geo = mc.ls(v=True, geometry=True)
|
||||
for target_mesh in scene_geo:
|
||||
try:
|
||||
shading_groups = list(set(mc.listConnections(target_mesh, type='shadingEngine')))
|
||||
for sg in shading_groups:
|
||||
if sg not in self.scene_shader_info.keys():
|
||||
self.scene_shader_info[sg] = list(set(mc.ls(mc.listConnections(sg), materials=True)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_materials_in_scene(self):
|
||||
"""
|
||||
Audits scene to gather all materials present in the Hypershade, and returns them in a list
|
||||
:return:
|
||||
"""
|
||||
material_list = []
|
||||
for shading_engine in mc.ls(type='shadingEngine'):
|
||||
if mc.sets(shading_engine, q=True):
|
||||
for material in mc.ls(mc.listConnections(shading_engine), materials=True):
|
||||
material_list.append(material)
|
||||
return material_list
|
||||
|
||||
def set_material(self, sha, sg, assignment_list):
|
||||
"""
|
||||
Assigns specified material to specified mesh
|
||||
:param sha: Material name
|
||||
:param sg: Shading Group
|
||||
:param assignment_list: List of geometry to assign shader to
|
||||
:return:
|
||||
"""
|
||||
assignment_list = list(set([x.replace('.', '|') for x in assignment_list]))
|
||||
_LOGGER.info('\n_\nSET MATERIAL: {} {{{{{{{{{{{{{{{{{{{{{{'.format(sha))
|
||||
_LOGGER.info('Assignment list--> {}'.format(assignment_list))
|
||||
for item in assignment_list:
|
||||
try:
|
||||
mc.sets(item, e=True, forceElement=sg)
|
||||
except Exception as e:
|
||||
_LOGGER.info('Material assignment failed: {}'.format(e))
|
||||
|
||||
def set_texture_maps(self, material_name, texture_list):
|
||||
"""
|
||||
Plugs texture files into texture slots in the shader for specified material name. Currently due to the
|
||||
aforementioned bug this functionality does not work as intended, but it remains for when Autodesk supplies a fix
|
||||
:param material_name:
|
||||
:param texture_list:
|
||||
:return:
|
||||
"""
|
||||
shader_translation_keys = {
|
||||
'BaseColor': 'color',
|
||||
'Roughness': 'roughness',
|
||||
'Metallic': 'metallic',
|
||||
'Emissive': 'emissive',
|
||||
'Normal': 'normal'
|
||||
}
|
||||
|
||||
_LOGGER.info('\n_\nSET_TEXTURE_MAPS {{{{{{{{{{{{{{{{{{{{{{')
|
||||
_LOGGER.info('Material--> {}'.format(material_name))
|
||||
for texture_type, texture_path in texture_list.items():
|
||||
try:
|
||||
_LOGGER.info('Texture type: {} Texture path: {}'.format(texture_type, texture_path))
|
||||
file_count = len(mc.ls(type='file')) + 1
|
||||
texture_file = 'file{}'.format(file_count)
|
||||
mc.shadingNode('file', asTexture=True, name=texture_file)
|
||||
mc.setAttr('{}.fileTextureName'.format(texture_file), texture_path, type="string")
|
||||
_LOGGER.info('TexturePath: {}'.format(texture_path))
|
||||
_LOGGER.info('Attributes: {}'.format(mc.listAttr(material_name)))
|
||||
mc.setAttr('{}.use_{}_map'.format(material_name, shader_translation_keys[texture_type]), 1)
|
||||
mc.connectAttr('{}.outColor'.format(texture_file),
|
||||
'{}.TEX_{}_map'.format(material_name, shader_translation_keys[texture_type]), force=True)
|
||||
except Exception as e:
|
||||
_LOGGER.info('Conversion failed: {}'.format(e))
|
||||
|
||||
# Revise to make generic and put in Maya utility class --------------------------------------------------------
|
||||
# def get_textures_by_connections(self, material_name):
|
||||
# _LOGGER.info(':::::: Get textures by connections ::::::')
|
||||
# material_files = [x for x in mc.listConnections(material_name, plugs=1, source=1) if x.startswith('file')]
|
||||
# searched_values = []
|
||||
# for material_file in material_files:
|
||||
# try:
|
||||
# texture_path = mc.getAttr('{}.fileTextureName'.format(material_file.split('.')[0]))
|
||||
# texture_path_list = texture_path.split('/')
|
||||
# if 'ShaderFX' not in texture_path_list:
|
||||
# if texture_path:
|
||||
# search_string = self.get_base_texture_name(texture_path)
|
||||
# if search_string not in searched_values:
|
||||
# _LOGGER.info('TexturePath: {}'.format(texture_path))
|
||||
# searched_values.append(search_string)
|
||||
# search_results_list = self.get_texture_set(search_string, material_name)
|
||||
# if search_results_list:
|
||||
# self.material_list[material_name] = search_results_list
|
||||
#
|
||||
# except mc.MayaAttributeError:
|
||||
# pass
|
||||
|
||||
# def get_texture_set(self, search_string, material_name):
|
||||
# target_key = self.base_directory.split('\\')[-1]
|
||||
# for key, values in self.materials_db.items():
|
||||
# if values['directoryname'] == target_key:
|
||||
# for texture_key, texture_set in values['textures'].items():
|
||||
# for k, v in texture_set.items():
|
||||
# texture_base = self.get_base_texture_name(v).lower()
|
||||
# if texture_base.find('_') != -1:
|
||||
# texture_base = '_'.join(texture_base.split('_')[:-1])
|
||||
# if texture_base == search_string:
|
||||
# _LOGGER.info('MatchFound:::::::::::::::')
|
||||
# _LOGGER.info('Finding modifications... MaterialName: {}'.format(material_name))
|
||||
# if material_name in self.textures_modifications.keys():
|
||||
# texture_set['modifications'] = self.textures_modifications[material_name]
|
||||
# _LOGGER.info('Texture modifications found: {}'.format(self.textures_modifications[material_name]))
|
||||
# return texture_set
|
||||
# return None
|
||||
|
||||
# def get_base_texture_name(self, texture_path):
|
||||
# path_base = os.path.basename(texture_path)
|
||||
# base_texture_name = os.path.splitext(path_base)[0]
|
||||
# if path_base.find('_') != -1:
|
||||
# base = path_base.split('_')[-1]
|
||||
# suffix = base.split('.')[0]
|
||||
# naming_key_found = None
|
||||
# for key, values in self.texture_naming_dict.items():
|
||||
# for v in values:
|
||||
# if suffix == v:
|
||||
# base_list = path_base.split('_')[:-1]
|
||||
# base_texture_name = '_'.join(base_list)
|
||||
# return base_texture_name
|
||||
# return base_texture_name
|
||||
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# Maya Specific Shader Mapping #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
|
||||
_LOGGER.info('MAYA STANDALONE FILE FIRING: {}'.format(sys.argv))
|
||||
file_list = sys.argv[1:-3]
|
||||
base_directory = sys.argv[-3]
|
||||
destination_directory = sys.argv[-2]
|
||||
modify_naming = sys.argv[-1]
|
||||
CreateMayaFiles(file_list, base_directory, destination_directory, modify_naming)
|
||||
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
# coding:utf-8
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# This file is contains OpenImageIO operations for file texture conversions
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
# built-ins
|
||||
from shutil import copyfile
|
||||
import logging as _logging
|
||||
from pathlib import Path
|
||||
import constants
|
||||
import os
|
||||
|
||||
# 3rd Party (we may or do provide)
|
||||
import OpenImageIO
|
||||
from OpenImageIO import ImageInput, ImageOutput, ImageBuf, ImageSpec, ImageBufAlgo, ROI
|
||||
|
||||
|
||||
module_name = 'legacy_asset_converter.main.image_conversion'
|
||||
_LOGGER = _logging.getLogger(module_name)
|
||||
|
||||
|
||||
def get_pbr_textures(legacy_textures, destination_directory, search_path, base_directory):
|
||||
pbr_textures = {}
|
||||
for texture_type, texture_path in legacy_textures.items():
|
||||
_LOGGER.info(f'TEXTURETYPE::> {texture_type} TEXTUREPATH::> {texture_path}')
|
||||
if not texture_path.is_file():
|
||||
_LOGGER.info(f'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( Missing:::: {texture_path}')
|
||||
existing_path = resolve_path(texture_path, base_directory)
|
||||
if not existing_path:
|
||||
continue
|
||||
_LOGGER.info(f'Found texture [{existing_path}]. Continuing...')
|
||||
texture_path = Path(existing_path)
|
||||
if texture_type == 'diffuse':
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'BaseColor')
|
||||
pbr_textures['BaseColor'] = transfer_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
elif texture_type == 'specular':
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'Metallic')
|
||||
pbr_textures['Metallic'] = convert_metallic_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
elif texture_type == 'emittance':
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'Emissive')
|
||||
pbr_textures['Emissive'] = transfer_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
elif texture_type == 'scattering':
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'Scattering')
|
||||
pbr_textures['SubsurfaceScattering'] = transfer_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
elif texture_type == 'bumpmap':
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'Normal')
|
||||
pbr_textures['Normal'] = convert_normal_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
if texture_path.stem.endswith('ddna'):
|
||||
dst = get_converted_filename(texture_path, destination_directory, 'Roughness')
|
||||
pbr_textures['Roughness'] = convert_roughness_texture(dst, texture_path) if not os.path.isfile(dst) else dst
|
||||
else:
|
||||
pass
|
||||
|
||||
if pbr_textures and 'Metallic' not in pbr_textures.keys():
|
||||
_LOGGER.info(f'Metallic Not found... adding: {pbr_textures}')
|
||||
try:
|
||||
pbr_texture_keys = list(pbr_textures.keys())
|
||||
texture_path = Path(pbr_textures[pbr_texture_keys[0]])
|
||||
filename = texture_path.name.replace(get_texture_type(texture_path), 'Metallic')
|
||||
_LOGGER.info(f'Filename: {filename}')
|
||||
target_file_path = destination_directory / filename
|
||||
_LOGGER.info(f'TargetFilePath: {target_file_path}')
|
||||
if target_file_path.is_file():
|
||||
_LOGGER.info(f'File found: {target_file_path}')
|
||||
pbr_textures['Metallic'] = os.path.normpath(target_file_path)
|
||||
else:
|
||||
_LOGGER.info(f'Creating Generic Metallic File: {target_file_path}')
|
||||
convert_metallic_texture(target_file_path)
|
||||
pbr_textures['Metallic'] = target_file_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_LOGGER.info(f'OUTPUT // -------------->> {pbr_textures}')
|
||||
return pbr_textures
|
||||
|
||||
|
||||
def resolve_path(texture_path, base_directory):
|
||||
_LOGGER.info(f'Resolving Path... Filename[{texture_path.name}: {base_directory}')
|
||||
for (root, dirs, files) in os.walk(base_directory.parent, topdown=True):
|
||||
for file in files:
|
||||
target_file = file.split('.')[0].lower()
|
||||
if target_file == texture_path.stem.lower():
|
||||
return os.path.abspath(os.path.join(root, file))
|
||||
return None
|
||||
|
||||
|
||||
def transfer_texture(dst, src, overwrite=False):
|
||||
if not os.path.exists(dst) or overwrite:
|
||||
return Path(transfer_file(src, dst))
|
||||
return Path(dst)
|
||||
|
||||
|
||||
def convert_normal_texture(dst, src, overwrite=False):
|
||||
if not os.path.exists(dst) or overwrite:
|
||||
try:
|
||||
rgba = ImageBuf(str(src))
|
||||
spec = get_image_spec(rgba)
|
||||
rgb = ImageBufAlgo.channels(rgba, (0, 1, 2))
|
||||
write_image(rgba, dst, spec['format'])
|
||||
_LOGGER.info(f'Output Normal Map: {dst}')
|
||||
except Exception as e:
|
||||
_LOGGER.info(f'{src} -- Normal Map Conversion Failed. Error: {e}')
|
||||
return None
|
||||
return Path(dst)
|
||||
|
||||
|
||||
def convert_roughness_texture(dst, src, overwrite=False):
|
||||
if not os.path.exists(dst) or overwrite:
|
||||
try:
|
||||
rgba = ImageBuf(str(src))
|
||||
spec = get_image_spec(rgba)
|
||||
alpha = ImageBufAlgo.channels(rgba, (3,))
|
||||
roughness = ImageBufAlgo.invert(alpha)
|
||||
write_image(roughness, dst, spec['format'])
|
||||
_LOGGER.info(f'Output Roughness Map: {dst}')
|
||||
except Exception as e:
|
||||
_LOGGER.info(f'{src} -- Roughness Map Conversion Failed. Error: {e}')
|
||||
return None
|
||||
return Path(dst)
|
||||
|
||||
|
||||
def convert_metallic_texture(dst, src=None, overwrite=False):
|
||||
_LOGGER.info(f'Convert Metallic ::::: >> {dst}')
|
||||
if not os.path.exists(dst) or overwrite:
|
||||
try:
|
||||
if src:
|
||||
_LOGGER.info(f'Converting Metallic image using: {src}')
|
||||
# Get min/max pixels ---------------------->>
|
||||
buf = ImageBuf(str(src))
|
||||
minval, maxval = find_min_max(buf)
|
||||
buf.clear()
|
||||
print(f'Minval: {minval}')
|
||||
print(f'Maxval: {maxval}')
|
||||
|
||||
# Run remap filter using mix/max values -------->>
|
||||
buf = ImageBuf(str(src))
|
||||
spec = get_image_spec(buf)
|
||||
img_width = spec['resolution'][0]
|
||||
img_height = spec['resolution'][1]
|
||||
remap = get_contrast_remap(buf, minval, maxval)
|
||||
remove_color = remove_color_from_image(remap, dst)
|
||||
else:
|
||||
_LOGGER.info(f'Creating Dielectric Metallic Map')
|
||||
buf = ImageBuf(ImageSpec(4, 4, 1, "uint8"))
|
||||
ImageBufAlgo.fill(buf, constants.DIELECTRIC_METALLIC_COLOR, ROI(0, 1, 0, 1))
|
||||
write_image(buf, os.path.normpath(dst), 'uint8')
|
||||
_LOGGER.info(f'Output Metallic Map: {dst}')
|
||||
except Exception as e:
|
||||
src = 'None' if not src else src
|
||||
_LOGGER.info(f'{src} -- Metallic Map Conversion Failed. Error: {e}')
|
||||
return None
|
||||
return Path(dst)
|
||||
|
||||
|
||||
def get_contrast_remap(buf, minval, maxval):
|
||||
range_threshold = (maxval - minval) * .4
|
||||
ceiling_adjustment = .65
|
||||
white_point = float(round(maxval * ceiling_adjustment, 3))
|
||||
black_point = float(round(white_point - range_threshold, 3))
|
||||
percentage = 15
|
||||
black_point = (percentage * (white_point - black_point) / 100) + black_point
|
||||
remap = ImageBufAlgo.contrast_remap(buf, black=black_point, white=white_point)
|
||||
return remap
|
||||
|
||||
|
||||
def remove_color_from_image(buf, dst):
|
||||
"""
|
||||
Some operations leave color in what should remain grayscale images (i.e. 'contrast_remap'). This function attempts
|
||||
to gently remove color information without affecting the luminosity of the image
|
||||
:param buf:
|
||||
:param dst:
|
||||
:return:
|
||||
"""
|
||||
buf.read(convert='float')
|
||||
lin = ImageBufAlgo.colorconvert(buf, "sRGB", "linear")
|
||||
luma = ImageBufAlgo.channel_sum(buf, constants.WEIGHTED_RGB_VALUES)
|
||||
luma = ImageBufAlgo.colorconvert(luma, "linear", "sRGB")
|
||||
write_image(luma, dst, 'uint8')
|
||||
|
||||
|
||||
def find_min_max(buf):
|
||||
"""
|
||||
When attempting to automate the conversion of specular maps to metallic maps, the first step is to find high values
|
||||
of luminosity in the image and compare against the low values. The intention is to expose what would likely be
|
||||
metal and non-metal areas, and with this information attempt to clamp values to either black and white for the
|
||||
generation of the metallic map. This function decreases the size of the image to a manageable representation of
|
||||
pixel luminosity values, scans for the high and low values and returns those values to be further manipulated by
|
||||
the contrast remap filter
|
||||
:param buf: The image buffer containing the pixel information that needs to be scanned
|
||||
:return:
|
||||
"""
|
||||
# Resize Image ------------------->>
|
||||
goal_width = 512
|
||||
goal_height = 512
|
||||
|
||||
spec = buf.spec()
|
||||
w = spec.width
|
||||
h = spec.height
|
||||
nchans = spec.nchannels
|
||||
aspect = float(w) / float(h)
|
||||
if aspect >= 1.0:
|
||||
goal_height = int(h * goal_height / w)
|
||||
else:
|
||||
goal_width = (w * goal_width / h)
|
||||
resized = ImageBuf(ImageSpec(goal_width, goal_height, spec.nchannels, spec.format))
|
||||
ImageBufAlgo.resize(resized, buf)
|
||||
|
||||
# Scan buffer for min/max
|
||||
pixels = resized.get_pixels(OpenImageIO.UINT8)
|
||||
minval = 255
|
||||
maxval = 0
|
||||
test_dimensions = 512
|
||||
|
||||
for y in range(test_dimensions):
|
||||
for x in range(test_dimensions):
|
||||
pixel_value = pixels[y][x]
|
||||
average_value = sum(pixel_value) / 3
|
||||
|
||||
if average_value < minval:
|
||||
minval = average_value
|
||||
if average_value > maxval:
|
||||
maxval = average_value
|
||||
|
||||
minimum_level = minval / 255
|
||||
maximum_level = maxval / 255
|
||||
|
||||
return minimum_level, maximum_level
|
||||
|
||||
|
||||
def get_converted_filename(src, dst, texture_type):
|
||||
"""
|
||||
Takes the structure of an existing filename extracted from the legacy mtl files, and renames it, base on the
|
||||
texture type argument that is passed to it
|
||||
:param src: The legacy filename to be manipulated
|
||||
:param dst: The destination directory that the new file will be saved to
|
||||
:param texture_type: PBR texture type
|
||||
:return:
|
||||
"""
|
||||
if src.is_file():
|
||||
filename = src.name.replace(get_texture_type(src), texture_type)
|
||||
dst = os.path.normpath(dst / filename)
|
||||
_LOGGER.info(f'+=+=+=+=+=+=+=+=+=+=+=+ {dst} -- {os.path.isfile(dst)}')
|
||||
return dst
|
||||
|
||||
|
||||
def get_image_spec(target_image):
|
||||
"""
|
||||
Pulls metadata about the image to be leveraged in file operations
|
||||
:param target_image: Path to the target image intended for alterations
|
||||
:return:
|
||||
"""
|
||||
spec = target_image.spec()
|
||||
info = {'resolution': (spec.width, spec.height, spec.x, spec.y), 'channels': spec.channelnames,
|
||||
'format': str(spec.format)}
|
||||
if spec.channelformats:
|
||||
info['channelformats'] = str(spec.channelformats)
|
||||
info['alpha channel'] = str(spec.alpha_channel)
|
||||
info['z channel'] = str(spec.z_channel)
|
||||
info['deep'] = str(spec.deep)
|
||||
for i in range(len(spec.extra_attribs)):
|
||||
if type(spec.extra_attribs[i].value) == str:
|
||||
info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value
|
||||
else:
|
||||
info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value
|
||||
return info
|
||||
|
||||
|
||||
def get_texture_basename(image_path):
|
||||
"""
|
||||
Attempts to extract the naming convention base for each texture set
|
||||
:param image_path: Path to an image from the texture set to use for basename extraction
|
||||
:return:
|
||||
"""
|
||||
image_name = image_path.stem
|
||||
if image_name.find('_') != -1:
|
||||
image_components = image_name.split('_')
|
||||
return ('_').join(image_components[:-1])
|
||||
return None
|
||||
|
||||
|
||||
def get_texture_type(image_path):
|
||||
"""
|
||||
Gets the PBR texture type from the filename.
|
||||
:param image_path: Path to the image from which to extract pbr type
|
||||
:return:
|
||||
"""
|
||||
filename = Path(image_path)
|
||||
texture_type = filename.stem.split('_')[-1]
|
||||
return texture_type
|
||||
|
||||
|
||||
def set_image_spec(spec):
|
||||
"""
|
||||
Writes new specification for images manipulated or created when needed
|
||||
:param spec:
|
||||
:return:
|
||||
"""
|
||||
output_spec = ImageSpec()
|
||||
output_spec.set_format(OpenImageIO.UINT8)
|
||||
output_spec.width = spec['resolution'][0]
|
||||
output_spec.height = spec['resolution'][0]
|
||||
output_spec.nchannels = 1
|
||||
return output_spec
|
||||
|
||||
|
||||
def write_image(image, filename, image_format):
|
||||
"""
|
||||
Writes final processed image after operations have been completed and final result in buffer
|
||||
|
||||
:param image: Image buffer
|
||||
:param filename: Image name to save
|
||||
:param image_format: File format for export
|
||||
:return:
|
||||
"""
|
||||
if not image.has_error:
|
||||
image.set_write_format(image_format)
|
||||
image.write(filename)
|
||||
if image.has_error:
|
||||
_LOGGER.info(f'Error writing {filename}: {image.geterror()}')
|
||||
|
||||
|
||||
def transfer_file(src, dst):
|
||||
"""
|
||||
Some legacy textures only need to be renamed an moved to the processed folder with no further manipulation
|
||||
required. This function handles this transfer operation
|
||||
:param src: Source of the image intended for transfer
|
||||
:param dst: The destination path for the transferred image
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info(f'{src} +++TRANSFER FILE+++>> {dst}')
|
||||
try:
|
||||
copyfile(src, dst)
|
||||
return dst
|
||||
except Exception as e:
|
||||
_LOGGER.info(f'Copy error encountered: {e}')
|
||||
return None
|
||||
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
# coding:utf-8
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# This is a temporary workaround script for previewing Stingray materials applied to FBX files in material conversion.
|
||||
# The reason this is needed is because of a bug found in automating Stingray material assignmens
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
import pymel.core as pm
|
||||
import maya.mel as mel
|
||||
import constants
|
||||
import random
|
||||
import os
|
||||
|
||||
|
||||
def get_materials_in_scene():
|
||||
for shading_engine in pm.ls(type=pm.nt.ShadingEngine):
|
||||
if len(shading_engine):
|
||||
for material in shading_engine.surfaceShader.listConnections():
|
||||
yield material
|
||||
|
||||
|
||||
def get_file_textures(material_name):
|
||||
try:
|
||||
file_textures = []
|
||||
material_files = [x for x in pm.listConnections(material_name, plugs=1, source=1) if x.startswith('file')]
|
||||
for file_name in material_files:
|
||||
try:
|
||||
file_textures.append(pm.getAttr('{}.fileTextureName'.format(file_name.split('.')[0])))
|
||||
except pm.MayaAttributeError:
|
||||
pass
|
||||
return file_textures
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
|
||||
def get_search_string(texture_name):
|
||||
"""
|
||||
Creates a "base" string derived from the passed texture name for finding associated files
|
||||
:param texture_name: The texture name from which to extract the search string
|
||||
:return:
|
||||
"""
|
||||
filename = os.path.basename(texture_name)
|
||||
base = filename.split('.')[0]
|
||||
basename_list = base.split('_')
|
||||
search_string = '_'.join(basename_list[:-1])
|
||||
return search_string
|
||||
|
||||
|
||||
def get_texture_type(search_string, file_name):
|
||||
"""
|
||||
Gets the type of PBR texture for assignments based on the texture filename
|
||||
:param search_string: The convention basename for file texture
|
||||
:param file_name: This is a filename extracted from the mtl file- used purely as a template for newly created PBR textures
|
||||
:return:
|
||||
"""
|
||||
texture_types = ['BaseColor', 'Metallic', 'Normal', 'Emissive', 'Roughness']
|
||||
target_string = file_name.replace(search_string, '')
|
||||
base = target_string.split('.')[0]
|
||||
if base[1:] in texture_types:
|
||||
return base[1:]
|
||||
return None
|
||||
|
||||
|
||||
def get_output_file_size(file_path):
|
||||
"""
|
||||
When creating texture masks for further processing, this function checks output filesize to determine if there are
|
||||
any UVs present for the object it's created for. There are many assets without UVS or with UVs that entirely
|
||||
encapsulate the 0 to 1 UV space- and they come out in the size fiting between the range below. Without any content
|
||||
there is no need for the generated mask- so it is summarily removed
|
||||
:param file_path: Path to the vertex map masks generated by object UVs
|
||||
:return:
|
||||
"""
|
||||
size = os.stat(file_path).st_size
|
||||
if (constants.EMPTY_IMAGE_LOW < size < constants.EMPTY_IMAGE_HIGH) == True:
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
def attach_texture(texture_type, material, shading_group_name, file_path):
|
||||
material_info = {
|
||||
'BaseColor': 'color',
|
||||
'Metallic': 'metallic',
|
||||
'Normal': 'normal',
|
||||
'Emissive': 'emissive',
|
||||
'Roughness': 'roughness'
|
||||
}
|
||||
|
||||
file_count = len(pm.ls(type='file')) + 1
|
||||
texture_file = 'file{}'.format(file_count)
|
||||
file_node = pm.shadingNode('file', asTexture=True, name=texture_file)
|
||||
pm.setAttr('{}.fileTextureName'.format(texture_file), file_path, type="string")
|
||||
pm.setAttr('{}.use_{}_map'.format(material, material_info[texture_type]), True)
|
||||
pm.connectAttr('{}.outColor'.format(texture_file), '{}.TEX_{}_map'.format(material, material_info[texture_type]),
|
||||
force=True)
|
||||
|
||||
|
||||
def combine_meshes():
|
||||
"""
|
||||
Combines geometry based on material assignment. This function was created before the incorporation of .assetinfo
|
||||
files while processing. I intend to revamp the system, as this practice was found to be problematic when LOD meshes
|
||||
were found in files containing the same materials. Leaving this in for now until a better system can be created as
|
||||
this still helps in many cases to produce a mesh for preview
|
||||
:return:
|
||||
"""
|
||||
pm.showHidden(all=True)
|
||||
pm.select(pm.ls(geometry=True))
|
||||
selected_geo = pm.selected()
|
||||
print(selected_geo)
|
||||
if len(selected_geo) > 1:
|
||||
combined_mesh = pm.polyUnite(selected_geo)
|
||||
pm.delete(ch=True)
|
||||
return combined_mesh[0]
|
||||
else:
|
||||
return selected_geo
|
||||
|
||||
|
||||
def create_material_masks(target_mesh, material_list, base_directory):
|
||||
"""
|
||||
Fuction for generating vertex map masks of existing UVs. If metallic maps needed to be constructed by hand this
|
||||
made mask assignments in Substance Designer a quicker process. There might still be a need for this process for
|
||||
other things so I'm leaving this in here for now- eventually it might land in a Maya utility class.
|
||||
:param target_mesh: The mesh to generate maps from
|
||||
:param material_list: The list of materials needing masks generated for
|
||||
:param base_directory: The directory to save vertex map masks to
|
||||
:return:
|
||||
"""
|
||||
container = separate_applied_materials(target_mesh)
|
||||
children = pm.listRelatives(container, children=True)
|
||||
|
||||
for item in children:
|
||||
if not item.endswith('_pbr_GEO'):
|
||||
pm.delete(item)
|
||||
else:
|
||||
pm.hide(item)
|
||||
|
||||
print('\n::::::::::::::::::::::::::::::::')
|
||||
print('Bake material masks')
|
||||
print('\n::::::::::::::::::::::::::::::::')
|
||||
print('Children: {}'.format(children))
|
||||
print('Material List: {}'.format(material_list))
|
||||
|
||||
for material in material_list:
|
||||
print('Material: {}'.format(material))
|
||||
separated_mesh_object = '{}_GEO'.format(material)
|
||||
if separated_mesh_object in children:
|
||||
pm.showHidden(separated_mesh_object)
|
||||
bake_vertex_colors(separated_mesh_object, material, base_directory)
|
||||
cleanup_object(separated_mesh_object, material)
|
||||
print('\n')
|
||||
|
||||
pm.setToolTo('selectSuperContext')
|
||||
pm.select(clear=True)
|
||||
|
||||
|
||||
def cleanup_object(target_mesh, material):
|
||||
"""
|
||||
Vertex masks are created using vertex colors, but once the maps have been generated the vertex colors should be
|
||||
removed. This function handles the cleanup process.
|
||||
:param target_mesh:
|
||||
:param material:
|
||||
:return:
|
||||
"""
|
||||
pm.select(target_mesh)
|
||||
pm.polyColorPerVertex(rem=True)
|
||||
pm.delete(ch=True)
|
||||
pm.hyperShade(assign=material)
|
||||
pm.hide()
|
||||
pm.select(clear=True)
|
||||
|
||||
|
||||
def bake_vertex_colors(target_mesh, material, base_directory):
|
||||
file_path = get_file_path(material, base_directory)
|
||||
print('Baking verts for: {} Path: {}'.format(target_mesh, file_path))
|
||||
pm.select(target_mesh)
|
||||
pm.hyperShade(assign='lambert1')
|
||||
mel.eval('artAttrColorPerVertexToolScript 4;')
|
||||
pm.polyOptions(colorMaterialChannel='DIFFUSE')
|
||||
pm.polyColorPerVertex(r=1, g=1, b=1, a=1, cdo=True)
|
||||
pm.select(clear=True)
|
||||
|
||||
try:
|
||||
pm.select(target_mesh, r=True)
|
||||
mel.eval('artAttrColorPerVertexToolScript 4;')
|
||||
context = pm.currentCtx()
|
||||
pm.artAttrPaintVertexCtx(context, e=True, esf=file_path, fsx=4096, fsy=4096)
|
||||
get_output_file_size(file_path)
|
||||
except RuntimeError as e:
|
||||
print('+++++++++++++++++')
|
||||
print('Bake Failed: {}'.format(e))
|
||||
print('+++++++++++++++++')
|
||||
|
||||
|
||||
def get_file_path(material, base_directory):
|
||||
texture_list = get_file_textures(material)
|
||||
for item in texture_list:
|
||||
search_string = get_search_string(item)
|
||||
target_file = os.path.join(base_directory, '{}_Metallic.tif'.format(search_string))
|
||||
if os.path.isfile(target_file):
|
||||
return target_file
|
||||
return None
|
||||
|
||||
|
||||
def separate_applied_materials(target_object):
|
||||
target_object = target_object[0] if isinstance(target_object, list) else target_object
|
||||
try:
|
||||
shading_groups = list(set(pm.listConnections(target_object.getShape(), type='shadingEngine')))
|
||||
except AttributeError:
|
||||
shading_groups = list(set(pm.listConnections(target_object, type='shadingEngine')))
|
||||
|
||||
materials_container = pm.group(empty=1, n='separated_materials')
|
||||
for count, sg in enumerate(shading_groups):
|
||||
material = pm.ls(pm.listConnections(sg), materials=True)[0]
|
||||
target_material = pm.listConnections(sg + '.surfaceShader')
|
||||
clone = '{}_GEO'.format(target_material[0])
|
||||
pm.duplicate(target_object, n=clone)
|
||||
pm.parent(clone, 'separated_materials')
|
||||
material = pm.listConnections(sg, s=True, d=False)
|
||||
pm.select(clone)
|
||||
pm.runtime.ConvertSelectionToFaces()
|
||||
temp_set = str(pm.sets())
|
||||
temp_grps = pm.listConnections(material[0], type='shadingEngine')
|
||||
pm.select(pm.sets(temp_grps[0], int=temp_set))
|
||||
pm.runtime.InvertSelection()
|
||||
pm.delete()
|
||||
|
||||
pm.delete(target_object)
|
||||
return materials_container
|
||||
|
||||
|
||||
def run():
|
||||
"""
|
||||
The entry function of the script. This finds existing materials on FBX objects, converts to Stingray materials,
|
||||
and assigns corresponding textures.
|
||||
:return:
|
||||
"""
|
||||
file_name = pm.sceneName()
|
||||
base_directory = os.path.dirname(pm.sceneName())
|
||||
print('Base Directory: {}'.format(base_directory))
|
||||
textured_materials = []
|
||||
for item in get_materials_in_scene():
|
||||
if str(type(item)) == "<class 'pymel.core.nodetypes.StingrayPBS'>":
|
||||
print('Material--> {}'.format(item))
|
||||
shading_group = pm.listConnections(item, type="shadingEngine")[0]
|
||||
file_textures = get_file_textures(item[:-4])
|
||||
if file_textures:
|
||||
print('FileTexture: {}'.format(file_textures))
|
||||
textured_materials.append(item)
|
||||
if len(file_textures) > 1:
|
||||
print('Multiple textures found :::::::::::: {}'.format(file_textures))
|
||||
print('Scene: {} Material: {}'.format(file_name, item))
|
||||
else:
|
||||
search_string = get_search_string(file_textures[0])
|
||||
if search_string:
|
||||
print('Search String: {}'.format(search_string))
|
||||
print('Found these related textures:')
|
||||
for target_file in os.listdir(base_directory):
|
||||
if target_file.startswith(search_string):
|
||||
print('-->>>>>> {}'.format(target_file))
|
||||
texture_type = get_texture_type(search_string, target_file)
|
||||
if texture_type:
|
||||
textured_materials.append(item)
|
||||
file_path = os.path.join(base_directory, target_file)
|
||||
attach_texture(texture_type, item, shading_group, file_path)
|
||||
|
||||
print('')
|
||||
|
||||
target_mesh = combine_meshes()
|
||||
textured_materials = list(set(textured_materials))
|
||||
create_material_masks(target_mesh, textured_materials, base_directory)
|
||||
pm.saveFile(force=True)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# coding:utf-8
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# This file contains Lumberyard specific file and helper functions for handling material conversion
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import logging as _logging
|
||||
from pathlib import Path
|
||||
from box import Box
|
||||
import constants
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
module_name = 'legacy_asset_converter.main.lumberyard_data'
|
||||
_LOGGER = _logging.getLogger(module_name)
|
||||
|
||||
|
||||
def walk_directories(target_path):
|
||||
"""
|
||||
Records file and directory structure information for all content within specified base folder for operation
|
||||
|
||||
:target path: The base directory from which to perform the os.walk
|
||||
:return:
|
||||
"""
|
||||
directory_audit = {}
|
||||
scan_type, base_directory = get_base_directory(target_path)
|
||||
base_directory_name = base_directory.name
|
||||
exclusion_list = constants.DIRECTORY_EXCLUSION_LIST
|
||||
|
||||
directory_index = 0
|
||||
for (root, dirs, files) in os.walk(base_directory, topdown=True):
|
||||
temp_dictionary = {}
|
||||
root = Path(root)
|
||||
directory_name = root.name
|
||||
is_object_directory = True if [x for x in root.iterdir() if x.suffix == '.fbx'] else False
|
||||
_LOGGER.info(f'\n_\nScanning Directory: {directory_name} -- Object directory? {is_object_directory}')
|
||||
|
||||
if is_object_directory:
|
||||
if directory_name not in exclusion_list:
|
||||
temp_dictionary = Box({'directoryname': directory_name, 'directorypath': str(root)})
|
||||
files_dictionary = scan_directory(root)
|
||||
|
||||
subdirectories = [x for x in root.iterdir() if x.is_dir()]
|
||||
for subdirectory in subdirectories:
|
||||
target_path = subdirectory.name
|
||||
if not [x for x in subdirectory.iterdir() if x.suffix == '.fbx'] and target_path \
|
||||
not in (exclusion_list +[base_directory_name]):
|
||||
files_dictionary.update(scan_directory(root / subdirectory))
|
||||
temp_dictionary['files'] = files_dictionary
|
||||
directory_audit[directory_index] = temp_dictionary
|
||||
directory_index += 1
|
||||
return Box(directory_audit)
|
||||
|
||||
|
||||
def scan_directory(directory_path):
|
||||
"""
|
||||
Finds all FBX files, which are needed for the materials conversion process
|
||||
|
||||
:param directory_path:
|
||||
:return:
|
||||
"""
|
||||
files_dictionary = {}
|
||||
for filename in directory_path.iterdir():
|
||||
filename = Path(filename)
|
||||
extension = filename.suffix
|
||||
if extension in (constants.IMAGE_TYPES + constants.LUMBERYARD_DATA_FILES + ['.fbx']):
|
||||
files_dictionary[extension] = [directory_path / filename] if filename.suffix not in list(files_dictionary) else \
|
||||
files_dictionary[extension] + [directory_path / filename]
|
||||
return files_dictionary
|
||||
|
||||
|
||||
def get_base_directory(target_path):
|
||||
"""
|
||||
The "walk" function can be passed both directory paths as well as file paths (for single asset processing). In the
|
||||
event that a file path is passed, this will get the file path's containing folder for auditing related files
|
||||
:param target_path: The path to check
|
||||
:return:
|
||||
"""
|
||||
base_directory = target_path
|
||||
scan_type = 'directory'
|
||||
if target_path.is_file():
|
||||
scan_type = 'file'
|
||||
base_directory = Path(target_path.parent)
|
||||
return scan_type, base_directory
|
||||
|
||||
|
||||
def get_material_info(target_directory, filename):
|
||||
"""
|
||||
Extracts material information from legacy .mtl files for conversion to the .material format
|
||||
:param target_directory:
|
||||
:param filename:
|
||||
:return:
|
||||
"""
|
||||
materials_list = {}
|
||||
texture_modifications = {}
|
||||
target_mtl = target_directory / filename
|
||||
if target_mtl.is_file():
|
||||
tree = ET.parse(target_mtl)
|
||||
root = tree.getroot()
|
||||
for material in root.iter('Material'):
|
||||
material_attributes = {}
|
||||
if 'Name' in material.attrib.keys():
|
||||
for key, value in material.attrib.items():
|
||||
material_attributes[key] = value
|
||||
|
||||
material_textures = {}
|
||||
for textures in material.findall('Textures'):
|
||||
for texture in textures.findall('Texture'):
|
||||
map_name = texture.get('Map')
|
||||
file_path = texture.get('File')
|
||||
material_textures[map_name.lower()] = Path(file_path)
|
||||
if texture.findall('TexMod'):
|
||||
listing = texture.findall('TexMod')
|
||||
for child in listing:
|
||||
texture_modifications[material_attributes['Name']] = child.attrib
|
||||
if material_attributes:
|
||||
temp_list = Box({'assigned': [], 'attributes': material_attributes, 'modifications': texture_modifications,
|
||||
'textures': material_textures})
|
||||
materials_list[material_attributes['Name']] = temp_list
|
||||
return Box(materials_list)
|
||||
|
||||
|
||||
def get_asset_info(target_directory, filename, target_material):
|
||||
"""
|
||||
Gathers material assignments for specified assets for .assetinfo files that typically accompany FBX files
|
||||
:param target_directory: The directory to search for .assetinfo file
|
||||
:param filename: The name of the FBX file for which information is being gathered
|
||||
:param target_material: The material contained in the FBX file that the script is finding geo assignments for
|
||||
:return:
|
||||
"""
|
||||
target_file = target_directory / filename.lower()
|
||||
assignment_list = []
|
||||
if target_file.is_file():
|
||||
try:
|
||||
tree = ET.parse(target_file)
|
||||
root = tree.getroot()
|
||||
for layer_listing in root.iter('Class'):
|
||||
target_value = layer_listing.get('value')
|
||||
if target_value and target_value.find('.') != -1:
|
||||
search_string = target_value.split('.')[-1]
|
||||
|
||||
if search_string == target_material:
|
||||
asset_list = target_value.split('.')
|
||||
crop_path = asset_list[2:-1]
|
||||
asset_path = '.'.join(crop_path)
|
||||
if asset_path not in assignment_list:
|
||||
assignment_list.append(asset_path)
|
||||
except Exception as e:
|
||||
_LOGGER.info('AssetInfo file found, but information extraction failed.')
|
||||
else:
|
||||
_LOGGER.info('AssetInfo file not found. Cannot gather material assignments.')
|
||||
return assignment_list
|
||||
|
||||
|
||||
def export_lumberyard_material(output, material_description):
|
||||
"""
|
||||
Takes one final dictionary with information gathered in the process and saves with JSON formatting into a
|
||||
.material file
|
||||
|
||||
:param output:
|
||||
:param material_description:
|
||||
:return:
|
||||
"""
|
||||
with open(output, 'w', encoding='utf-8') as material_file:
|
||||
json.dump(material_description, material_file, ensure_ascii=False, indent=4)
|
||||
|
||||
+1845
File diff suppressed because it is too large
Load Diff
+85
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"description": "",
|
||||
"materialType": "Materials/Types/StandardPBR.materialtype",
|
||||
"parentMaterial": "",
|
||||
"propertyLayoutVersion": 3,
|
||||
"properties": {
|
||||
"general": {
|
||||
"texcoord": 0
|
||||
},
|
||||
"ambientOcclusion": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"baseColor": {
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"emissive": {
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"enable": false,
|
||||
"intensity": -5.0,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"metallic": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"roughness": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif"
|
||||
},
|
||||
"specularF0": {
|
||||
"factor": 0.5,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"normal": {
|
||||
"factor": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif"
|
||||
},
|
||||
"subsurfaceScattering": {
|
||||
"enableSubsurfaceScattering": true,
|
||||
"influenceMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif",
|
||||
"quality": 0.4,
|
||||
"scatterDistance": 8,
|
||||
"subsurfaceScatterFactor": 1
|
||||
},
|
||||
"opacity": {
|
||||
"doubleSided": false,
|
||||
"factor": 1.0,
|
||||
"cutoutAlpha": false,
|
||||
"cutoutThreshold": 0.5,
|
||||
"useBaseColorTextureAlpha": false,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"uv": {
|
||||
"center": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsetU": 0,
|
||||
"offsetV": 0,
|
||||
"rotateDegrees": 0,
|
||||
"scale": 1.0,
|
||||
"tileU": 1.0,
|
||||
"tileV": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d42de1023180dc9ff4f9db8c9718c0d476ecf1fe7be99e53b26ee7939ea55174
|
||||
size 133641
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08fa77d80e790d2580f4a6ae6b204357a091c4e675e038e4ef9e1c0bebb5b6f6
|
||||
size 11285
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
its licensors.
|
||||
|
||||
For complete copyright and license terms please see the LICENSE at the root of this
|
||||
distribution (the "License"). All use of this software is governed by the License,
|
||||
or, if provided, by the license below or the license accompanying this file. Do not
|
||||
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
import socket
|
||||
import pickle
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
# BUFFER_SIZE = 4096
|
||||
# port = 20201
|
||||
#
|
||||
# if len(sys.argv) > 1:
|
||||
# port = sys.argv[1]
|
||||
#
|
||||
# def SendCommand(target_command):
|
||||
# maya_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
# maya_socket.connect(('localhost', port))
|
||||
#
|
||||
# try:
|
||||
# maya_socket.send("import maya.cmds as cmds".encode())
|
||||
# data = maya_socket.recv(BUFFER_SIZE)
|
||||
# maya_socket.send("cmds.polySphere()".encode())
|
||||
# data = maya_socket.recv(BUFFER_SIZE)
|
||||
#
|
||||
# # This is a workaround for replacing "null bytes" and converting
|
||||
# # return information as a list (as opposed to a string)
|
||||
# result = eval(data.decode().replace('\x00', ''))
|
||||
# print(result[0])
|
||||
# except Exception as e:
|
||||
# print ('Connection Failed: {}'.format(e))
|
||||
# finally:
|
||||
# maya_socket.close()
|
||||
#
|
||||
#
|
||||
# # maya.send('import maya.cmds as mc; mc.polyCube()')
|
||||
# # maya.close()
|
||||
#
|
||||
# if __name__=='__main__':
|
||||
# target_command = "import maya.cmds as mc; mc.polySphere();"
|
||||
# SendCommand(target_command)
|
||||
|
||||
|
||||
|
||||
|
||||
#Port Number 20201
|
||||
# MayaVersion + 0 (Mel) or 1 (Python)
|
||||
#
|
||||
# import maya.cmds as mc
|
||||
# mc.commandPort(name=":20201", sourceType="python")
|
||||
#
|
||||
#
|
||||
# His user setup has this in it:
|
||||
#
|
||||
# if not mc.about(batch=True):
|
||||
# mc.commandPort(name=":20200", sourceType="mel")
|
||||
# mc.commandPort(name=":20201", sourceType="python")
|
||||
|
||||
|
||||
class MayaClient(object):
|
||||
PORT = 20201
|
||||
BUFFER_SIZE = 4096
|
||||
|
||||
def __init__(self):
|
||||
self.maya_socket = None
|
||||
self.port = self.__class__.PORT
|
||||
|
||||
def connect(self, port=-1):
|
||||
if port >= 0:
|
||||
self.port = port
|
||||
try:
|
||||
self.maya_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.maya_socket.connect(('localhost', self.PORT))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
try:
|
||||
self.maya_socket.close()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return True
|
||||
|
||||
def send(self, cmd):
|
||||
try:
|
||||
self.maya_socket.sendall(cmd.encode())
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
return self.recv()
|
||||
|
||||
def recv(self):
|
||||
try:
|
||||
data = self.maya_socket.recv(MayaClient.BUFFER_SIZE)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
return data.decode().replace('\x00', '')
|
||||
|
||||
##################
|
||||
# COMMANDS #####
|
||||
##################
|
||||
|
||||
def echo(self, text):
|
||||
cmd = "eval(\"'{}'\")".format(text)
|
||||
return self.send(cmd)
|
||||
|
||||
def new_file(self):
|
||||
cmd = "cmds.file(new=True, force=True)"
|
||||
return self.send(cmd)
|
||||
|
||||
def create_primitive(self, shape):
|
||||
cmd = ''
|
||||
if shape == 'sphere':
|
||||
cmd += 'cmds.polySphere()'
|
||||
elif shape == 'cube':
|
||||
cmd += 'cmds.polyCube()'
|
||||
else:
|
||||
print('Invalid Shape: {}'.format(shape))
|
||||
return None
|
||||
result = self.send(cmd)
|
||||
return eval(result)
|
||||
|
||||
def translate(self, node, translation):
|
||||
cmd = "cmds.setAttr('{0}.translate', {1}, {2}, {3})".format(node, *translation)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
maya_client = MayaClient()
|
||||
if maya_client.connect():
|
||||
print('Connected successfully')
|
||||
print('Echo: {}'.format(maya_client.echo('hello world')))
|
||||
|
||||
file_name = maya_client.new_file()
|
||||
print(file_name)
|
||||
|
||||
nodes = maya_client.create_primitive('sphere')
|
||||
print(nodes)
|
||||
|
||||
maya_client.translate(nodes[0], [0, 10, 0])
|
||||
|
||||
nodes = maya_client.create_primitive('cube')
|
||||
print(nodes)
|
||||
|
||||
|
||||
if maya_client.disconnect():
|
||||
print('Disconnected successfully')
|
||||
else:
|
||||
print('Failed to connect')
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
maya_client = MayaClient()
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
# coding:utf-8
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# File Description:
|
||||
# Contains helper functions to assist when processing files using the DCCsi. This file will be consistently added to
|
||||
# as new helper functions are needed
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
import logging as _logging
|
||||
from pathlib import Path
|
||||
import pathlib
|
||||
from box import Box
|
||||
import os
|
||||
|
||||
|
||||
module_name = 'legacy_asset_converter.main.utilities'
|
||||
_LOGGER = _logging.getLogger(module_name)
|
||||
|
||||
|
||||
def convert_box_dict_to_standard(target_dict):
|
||||
"""
|
||||
This converts all aspects of a dictionary to be compatible for Python 2.x and 3.x. Converting
|
||||
dictionaries back and forth is needed in the event that they need to be processed with DCC apps (
|
||||
specifically Maya) that run on earlier versions of Python. The Pathlib and Box modules will cause
|
||||
errors otherwise.
|
||||
|
||||
:param target_dict: This specifies which dictionary to convert
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Convert Box database with Pathlib to standard dictionary listings for Maya 2.7')
|
||||
converted_dictionary = {}
|
||||
for key, values in target_dict.items():
|
||||
updated_dictionary = {}
|
||||
for k, v in values.items():
|
||||
if str(type(v)) == "<class 'box.box.Box'>":
|
||||
updated_dictionary[k] = v.to_dict()
|
||||
elif str(type(v)) == "<class 'box.box_list.BoxList'>":
|
||||
updated_dictionary[k] = [os.path.normpath(x) for x in v.to_list()]
|
||||
elif isinstance(v, pathlib.WindowsPath):
|
||||
updated_dictionary[k] = os.path.abspath(v)
|
||||
else:
|
||||
updated_dictionary[k] = v
|
||||
converted_dictionary[key] = clear_pathlib_instances(updated_dictionary)
|
||||
return converted_dictionary
|
||||
|
||||
|
||||
def convert_standard_dict_to_box(target_dict):
|
||||
"""
|
||||
This converts all aspects of a dictionary to the preferred formatting for Python 3.x to run it.
|
||||
To allow more clarity when handling dictionaries extensive use of Box and Pathlib is using in
|
||||
the handling of data. Unfortunately this is not compatible with Python 2.7 so in some situations
|
||||
the formatting needs to be removed. This reinstates formatting to include Box and Pathlib.
|
||||
|
||||
:param target_dict: This specifies which dictionary to convert
|
||||
:return:
|
||||
"""
|
||||
_LOGGER.info('Convert Standard dictionary database back to Box Dictionary with Pathlib')
|
||||
converted_dictionary = {}
|
||||
for key, values in target_dict.items():
|
||||
updated_dictionary = Box({})
|
||||
for k, v in values.items():
|
||||
if str(type(v)) == "<class 'dict'>":
|
||||
updated_dictionary[k] = Box(v)
|
||||
elif str(type(v)) == "<class 'list'>":
|
||||
updated_dictionary[k] = BoxList(v)
|
||||
elif os.path.isfile(v):
|
||||
updated_dictionary[k] = Path(v)
|
||||
else:
|
||||
updated_dictionary[k] = v
|
||||
converted_dictionary[key] = add_pathlib_instances(updated_dictionary)
|
||||
return Box(converted_dictionary)
|
||||
|
||||
|
||||
def add_pathlib_instances(target_dictionary):
|
||||
"""
|
||||
Converts string paths to pathlib instances.
|
||||
:param target_dictionary:
|
||||
:return:
|
||||
"""
|
||||
pathlib_added = {}
|
||||
for k, v in target_dictionary.items():
|
||||
if isinstance(v, dict):
|
||||
nested = add_pathlib_instances(v)
|
||||
if len(nested.keys()):
|
||||
pathlib_added[k] = nested
|
||||
elif isinstance(v, Box):
|
||||
nested = add_pathlib_instances(v)
|
||||
if len(nested.keys()):
|
||||
pathlib_added[k] = nested
|
||||
elif isinstance(v, str):
|
||||
if os.path.isfile(v):
|
||||
pathlib_added[k] = Path(v)
|
||||
elif os.path.isdir(v):
|
||||
pathlib_added[k] = Path(v)
|
||||
else:
|
||||
pathlib_added[k] = v
|
||||
else:
|
||||
pathlib_added[k] = v
|
||||
return pathlib_added
|
||||
|
||||
|
||||
def clear_pathlib_instances(target_dictionary):
|
||||
"""
|
||||
Removes all pathlib instances and replaces with string paths
|
||||
:param target_dictionary:
|
||||
:return:
|
||||
"""
|
||||
clean = {}
|
||||
for k, v in target_dictionary.items():
|
||||
if isinstance(v, dict):
|
||||
nested = clear_pathlib_instances(v)
|
||||
if len(nested.keys()):
|
||||
clean[k] = nested
|
||||
elif isinstance(v, pathlib.WindowsPath):
|
||||
clean[k] = os.path.abspath(v)
|
||||
else:
|
||||
clean[k] = v
|
||||
return clean
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
//Maya 2020 Project Definition
|
||||
|
||||
workspace -fr "fluidCache" "";
|
||||
workspace -fr "JT_ATF" "";
|
||||
workspace -fr "images" "Assets/Textures";
|
||||
workspace -fr "offlineEdit" ".maya_data/scenes/edits";
|
||||
workspace -fr "STEP_ATF Export" "";
|
||||
workspace -fr "furShadowMap" "";
|
||||
workspace -fr "SVG" "";
|
||||
workspace -fr "scripts" "Maya/Scripts";
|
||||
workspace -fr "DAE_FBX" "";
|
||||
workspace -fr "shaders" "Maya/Shaders";
|
||||
workspace -fr "NX_ATF" "";
|
||||
workspace -fr "CATIAV5_ATF Export" "";
|
||||
workspace -fr "furFiles" "";
|
||||
workspace -fr "OBJ" ".maya_data/obj";
|
||||
workspace -fr "PARASOLID_ATF Export" "";
|
||||
workspace -fr "FBX export" "Assets/Objects";
|
||||
workspace -fr "furEqualMap" "";
|
||||
workspace -fr "textures" "Assets/textures";
|
||||
workspace -fr "BIF" "";
|
||||
workspace -fr "lights" ".maya_data/renderData/shaders";
|
||||
workspace -fr "DAE_FBX export" "";
|
||||
workspace -fr "aliasWire" ".maya_data/data";
|
||||
workspace -fr "CATIAV5_ATF" "";
|
||||
workspace -fr "SAT_ATF Export" "";
|
||||
workspace -fr "movie" ".maya_data/movies";
|
||||
workspace -fr "ASS Export" "";
|
||||
workspace -fr "mayaAscii" "";
|
||||
workspace -fr "move" ".maya_data";
|
||||
workspace -fr "autoSave" ".maya_data/autoSave";
|
||||
workspace -fr "NX_ATF Export" "";
|
||||
workspace -fr "sound" ".maya_data/sound";
|
||||
workspace -fr "mayaBinary" "";
|
||||
workspace -fr "timeEditor" "";
|
||||
workspace -fr "RIBexport" ".maya_data/data";
|
||||
workspace -fr "DWG_ATF" "";
|
||||
workspace -fr "mentalray" ".maya_data/renderData/mentalray";
|
||||
workspace -fr "JT_ATF Export" "";
|
||||
workspace -fr "iprImages" ".maya_data/renderData/iprImages";
|
||||
workspace -fr "FBX" "Assets/Objects";
|
||||
workspace -fr "renderData" ".maya_data/renderData";
|
||||
workspace -fr "CATIAV4_ATF" "";
|
||||
workspace -fr "fileCache" "";
|
||||
workspace -fr "Fbx" "Objects";
|
||||
workspace -fr "eps" "";
|
||||
workspace -fr "IGESexport" ".maya_data/data";
|
||||
workspace -fr "3dPaintTextures" ".maya_data/3dPaintTextures";
|
||||
workspace -fr "DXF_ATF Export" "";
|
||||
workspace -fr "mel" ".maya_data/mel";
|
||||
workspace -fr "translatorData" "";
|
||||
workspace -fr "IGES" ".maya_data/data";
|
||||
workspace -fr "particles" ".maya_data/particles";
|
||||
workspace -fr "DXFexport" ".maya_data/data";
|
||||
workspace -fr "DXF_ATF" "";
|
||||
workspace -fr "scene" "Assets/Objects";
|
||||
workspace -fr "renderScenes" ".maya_data/renderScenes";
|
||||
workspace -fr "SAT_ATF" "";
|
||||
workspace -fr "PROE_ATF" "";
|
||||
workspace -fr "WIRE_ATF Export" "";
|
||||
workspace -fr "sourceImages" "ArtSource/Images";
|
||||
workspace -fr "RIB" ".maya_data/data";
|
||||
workspace -fr "furImages" "";
|
||||
workspace -fr "clips" ".maya_data/clips";
|
||||
workspace -fr "Adobe(R) Illustrator(R)" ".maya_data/data";
|
||||
workspace -fr "animExport" ".maya_data/data";
|
||||
workspace -fr "mentalRay" ".maya_data/mentalRay";
|
||||
workspace -fr "STEP_ATF" "";
|
||||
workspace -fr "DWG_ATF Export" "";
|
||||
workspace -fr "depth" ".maya_data/renderData/depth";
|
||||
workspace -fr "sceneAssembly" "";
|
||||
workspace -fr "IGES_ATF Export" "";
|
||||
workspace -fr "teClipExports" "";
|
||||
workspace -fr "IGES_ATF" "";
|
||||
workspace -fr "PARASOLID_ATF" "";
|
||||
workspace -fr "ASS" "";
|
||||
workspace -fr "Substance" ".maya_data/data";
|
||||
workspace -fr "audio" ".maya_data/sound";
|
||||
workspace -fr "EPS" ".maya_data/data";
|
||||
workspace -fr "Alembic" "Assets/Objects";
|
||||
workspace -fr "diskCache" ".maya_data/cache";
|
||||
workspace -fr "illustrator" "";
|
||||
workspace -fr "WIRE_ATF" "";
|
||||
workspace -fr "templates" "ArtSource/SceneTemplates";
|
||||
workspace -fr "animImport" ".maya_data/data";
|
||||
workspace -fr "OBJexport" "Assets/Objects";
|
||||
workspace -fr "furAttrMap" "";
|
||||
workspace -fr "DXF" ".maya_data/data";
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -- This line is 75 characters -------------------------------------------
|
||||
|
||||
__all__ = ['maya_materials_export'] # populate with modules to control imports
|
||||
|
||||
# from maya_mapping import MayaMapping
|
||||
# from max_mapping import MaxMapping
|
||||
# from blender_mapping import BlenderMapping
|
||||
|
||||
# def get_dcc_mapping(app):
|
||||
# app = app.lower()
|
||||
# if app == 'maya':
|
||||
# return MayaMapping()
|
||||
# elif app == '3dsmax':
|
||||
# return MaxMapping()
|
||||
# elif app == 'blender':
|
||||
# return BlenderMapping()
|
||||
# else:
|
||||
# return NullMapping(app)
|
||||
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -------------------------------------------------------------------------
|
||||
"""
|
||||
Usage
|
||||
=====
|
||||
Put usage instructions here.
|
||||
|
||||
Output
|
||||
======
|
||||
Put output information here.
|
||||
|
||||
Relevant Links:
|
||||
https://pythonhosted.org/an_example_pypi_project/sphinx.html
|
||||
https://github.com/ideasman42/pyfbx_i42
|
||||
https://pypi.org/project/py-fbx/
|
||||
https://www.quora.com/How-do-I-execute-Maya-script-without-lauching-Maya
|
||||
https://stackoverflow.com/questions/27437733/use-external-python-script-to-open-maya-and-run-another-script-inside-maya
|
||||
|
||||
Notes:
|
||||
-- Materials information can be extracted from ASCII fbx pretty easily
|
||||
-- binary is possible but more difficult
|
||||
-- FBX files could be exported as ASCII files
|
||||
-- I could use regex there to extract material information
|
||||
-- I couldn't get pyfbx_i42 to work,
|
||||
-- ^ purportedly it can extract information from binary files.
|
||||
-- You may just have to use the specified python versions
|
||||
|
||||
-- Jonny wants me to use pathlib wherever possible for OO pathing, as well as python-box (aka Box) for dict access
|
||||
|
||||
# Things to do:
|
||||
--> Create the cube demo for Jonny with 4 attached materials
|
||||
--> Create mapping widget for stacked layout
|
||||
--> Allow export of JSON file for demo
|
||||
--> Allow custom field entries for description, etc.
|
||||
--> Need to figure out how to clear pointers for stored data properly
|
||||
"""
|
||||
|
||||
from PySide2 import QtWidgets, QtCore, QtGui
|
||||
from PySide2.QtCore import Signal, Slot, QThread, QAbstractItemModel, QModelIndex, QObject
|
||||
from maya import OpenMayaUI as omui
|
||||
from maya.standalone import initialize
|
||||
from shiboken2 import wrapInstance
|
||||
import pymel.core as pm
|
||||
# import sphinx
|
||||
# import azpy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
mayaMainWindowPtr = omui.MQtUtil.mainWindow()
|
||||
mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtWidgets.QWidget)
|
||||
|
||||
|
||||
class MayaToLumberyard(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(MayaToLumberyard, self).__init__(parent)
|
||||
|
||||
self.app = QtWidgets.QApplication.instance()
|
||||
self.setParent(mayaMainWindow)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setGeometry(50, 50, 600, 500)
|
||||
self.setObjectName('MaterialsToLumberyard')
|
||||
self.setWindowTitle('Maya To Lumberyard')
|
||||
self.isTopLevel()
|
||||
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowMinMaxButtonsHint)
|
||||
|
||||
self.desktop_location = os.path.join(os.path.expanduser('~'), 'Desktop')
|
||||
self.bold_font_large = QtGui.QFont('Plastique', 7, QtGui.QFont.Bold)
|
||||
self.medium_font = QtGui.QFont('Plastique', 7, QtGui.QFont.Normal)
|
||||
self.target_file_list = []
|
||||
self.materials_dict = {}
|
||||
self.material_definitions = {}
|
||||
self.processed_materials = []
|
||||
self.current_scene = pm.sceneName()
|
||||
self.model = None
|
||||
self.total_transfer_materials = 1
|
||||
|
||||
self.main_container = QtWidgets.QVBoxLayout(self)
|
||||
self.main_container.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.setLayout(self.main_container)
|
||||
|
||||
# Header Bar ------>
|
||||
self.header_bar_layout = QtWidgets.QHBoxLayout()
|
||||
self.select_files_button = QtWidgets.QPushButton('Select Files')
|
||||
self.select_files_button.clicked.connect(self.choose_files_clicked)
|
||||
self.select_files_button.setFixedSize(80, 35)
|
||||
self.header_bar_layout.addWidget(self.select_files_button)
|
||||
self.header_bar_layout.addSpacing(15)
|
||||
self.use_current_file_checkbox = QtWidgets.QCheckBox('Use Current File')
|
||||
self.use_current_file_checkbox.setFont(self.bold_font_large)
|
||||
self.use_current_file_checkbox.clicked.connect(self.use_current_file_clicked)
|
||||
self.header_bar_layout.addWidget(self.use_current_file_checkbox)
|
||||
self.header_bar_layout.addSpacing(100)
|
||||
self.switch_layout_combobox = QtWidgets.QComboBox()
|
||||
self.switch_layout_combobox.setEnabled(False)
|
||||
self.switch_layout_combobox.setFixedSize(250, 30)
|
||||
self.combobox_items = ['Target Files', 'Extracted Values', 'Material Tree']
|
||||
self.switch_layout_combobox.setStyleSheet('QComboBox {padding-left:6px;}')
|
||||
self.switch_layout_combobox.addItems(self.combobox_items)
|
||||
self.header_bar_layout.addWidget(self.switch_layout_combobox)
|
||||
self.header_bar_layout.addSpacing(4)
|
||||
self.main_container.addSpacing(5)
|
||||
self.main_container.addLayout(self.header_bar_layout)
|
||||
|
||||
# Separation Line ------>
|
||||
self.separatorLayout1 = QtWidgets.QHBoxLayout()
|
||||
self.line1 = QtWidgets.QLabel()
|
||||
self.line1.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken)
|
||||
self.line1.setLineWidth(1)
|
||||
self.line1.setFixedHeight(10)
|
||||
self.separatorLayout1.addWidget(self.line1)
|
||||
self.main_container.addLayout(self.separatorLayout1)
|
||||
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
# File Source Table / Attributes (Stacked Layout) #
|
||||
# ++++++++++++++++++++++++++++++++++++++++++++++++#
|
||||
self.content_stacked_layout = QtWidgets.QStackedLayout()
|
||||
self.main_container.addLayout(self.content_stacked_layout)
|
||||
self.switch_layout_combobox.currentIndexChanged.connect(self.layout_combobox_changed)
|
||||
|
||||
# --- Files Table
|
||||
self.target_files_table = QtWidgets.QTableWidget()
|
||||
self.target_files_table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.target_files_table.setColumnCount(2)
|
||||
self.target_files_table.setAlternatingRowColors(True)
|
||||
self.target_files_table.setHorizontalHeaderLabels(['File List', ''])
|
||||
self.target_files_table.horizontalHeader().setStyleSheet('QHeaderView::section {padding-top:9px; padding-left:10px;}')
|
||||
self.target_files_table.verticalHeader().hide()
|
||||
files_header = self.target_files_table.horizontalHeader()
|
||||
files_header.setFixedHeight(30)
|
||||
files_header.setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
files_header.setContentsMargins(10, 10, 0, 0)
|
||||
files_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
files_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
||||
self.target_files_table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.content_stacked_layout.addWidget(self.target_files_table)
|
||||
|
||||
# --- Scene Information Table
|
||||
self.material_tree_view = QtWidgets.QTreeView()
|
||||
self.headers = ['Key', 'Value']
|
||||
self.material_tree_view.setStyleSheet('QTreeView::item {height:25px;} QHeaderView::section {height:30px; padding-left:10px}')
|
||||
|
||||
self.material_tree_view.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.material_tree_view.setAlternatingRowColors(True)
|
||||
self.material_tree_view.setUniformRowHeights(True)
|
||||
self.content_stacked_layout.addWidget(self.material_tree_view)
|
||||
|
||||
# --- LY Material Definitions
|
||||
self.material_definitions_widget = QtWidgets.QWidget()
|
||||
self.material_definitions_layout = QtWidgets.QHBoxLayout(self.material_definitions_widget)
|
||||
self.material_definitions_layout.setSpacing(0)
|
||||
self.material_definitions_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.material_definitions_frame = QtWidgets.QFrame(self.material_definitions_widget)
|
||||
self.material_definitions_frame.setGeometry(0, 0, 5000, 5000)
|
||||
self.material_definitions_frame.setStyleSheet('background-color:rgb(150,150,150);')
|
||||
|
||||
self.title_bar_widget = QtWidgets.QWidget()
|
||||
self.title_bar_layout = QtWidgets.QHBoxLayout(self.title_bar_widget)
|
||||
self.title_bar_layout.setContentsMargins(17, 17, 17, 0)
|
||||
self.title_bar_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.title_bar_frame = QtWidgets.QFrame(self.title_bar_widget)
|
||||
self.title_bar_frame.setGeometry(0, 0, 5000, 60)
|
||||
self.title_bar_frame.setStyleSheet('background-color:rgb(102,69,153);')
|
||||
self.material_definitions_layout.addWidget(self.title_bar_widget)
|
||||
|
||||
self.material_name = QtWidgets.QCheckBox('StingrayPBS2')
|
||||
self.material_name.setStyleSheet('spacing:10px; color:white')
|
||||
self.material_name.setFont(self.bold_font_large)
|
||||
self.material_name.setChecked(True)
|
||||
self.title_bar_layout.addWidget(self.material_name)
|
||||
|
||||
# Forward/Back Buttons -------------------------->>
|
||||
self.previous_next_button_layout = QtWidgets.QHBoxLayout()
|
||||
self.item_count = QtWidgets.QLabel('1 of 10')
|
||||
self.previous_next_button_layout.addWidget(self.item_count)
|
||||
self.previous_next_button_layout.addSpacing(10)
|
||||
self.previous_next_button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.previous_next_button_layout.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.previous_button = QtWidgets.QToolButton()
|
||||
self.previous_button.setArrowType(QtCore.Qt.LeftArrow)
|
||||
self.previous_button.setStyleSheet('background-color:rgb(110,110,110);')
|
||||
self.previous_button.clicked.connect(self.previous_button_clicked)
|
||||
self.previous_next_button_layout.addWidget(self.previous_button)
|
||||
self.next_button = QtWidgets.QToolButton()
|
||||
self.next_button.setArrowType(QtCore.Qt.RightArrow)
|
||||
self.next_button.setStyleSheet('background-color:rgb(110,110,110);')
|
||||
self.next_button.clicked.connect(self.next_button_clicked)
|
||||
self.previous_next_button_layout.addWidget(self.next_button)
|
||||
self.title_bar_layout.addLayout(self.previous_next_button_layout)
|
||||
self.content_stacked_layout.addWidget(self.material_definitions_widget)
|
||||
|
||||
# File processing buttons ------>
|
||||
self.process_files_layout = QtWidgets.QHBoxLayout()
|
||||
self.main_container.addLayout(self.process_files_layout)
|
||||
self.process_files_button = QtWidgets.QPushButton('Process Listed Files')
|
||||
self.process_files_button.setFixedHeight(50)
|
||||
self.process_files_button.clicked.connect(self.process_files_clicked)
|
||||
self.process_files_layout.addWidget(self.process_files_button)
|
||||
self.reset_button = QtWidgets.QPushButton('Reset')
|
||||
self.reset_button.setFixedSize(50, 50)
|
||||
self.reset_button.clicked.connect(self.reset_clicked)
|
||||
self.reset_button.setEnabled(False)
|
||||
self.process_files_layout.addWidget(self.reset_button)
|
||||
self.initialize_window()
|
||||
|
||||
def initialize_window(self):
|
||||
if self.current_scene:
|
||||
self.target_file_list = [self.current_scene]
|
||||
self.use_current_file_checkbox.setChecked(True)
|
||||
self.populate_files_table()
|
||||
|
||||
def populate_files_table(self):
|
||||
self.target_files_table.setRowCount(0)
|
||||
for index, entry in enumerate(self.target_file_list):
|
||||
entry = entry[1] if type(entry) == list else entry
|
||||
self.target_files_table.insertRow(index)
|
||||
item = QtWidgets.QTableWidgetItem(' {}'.format(entry))
|
||||
self.target_files_table.setRowHeight(index, 45)
|
||||
remove_button = QtWidgets.QPushButton(' Remove ')
|
||||
remove_button.setStyleSheet('border-width:0px; background-color:rgb(100,100,100);')
|
||||
remove_button.clicked.connect(self.remove_file_clicked)
|
||||
self.target_files_table.setItem(index, 0, item)
|
||||
self.target_files_table.setCellWidget(index, 1, remove_button)
|
||||
|
||||
def process_file_list(self):
|
||||
file_processing_errors = []
|
||||
for maya_file_location in self.target_file_list:
|
||||
try:
|
||||
if maya_file_location != self.current_scene:
|
||||
pm.openFile(maya_file_location, force=True)
|
||||
self.current_scene = maya_file_location
|
||||
self.get_scene_materials_description()
|
||||
except Exception as e:
|
||||
file_processing_errors.append([maya_file_location, e])
|
||||
|
||||
# Create Model with extracted values from file list
|
||||
self.set_material_model()
|
||||
|
||||
# Setup Lumberyard Material File Values
|
||||
self.map_materials()
|
||||
print ('MaterialDefinitions:'.format(self.material_definitions))
|
||||
print json.dumps(self.material_definitions, sort_keys=True, indent=4)
|
||||
|
||||
# Update UI Layout
|
||||
self.set_material_view()
|
||||
self.switch_layout_combobox.setCurrentIndex(2)
|
||||
self.set_ui_buttons()
|
||||
|
||||
def map_materials(self):
|
||||
root = self.model.rootItem
|
||||
for row in range(self.model.rowCount()):
|
||||
name = self.model.get_attribute_value('MaterialName', root.child(row))
|
||||
material_type = self.model.get_attribute_value('MaterialType', root.child(row))
|
||||
file_connections = {}
|
||||
shader_attributes = {}
|
||||
|
||||
for childIndex in range(root.child(row).childCount()):
|
||||
child_item = root.child(row).child(childIndex)
|
||||
child_value = child_item.itemData
|
||||
if child_item.childCount():
|
||||
target_dict = file_connections if child_value[0] == 'FileConnections' else shader_attributes
|
||||
for subChildIndex in range(child_item.childCount()):
|
||||
sub_child_data = child_item.child(subChildIndex).itemData
|
||||
target_dict[sub_child_data[0]] = sub_child_data[1]
|
||||
self.set_pbr_material_description(name, material_type, file_connections)
|
||||
|
||||
def reset_all_values(self):
|
||||
pass
|
||||
# Need to figure out how to clear pointers for stored data properly
|
||||
# self.target_files_table.setRowCount(0)
|
||||
# self.model.beginResetModel()
|
||||
# self.model.qDeleteAll(mResults)
|
||||
# self.model.endResetModel()
|
||||
# self.initialize_window()
|
||||
|
||||
############################
|
||||
# Getters/Setters ##########
|
||||
############################
|
||||
|
||||
@staticmethod
|
||||
def get_materials(target_mesh):
|
||||
shading_group = pm.listConnections(pm.PyNode(target_mesh), type='shadingEngine')
|
||||
materials = pm.ls(pm.listConnections(shading_group), materials=1)
|
||||
return list(set(materials))
|
||||
|
||||
@staticmethod
|
||||
def get_shader(material_name):
|
||||
connections = pm.listConnections(material_name, type='shadingEngine')[0]
|
||||
shader_name = '{}.surfaceShader'.format(connections)
|
||||
shader = pm.listConnections(shader_name)[0]
|
||||
return shader
|
||||
|
||||
@staticmethod
|
||||
def get_shader_information(shader):
|
||||
shader_file_connections = {}
|
||||
for node in pm.listConnections(shader, type='file', c=True):
|
||||
shader_file_connections[str(node[0])] = str(pm.getAttr(node[1].fileTextureName))
|
||||
shader_attributes = {}
|
||||
for shader_attribute in pm.listAttr(shader, s=True, iu=True):
|
||||
try:
|
||||
shader_attributes[str(shader_attribute)] = pm.getAttr('{}.{}'.format(shader, shader_attribute))
|
||||
except pm.MayaAttributeError as e:
|
||||
print ('MayaAttributeError: {}'.format(e))
|
||||
|
||||
return shader_file_connections, shader_attributes
|
||||
|
||||
@staticmethod
|
||||
def get_shader_properties(name, material_type, file_connections):
|
||||
""" This system will probably need rethinking if DCCs and compatible materials grow """
|
||||
attr_list = {}
|
||||
if material_type == 'StingrayPBS':
|
||||
naming_exceptions = {'color': 'baseColor', 'ao': 'ambientOcclusion'}
|
||||
maps = 'color, metallic, roughness, normal, emissive, ao, opacity'.split(', ')
|
||||
for m in maps:
|
||||
texture_attribute = 'TEX_{}_map'.format(m)
|
||||
for tex in file_connections.keys():
|
||||
if tex.find(texture_attribute) != -1:
|
||||
key = m if m not in naming_exceptions else naming_exceptions.get(m)
|
||||
attr_list[key] = {'useTexture': 'true',
|
||||
'textureMap': file_connections.get('{}.{}'.format(name, texture_attribute))}
|
||||
return attr_list
|
||||
|
||||
@staticmethod
|
||||
def get_increment(name):
|
||||
last_number = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')
|
||||
number_found = last_number.search(name)
|
||||
if number_found:
|
||||
next_number = str(int(number_found.group(1)) + 1)
|
||||
start, end = number_found.span(1)
|
||||
name = name[:max(end - len(next_number), start)] + next_number + name[end:]
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def get_material_template(shader_type):
|
||||
definitions = os.path.join(os.path.dirname(os.path.abspath(__file__)), '{}.material'.format(shader_type))
|
||||
if os.path.exists(definitions):
|
||||
with open(definitions) as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_scene_materials_description(self):
|
||||
scene_geo = pm.ls(v=True, geometry=True)
|
||||
for target_mesh in scene_geo:
|
||||
material_list = self.get_materials(target_mesh)
|
||||
for material_name in material_list:
|
||||
material_type = pm.nodeType(material_name, api=True)
|
||||
material_listed = [x for x in self.materials_dict if self.materials_dict[x]['MaterialName'] == material_name]
|
||||
|
||||
if not material_listed:
|
||||
self.set_material_dict(str(material_name), str(material_type), target_mesh)
|
||||
else:
|
||||
mesh_list = self.materials_dict[material_name].get('AppliedMesh')
|
||||
if not isinstance(mesh_list, list):
|
||||
self.materials_dict[material_name]['AppliedMesh'] = [mesh_list, target_mesh]
|
||||
else:
|
||||
mesh_list.append(target_mesh)
|
||||
|
||||
def set_material_dict(self, material_name, material_type, material_mesh):
|
||||
shader = self.get_shader(material_name)
|
||||
shader_file_connections, shader_attributes = self.get_shader_information(shader)
|
||||
material_dict = {'MaterialName': material_name, 'MaterialType': material_type, 'AppliedMesh': material_mesh,
|
||||
'FileConnections': shader_file_connections, 'SceneName': str(self.current_scene),
|
||||
'MaterialAttributes': shader_attributes}
|
||||
material_name = 'Material_{}'.format(self.total_transfer_materials)
|
||||
self.materials_dict[material_name] = material_dict
|
||||
self.total_transfer_materials += 1
|
||||
|
||||
def set_material_model(self):
|
||||
self.model = MaterialsModel(self.headers, self.materials_dict)
|
||||
|
||||
def set_material_view(self):
|
||||
self.material_tree_view.setModel(self.model)
|
||||
self.material_tree_view.expandAll()
|
||||
self.material_tree_view.resizeColumnToContents(0)
|
||||
|
||||
def set_pbr_material_description(self, name, material_type, file_connections):
|
||||
# Build dictionary for material description based on extracted values
|
||||
default_settings = self.get_material_template('standardpbr.template')
|
||||
material = {'description': name,
|
||||
'materialType': default_settings.get('materialType'),
|
||||
'parentMaterial': default_settings.get('parentMaterial'),
|
||||
'propertyLayoutVersion': default_settings.get('propertyLayoutVersion'),
|
||||
'properties': self.get_shader_properties(name, material_type, file_connections)}
|
||||
self.material_definitions[name if name not in self.material_definitions.keys() else self.get_increment(name)] = material
|
||||
|
||||
def set_ui_buttons(self):
|
||||
display_index = self.content_stacked_layout.currentIndex()
|
||||
self.switch_layout_combobox.setEnabled(True)
|
||||
# Target Files
|
||||
if display_index == 0:
|
||||
self.use_current_file_checkbox.setEnabled(True)
|
||||
self.select_files_button.setEnabled(True)
|
||||
self.reset_button.setEnabled(True)
|
||||
self.process_files_button.setText('Process Listed Files')
|
||||
|
||||
# Extracted Values
|
||||
elif display_index == 1:
|
||||
self.reset_button.setEnabled(True)
|
||||
self.process_files_button.setEnabled(False)
|
||||
self.use_current_file_checkbox.setEnabled(False)
|
||||
self.select_files_button.setEnabled(False)
|
||||
|
||||
# Material Tree
|
||||
else:
|
||||
self.use_current_file_checkbox.setEnabled(False)
|
||||
self.select_files_button.setEnabled(False)
|
||||
self.process_files_button.setText('Export Selected Materials')
|
||||
if self.material_definitions:
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
############################
|
||||
# Button Actions ###########
|
||||
############################
|
||||
|
||||
def use_current_file_clicked(self):
|
||||
self.current_scene = pm.sceneName()
|
||||
if self.use_current_file_checkbox.isChecked():
|
||||
self.target_file_list.insert(0, self.current_scene)
|
||||
self.target_file_list = list(set(self.target_file_list))
|
||||
else:
|
||||
if self.current_scene in self.target_file_list:
|
||||
del self.target_file_list[self.target_file_list.index(self.current_scene)]
|
||||
self.populate_files_table()
|
||||
|
||||
def remove_file_clicked(self):
|
||||
file_index = self.target_files_table.indexAt(self.sender().pos())
|
||||
target_file = self.target_file_list[file_index.row()]
|
||||
if target_file == pm.sceneName():
|
||||
self.use_current_file_checkbox.setChecked(False)
|
||||
del self.target_file_list[file_index.row()]
|
||||
self.populate_files_table()
|
||||
|
||||
def process_files_clicked(self):
|
||||
self.process_file_list()
|
||||
|
||||
def choose_files_clicked(self):
|
||||
dialog = QtWidgets.QFileDialog(self, 'Shift-Select Target Files', self.desktop_location)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
|
||||
dialog.setNameFilter('Maya Files (*.ma *.mb *.fbx)')
|
||||
dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
||||
file_view = dialog.findChild(QtWidgets.QListView, 'listView')
|
||||
|
||||
# Workaround for selecting multiple files with File Dialog
|
||||
if file_view:
|
||||
file_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
f_tree_view = dialog.findChild(QtWidgets.QTreeView)
|
||||
if f_tree_view:
|
||||
f_tree_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
|
||||
if dialog.exec_() == QtWidgets.QDialog.Accepted:
|
||||
self.target_file_list = dialog.selectedFiles()
|
||||
if self.target_file_list:
|
||||
self.populate_files_table()
|
||||
self.process_files_button.setEnabled(True)
|
||||
|
||||
def layout_combobox_changed(self):
|
||||
self.content_stacked_layout.setCurrentIndex(self.switch_layout_combobox.currentIndex())
|
||||
self.set_ui_buttons()
|
||||
|
||||
def reset_clicked(self):
|
||||
self.reset_all_values()
|
||||
|
||||
def previous_button_clicked(self):
|
||||
print ('Previous button clicked')
|
||||
|
||||
def next_button_clicked(self):
|
||||
print ('Next button clicked')
|
||||
|
||||
|
||||
class MaterialsModel(QAbstractItemModel):
|
||||
def __init__(self, headers, data, parent=None):
|
||||
super(MaterialsModel, self).__init__(parent)
|
||||
|
||||
self.rootItem = TreeNode(headers)
|
||||
self.parents = [self.rootItem]
|
||||
self.indentations = [0]
|
||||
self.create_data(data)
|
||||
|
||||
def create_data(self, data, indent=-1):
|
||||
if type(data) == dict:
|
||||
indent += 1
|
||||
position = 4 * indent
|
||||
for key, value in data.iteritems():
|
||||
if position > self.indentations[-1]:
|
||||
if self.parents[-1].childCount() > 0:
|
||||
self.parents.append(self.parents[-1].child(self.parents[-1].childCount() - 1))
|
||||
self.indentations.append(position)
|
||||
else:
|
||||
while position < self.indentations[-1] and len(self.parents) > 0:
|
||||
self.parents.pop()
|
||||
self.indentations.pop()
|
||||
parent = self.parents[-1]
|
||||
parent.insertChildren(parent.childCount(), 1, parent.columnCount())
|
||||
parent.child(parent.childCount() - 1).setData(0, key)
|
||||
value_string = str(value) if type(value) != dict else str('')
|
||||
parent.child(parent.childCount() - 1).setData(1, value_string)
|
||||
try:
|
||||
self.create_data(value, indent)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_attribute_value(search_string, search_column):
|
||||
for childIndex in range(search_column.childCount()):
|
||||
child_item = search_column.child(childIndex)
|
||||
child_value = child_item.itemData
|
||||
if child_value[0] == search_string:
|
||||
return child_value[1]
|
||||
return None
|
||||
|
||||
def index(self, row, column, index=QModelIndex()):
|
||||
""" Returns the index of the item in the model specified by the given row, column and parent index """
|
||||
if not self.hasIndex(row, column, index):
|
||||
return QModelIndex()
|
||||
if not index.isValid():
|
||||
item = self.rootItem
|
||||
else:
|
||||
item = index.internalPointer()
|
||||
|
||||
child = item.child(row)
|
||||
if child:
|
||||
return self.createIndex(row, column, child)
|
||||
return QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
"""
|
||||
Returns the parent of the model item with the given index If the item has no parent,
|
||||
an invalid QModelIndex is returned
|
||||
"""
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
item = index.internalPointer()
|
||||
if not item:
|
||||
return QModelIndex()
|
||||
|
||||
parent = item.parentItem
|
||||
if parent == self.rootItem:
|
||||
return QModelIndex()
|
||||
else:
|
||||
return self.createIndex(parent.childNumber(), 0, parent)
|
||||
|
||||
def rowCount(self, index=QModelIndex()):
|
||||
"""
|
||||
Returns the number of rows under the given parent. When the parent is valid it means that
|
||||
rowCount is returning the number of children of parent
|
||||
"""
|
||||
if index.isValid():
|
||||
parent = index.internalPointer()
|
||||
else:
|
||||
parent = self.rootItem
|
||||
return parent.childCount()
|
||||
|
||||
def columnCount(self, index=QModelIndex()):
|
||||
""" Returns the number of columns for the children of the given parent """
|
||||
return self.rootItem.columnCount()
|
||||
|
||||
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||
""" Returns the data stored under the given role for the item referred to by the index """
|
||||
if index.isValid() and role == QtCore.Qt.DisplayRole:
|
||||
return index.internalPointer().data(index.column())
|
||||
elif not index.isValid():
|
||||
return self.rootItem.data(index.column())
|
||||
|
||||
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
|
||||
""" Returns the data for the given role and section in the header with the specified orientation """
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
return self.rootItem.data(section)
|
||||
|
||||
|
||||
class TreeNode(object):
|
||||
def __init__(self, data, parent=None):
|
||||
self.parentItem = parent
|
||||
self.itemData = data
|
||||
self.children = []
|
||||
|
||||
def child(self, row):
|
||||
return self.children[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self.children)
|
||||
|
||||
def childNumber(self):
|
||||
if self.parentItem is not None:
|
||||
return self.parentItem.children.index(self)
|
||||
|
||||
def columnCount(self):
|
||||
return len(self.itemData)
|
||||
|
||||
def data(self, column):
|
||||
return self.itemData[column]
|
||||
|
||||
def insertChildren(self, position, count, columns):
|
||||
if position < 0 or position > len(self.children):
|
||||
return False
|
||||
for row in range(count):
|
||||
data = [v for v in range(columns)]
|
||||
item = TreeNode(data, self)
|
||||
self.children.insert(position, item)
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def setData(self, column, value):
|
||||
if column < 0 or column >= len(self.itemData):
|
||||
return False
|
||||
self.itemData[column] = value
|
||||
|
||||
|
||||
def delete_instances():
|
||||
for obj in mayaMainWindow.children():
|
||||
if str(type(obj)) == "<class 'DCC_Materials.maya_materials_export.MayaToLumberyard'>":
|
||||
if obj.__class__.__name__ == "MayaToLumberyard":
|
||||
obj.setParent(None)
|
||||
obj.deleteLater()
|
||||
|
||||
|
||||
def show_ui():
|
||||
delete_instances()
|
||||
ui = MayaToLumberyard(mayaMainWindow)
|
||||
ui.show()
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# !/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
import pymel.core as pmc
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
def syspath():
|
||||
print 'sys.path:'
|
||||
for p in sys.path:
|
||||
print ' ' + p
|
||||
|
||||
|
||||
def info(obj):
|
||||
"""Prints information about the object."""
|
||||
|
||||
lines = ['Info for %s' % obj.name(),
|
||||
'Attributes:']
|
||||
# Get the name of all attributes
|
||||
for a in obj.listAttr():
|
||||
lines.append(' ' + a.name())
|
||||
lines.append('MEL type: %s' % obj.type())
|
||||
lines.append('MRO:')
|
||||
lines.extend([' ' + t.__name__ for t in type(obj).__mro__])
|
||||
result = '\n'.join(lines)
|
||||
print result
|
||||
|
||||
|
||||
def _is_pymel(obj):
|
||||
try: # (1)
|
||||
module = obj.__module__ # (2)
|
||||
except AttributeError: # (3)
|
||||
try:
|
||||
module = obj.__name__ # (4)
|
||||
except AttributeError:
|
||||
return None # (5)
|
||||
return module.startswith('pymel') # (6)
|
||||
|
||||
|
||||
def _py_to_helpstr(obj):
|
||||
if isinstance(obj, basestring):
|
||||
return 'search.html?q=%s' % (obj.replace(' ', '+'))
|
||||
if not _is_pymel(obj):
|
||||
return None
|
||||
if isinstance(obj, types.ModuleType):
|
||||
return ('generated/%(module)s.html#module-%(module)s' %
|
||||
dict(module=obj.__name__))
|
||||
if isinstance(obj, types.MethodType):
|
||||
return ('generated/classes/%(module)s/'
|
||||
'%(module)s.%(typename)s.html'
|
||||
'#%(module)s.%(typename)s.%(methname)s' % dict(
|
||||
module=obj.__module__,
|
||||
typename=obj.im_class.__name__,
|
||||
methname=obj.__name__))
|
||||
if isinstance(obj, types.FunctionType):
|
||||
return ('generated/functions/%(module)s/'
|
||||
'%(module)s.%(funcname)s.html'
|
||||
'#%(module)s.%(funcname)s' % dict(
|
||||
module=obj.__module__,
|
||||
funcname=obj.__name__))
|
||||
if not isinstance(obj, type):
|
||||
obj = type(obj)
|
||||
return ('generated/classes/%(module)s/'
|
||||
'%(module)s.%(typename)s.html'
|
||||
'#%(module)s.%(typename)s' % dict(
|
||||
module=obj.__module__,
|
||||
typename=obj.__name__))
|
||||
|
||||
|
||||
def test_py_to_helpstr():
|
||||
def dotest(obj, ideal):
|
||||
result = _py_to_helpstr(obj)
|
||||
assert result == ideal, '%s != %s' % (result, ideal)
|
||||
dotest('maya rocks', 'search.html?q=maya+rocks')
|
||||
dotest(pmc.nodetypes,
|
||||
'generated/pymel.core.nodetypes.html'
|
||||
'#module-pymel.core.nodetypes')
|
||||
dotest(pmc.nodetypes.Joint,
|
||||
'generated/classes/pymel.core.nodetypes/'
|
||||
'pymel.core.nodetypes.Joint.html'
|
||||
'#pymel.core.nodetypes.Joint')
|
||||
dotest(pmc.nodetypes.Joint(),
|
||||
'generated/classes/pymel.core.nodetypes/'
|
||||
'pymel.core.nodetypes.Joint.html'
|
||||
'#pymel.core.nodetypes.Joint')
|
||||
dotest(pmc.nodetypes.Joint().getTranslation,
|
||||
'generated/classes/pymel.core.nodetypes/'
|
||||
'pymel.core.nodetypes.Joint.html'
|
||||
'#pymel.core.nodetypes.Joint.getTranslation')
|
||||
dotest(pmc.joint,
|
||||
'generated/functions/pymel.core.animation/'
|
||||
'pymel.core.animation.joint.html'
|
||||
'#pymel.core.animation.joint')
|
||||
dotest(object(), None)
|
||||
dotest(10, None)
|
||||
dotest([], None)
|
||||
dotest(sys, None)
|
||||
|
||||
|
||||
def test_py_to_helpstrFAIL():
|
||||
assert 1 == 2, '1 != 2'
|
||||
|
||||
|
||||
import webbrowser # (1)
|
||||
HELP_ROOT_URL = ('http://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/PyMel/')# (2)
|
||||
|
||||
|
||||
def pmhelp(obj): # (3)
|
||||
"""Gives help for a pymel or python object.
|
||||
|
||||
If obj is not a PyMEL object, use Python's built-in
|
||||
`help` function.
|
||||
If obj is a string, open a web browser to a search in the
|
||||
PyMEL help for the string.
|
||||
Otherwise, open a web browser to the page for the object.
|
||||
"""
|
||||
tail = _py_to_helpstr(obj)
|
||||
if tail is None:
|
||||
help(obj) # (4)
|
||||
else:
|
||||
webbrowser.open(HELP_ROOT_URL + tail) # (5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_py_to_helpstr()
|
||||
print 'Tests ran successfully.'
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"description": "",
|
||||
"materialType": "Materials/Types/StandardPBR.materialtype",
|
||||
"parentMaterial": "",
|
||||
"propertyLayoutVersion": 3,
|
||||
"properties": {
|
||||
"ambientOcclusion": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"baseColor": {
|
||||
"color": [ 1.0, 1.0, 1.0 ],
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"emissive": {
|
||||
"color": [ 1.0, 1.0, 1.0 ],
|
||||
"intensity": 1.0,
|
||||
"useTexture": false,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif"
|
||||
},
|
||||
"metallic": {
|
||||
"factor": 0.0,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"roughness": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif"
|
||||
},
|
||||
"specularF0": {
|
||||
"factor": 0.5,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
},
|
||||
"normal": {
|
||||
"factor": 1.0,
|
||||
"useTexture": true,
|
||||
"textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif"
|
||||
},
|
||||
"opacity": {
|
||||
"doubleSided": false,
|
||||
"factor": 1.0,
|
||||
"cutoutAlpha": false,
|
||||
"cutoutThreshold": 0.5,
|
||||
"useBaseColorTextureAlpha": false,
|
||||
"useTexture": false,
|
||||
"textureMap": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -- This line is 75 characters -------------------------------------------
|
||||
"""
|
||||
Module Documentation:
|
||||
DccScriptingInterface:: SDK//maya//scripts//set_shelf.py
|
||||
|
||||
This module manages a custom shelf in maya for the DCCsi
|
||||
Reference: https://gist.github.com/vshotarov/1c3176fe9e38dcaadd1e56c2f15c95d9
|
||||
"""
|
||||
# -------------------------------------------------------------------------
|
||||
# -- Standard Python modules
|
||||
# none
|
||||
|
||||
# -- External Python modules
|
||||
# none
|
||||
|
||||
# -- DCCsi Extension Modules
|
||||
# none
|
||||
|
||||
# -- Maya Extension Modules
|
||||
import maya.cmds as mc
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _null(*args):
|
||||
pass
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
class customShelf(_Custom_Shelf):
|
||||
'''This is an example shelf.'''
|
||||
|
||||
def build(self):
|
||||
self.add_button(label="button1")
|
||||
self.add_button("button2")
|
||||
self.add_button("popup")
|
||||
p = mc.popupMenu(b=1)
|
||||
self.add_menu_item(p, "popupMenuItem1")
|
||||
self.add_menu_item(p, "popupMenuItem2")
|
||||
sub = self.add_submenu(p, "subMenuLevel1")
|
||||
self.add_menu_item(sub, "subMenuLevel1Item1")
|
||||
sub2 = self.add_submenu(sub, "subMenuLevel2")
|
||||
self.add_menu_item(sub2, "subMenuLevel2Item1")
|
||||
self.add_menu_item(sub2, "subMenuLevel2Item2")
|
||||
self.add_menu_item(sub, "subMenuLevel1Item2")
|
||||
self.add_menu_item(p, "popupMenuItem3")
|
||||
self.add_button("button3")
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
class _Custom_Shelf():
|
||||
'''A simple class to build custom shelves in maya.
|
||||
The build method is empty and an inheriting class should override'''
|
||||
|
||||
def __init__(self, name="DCCsi", icon_path=""):
|
||||
self._name = name
|
||||
|
||||
self._icon_path = icon_path
|
||||
|
||||
self._label_background_color = (0, 0, 0, 0)
|
||||
self._label_colour = (.9, .9, .9)
|
||||
|
||||
self._clean_old_shlef()
|
||||
|
||||
mc.setParent(self._name)
|
||||
|
||||
self.build()
|
||||
|
||||
def build(self):
|
||||
'''Override this method in custom class.
|
||||
Otherwise, nothing is added to the shelf.'''
|
||||
pass
|
||||
|
||||
def add_button(self,
|
||||
label='<NotSet>',
|
||||
icon="commandButton.png",
|
||||
command=_null,
|
||||
doubleCommand=_null):
|
||||
'''Adds a shelf button with the specified label,
|
||||
command, double click command and image.'''
|
||||
mc.setParent(self._name)
|
||||
if icon:
|
||||
icon = self._icon_path + icon
|
||||
mc.shelfButton(width=37, height=37,
|
||||
image=icon,
|
||||
label=label,
|
||||
command=command,
|
||||
doubleClickCommand=doubleCommand,
|
||||
imageOverlayLabel=label,
|
||||
overlayLabelBackColor=self._label_background_color,
|
||||
overlayLabelColor=self._label_colour)
|
||||
|
||||
def add_menu_item(self, parent, label, command=_null, icon=""):
|
||||
'''Adds a shelf button with the specified label,
|
||||
command, double click command and image.'''
|
||||
if icon:
|
||||
icon = self._icon_path + icon
|
||||
return mc.menuItem(p=parent, l=label, c=command, i="")
|
||||
|
||||
def add_submenu(self, parent, label, icon=None):
|
||||
'''Adds a sub menu item with the specified label and icon
|
||||
to the specified parent popup menu.'''
|
||||
if icon:
|
||||
icon = self._icon_path + icon
|
||||
return mc.menuItem(p=parent, l=label, i=icon, subMenu=1)
|
||||
|
||||
def _clean_old_shlef(self):
|
||||
'''Checks if the shelf exists and empties it if it does or
|
||||
creates it if it does not.'''
|
||||
if mc.shelfLayout(self._name, ex=1):
|
||||
if mc.shelfLayout(self._name, q=1, ca=1):
|
||||
for each in mc.shelfLayout(self._name, q=1, ca=1):
|
||||
mc.deleteUI(each)
|
||||
else:
|
||||
mc.shelfLayout(self._name, p="ShelfLayout")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Module Tests
|
||||
# ==========================================================================
|
||||
if __name__ == '__main__':
|
||||
customShelf()
|
||||
pass
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
# -- This line is 75 characters -------------------------------------------
|
||||
|
||||
|
||||
|
||||
+3
-4
@@ -46,6 +46,9 @@ import logging as _logging
|
||||
import azpy
|
||||
from azpy.constants import *
|
||||
from azpy.env_base import _BASE_ENVVAR_DICT
|
||||
from azpy.env_bool import env_bool
|
||||
from azpy.constants import ENVAR_DCCSI_GDEBUG
|
||||
from azpy.constants import ENVAR_DCCSI_DEV_MODE
|
||||
|
||||
# -- maya imports
|
||||
import maya.cmds as cmds
|
||||
@@ -55,10 +58,6 @@ import maya.mel as mel
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
from azpy import env_bool
|
||||
from azpy.constants import ENVAR_DCCSI_GDEBUG
|
||||
from azpy.constants import ENVAR_DCCSI_DEV_MODE
|
||||
|
||||
# global space
|
||||
_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False)
|
||||
_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False)
|
||||
|
||||
Reference in New Issue
Block a user