# 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 ') 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()