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.
323 lines
13 KiB
Python
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()
|