"""Procedural Aussie shitbox factory. Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py Each car: side-silhouette profile extruded across the width (body + greenhouse), cylinder wheels, and separate det_* panel objects (doors, mirrors, bumpers, bonnet, boot) that the game rips off on impact. Exports cars//car.glb. Convention: +Y is the nose in Blender (imports facing -Z, Godot-forward). Ground is z=0 while building; the body root is dropped -0.75 at the end to match the car controller's ride height. ponytail: silhouettes are hand-tuned point lists, not reference-traced. Feed me side-on photos per car when a shape needs to get closer. """ import os import math import bpy import bmesh from mathutils import Matrix OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cars") GLASS = (0.12, 0.16, 0.20, 1) TYRE = (0.08, 0.08, 0.08, 1) CHROME = (0.75, 0.76, 0.78, 1) DARK = (0.15, 0.15, 0.15, 1) LIGHT_F = (0.95, 0.92, 0.75, 1) LIGHT_R = (0.7, 0.1, 0.08, 1) # L, W, heights: nose/belt/tail, lengths: bonnet/boot, roof height + rear roof end, # axle overhangs, wheel radius, paint, chrome bumpers, hatch rear CARS = { "datto_120y": dict(L=3.92, W=1.50, nose=0.62, belt=0.78, tail=0.72, bonnet=1.00, boot=0.85, roof=1.32, roof_rear=0.75, ohang_f=0.72, ohang_r=0.80, wheel=0.27, paint=(0.78, 0.62, 0.16, 1), chrome_bumpers=True, hatch=False), "gemmy_tx": dict(L=4.13, W=1.57, nose=0.60, belt=0.76, tail=0.70, bonnet=1.15, boot=0.95, roof=1.28, roof_rear=0.80, ohang_f=0.80, ohang_r=0.85, wheel=0.28, paint=(0.72, 0.30, 0.10, 1), chrome_bumpers=True, hatch=False), "kingswood_hq": dict(L=4.81, W=1.88, nose=0.68, belt=0.85, tail=0.78, bonnet=1.50, boot=1.30, roof=1.36, roof_rear=1.05, ohang_f=0.90, ohang_r=1.05, wheel=0.31, paint=(0.66, 0.74, 0.55, 1), chrome_bumpers=True, hatch=False), "vl_terbo": dict(L=4.72, W=1.72, nose=0.56, belt=0.80, tail=0.82, bonnet=1.30, boot=1.05, roof=1.33, roof_rear=0.90, ohang_f=0.85, ohang_r=0.95, wheel=0.30, paint=(0.92, 0.92, 0.90, 1), chrome_bumpers=False, hatch=False), "xf_falcon": dict(L=4.91, W=1.86, nose=0.66, belt=0.84, tail=0.80, bonnet=1.45, boot=1.25, roof=1.37, roof_rear=1.00, ohang_f=0.90, ohang_r=1.00, wheel=0.30, paint=(0.45, 0.55, 0.68, 1), chrome_bumpers=True, hatch=False), "excel_x3": dict(L=4.10, W=1.62, nose=0.58, belt=0.80, tail=0.96, bonnet=0.95, boot=0.30, roof=1.36, roof_rear=0.95, ohang_f=0.78, ohang_r=0.70, wheel=0.27, paint=(0.10, 0.55, 0.52, 1), chrome_bumpers=False, hatch=True), } def wipe(): for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True) for coll in (bpy.data.meshes, bpy.data.materials): for block in list(coll): if block.users == 0: coll.remove(block) def material(name, rgba, metallic=0.0, rough=0.7): mat = bpy.data.materials.new(name) mat.use_nodes = True bsdf = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED") bsdf.inputs["Base Color"].default_value = rgba bsdf.inputs["Metallic"].default_value = metallic bsdf.inputs["Roughness"].default_value = rough return mat def add_obj(name, mesh, mat): obj = bpy.data.objects.new(name, mesh) obj.data.materials.append(mat) bpy.context.collection.objects.link(obj) return obj def extrude_profile(name, profile, half_w, mat): """Closed (y,z) polygon extruded from -half_w to +half_w along x.""" n = len(profile) verts = [(-half_w, y, z) for y, z in profile] + [(half_w, y, z) for y, z in profile] faces = [list(range(n)), [2 * n - 1 - i for i in range(n)]] for i in range(n): j = (i + 1) % n faces.append([i, j, n + j, n + i]) mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) bm = bmesh.new() bm.from_mesh(mesh) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) bm.to_mesh(mesh) bm.free() mesh.update() return add_obj(name, mesh, mat) def box(name, center, size, mat, rot_x=0.0): mesh = bpy.data.meshes.new(name) bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) bmesh.ops.scale(bm, vec=size, verts=bm.verts) if rot_x: bmesh.ops.rotate(bm, cent=(0, 0, 0), matrix=Matrix.Rotation(rot_x, 3, "X"), verts=bm.verts) bmesh.ops.translate(bm, vec=center, verts=bm.verts) bm.to_mesh(mesh) bm.free() return add_obj(name, mesh, mat) def wheel(name, x, y, r, mat): mesh = bpy.data.meshes.new(name) bm = bmesh.new() bmesh.ops.create_cone(bm, cap_ends=True, segments=14, radius1=r, radius2=r, depth=0.24) bmesh.ops.rotate(bm, cent=(0, 0, 0), matrix=Matrix.Rotation(math.pi / 2, 3, "Y"), verts=bm.verts) bmesh.ops.translate(bm, vec=(x, y, r), verts=bm.verts) bm.to_mesh(mesh) bm.free() return add_obj(name, mesh, mat) def build(car_id, s): wipe() paint = material("paint", s["paint"], rough=0.45) paint_dk = material("paint_dark", tuple(c * 0.8 for c in s["paint"][:3]) + (1,), rough=0.5) glass = material("glass", GLASS, rough=0.2) tyre = material("tyre", TYRE, rough=0.9) chrome = material("chrome", CHROME, metallic=1.0, rough=0.25) dark = material("dark", DARK, rough=0.8) bumper_mat = chrome if s["chrome_bumpers"] else paint_dk L, W = s["L"], s["W"] f, r, hw = L / 2, -L / 2, W / 2 bottom = 0.22 ws_base = f - s["bonnet"] # windscreen base y boot_y = r + s["boot"] # rear window base y nose2 = s["nose"] + 0.02 body_profile = [ (f, bottom + 0.15), (f, s["nose"]), (f - 0.3, nose2), (ws_base, s["belt"]), (boot_y, s["belt"]), (r + 0.05, s["tail"]), (r, s["tail"] - 0.05), (r, bottom + 0.15), (r + 0.2, bottom), (f - 0.2, bottom), ] body = extrude_profile("body", body_profile, hw, paint) rr_y = r + 0.12 if s["hatch"] else boot_y rr_z = s["tail"] - 0.03 if s["hatch"] else s["belt"] gh_profile = [ (ws_base, s["belt"] - 0.02), (ws_base - 0.55, s["roof"]), (boot_y + s["roof_rear"], s["roof"]), (rr_y, rr_z), ] parts = [extrude_profile("glass", gh_profile, hw * 0.84, glass)] for side, sx in (("l", -1), ("r", 1)): door_z = (bottom + 0.3 + s["belt"]) / 2 door_h = s["belt"] - bottom - 0.32 span = (ws_base - 0.05) - (boot_y + 0.05) half = span / 2 parts.append(box("det_door_f%s" % side, (sx * hw, ws_base - 0.05 - half / 2, door_z), (0.06, half - 0.06, door_h), paint_dk)) parts.append(box("det_door_r%s" % side, (sx * hw, boot_y + 0.05 + half / 2, door_z), (0.06, half - 0.06, door_h), paint_dk)) parts.append(box("det_mirror_%s" % side, (sx * (hw + 0.09), ws_base + 0.05, s["belt"] + 0.10), (0.14, 0.09, 0.12), dark)) parts.append(box("det_bumper_f", (0, f + 0.05, bottom + 0.16), (W + 0.06, 0.15, 0.16), bumper_mat)) parts.append(box("det_bumper_r", (0, r - 0.05, bottom + 0.16), (W + 0.06, 0.15, 0.16), bumper_mat)) # bonnet plate follows the bonnet slope by1, bz1, by2, bz2 = f - 0.35, nose2, ws_base + 0.03, s["belt"] ang = math.atan2(bz2 - bz1, by2 - by1) parts.append(box("det_bonnet", ((0, (by1 + by2) / 2, (bz1 + bz2) / 2 + 0.02)), (W * 0.7, math.hypot(by2 - by1, bz2 - bz1) - 0.1, 0.04), paint_dk, rot_x=-ang)) if not s["hatch"]: ty1, tz1, ty2, tz2 = boot_y - 0.03, s["belt"], r + 0.1, s["tail"] ang = math.atan2(tz2 - tz1, ty2 - ty1) parts.append(box("det_boot", ((0, (ty1 + ty2) / 2, (tz1 + tz2) / 2 + 0.02)), (W * 0.7, math.hypot(ty2 - ty1, tz2 - tz1) - 0.1, 0.04), paint_dk, rot_x=-ang)) trim = [ box("trim_grille", (0, f + 0.01, s["nose"] - 0.14), (W * 0.5, 0.05, 0.12), dark), box("trim_light_fl", (-W * 0.33, f + 0.02, s["nose"] - 0.13), (0.24, 0.05, 0.13), material("lf", LIGHT_F)), box("trim_light_fr", (W * 0.33, f + 0.02, s["nose"] - 0.13), (0.24, 0.05, 0.13), material("lf2", LIGHT_F)), box("trim_light_rl", (-W * 0.32, r - 0.02, s["tail"] - 0.14), (0.26, 0.05, 0.12), material("lr", LIGHT_R)), box("trim_light_rr", (W * 0.32, r - 0.02, s["tail"] - 0.14), (0.26, 0.05, 0.12), material("lr2", LIGHT_R)), ] ax_f, ax_r = f - s["ohang_f"], r + s["ohang_r"] wx = hw - 0.10 wheels = [wheel("wheel_fl", -wx, ax_f, s["wheel"], tyre), wheel("wheel_fr", wx, ax_f, s["wheel"], tyre), wheel("wheel_rl", -wx, ax_r, s["wheel"], tyre), wheel("wheel_rr", wx, ax_r, s["wheel"], tyre)] for obj in parts + trim + wheels: obj.parent = body body.location.z = -0.75 # node origin sits at the controller's ride height out_dir = os.path.join(OUT, car_id) os.makedirs(out_dir, exist_ok=True) path = os.path.join(out_dir, "car.glb") for obj in bpy.data.objects: obj.select_set(True) bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=True) print("built", car_id, "->", path) for car_id, spec in CARS.items(): build(car_id, spec) print("done: %d cars" % len(CARS))