"""Convert a downloaded model (glb/gltf/fbx/obj/blend) into a game-ready car GLB. Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/import_shitbox.py -- \ "" [yaw_deg] Normalizes: uniform scale so the longest horizontal extent = target length, optional yaw (degrees) so the nose faces +Y (Godot -Z), centered, ground at -0.75 (the controller's ride height). Exports cars//car.glb. ponytail: imported models have no det_* panels, so they don't shed parts -- cutting doors out of downloaded meshes is per-model surgery for later. """ import math import os import sys import bpy from mathutils import Vector argv = sys.argv[sys.argv.index("--") + 1:] src, car_id, target_len = argv[0], argv[1], float(argv[2]) yaw = math.radians(float(argv[3])) if len(argv) > 3 else 0.0 pitch = math.radians(float(argv[4])) if len(argv) > 4 else 0.0 ext = os.path.splitext(src)[1].lower() if ext == ".blend": bpy.ops.wm.open_mainfile(filepath=src) else: bpy.ops.wm.read_factory_settings(use_empty=True) if ext in (".glb", ".gltf"): bpy.ops.import_scene.gltf(filepath=src) elif ext == ".fbx": bpy.ops.import_scene.fbx(filepath=src) elif ext == ".obj": bpy.ops.wm.obj_import(filepath=src) else: raise SystemExit("unsupported: " + ext) for obj in list(bpy.data.objects): if obj.type not in ("MESH", "EMPTY"): bpy.data.objects.remove(obj, do_unlink=True) meshes = [o for o in bpy.data.objects if o.type == "MESH"] assert meshes, "no meshes found" root = bpy.data.objects.new("body", None) bpy.context.collection.objects.link(root) for obj in bpy.data.objects: if obj is not root and obj.parent is None: mw = obj.matrix_world.copy() obj.parent = root obj.matrix_world = mw def bbox(): lo = Vector((1e9, 1e9, 1e9)) hi = Vector((-1e9, -1e9, -1e9)) dg = bpy.context.evaluated_depsgraph_get() for o in meshes: for c in o.evaluated_get(dg).bound_box: w = o.matrix_world @ Vector(c) lo = Vector(map(min, lo, w)) hi = Vector(map(max, hi, w)) return lo, hi root.rotation_euler = (pitch, 0, yaw) # XYZ order: pitch applies before yaw bpy.context.view_layer.update() lo, hi = bbox() size = hi - lo scale = target_len / max(size.x, size.y) root.scale = (scale,) * 3 bpy.context.view_layer.update() lo, hi = bbox() center = (lo + hi) / 2 root.location = Vector((-center.x, -center.y, -lo.z - 0.75)) bpy.context.view_layer.update() # cap textures at 1K -- these packs ship 4K maps, 16x the pixels a shitbox needs for img in bpy.data.images: if img.size[0] > 1024 or img.size[1] > 1024: img.scale(min(img.size[0], 1024), min(img.size[1], 1024)) # cap geometry -- some packs are near-scan density; a shitbox needs 150k tris tops BUDGET = 150_000 total = sum(len(o.data.polygons) for o in meshes) if total > BUDGET: for o in meshes: mod = o.modifiers.new("dec", "DECIMATE") mod.ratio = BUDGET / total print("decimating %d -> %d tris" % (total, BUDGET)) out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cars", car_id) os.makedirs(out, exist_ok=True) for obj in bpy.data.objects: obj.select_set(True) path = os.path.join(out, "car.glb") bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=True, export_apply=True) lo, hi = bbox() print("imported %s -> %s | size %.2f x %.2f x %.2f" % (src, path, hi.x - lo.x, hi.y - lo.y, hi.z - lo.z))