240 lines
9.3 KiB
Python
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)
|