You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/AtomLyIntegration/CommonFeatures/Assets/Editor/Scripts/LegacyContentConversion/LegacyComponentConverter.py

362 lines
16 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
Lumberyard Legacy Renderer to Atom Component Conversion Script
What does this script do?
================================================
This script walks through all the .slice, .layer, .ly, and .cry files in a
project and attempts to convert the following components into something
reasonably similar that renders in Atom:
Mesh
Actor
Do materials get carried over?
================================================
For the mesh component, this script will attempt to create an atom mesh component
that uses the same material, pre-supposing that you have already run the
LegacyMaterialConverter.py script sto generate Atom .material files out of legacy .mtl files
For meshes that only have one sub-mesh, this is straightforward as the mesh will only
have one material to apply, and this script will look for a material with the same
name but a .material extension.
For mult-materials, this is a little tricky since Atom does not follow the same
ordered sub-material convention used by the legacy renderer. However, legacy .mtl
files that were generated when adding a .fbx to a project use a naming convention
that can be used by this script to match with the default materials that come from Atom.
So as long as you were using the initial .mtl generated by Lumberyard and have not
re-named the submaterials, it should find a match. This applies to both the
ActorComponent and the MeshComponent
How do I run this script from a command line?
================================================
1) Check out any .slice, .layer, .ly, and .cry files you want to convert from source control
- This script will remove the legacy components entirely, so make sure you have your files
backed up before you run this script in case you want to run it again
<<<<<<< HEAD
2) From the dev folder, run LegacyComponentConverter.py project=<ProjectName> --include_gems
=======
2) From the Lumberyard root folder, run LegacyComponentConverter.py project=<ProjectName> --include_gems
>>>>>>> main
- --include_gems is optional
- if you include Gems, it will run all all Gems, not just the ones enabled by your project
What are the artifacts of this script?
================================================
The .slice, .layer, .ly, and .cry files will be converted in-place. No new files will be created
Is this script destructive?
================================================
Yes! This is a one-way conversion that will clear the old data once converted. You should back up
your files before running this conversion in case you want to modify the script and re-run it on
your original level.
"""
CONVERTED_LOG_NAME = "ComponentConversion_ConvertedLegacyFiles.log"
UNCONVERTED_LOG_NAME = "ComponentConversion_UnsupportedLegacyFiles.log"
STATS_LOG_NAME = "ComponentConversion_LegacyComponentStats.log"
BUILD_PATH = None
GEMS_PATH = None
# Normal imports
import sys
import xml.etree.ElementTree
import time
from zipfile import ZipFile
import tempfile
import subprocess
# Local python files
from LegacyConversionHelpers import *
from LegacyMeshComponentConverter import *
from LegacyMaterialComponentConverter import *
from LegacyActorComponentConverter import *
from LegacyPointLightComponentConverter import *
BUILD_PATH = "./"
GEMS_PATH = os.path.join(BUILD_PATH, "Gems")
class Component_File(object):
"""
Class to perform any read, write or conversion operations on material (*.mtl) files.
"""
def __init__(self, filename, projectDir, assetCatalogHelper, statsCollector):
self.filename = filename
self.normalizedProjectDir = os.path.normpath(projectDir)
self.needsConversion = False
self.hadException = False
self.assetCatalogHelper = assetCatalogHelper
self.materialComponentConverter = Material_Component_Converter(assetCatalogHelper)#TODO - I'm pretty sure this is dead code
self.xml = None
self.statsCollector = statsCollector
self.parse_xml()
def is_valid_xml(self):
"""
Performs a simple check to determine if the XML of the mtl is valid.
This is to prevent an assert on a material conversion operation and
preventing the script from finishing.
It's possible for a material's xml to be malformed, which is why this check is needed.
"""
return True
try:
if isinstance(self.xml.getroot(), xml.etree.ElementTree.Element):
return True
else:
return False
except:
return False
def parse_xml(self):
"""
Open and parse the file's xml, storing it for access later.
For .ly and .cry files, it will get the xml out of the .zip
"""
if self.filename.endswith(".cry") or self.filename.endswith(".ly"):
zipRead = ZipFile(self.filename, 'r')
contents = zipRead.read("levelentities.editor_xml")
zipRead.close()
# write the contents to a temporary file so we can parse it with ElementTree
tmpFile = tempfile.NamedTemporaryFile(delete=False)
tmpFile.write(contents)
tmpFile.close()
self.xml = xml.etree.ElementTree.parse(tmpFile.name)
self.gather_elements()
os.unlink(tmpFile.name)
os.path.exists(tmpFile.name)
elif os.path.exists(self.filename):
#try:
# TODO try-except is supposed to make it so one bad xml doesn't crash the lot
# need to clean up stuff so the logging/conversion later doesn't crash
# for now, better to crash here so we see where the exception is being thrown
self.xml = xml.etree.ElementTree.parse(self.filename)
self.gather_elements()
#except OSError as err:
# print("OS error: {0}".format(err))
# self.xml = None
# self.needsConversion = False
# self.hadException = True
#except ValueError:
# print("Could not convert data to an integer.")
# self.xml = None
# self.needsConversion = False
# self.hadException = True
#except:
# print("Unexpected error:", sys.exc_info()[0])
# self.xml = None
# self.needsConversion = False
# self.hadException = True
def gather_elements(self):
"""
Once the xml has been parsed, mine through it to find all of the
neccessary elements that need to be modified.
"""
print("starting to parse {0}".format(self.filename))
componentConverters = []
componentConverters.append(Mesh_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
componentConverters.append(Actor_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
componentConverters.append(Point_Light_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
if self.is_valid_xml():
root = self.xml.getroot()
if root.tag == "ObjectStream" or True:
# First, get a dictionary of child->parent mapping for later use inserting sibling elements
self.parent_map = {c:p for p in root.iter('Class') for c in p}
# Now go through and look for mesh components
for child in root.iter('Class'):
# If we run into one of the components we just added, skip it. It doesn't need to be converted,
# and it doesn't exist in the pre-built parent_map so it would throw an exception if we tried to access it
if child in self.parent_map:
parent = self.parent_map[child]
for componentConverter in componentConverters:
componentConverter.reset()
if componentConverter.is_this_the_component_im_looking_for(child, parent):
self.needsConversion = True
componentConverter.gather_info_for_conversion(child, parent)
# TODO - we're about to change the tree structure while iterating, which is apparently undefined but appears to work. Might be better to just build up a list of things that need to be modified, then do a second pass to replace the legacy component
# Seems to be okay since we only change or add elements, never remove entirely
componentConverter.convert(child, parent)
self.xml._setroot(root)
print("finished parsing {0}".format(self.filename))
def can_be_converted(self):
"""
Determines if this material file can be converted by checking if
it is using the Illum Shader
"""
return self.needsConversion
def can_write(self):
"""
Checks to make sure the mtl file is writable.
This is to prevent the script from asserting during a
save attempt and preventing the script from finishing.
"""
fullFilePath = self.get_atom_file_path()
if os.path.exists(fullFilePath):
if os.access(fullFilePath, os.W_OK):
return True
else:
with open(fullFilePath,"a+") as f:
f.close()
return True
return False
def get_atom_file_path(self):
# This is just a way to optionally create a new file for comparing with the original
# TODO: control this via command line
atomFileName = self.filename
return atomFileName#.replace('.slice', '_atom.slice')
def convert(self):
"""
Creates the new level/slice file
"""
# TODO - will not work if .slice is part of the path instead of the extension
if self.needsConversion:
if self.filename.endswith(".cry") or self.filename.endswith(".ly"):
# We can't just update the .cry file, we need to rebuild all the contents
#Make a temporary file
tmpFile, tmpFileName = tempfile.mkstemp(dir=os.path.dirname(self.filename))
os.close(tmpFile)
#Create a temporary copy of the .cry file
with ZipFile(self.filename, 'r') as zin:
with ZipFile(tmpFileName, 'w') as zout:
#Loop through the file list and write out every file but level.editor_xml with no modifications
#when we hit the level data we want to edit, write it out with the new contents
for item in zin.infolist():
if item.filename == "levelentities.editor_xml":
xmlString = xml.etree.ElementTree.tostring(self.xml.getroot())
zout.writestr(item, xmlString)
else:
zout.writestr(item, zin.read(item.filename))
#Remove old cry file and rename the temp file
os.remove(self.filename)
os.rename(tmpFileName, self.filename)
else:
self.xml.write(self.get_atom_file_path())
return False
def getUpdatedStatsCollector(self):
"""
Returns the stats collector that was passed in intially, with any modifications that were made
"""
return self.statsCollector
###############################################################################
def main():
'''sys.__name__ wrapper function'''
msgStr = "This tool will scan all of your project's level/layer/slice files\n\
convert any compatible legacy components into the equivalent Atom components\n\
This script will overwrite the original files, and will remove the legacy components\n\
upon conversion, decimating the previous contents of those components.\n"
commandLineOptions = Common_Command_Line_Options(sys.argv[0], sys.argv[1])
if commandLineOptions.isHelp:
print (commandLineOptions.helpString)
return
start_time = time.time()
total_converted = 0
extensionList = [".slice", ".layer", ".ly", ".cry"]
fileList = get_file_list(commandLineOptions.projectName, commandLineOptions.includeGems, extensionList, BUILD_PATH, GEMS_PATH)
assetCatalogDictionaries = get_asset_catalog_dictionaries(BUILD_PATH, commandLineOptions.projectName)
# Create a log file to store converted component file filenames
# and to check to see if the component file has already been converted.
convertedLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME))
# Create a log file to store component file filenames that need conversion
# but cannot becuase they are read only.
unconvertedLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, UNCONVERTED_LOG_NAME), include_previous = False)
statsLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, STATS_LOG_NAME), include_previous = False)
statsCollector = Stats_Collector()
# Go through each component file to perform the conversion on it
print("==============================")
componentFileIndex = -1
for componentFileInfo in fileList:
componentFileIndex += 1
componentFileName = componentFileInfo.filename
copmonentFileProjectDir = componentFileInfo.normalizedProjectDir
print(componentFileName)
#if convertedLogFile.has_line(componentFileName.lstrip(BUILD_PATH)): # Use this to only convert files that haven't already been converted
# print("--> Previously converted, not doing")
# continue
if commandLineOptions.endsWithStr == "" or componentFileName.lower().endswith(commandLineOptions.endsWithStr.lower()):
componentFile = Component_File(componentFileName, copmonentFileProjectDir, assetCatalogDictionaries, statsCollector)
if componentFile.can_be_converted():
if commandLineOptions.useP4:
subprocess.check_call(['p4', 'edit', componentFileName])
if componentFile.can_write():
componentFile.convert()
convertedLogFile.add_line_no_duplicates(componentFile.get_atom_file_path())
print("--> Converted")
total_converted += 1
else:
unconvertedLogFile.add_line_no_duplicates("{0} - cannot access file (read-only)".format(componentFile.get_atom_file_path()))
print("--> Could not write to destination component file (read-only). Not converted.")
else:
print("--> did not need conversion.")
statsCollector = componentFile.getUpdatedStatsCollector()
print("\n")
# Fill out the stats log
statsLogFile.add_line("Mesh/Actor Components without a material overrride: {0}".format(statsCollector.noMaterialOverrideCount))
statsLogFile.add_line("Mesh/Actor Components with a material overrride: {0}".format(statsCollector.materialOverrideCount))
statsLogFile.add_line("Total Mesh/Actor Components: {0}".format(statsCollector.noMaterialOverrideCount + statsCollector.materialOverrideCount))
# Finally, save the log files to disk
convertedLogFile.save()
unconvertedLogFile.save()
statsLogFile.save()
total_time = time.time() - start_time
log_str = "You can view a list of converted component files in this log file:\n{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME)
unconverted_log_str = "You can view a list of component files that were not converted in this log file:\n{0}\\{1}".format(BUILD_PATH, UNCONVERTED_LOG_NAME)
stats_log_str = "You can view a list of component files stats, such as feature and shader usage, in this log file:\n{0}\\{1}".format(BUILD_PATH, STATS_LOG_NAME)
print("==============================\n")
print("Conversion completed in {0} seconds.\n".format(total_time))
print("Converted {0} component file(s)".format(total_converted))
# Inform the user about the log files
print("{0}".format(log_str))
print("{0}".format(unconverted_log_str))
print("{0}".format(stats_log_str))
if __name__ == '__main__':
# GLOBAL NOTE:
# - All python scripts should execute through a main() function.
main()