3
0

LegacyComponentConverter.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Lumberyard Legacy Renderer to Atom Component Conversion Script
  6. What does this script do?
  7. ================================================
  8. This script walks through all the .slice, .layer, .ly, and .cry files in a
  9. project and attempts to convert the following components into something
  10. reasonably similar that renders in Atom:
  11. Mesh
  12. Material
  13. PointLight
  14. Actor
  15. Do materials get carried over?
  16. ================================================
  17. For the mesh component, this script will attempt to create an atom mesh component
  18. that uses the same material, pre-supposing that you have already run the
  19. LegacyAssetConverter script to generate Atom .material files out of legacy .mtl files.
  20. (Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\Maya\Scripts\Python\legacy_asset_converter\main.py)
  21. For meshes that only have one sub-mesh, this is straightforward as the mesh will only
  22. have one material to apply, and this script will look for a material with the same
  23. name but a .material extension.
  24. For mult-materials, this is a little tricky since Atom does not follow the same
  25. ordered sub-material convention used by the legacy renderer. However, legacy .mtl
  26. files that were generated when adding a .fbx to a project use a naming convention
  27. that can be used by this script to match with the default materials that come from Atom.
  28. So as long as you were using the initial .mtl generated by Lumberyard and have not
  29. re-named the submaterials, it should find a match. This applies to both the
  30. ActorComponent and the MeshComponent
  31. What does this script needs?
  32. ================================================
  33. This script will parse the assetcatalog.xml from the cache folder to get asset ids and path.
  34. This file is in binary by default, you need to rebuild the engine with a modification to make it useable by this script.
  35. - In lumberyard, modify dev\Code\Tools\AssetProcessor\native\AssetManager\AssetCatalog.cpp line 343 to use AZ::ObjectStream::ST_XML instead of ST_BINARY
  36. - In o3de (if you use assetCatalogOverridePath), modify Code\Framework\AzFramework\AzFramework\Asset\AssetCatalog.cpp
  37. and Code\Tools\AssetProcessor\native\AssetManager\AssetCatalog.cpp in the same fashion
  38. Note that after the modification and rebuild, you will need to delete the cache folder and launch the engine for the catalog to be rebuilt
  39. How do I run this script from a command line?
  40. ================================================
  41. 1) Check out any .slice, .layer, .ly, and .cry files you want to convert from source control
  42. - This script will remove the legacy components entirely, so make sure you have your files
  43. backed up before you run this script in case you want to run it again
  44. 2) From the Lumberyard root folder, run LegacyComponentConverter.py -project=<ProjectName> -include_gems -assetCatalogOverridePath=<ExternalProjectPath\Cache\pc\assetcatalog.xml>
  45. - -include_gems is optional. If you include Gems, it will run all all Gems, not just the ones enabled by your project
  46. - -assetCatalogOverridePath is optional. It will replace mesh/material asset id from the source project to this target project
  47. based on relative asset path from the project root (in general, Objects/Models/yourasset.fbx)
  48. What are the artifacts of this script?
  49. ================================================
  50. The .slice, .layer, .ly, and .cry files will be converted in-place. No new files will be created
  51. Is this script destructive?
  52. ================================================
  53. Yes! This is a one-way conversion that will clear the old data once converted. You should back up
  54. your files before running this conversion in case you want to modify the script and re-run it on
  55. your original level.
  56. """
  57. CONVERTED_LOG_NAME = "ComponentConversion_ConvertedLegacyFiles.log"
  58. UNCONVERTED_LOG_NAME = "ComponentConversion_UnsupportedLegacyFiles.log"
  59. STATS_LOG_NAME = "ComponentConversion_LegacyComponentStats.log"
  60. # Normal imports
  61. import sys
  62. import xml.etree.ElementTree as ET
  63. import time
  64. from zipfile import ZipFile
  65. import tempfile
  66. import subprocess
  67. # Local python files
  68. from LegacyConversionHelpers import *
  69. from LegacyMeshComponentConverter import *
  70. from LegacyMaterialComponentConverter import *
  71. from LegacyActorComponentConverter import *
  72. from LegacyPointLightComponentConverter import *
  73. from LegacyTransformComponentConverter import *
  74. BUILD_PATH = "./" # Use current working directory, we expect to be in lumberyard dev folder
  75. GEMS_PATH = os.path.join(BUILD_PATH, "Gems")
  76. class Component_File(object):
  77. """
  78. Class to perform any read, write or conversion operations on material (*.mtl) files.
  79. """
  80. def __init__(self, filename, projectDir, assetCatalogHelper, statsCollector):
  81. self.filename = filename
  82. self.normalizedProjectDir = os.path.normpath(projectDir)
  83. self.needsConversion = False
  84. self.hadException = False
  85. self.assetCatalogHelper = assetCatalogHelper
  86. self.materialComponentConverter = Material_Component_Converter(assetCatalogHelper)#TODO - I'm pretty sure this is dead code
  87. self.xml = None
  88. self.statsCollector = statsCollector
  89. self.parse_xml()
  90. def is_valid_xml(self):
  91. """
  92. Performs a simple check to determine if the XML of the mtl is valid.
  93. This is to prevent an assert on a material conversion operation and
  94. preventing the script from finishing.
  95. It's possible for a material's xml to be malformed, which is why this check is needed.
  96. """
  97. return True
  98. try:
  99. if isinstance(self.xml.getroot(), xml.etree.ElementTree.Element):
  100. return True
  101. else:
  102. return False
  103. except:
  104. return False
  105. def parse_xml(self):
  106. """
  107. Open and parse the file's xml, storing it for access later.
  108. For .ly and .cry files, it will get the xml out of the .zip
  109. """
  110. if self.filename.endswith(".cry") or self.filename.endswith(".ly"):
  111. zipRead = ZipFile(self.filename, 'r')
  112. contents = zipRead.read("levelentities.editor_xml")
  113. zipRead.close()
  114. # write the contents to a temporary file so we can parse it with ElementTree
  115. tmpFile = tempfile.NamedTemporaryFile(delete=False)
  116. tmpFile.write(contents)
  117. tmpFile.close()
  118. self.xml = xml.etree.ElementTree.parse(tmpFile.name)
  119. self.gather_elements()
  120. os.unlink(tmpFile.name)
  121. os.path.exists(tmpFile.name)
  122. elif os.path.exists(self.filename):
  123. #try:
  124. # TODO try-except is supposed to make it so one bad xml doesn't crash the lot
  125. # need to clean up stuff so the logging/conversion later doesn't crash
  126. # for now, better to crash here so we see where the exception is being thrown
  127. self.xml = xml.etree.ElementTree.parse(self.filename)
  128. self.gather_elements()
  129. #except OSError as err:
  130. # print("OS error: {0}".format(err))
  131. # self.xml = None
  132. # self.needsConversion = False
  133. # self.hadException = True
  134. #except ValueError:
  135. # print("Could not convert data to an integer.")
  136. # self.xml = None
  137. # self.needsConversion = False
  138. # self.hadException = True
  139. #except:
  140. # print("Unexpected error:", sys.exc_info()[0])
  141. # self.xml = None
  142. # self.needsConversion = False
  143. # self.hadException = True
  144. def gather_elements(self):
  145. """
  146. Once the xml has been parsed, mine through it to find all of the
  147. neccessary elements that need to be modified.
  148. """
  149. print("starting to parse {0}".format(self.filename))
  150. componentConverters = []
  151. componentConverters.append(Mesh_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
  152. componentConverters.append(Actor_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
  153. componentConverters.append(Point_Light_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
  154. componentConverters.append(Transform_Component_Converter(self.assetCatalogHelper, self.statsCollector, self.normalizedProjectDir))
  155. if self.is_valid_xml():
  156. root = self.xml.getroot()
  157. if root.tag == "ObjectStream" or True:
  158. # First, get a dictionary of child->parent mapping for later use inserting sibling elements
  159. self.parent_map = {c:p for p in root.iter('Class') for c in p}
  160. # Now go through and look for mesh components
  161. for child in root.iter('Class'):
  162. # If we run into one of the components we just added, skip it. It doesn't need to be converted,
  163. # and it doesn't exist in the pre-built parent_map so it would throw an exception if we tried to access it
  164. if child in self.parent_map:
  165. parent = self.parent_map[child]
  166. for componentConverter in componentConverters:
  167. componentConverter.reset()
  168. if componentConverter.is_this_the_component_im_looking_for(child, parent):
  169. self.needsConversion = True
  170. componentConverter.gather_info_for_conversion(child, parent)
  171. # 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
  172. # Seems to be okay since we only change or add elements, never remove entirely
  173. componentConverter.convert(child, parent)
  174. self.xml._setroot(root)
  175. # pretty print
  176. ET.indent(self.xml, space='\t')
  177. print("finished parsing {0}".format(self.filename))
  178. def can_be_converted(self):
  179. """
  180. Determines if this material file can be converted by checking if
  181. it is using the Illum Shader
  182. """
  183. return self.needsConversion
  184. def can_write(self):
  185. """
  186. Checks to make sure the mtl file is writable.
  187. This is to prevent the script from asserting during a
  188. save attempt and preventing the script from finishing.
  189. """
  190. fullFilePath = self.get_atom_file_path()
  191. if os.path.exists(fullFilePath):
  192. if os.access(fullFilePath, os.W_OK):
  193. return True
  194. else:
  195. with open(fullFilePath,"a+") as f:
  196. f.close()
  197. return True
  198. return False
  199. def get_atom_file_path(self):
  200. # This is just a way to optionally create a new file for comparing with the original
  201. # TODO: control this via command line
  202. atomFileName = self.filename
  203. return atomFileName#.replace('.slice', '_atom.slice')
  204. def convert(self):
  205. """
  206. Creates the new level/slice file
  207. """
  208. # TODO - will not work if .slice is part of the path instead of the extension
  209. if self.needsConversion:
  210. if self.filename.endswith(".cry") or self.filename.endswith(".ly"):
  211. # We can't just update the .cry file, we need to rebuild all the contents
  212. #Make a temporary file
  213. tmpFile, tmpFileName = tempfile.mkstemp(dir=os.path.dirname(self.filename))
  214. os.close(tmpFile)
  215. #Create a temporary copy of the .cry file
  216. with ZipFile(self.filename, 'r') as zin:
  217. with ZipFile(tmpFileName, 'w') as zout:
  218. #Loop through the file list and write out every file but level.editor_xml with no modifications
  219. #when we hit the level data we want to edit, write it out with the new contents
  220. for item in zin.infolist():
  221. if item.filename == "levelentities.editor_xml":
  222. xmlString = xml.etree.ElementTree.tostring(self.xml.getroot())
  223. zout.writestr(item, xmlString)
  224. else:
  225. zout.writestr(item, zin.read(item.filename))
  226. #Remove old cry file and rename the temp file
  227. os.remove(self.filename)
  228. os.rename(tmpFileName, self.filename)
  229. else:
  230. self.xml.write(self.get_atom_file_path())
  231. return False
  232. def getUpdatedStatsCollector(self):
  233. """
  234. Returns the stats collector that was passed in intially, with any modifications that were made
  235. """
  236. return self.statsCollector
  237. ###############################################################################
  238. def main():
  239. '''sys.__name__ wrapper function'''
  240. msgStr = "This tool will scan all of your lumberyard project's level/layer/slice files\n\
  241. convert any compatible legacy components into the equivalent Atom components\n\
  242. This script will overwrite the original files, and will remove the legacy components\n\
  243. upon conversion, decimating the previous contents of those components.\n"
  244. commandLineOptions = Common_Command_Line_Options(sys.argv)
  245. if commandLineOptions.isHelp:
  246. print (commandLineOptions.helpString)
  247. return
  248. start_time = time.time()
  249. total_converted = 0
  250. extensionList = [".slice", ".layer", ".ly", ".cry"]
  251. fileList = get_file_list(commandLineOptions.projectName, commandLineOptions.includeGems, extensionList, BUILD_PATH, GEMS_PATH)
  252. if commandLineOptions.assetCatalogOverridePath:
  253. assetCatalogPath = commandLineOptions.assetCatalogOverridePath
  254. assetCatalogDictionaries = get_asset_catalog_dictionaries(assetCatalogPath)
  255. else:
  256. assetCatalogPath = os.path.join("Cache", commandLineOptions.projectName, get_default_asset_platform(), commandLineOptions.projectName, "assetcatalog.xml")
  257. assetCatalogDictionaries = get_asset_catalog_dictionaries(assetCatalogPath)
  258. # Create a log file to store converted component file filenames
  259. # and to check to see if the component file has already been converted.
  260. convertedLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME))
  261. # Create a log file to store component file filenames that need conversion
  262. # but cannot becuase they are read only.
  263. unconvertedLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, UNCONVERTED_LOG_NAME), include_previous = False)
  264. statsLogFile = Log_File(filename="{0}\\{1}".format(BUILD_PATH, STATS_LOG_NAME), include_previous = False)
  265. statsCollector = Stats_Collector()
  266. # Go through each component file to perform the conversion on it
  267. print("==============================")
  268. componentFileIndex = -1
  269. for componentFileInfo in fileList:
  270. componentFileIndex += 1
  271. componentFileName = componentFileInfo.filename
  272. copmonentFileProjectDir = componentFileInfo.normalizedProjectDir
  273. print(componentFileName)
  274. #if convertedLogFile.has_line(componentFileName.lstrip(BUILD_PATH)): # Use this to only convert files that haven't already been converted
  275. # print("--> Previously converted, not doing")
  276. # continue
  277. if commandLineOptions.endsWithStr == "" or componentFileName.lower().endswith(commandLineOptions.endsWithStr.lower()):
  278. componentFile = Component_File(componentFileName, copmonentFileProjectDir, assetCatalogDictionaries, statsCollector)
  279. if componentFile.can_be_converted():
  280. if commandLineOptions.useP4:
  281. subprocess.check_call(['p4', 'edit', componentFileName])
  282. if componentFile.can_write():
  283. componentFile.convert()
  284. convertedLogFile.add_line_no_duplicates(componentFile.get_atom_file_path())
  285. print("--> Converted")
  286. total_converted += 1
  287. else:
  288. unconvertedLogFile.add_line_no_duplicates("{0} - cannot access file (read-only)".format(componentFile.get_atom_file_path()))
  289. print("--> Could not write to destination component file (read-only). Not converted.")
  290. else:
  291. print("--> did not need conversion.")
  292. statsCollector = componentFile.getUpdatedStatsCollector()
  293. print("\n")
  294. # Fill out the stats log
  295. statsLogFile.add_line("Mesh/Actor Components without a material overrride: {0}".format(statsCollector.noMaterialOverrideCount))
  296. statsLogFile.add_line("Mesh/Actor Components with a material overrride: {0}".format(statsCollector.materialOverrideCount))
  297. statsLogFile.add_line("Total Mesh/Actor Components: {0}".format(statsCollector.noMaterialOverrideCount + statsCollector.materialOverrideCount))
  298. # Finally, save the log files to disk
  299. convertedLogFile.save()
  300. unconvertedLogFile.save()
  301. statsLogFile.save()
  302. total_time = time.time() - start_time
  303. log_str = "You can view a list of converted component files in this log file:\n{0}\\{1}".format(BUILD_PATH, CONVERTED_LOG_NAME)
  304. 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)
  305. 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)
  306. print("==============================\n")
  307. print("Conversion completed in {0} seconds.\n".format(total_time))
  308. print("Converted {0} component file(s)".format(total_converted))
  309. # Inform the user about the log files
  310. print("{0}".format(log_str))
  311. print("{0}".format(unconverted_log_str))
  312. print("{0}".format(stats_log_str))
  313. if __name__ == '__main__':
  314. # GLOBAL NOTE:
  315. # - All python scripts should execute through a main() function.
  316. main()