"""Author the roadside district kit in its own scene and library. Run headless only, so no open session is touched: blender -b --factory-startup --python assets/blender/districts/author-districts.py It builds the "Roadside Districts" scene from scratch, drops the factory scene and saves nightdrive-districts.blend. Convention: Blender +X faces the road (three.js +X), +Y runs down the road (three.js -Z), Z is up; every piece stands on z=0 at its origin. Each piece is two meshes: _body (graphite) and _light (emissive). """ import bpy, bmesh, math from mathutils import Matrix, Euler, Vector from pathlib import Path if not bpy.app.background: raise SystemExit('Run author-districts.py headless (blender -b); it saves a new file.') SCENE = 'Roadside Districts' LIB = Path(__file__).resolve().parent / 'nightdrive-districts.blend' if SCENE in bpy.data.scenes: old = bpy.data.scenes[SCENE] for obj in list(old.objects): mesh = obj.data bpy.data.objects.remove(obj) if mesh and mesh.users == 0: bpy.data.meshes.remove(mesh) bpy.data.scenes.remove(old) scene = bpy.data.scenes.new(SCENE) class Part: def __init__(self): self.bm = bmesh.new() def box(self, c, s, r=(0, 0, 0)): geom = bmesh.ops.create_cube(self.bm, size=1.0) m = Matrix.Translation(Vector(c)) @ Euler([math.radians(a) for a in r]).to_matrix().to_4x4() @ Matrix.Diagonal(Vector((*s, 1.0))) bmesh.ops.transform(self.bm, matrix=m, verts=geom['verts']) def beam(self, a, b, t): """Square beam of thickness t between points a and b.""" a, b = Vector(a), Vector(b) d = b - a geom = bmesh.ops.create_cube(self.bm, size=1.0) rot = d.to_track_quat('Z', 'Y').to_matrix().to_4x4() m = Matrix.Translation((a + b) / 2) @ rot @ Matrix.Diagonal(Vector((t, t, d.length, 1.0))) bmesh.ops.transform(self.bm, matrix=m, verts=geom['verts']) def piece(name, body, light): for suffix, part in (('body', body), ('light', light)): bmesh.ops.remove_doubles(part.bm, verts=part.bm.verts, dist=1e-4) mesh = bpy.data.meshes.new(f'{name}_{suffix}') part.bm.to_mesh(mesh) part.bm.free() obj = bpy.data.objects.new(f'{name}_{suffix}', mesh) scene.collection.objects.link(obj) # Overhead gantry spanning the carriageway (x -11..11 in three.js units). b, l = Part(), Part() for x in (-11.2, 11.2): b.box((x, 0, 0.2), (1.2, 1.6, 0.4)) b.box((x, -0.45, 3.9), (0.34, 0.34, 7.4)) b.box((x, 0.45, 3.9), (0.34, 0.34, 7.4)) for z in (1.6, 3.4, 5.2): b.beam((x, -0.45, z), (x, 0.45, z + 1.6), 0.12) for y in (-0.45, 0.45): for z in (6.3, 7.5): b.box((0, y, z), (22.8, 0.22, 0.22)) for i in range(12): x = -10.4 + i * 1.9 b.beam((x, -0.45, 6.3), (x + 1.9, -0.45, 7.5), 0.1) b.beam((x, 0.45, 6.3), (x + 1.9, 0.45, 7.5), 0.1) b.box((x, 0, 6.3), (0.1, 0.9, 0.1)) for cx, w in ((-5.2, 7.0), (4.6, 6.0)): b.box((cx, -0.62, 5.0), (w, 0.14, 2.6)) l.box((cx, -0.71, 6.32), (w, 0.04, 0.07)) l.box((cx, -0.71, 3.68), (w, 0.04, 0.07)) for k, f in enumerate((0.62, 0.4, 0.5)): l.box((cx - w * (1 - f) / 2 + 0.4, -0.71, 5.6 - k * 0.55), (w * f - 0.8, 0.04, 0.16)) l.box((-1.4, -0.71, 4.4), (0.9, 0.04, 0.16), (0, 45, 0)) l.box((-1.65, -0.71, 4.6), (0.16, 0.04, 0.7)) for x in range(-10, 11, 4): l.box((x, 0, 6.15), (0.6, 0.12, 0.05)) piece('gantry', b, l) # Open-air server row with an overhead cable tray (Infrastructure). b, l = Part(), Part() for i in range(6): y = -2.75 + i * 1.1 b.box((0, y, 1.15), (1.2, 1.0, 2.3)) b.box((0, y, 2.36), (1.0, 0.9, 0.12)) for k in range(9): width = (0.82, 0.62, 0.74, 0.4, 0.82, 0.55, 0.7, 0.3, 0.6)[(k + i * 2) % 9] l.box((0.61, y - (0.82 - width) / 2, 0.45 + k * 0.2), (0.03, width, 0.045)) l.box((0.61, y + 0.38, 2.12), (0.03, 0.08, 0.08)) for y in (-3.4, 3.4): b.box((-0.3, y, 1.6), (0.18, 0.18, 3.2)) b.box((0.2, y, 3.15), (1.2, 0.16, 0.12)) b.box((0.35, 0, 3.05), (0.04, 7.0, 0.16)) b.box((-0.35, 0, 3.05), (0.04, 7.0, 0.16)) for k in range(12): b.box((0, -3.3 + k * 0.6, 3.0), (0.7, 0.05, 0.04)) l.box((0, 0, 2.94), (0.5, 6.8, 0.03)) piece('rack-row', b, l) # Lattice radio mast with dishes (Toolkit / Infrastructure horizon). b, l = Part(), Part() H = 26.0 corners = [(-1, -1), (1, -1), (1, 1), (-1, 1)] def half(z): return 1.6 - z * 0.045 for cx, cy in corners: b.beam((cx * half(0), cy * half(0), 0), (cx * half(H), cy * half(H), H), 0.16) for lv in range(9): z0, z1 = lv * H / 9, (lv + 1) * H / 9 for j in range(4): (ax, ay), (bx, by) = corners[j], corners[(j + 1) % 4] b.beam((ax * half(z1), ay * half(z1), z1), (bx * half(z1), by * half(z1), z1), 0.09) b.beam((ax * half(z0), ay * half(z0), z0), (bx * half(z1), by * half(z1), z1), 0.07) b.box((0, 0, H + 3), (0.12, 0.12, 6)) for z, ang in ((18, 30), (13, -40), (21, 160)): d = Vector((math.cos(math.radians(ang)), math.sin(math.radians(ang)), 0)) b.box(tuple(d * 1.4 + Vector((0, 0, z))), (0.18, 1.4, 1.4), (0, 0, ang)) for z in (H + 6, H, 17, 9): l.box((0, 0, z), (0.34, 0.34, 0.34)) piece('mast', b, l) # Workshop with half-open roller door and a lift (Selected builds). b, l = Part(), Part() W, D, Hh = 8.0, 6.0, 4.2 b.box((-D, 0, Hh / 2), (0.2, W, Hh)) b.box((-D / 2, -W / 2, Hh / 2), (D, 0.2, Hh)) b.box((-D / 2, W / 2, Hh / 2), (D, 0.2, Hh)) for s in (-1, 1): b.box((-D / 2, s * W / 4, Hh + 0.75), (D + 0.4, W / 2 + 0.25, 0.14), (s * 20.5, 0, 0)) b.box((0, -W / 2 + 0.3, Hh / 2), (0.3, 0.6, Hh)) b.box((0, W / 2 - 0.3, Hh / 2), (0.3, 0.6, Hh)) for k in range(5): b.box((0.05, 0, Hh - 0.25 - k * 0.32), (0.1, W - 1.2, 0.26)) b.box((-4.8, -2.2, 0.45), (1.6, 1.0, 0.9)) for y in (0.4, 3.0): b.box((-2.6, y, 1.1), (0.25, 0.25, 2.2)) b.box((-2.6, 1.7, 1.15), (2.4, 2.8, 0.08)) l.box((-D / 2, 0, Hh + 0.1), (D - 0.4, 0.06, 0.05)) l.box((0.25, 0, Hh + 0.35), (0.04, W - 1.4, 0.32)) l.box((0.22, -W / 2 + 0.62, Hh / 2), (0.04, 0.05, Hh)) l.box((0.22, W / 2 - 0.62, Hh / 2), (0.04, 0.05, Hh)) l.box((-4.0, -2.2, 0.96), (0.9, 0.5, 0.03)) piece('workshop', b, l) # Tower crane (Selected builds / Additional work horizon). b, l = Part(), Part() Hc = 30.0 for cx, cy in corners: b.box((cx * 0.75, cy * 0.75, Hc / 2), (0.18, 0.18, Hc)) for lv in range(10): z = (lv + 1) * Hc / 10 for j in range(4): (ax, ay), (bx, by) = corners[j], corners[(j + 1) % 4] b.beam((ax * 0.75, ay * 0.75, z), (bx * 0.75, by * 0.75, z), 0.08) b.beam((ax * 0.75, ay * 0.75, z - Hc / 10), (bx * 0.75, by * 0.75, z), 0.06) b.box((0, 0, Hc + 0.8), (2.2, 2.2, 1.6)) b.box((0, 0, Hc + 4), (0.3, 0.3, 4.8)) J = 26.0 for z in (Hc + 1.6, Hc + 2.8): b.box((J / 2 - 3, 0.5 if z < Hc + 2 else 0, z), (J, 0.18, 0.18)) b.box((J / 2 - 3, -0.5, Hc + 1.6), (J, 0.18, 0.18)) for k in range(9): x = -3 + k * J / 9 b.beam((x, 0, Hc + 2.8), (x + J / 9, 0.5, Hc + 1.6), 0.07) b.beam((x, 0, Hc + 2.8), (x + J / 9, -0.5, Hc + 1.6), 0.07) b.box((-7, 0, Hc + 1.7), (8, 0.8, 0.3)) b.box((-9.5, 0, Hc + 1.0), (2.2, 1.6, 1.4)) b.beam((J - 3, 0, Hc + 6.4), (-9, 0, Hc + 2.0), 0.05) b.box((15, 0, Hc - 6), (0.05, 0.05, 12)) b.box((15, 0, Hc - 12.2), (0.8, 0.6, 0.5)) l.box((0, 0, Hc + 6.5), (0.4, 0.4, 0.4)) l.box((J - 3, 0, Hc + 2.0), (0.36, 0.36, 0.36)) l.box((15, 0, Hc - 12.6), (0.5, 0.3, 0.06)) piece('crane', b, l) # Shipping container, stackable (Additional work). b, l = Part(), Part() b.box((0, 0, 1.3), (2.44, 6.06, 2.6)) for k in range(14): y = -2.7 + k * 0.415 b.box((1.24, y, 1.3), (0.06, 0.18, 2.36)) b.box((-1.24, y, 1.3), (0.06, 0.18, 2.36)) for x in (-1.15, 1.15): for y in (-3.0, 3.0): b.box((x, y, 1.3), (0.16, 0.16, 2.62)) l.box((1.28, 0, 2.56), (0.03, 5.9, 0.05)) l.box((0.0, -3.06, 1.3), (1.8, 0.03, 0.05)) piece('container', b, l) # Roadside billboard (Profile / Experience). b, l = Part(), Part() for y in (-2.5, 2.5): b.box((0, y, 3.0), (0.3, 0.3, 6.0)) b.box((0.1, 0, 7.0), (0.24, 8.4, 3.6)) b.box((0.6, 0, 5.05), (1.0, 8.2, 0.08)) l.box((0.24, 0, 8.86), (0.04, 8.4, 0.08)) l.box((0.24, 0, 5.14), (0.04, 8.4, 0.08)) l.box((0.24, -4.2, 7.0), (0.04, 0.08, 3.8)) l.box((0.24, 4.2, 7.0), (0.04, 0.08, 3.8)) for k, f in enumerate((0.7, 0.45, 0.55)): l.box((0.24, -4.2 + 8.4 * f / 2 + 0.6, 8.0 - k * 0.75), (0.04, 8.4 * f - 1.2, 0.22)) for y in (-3, 0, 3): l.box((1.05, y, 5.15), (0.2, 0.5, 0.08)) piece('billboard', b, l) # Destination arch over the road (Contact). b, l = Part(), Part() for s in (-1, 1): b.box((s * 12.5, 0, 7.0), (1.4, 1.4, 14.0)) b.box((s * 12.5, 0, 0.3), (2.4, 2.4, 0.6)) l.box((s * 11.78, 0, 7.0), (0.05, 0.12, 12.4)) b.box((0, 0, 14.6), (27.4, 1.6, 1.4)) b.box((0, 0, 16.0), (18.0, 1.0, 1.4)) l.box((0, -0.82, 14.2), (25.0, 0.04, 0.12)) l.box((0, -0.82, 15.0), (25.0, 0.04, 0.05)) l.box((0, -0.52, 16.5), (16.0, 0.04, 0.1)) for k in range(9): l.box((-8 + k * 2, 0, 13.88), (0.9, 0.5, 0.04)) piece('arch', b, l) tris = 0 for obj in scene.objects: obj.data.calc_loop_triangles() tris += len(obj.data.loop_triangles) for other in [s for s in bpy.data.scenes if s != scene]: bpy.data.scenes.remove(other) for block in (bpy.data.objects, bpy.data.meshes, bpy.data.cameras, bpy.data.lights, bpy.data.materials): for item in list(block): if item.users == 0: block.remove(item) bpy.ops.wm.save_as_mainfile(filepath=str(LIB), compress=True) print('pieces', len(scene.objects) // 2, 'triangles', tris, 'library', LIB)