Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
@@ -0,0 +1,88 @@
# 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 collections
import json
def get_shader_information():
"""
Queries all materials and corresponding material attributes and file textures in the Blender scene.
:return:
"""
# TODO - link file texture location to PBR material plugs- finding it difficult to track down how this is achieved
# in the Blender Python API documentation and/or in forums
materials_count = 1
shader_types = get_blender_shader_types()
materials_dictionary = {}
for target_mesh in [o for o in bpy.data.objects if type(o.data) is bpy.types.Mesh]:
material_information = collections.OrderedDict(DccApplication='Blender', AppliedMesh=target_mesh,
SceneName=bpy.data.filepath, MaterialAttributes={},
FileConnections={})
for target_material in target_mesh.data.materials:
material_information['MaterialName'] = target_material.name
shader_attributes = {}
shader_file_connections = {}
for node in target_material.node_tree.nodes:
socket = node.inputs[0]
print('NODE: {}'.format(node))
print('Socket: {}'.format(socket))
for material_input in node.inputs:
attribute_name = material_input.name
try:
attribute_value = material_input.default_value
print('Name: [{}] [{}] ValueType ::::::> {}'.format(attribute_name, attribute_value,
type(attribute_value)))
material_information['MaterialAttributes'].update({attribute_name: str(attribute_value)})
except Exception as e:
pass
print('\n')
if node.type == 'TEX_IMAGE':
material_information['FileConnections'].update({str(node): str(node.image.filepath)})
if node.name in shader_types.keys():
material_information['MaterialType'] = shader_types[node.name]
# material_information['MaterialAttributes'] = shader_attributes
materials_dictionary['Material_{}'.format(materials_count)] = material_information
materials_count += 1
print('_________________________________________________________________\n')
return materials_dictionary
def get_blender_shader_types():
"""
This returns all the material types present in the Blender scene
:return:
"""
shader_types = {}
ddir = lambda data, filter_str: [i for i in dir(data) if i.startswith(filter_str)]
get_nodes = lambda cat: [i for i in getattr(bpy.types, cat).category.items(None)]
cycles_categories = ddir(bpy.types, "NODE_MT_category_SH_NEW")
for cat in cycles_categories:
if cat == 'NODE_MT_category_SH_NEW_SHADER':
for node in get_nodes(cat):
shader_types[node.label] = node.nodetype
return shader_types
materials_dictionary = get_shader_information()
#print('Materials Dictionary:')
#print(materials_dictionary)
#parsed = json.loads(str(materials_dictionary))
#print(json.dumps(parsed, indent=4, sort_keys=True))
@@ -0,0 +1,55 @@
# 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 -------------------------------------------
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_material_converter('standalone', output, target_files)
def is_valid_file(file_name):
"""
Allows only supported DCC application files by extensions
:param file_name: The name of the file.
:return:
"""
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,68 @@
# 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 -------------------------------------------
import logging
logging.basicConfig(level=logging.DEBUG)
def get_maya_material_mapping(name, material_type, file_connections):
"""
Helps map found material DCC attribute values/file connections with Lumberyard materials.
:param name: Material name from within Maya
:param material_type: Maya Material type to match values to (i.e. Stingray PBS, aiStandardSurface(Arnold)
:param file_connections: List of all connected texture files from Maya
:return: Key value pairs for attributes/file textures assigned as Lumberyard material values
"""
material_properties = {}
if material_type == 'StingrayPBS':
logging.debug('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':
logging.debug('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 @@
# 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 -------------------------------------------
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,21 @@
# 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 MaxPlus
import sys
def get_material_information():
for mesh_object in MaxPlus.Core.GetRootNode().Children:
print('Object---> {}'.format(mesh_object))
get_material_information()
@@ -0,0 +1,172 @@
# 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 maya.cmds as mc
import collections
import logging
import json
import sys
import os
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(level=logging.INFO,
format='%(name)s - %(levelname)s - %(message)s',
datefmt='%m-%d %H:%M',
filename='output.log',
filemode='w')
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 target_file in file_list:
self.current_scene = os.path.abspath(target_file.replace('\'', ''))
mc.file(self.current_scene, open=True, 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 = mc.listConnections(target_mesh, type='shadingEngine')
materials = mc.ls(mc.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 = mc.listConnections(material_name, type='shadingEngine')[0]
shader_name = '{}.surfaceShader'.format(connections)
shader = mc.listConnections(shader_name)[0]
return shader
def get_shader_information(self, 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 = {}
materials = self.get_materials(material_mesh)
for material in materials:
material_files = [x for x in mc.listConnections(material, plugs=1, source=1) if x.startswith('file')]
for file_name in material_files:
file_texture = mc.getAttr('{}.fileTextureName'.format(file_name.split('.')[0]))
if os.path.basename(file_texture).split('.')[-1] != 'dds':
key_name = mc.listConnections(file_name, plugs=1, source=1)[0]
shader_file_connections[key_name] = file_texture
shader_attributes = {}
for shader_attribute in mc.listAttr(shader, s=True, iu=True):
try:
shader_attributes[str(shader_attribute)] = str(mc.getAttr('{}.{}'.format(shader, shader_attribute)))
except Exception 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 = collections.OrderedDict(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
logging.info('\n\n:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n'
'MATERIAL DEFINITION: {} \n'
':::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n{}'.format(
self.materials_dictionary[material_name]['MaterialType'],
json.dumps(self.materials_dictionary[material_name], indent=4)))
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 = mc.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 = mc.nodeType(material_name)
if material_type != 'lambert':
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[str(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