singe/util/convertModel.py

112 lines
5.1 KiB
Python

# Converts a model Singe does not read -- FBX, OBJ, DAE, 3DS, BLEND, STL and the rest -- into the
# .glb it does, by driving a converter that is already on the machine. Nothing here parses a model
# itself: a general importer is a large dependency with a long history of memory safety holes, and
# game content is fixed when the game ships, so the conversion belongs on the workstation rather
# than in the engine. Usage: python3 util/convertModel.py Model.fbx Model.glb
#
# It uses whichever of these it finds, in this order:
# blender -- the best results by far, and the only one that reliably carries skinning,
# animation and PBR materials across from FBX.
# assimp -- the CLI from Open Asset Import Library (apt install assimp-utils).
# obj2gltf -- npm, and OBJ only.
# A .gltf with external buffers comes back through packGlb.py, because Singe loads a .glb whose
# buffers are all inside it.
import os, shutil, subprocess, sys, tempfile
BLENDER_SCRIPT = """
import bpy, sys
out = sys.argv[sys.argv.index('--') + 1]
bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_apply=True)
"""
def blenderImport(source):
# Blender picks its importer by extension, and the operator names are not all alike.
ext = source.lower().rsplit('.', 1)[-1]
table = {
'fbx': 'bpy.ops.import_scene.fbx(filepath=%r)',
'obj': 'bpy.ops.wm.obj_import(filepath=%r)',
'dae': 'bpy.ops.wm.collada_import(filepath=%r)',
'stl': 'bpy.ops.wm.stl_import(filepath=%r)',
'ply': 'bpy.ops.wm.ply_import(filepath=%r)',
'x3d': 'bpy.ops.import_scene.x3d(filepath=%r)',
'blend': None,
'usd': 'bpy.ops.wm.usd_import(filepath=%r)',
'usdz': 'bpy.ops.wm.usd_import(filepath=%r)',
'usda': 'bpy.ops.wm.usd_import(filepath=%r)',
'usdc': 'bpy.ops.wm.usd_import(filepath=%r)',
}
if ext not in table:
return None
if table[ext] is None:
return '' # A .blend is opened rather than imported.
return table[ext] % source
def runBlender(blender, source, out):
statement = blenderImport(source)
if statement is None:
return False
script = tempfile.NamedTemporaryFile('w', suffix='.py', delete=False)
# A .blend is the file Blender opens; anything else starts from an empty scene.
if statement == '':
head = ''
opened = ['-b', source]
else:
head = "bpy.ops.wm.read_factory_settings(use_empty=True)\n" + statement + "\n"
opened = ['-b']
script.write("import bpy, sys\n" + head + BLENDER_SCRIPT.split('\n', 2)[2])
script.close()
command = [blender] + opened + ['--python', script.name, '--', out]
done = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
os.unlink(script.name)
if done.returncode != 0:
sys.stderr.write(done.stdout.decode('utf-8', 'replace'))
return done.returncode == 0
def runAssimp(assimp, source, out):
done = subprocess.run([assimp, 'export', source, out, '-fglb2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if done.returncode != 0:
sys.stderr.write(done.stdout.decode('utf-8', 'replace'))
return done.returncode == 0
def main():
if len(sys.argv) != 3:
sys.exit('usage: python3 util/convertModel.py <model> <output.glb>')
source, out = sys.argv[1], sys.argv[2]
if not os.path.exists(source):
sys.exit('No such file: ' + source)
if not out.lower().endswith('.glb'):
sys.exit('Singe loads .glb; name the output accordingly.')
blender = shutil.which('blender')
if blender and runBlender(blender, source, out):
print('Converted with Blender: ' + out)
return
assimp = shutil.which('assimp')
if assimp and runAssimp(assimp, source, out):
print('Converted with assimp: ' + out)
return
if source.lower().endswith('.obj'):
obj2gltf = shutil.which('obj2gltf')
if obj2gltf and subprocess.run([obj2gltf, '-i', source, '-o', out, '-b']).returncode == 0:
print('Converted with obj2gltf: ' + out)
return
# objToGlb.py is beside this script and reads the subset of OBJ the Singe assets use.
here = os.path.dirname(os.path.abspath(__file__))
if os.path.exists(os.path.join(here, 'objToGlb.py')):
done = subprocess.run([sys.executable, os.path.join(here, 'objToGlb.py'), source, out], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if done.returncode == 0:
print('Converted with util/objToGlb.py: ' + out)
return
# It reads only the subset of OBJ the Singe assets use, and it wants numpy, so say what
# went wrong rather than letting the last line below blame the file.
sys.stderr.write('util/objToGlb.py could not do it:\n' + done.stdout.decode('utf-8', 'replace'))
sys.exit('No converter on this machine could read ' + source + '.\n'
'Install Blender (best results, and the only one that carries skinning and animation\n'
'across reliably) or assimp-utils, then run this again.')
main()