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/Blast/Editor/Scripts/asset_builder_blast.py

323 lines
13 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
"""
def install_user_site():
import os
import sys
import azlmbr.paths
executableBinFolder = azlmbr.paths.executableFolder
# the PyAssImp module checks the Windows PATH for the assimp DLL file
if os.name == "nt":
os.environ['PATH'] = os.environ['PATH'] + os.pathsep + executableBinFolder
# PyAssImp module needs to find the shared library for assimp to load; "posix" handles Mac and Linux
if os.name == "posix":
if 'LD_LIBRARY_PATH' in os.environ:
os.environ['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH'] + os.pathsep + executableBinFolder
else:
os.environ['LD_LIBRARY_PATH'] = executableBinFolder
# add the user site packages folder to find the pyassimp egg link
import site
for item in sys.path:
if (item.find('site-packages') != -1):
site.addsitedir(item)
install_user_site()
import pyassimp
import azlmbr.asset
import azlmbr.asset.builder
import azlmbr.asset.entity
import azlmbr.blast
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.entity
import azlmbr.math
import os
import traceback
import binascii
import sys
# the UUID must be unique amongst all the asset builders in Python or otherwise
# a collision of builders will happen preventing one from running
busIdString = '{CF5C74D1-9ED4-4851-85B1-9B15090DBEC7}'
busId = azlmbr.math.Uuid_CreateString(busIdString, 0)
handler = None
jobKeyName = 'Blast Chunk Assets'
sceneManifestType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
dccMaterialType = azlmbr.math.Uuid_CreateString('{C88469CF-21E7-41EB-96FD-BF14FBB05EDC}', 0)
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
def get_source_fbx_filename(request):
fullPath = os.path.join(request.watchFolder, request.sourceFile)
basePath, filePart = os.path.split(fullPath)
filename = os.path.splitext(filePart)[0] + '.fbx'
filename = os.path.join(basePath, filename)
return filename
def raise_error(message):
raise RuntimeError(f'[ERROR]: {message}')
def generate_asset_info(chunkNames, request):
import azlmbr.blast
# write out an object stream with the extension of .fbx.assetinfo.generated
basePath, sceneFile = os.path.split(request.sourceFile)
assetinfoFilename = os.path.splitext(sceneFile)[0] + '.fbx.assetinfo.generated'
assetinfoFilename = os.path.join(basePath, assetinfoFilename)
assetinfoFilename = assetinfoFilename.replace('\\', '/').lower()
outputFilename = os.path.join(request.tempDirPath, assetinfoFilename)
storage = azlmbr.blast.BlastSliceAssetStorageComponent()
if (storage.GenerateAssetInfo(chunkNames, request.sourceFile, outputFilename)):
product = azlmbr.asset.builder.JobProduct(assetinfoFilename, sceneManifestType, 1)
product.dependenciesHandled = True
return product
raise_error('Failed to generate assetinfo.generated')
def export_fbx_manifest(request):
output = []
fbxFilename = get_source_fbx_filename(request)
sceneAsset = pyassimp.load(fbxFilename)
with sceneAsset as scene:
rootNode = scene.mRootNode.contents
for index in range(0, rootNode.mNumChildren):
child = rootNode.mChildren[index]
childNode = child.contents
childNodeName = bytes.decode(childNode.mName.data)
output.append(str(childNodeName))
return output
def convert_to_asset_paths(fbxFilename, gameRoot, chunkNameList):
realtivePath = fbxFilename[len(gameRoot) + 1:]
realtivePath = os.path.splitext(realtivePath)[0]
output = []
for chunk in chunkNameList:
assetPath = realtivePath + '-' + chunk + '.cgf'
assetPath = assetPath.replace('\\', '/')
assetPath = assetPath.lower()
output.append(assetPath)
return output
# creates a single job to compile for each platform
def create_jobs(request):
fbxSidecarFilename = get_source_fbx_filename(request)
if (os.path.exists(fbxSidecarFilename) is False):
print('[WARN] Sidecar FBX file {} is missing for blast file {}'.format(fbxSidecarFilename, request.sourceFile))
return azlmbr.asset.builder.CreateJobsResponse()
# see if the FBX file already has a .assetinfo source asset, if so then do not create a job
if (os.path.exists(f'{fbxSidecarFilename}.assetinfo')):
response = azlmbr.asset.builder.CreateJobsResponse()
response.result = azlmbr.asset.builder.CreateJobsResponse_ResultSuccess
return response
# create job descriptor for each platform
jobDescriptorList = []
for platformInfo in request.enabledPlatforms:
jobDesc = azlmbr.asset.builder.JobDescriptor()
jobDesc.jobKey = jobKeyName
jobDesc.priority = 12 # higher than the 'Scene compilation' or 'fbx'
jobDesc.set_platform_identifier(platformInfo.identifier)
jobDescriptorList.append(jobDesc)
response = azlmbr.asset.builder.CreateJobsResponse()
response.result = azlmbr.asset.builder.CreateJobsResponse_ResultSuccess
response.createJobOutputs = jobDescriptorList
return response
# handler to create jobs for a source asset
def on_create_jobs(args):
try:
request = args[0]
return create_jobs(request)
except:
log_exception_traceback()
return azlmbr.asset.builder.CreateJobsResponse()
def generate_blast_slice_asset(chunkNameList, request):
# get list of relative chunk paths
fbxFilename = get_source_fbx_filename(request)
assetPaths = convert_to_asset_paths(fbxFilename, request.watchFolder, chunkNameList)
outcome = azlmbr.asset.entity.PythonBuilderRequestBus(bus.Broadcast, 'CreateEditorEntity', 'BlastData')
if (outcome.IsSuccess() is False):
raise_error('could not create an editor entity')
blastDataEntityId = outcome.GetValue()
# create a component for the editor entity
gameType = azlmbr.entity.EntityType().Game
blastMeshDataTypeIdList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Blast Slice Storage Component"], gameType)
componentOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'AddComponentOfType', blastDataEntityId, blastMeshDataTypeIdList[0])
if (componentOutcome.IsSuccess() is False):
raise_error('failed to add component (Blast Slice Storage Component) to the blast_slice')
# build the blast slice using the chunk asset paths
blastMeshComponentId = componentOutcome.GetValue()[0]
outcome = editor.EditorComponentAPIBus(bus.Broadcast, 'BuildComponentPropertyTreeEditor', blastMeshComponentId)
if(outcome.IsSuccess() is False):
raise_error(f'failed to create Property Tree Editor for component ({blastMeshComponentId})')
pte = outcome.GetValue()
pte.set_visible_enforcement(True)
pte.set_value('Mesh Paths', assetPaths)
# write out an object stream with the extension of .blast_slice
basePath, sceneFile = os.path.split(request.sourceFile)
blastFilename = os.path.splitext(sceneFile)[0] + '.blast_slice'
blastFilename = os.path.join(basePath, blastFilename)
blastFilename = blastFilename.replace('\\', '/').lower()
tempFilename = os.path.join(request.tempDirPath, blastFilename)
entityList = [blastDataEntityId]
makeDynamic = False
outcome = azlmbr.asset.entity.PythonBuilderRequestBus(bus.Broadcast, 'WriteSliceFile', tempFilename, entityList, makeDynamic)
if (outcome.IsSuccess() is False):
raise_error(f'WriteSliceFile failed for blast_slice file ({blastFilename})')
# return a job product
blastSliceAsset = azlmbr.blast.BlastSliceAsset()
subId = binascii.crc32(blastFilename.encode('utf8'))
product = azlmbr.asset.builder.JobProduct(blastFilename, blastSliceAsset.GetAssetTypeId(), subId)
product.dependenciesHandled = True
return product
def read_in_string(data, dataLength):
stringData = ''
for idx in range(4, dataLength - 1):
char = bytes.decode(data[idx])
if (str.isascii(char)):
stringData += char
return stringData
def import_material_info(fbxFilename):
_, group_name = os.path.split(fbxFilename)
group_name = os.path.splitext(group_name)[0]
output = {}
output['group_name'] = group_name
output['material_name_list'] = []
sceneAsset = pyassimp.load(fbxFilename)
with sceneAsset as scene:
for materialIndex in range(0, scene.mNumMaterials):
material = scene.mMaterials[materialIndex].contents
for materialPropertyIdx in range(0, material.mNumProperties):
materialProperty = material.mProperties[materialPropertyIdx].contents
materialPropertyName = bytes.decode(materialProperty.mKey.data)
if (materialPropertyName.endswith('mat.name') and materialProperty.mType is 3):
stringData = read_in_string(materialProperty.mData, materialProperty.mDataLength)
output['material_name_list'].append(stringData)
return output
def write_material_file(sourceFile, destFolder):
# preserve source MTL files
rootPath, materialSourceFile = os.path.split(sourceFile)
materialSourceFile = os.path.splitext(materialSourceFile)[0] + '.mtl'
materialSourceFile = os.path.join(rootPath, materialSourceFile)
if (os.path.exists(materialSourceFile)):
print(f'{materialSourceFile} source already exists')
return None
# auto-generate a DCC material file
info = import_material_info(sourceFile)
materialGroupName = info['group_name']
materialNames = info['material_name_list']
materialFilename = materialGroupName + '.dccmtl.generated'
subId = binascii.crc32(materialFilename.encode('utf8'))
materialFilename = os.path.join(destFolder, materialFilename)
storage = azlmbr.blast.BlastSliceAssetStorageComponent()
storage.WriteMaterialFile(materialGroupName, materialNames, materialFilename)
product = azlmbr.asset.builder.JobProduct(materialFilename, dccMaterialType, subId)
product.dependenciesHandled = True
return product
def process_fbx_file(request):
# fill out response object
response = azlmbr.asset.builder.ProcessJobResponse()
productOutputs = []
# write out DCCMTL file as a product (if needed)
materialProduct = write_material_file(get_source_fbx_filename(request), request.tempDirPath)
if (materialProduct is not None):
productOutputs.append(materialProduct)
# prepare output folder
basePath, _ = os.path.split(request.sourceFile)
outputPath = os.path.join(request.tempDirPath, basePath)
os.makedirs(outputPath)
# parse FBX for chunk names
chunkNameList = export_fbx_manifest(request)
# create assetinfo generated (is product)
productOutputs.append(generate_asset_info(chunkNameList, request))
# write out the blast_slice object stream
productOutputs.append(generate_blast_slice_asset(chunkNameList, request))
response.outputProducts = productOutputs
response.resultCode = azlmbr.asset.builder.ProcessJobResponse_Success
response.dependenciesHandled = True
return response
# using the incoming 'request' find the type of job via 'jobKey' to determine what to do
def on_process_job(args):
try:
request = args[0]
if (request.jobDescription.jobKey.startswith(jobKeyName)):
return process_fbx_file(request)
return azlmbr.asset.builder.ProcessJobResponse()
except:
log_exception_traceback()
return azlmbr.asset.builder.ProcessJobResponse()
# register asset builder
def register_asset_builder():
assetPattern = azlmbr.asset.builder.AssetBuilderPattern()
assetPattern.pattern = '*.blast'
assetPattern.type = azlmbr.asset.builder.AssetBuilderPattern_Wildcard
builderDescriptor = azlmbr.asset.builder.AssetBuilderDesc()
builderDescriptor.name = "Blast Gem"
builderDescriptor.patterns = [assetPattern]
builderDescriptor.busId = busId
builderDescriptor.version = 5
outcome = azlmbr.asset.builder.PythonAssetBuilderRequestBus(azlmbr.bus.Broadcast, 'RegisterAssetBuilder', builderDescriptor)
if outcome.IsSuccess():
# created the asset builder to hook into the notification bus
handler = azlmbr.asset.builder.PythonBuilderNotificationBusHandler()
handler.connect(busId)
handler.add_callback('OnCreateJobsRequest', on_create_jobs)
handler.add_callback('OnProcessJobRequest', on_process_job)
return handler
# create the asset builder handler
try:
handler = register_asset_builder()
except:
handler = None
log_exception_traceback()