Integrating up through commit 90f050496

This commit is contained in:
alexpete
2021-04-07 14:03:29 -07:00
parent 8f2ed080a9
commit c2cbd430fe
2694 changed files with 285622 additions and 176874 deletions
@@ -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']
@@ -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)
@@ -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
@@ -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()
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a78856a5dddee1466183f56dad21148dcf2f093fb316f4aa3af6842912fff61
size 8087
@@ -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_())
@@ -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_())
@@ -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))
@@ -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)
@@ -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
@@ -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": ""
}
}
}
@@ -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.
#
@@ -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
@@ -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)
@@ -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()
@@ -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
}
}
}
@@ -0,0 +1,3 @@
.temp
materialsdb.dat
materialsdb.dir
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'
]
@@ -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()
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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)
@@ -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
}
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d42de1023180dc9ff4f9db8c9718c0d476ecf1fe7be99e53b26ee7939ea55174
size 133641
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08fa77d80e790d2580f4a6ae6b204357a091c4e675e038e4ef9e1c0bebb5b6f6
size 11285
@@ -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()
@@ -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
@@ -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";
@@ -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)
@@ -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()
@@ -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.'
@@ -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": ""
}
}
}
@@ -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
@@ -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 -------------------------------------------
@@ -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)