Stats quietly inverted: shitboxier IRL = better in game. Excel X3 is now the fastest car; TURDRUNNER is the slowest. tools/import_shitbox.py converts downloaded models (glb/gltf/fbx/obj/blend) to game GLBs -- normalizes scale, yaw, ride-height origin. First two imports: Corolla mk7 and Morris Mini. shitboxes/ stash gitignored + .gdignore'd (its .blend files broke Godot's headless import scan). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""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 -- \
|
|
"<input path>" <car_id> <target_length_m> [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_id>/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
|
|
|
|
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 = (0, 0, 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()
|
|
|
|
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)
|
|
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))
|