Files
animated_wallpaper/tools/plane_gen.py
T

196 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""
Generate vendor/plane3d.js - the low-poly airliner used by the flight layer.
The mesh is built here from primitives (no source model, no textures), so the
asset is CC0 by construction and reproducible:
python3 tools/plane_gen.py # writes vendor/plane3d.js
Output format (exactly what clouds3d.js expects):
window.__planeVerts = '<base64>' of non-indexed triangles, interleaved per
vertex: pos(3), normal(3), colIdx(1), float32 little endian.
colIdx: 0 = body (light albedo), 1 = glazing (dark albedo).
Convention: forward = +Z, up = +Y, nose-to-tail ~2.0 (the renderer scales
the model with uS and assumes that ~2.0 span, see PLANE_VERT_SRC).
"""
import base64
import math
import os
import struct
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BODY, GLASS = 0.0, 1.0
SIDES = 10
TRIS = []
def add_tri(a, b, c, col, ref):
"""Store one triangle; its flat normal is oriented away from ref."""
ux, uy, uz = b[0] - a[0], b[1] - a[1], b[2] - a[2]
vx, vy, vz = c[0] - a[0], c[1] - a[1], c[2] - a[2]
nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx
ln = math.sqrt(nx * nx + ny * ny + nz * nz)
if ln < 1e-9:
return
nx, ny, nz = nx / ln, ny / ln, nz / ln
mx = (a[0] + b[0] + c[0]) / 3.0 - ref[0]
my = (a[1] + b[1] + c[1]) / 3.0 - ref[1]
mz = (a[2] + b[2] + c[2]) / 3.0 - ref[2]
if nx * mx + ny * my + nz * mz < 0:
nx, ny, nz = -nx, -ny, -nz
TRIS.append((a, b, c, col, (nx, ny, nz)))
def add_quad(a, b, c, d, col, ref):
add_tri(a, b, c, col, ref)
add_tri(a, c, d, col, ref)
def ring(z, r, cy, cx=0.0, phase=0.0):
pts = []
for i in range(SIDES):
ang = phase + 2.0 * math.pi * i / SIDES
pts.append((cx + r * math.sin(ang), cy + r * math.cos(ang), z))
return pts
def add_cap(r, ref, col=BODY):
cx = sum(p[0] for p in r) / len(r)
cy = sum(p[1] for p in r) / len(r)
cz = sum(p[2] for p in r) / len(r)
for i in range(len(r)):
add_tri((cx, cy, cz), r[i], r[(i + 1) % len(r)], col, ref)
def tube(stations, cy, ref, cx=0.0, col=BODY):
"""stations: [(z, radius), ...] from one end to the other, caps closed."""
rings = [ring(z, r, cy, cx) for (z, r) in stations]
for k in range(len(rings) - 1):
a, b = rings[k], rings[k + 1]
for i in range(SIDES):
j = (i + 1) % SIDES
add_quad(a[i], a[j], b[j], b[i], col, ref)
add_cap(rings[0], ref, col)
add_cap(rings[-1], ref, col)
def slab(root_le, root_te, tip_le, tip_te, half_t):
"""Tapered wing/stabilizer: planform corners extruded by +-half_t in y."""
def off(p, dy):
return (p[0], p[1] + dy, p[2])
tlr, ttr = off(root_le, half_t), off(root_te, half_t)
tlt, ttt = off(tip_le, half_t), off(tip_te, half_t)
blr, btr = off(root_le, -half_t), off(root_te, -half_t)
blt, btt = off(tip_le, -half_t), off(tip_te, -half_t)
ref = ((root_le[0] + tip_le[0]) / 2.0, root_le[1] - 0.25,
(root_le[2] + tip_le[2]) / 2.0)
add_quad(tlr, tlt, ttt, ttr, BODY, ref) # top skin
add_quad(blr, btr, btt, blt, BODY, ref) # bottom skin
add_quad(tlr, blr, blt, tlt, BODY, ref) # leading edge
add_quad(ttr, ttt, btt, btr, BODY, ref) # trailing edge
add_quad(tlt, ttt, btt, blt, BODY, ref) # tip
def plate(outline, x, thickness, col=BODY):
"""Thin plate: outline is a list of (z, y), extruded around x."""
a = [(x - thickness / 2.0, p[1], p[0]) for p in outline]
b = [(x + thickness / 2.0, p[1], p[0]) for p in outline]
ym = sum(p[1] for p in outline) / len(outline)
zm = sum(p[0] for p in outline) / len(outline)
for i in range(1, len(a) - 1):
add_tri(a[0], a[i], a[i + 1], col, (x - 1.0, ym, zm))
add_tri(b[0], b[i + 1], b[i], col, (x + 1.0, ym, zm))
for i in range(len(a)):
j = (i + 1) % len(a)
add_quad(a[i], a[j], b[j], b[i], col, (x, ym, zm))
def window_band(ang_mid, cy, r, z0, z1, step, h, col=GLASS):
"""Glazing quads on the fuselage skin around the section angle ang_mid."""
x = r * 1.02 * math.sin(ang_mid)
ym = cy + r * 1.02 * math.cos(ang_mid)
y_lo, y_hi = ym - h / 2.0, ym + h / 2.0
ref = (0.0, cy, 0.5 * (z0 + z1))
z = z0
while z + step <= z1 + 1e-9:
za, zb = z + step * 0.18, z + step * 0.82
add_quad((x, y_hi, za), (x, y_hi, zb), (x, y_lo, zb), (x, y_lo, za),
col, ref)
z += step
def build():
cy = 0.115
# fuselage: nose cone, parallel section, tail cone
tube([(0.97, 0.020), (0.88, 0.075), (0.78, 0.105), (0.70, 0.115),
(-0.55, 0.115), (-0.75, 0.095), (-0.90, 0.055), (-1.00, 0.015)],
cy, (0.0, cy, 0.0))
# wings: swept and tapered, slight dihedral (root chord 0.56, tip 0.18)
for s in (1.0, -1.0):
slab((s * 0.09, 0.055, 0.30), (s * 0.09, 0.055, -0.26),
(s * 0.91, 0.125, -0.28), (s * 0.91, 0.125, -0.46), 0.016)
# winglet: small plate standing up at the tip
plate([(-0.28, 0.125), (-0.46, 0.125), (-0.46, 0.235), (-0.28, 0.215)],
s * 0.912, 0.014)
# horizontal stabilizers
for s in (1.0, -1.0):
slab((s * 0.07, 0.135, -0.66), (s * 0.07, 0.135, -0.94),
(s * 0.42, 0.165, -0.86), (s * 0.42, 0.165, -1.00), 0.011)
# vertical fin (outline in the (z, y) plane)
plate([(-0.48, 0.20), (-0.60, 0.60), (-0.86, 0.60), (-1.00, 0.26)],
0.0, 0.026)
# engine nacelles under the wings, with a thin pylon each
for s in (1.0, -1.0):
ex, ey = s * 0.36, -0.055
tube([(0.24, 0.030), (0.18, 0.072), (-0.06, 0.075), (-0.14, 0.045)],
ey, (ex, ey, 0.05), cx=ex)
plate([(0.16, 0.015), (-0.02, 0.015), (-0.02, 0.075), (0.16, 0.075)],
ex, 0.030)
# window bands on both flanks, cockpit glazing on the upper flanks
d = math.radians
window_band(d(90), cy, 0.115, -0.42, 0.58, 0.075, 0.048)
window_band(d(270), cy, 0.115, -0.42, 0.58, 0.075, 0.048)
window_band(d(54), cy, 0.115, 0.64, 0.82, 0.09, 0.055)
window_band(d(306), cy, 0.115, 0.64, 0.82, 0.09, 0.055)
def emit():
out = bytearray()
for (a, b, c, col, n) in TRIS:
for p in (a, b, c):
out += struct.pack('<fffffff',
p[0], p[1], p[2], n[0], n[1], n[2], col)
b64 = base64.b64encode(bytes(out)).decode('ascii')
head = ("""/* Airliner low-poly mesh (%d triangles, flat shaded, no textures).
* Generated procedurally by tools/plane_gen.py - original geometry, CC0 1.0.
* Non-indexed interleaved: pos(3) nrm(3) colIdx(1); unit extent 1.0,
* forward = +Z, up = +Y, nose-to-tail ~2.0 (see PLANE_VERT_SRC in clouds3d.js).
* colIdx: 0 = body, 1 = glazing.
*/
window.__planeVerts = '""" % len(TRIS))
path = os.path.join(ROOT, 'vendor', 'plane3d.js')
with open(path, 'w', encoding='utf-8') as f:
f.write(head + b64 + "';\n")
pts = [p for t in TRIS for p in t[:3]]
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]; zs = [p[2] for p in pts]
glass = sum(1 for t in TRIS if t[3] == GLASS)
print('wrote %s' % path)
print('triangles %d (glazing %d), bytes %d' % (len(TRIS), glass, len(out)))
print('x %.3f..%.3f y %.3f..%.3f z %.3f..%.3f'
% (min(xs), max(xs), min(ys), max(ys), min(zs), max(zs)))
if __name__ == '__main__':
build()
emit()