112 lines
5.1 KiB
Python
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()
|