Files
Portfolio/assets/blender/districts/author-districts.py

240 lines
9.3 KiB
Python

"""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: <piece>_body (graphite) and <piece>_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)