"""G2 tower family of Champions of Luneraria (one generator, three GLBs). Grenzwacht (Tower 1, outer lane tower): slender octagonal watchtower, open gallery, two crescent horns cradling a medium crystal, few metal bands. Bastion (Tower 2, inner lane tower): broad battered bastion with rear buttresses, shield plates on the lane side, machicolated parapet, three horns, armillary ring and a large crystal with satellite shards. Kernwacht (base tower): no round shaft at all - a stout rune drum carrying two crescent blades that meet in a lancet arch around the crystal, the same blade language as the Ancient, so the base towers read as part of the Ancient's sanctuary. All three share the octagonal plan, pale moonstone ashlar on a dark basalt plinth, team metal (tinted at runtime), rune friezes and the crescent crystal cradle. Local frame: +z faces the lane (attackers), -z is the outer side (buttresses, door, pennant pole). Nodes per GLB (pivot at the ground centre unless noted): Lower plinth and lower shaft (stays standing until the ruin replaces it) Upper everything above the split height; pivot at (0, split, 0) so it can tip Upper_Damaged heavy-damage variant of Upper (missing merlons, bent horn, loose plate) Debris rubble at the foot, shown from heavy damage on Crystal the energy crystal, pivot at its centre Ring / Shard_n rotating armillary ring and orbiting shards (Bastion, Kernwacht) Pennant / Banner_L / Banner_R cloth (team silhouette and emblem come from the shader) Ruin the destroyed tower (stump, rubble, broken crystal), pivot at the ground Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_towers.py """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import lune_bpy as L from lune_bpy import V, pi, sin, cos from mathutils import noise, Matrix OCT = pi / 8 def octa(name, profile, mat, **kw): return L.lathe(name, profile, 8, mat, rotation=OCT, **kw) def rivets(prefix, radius, y, count, mat, size=.07, rotation=OCT): out = [] for k in range(count): a = 2 * pi * k / count + rotation out.append(L.box('%s_%d' % (prefix, k), (cos(a) * radius, y, sin(a) * radius), (size, size, size * .6), mat, yaw=-a + pi / 2, bev=.012)) return out def merlons(prefix, radius, y, count, size, mat, skip=(), jitter=0.0, seed=0): out = [] for k in range(count): if k in skip: continue a = 2 * pi * (k + .5) / count tilt = jitter * noise.noise(V((k * 1.3, seed, 0))) out.append(L.box('%s_%d' % (prefix, k), (cos(a) * radius, y + size[1] / 2, sin(a) * radius), size, mat, yaw=-a + pi / 2, roll=tilt, bev=.03, segments=2)) return out def horn(name, angle, r0, y0, r_mid, y_top, r_top, width, thick, mat, bent=0.0): """Crescent horn in the vertical plane at `angle`: rises from (r0, y0), bows out to r_mid and curls back in to (r_top, y_top). Flat face shows sideways (tangent direction).""" d = V((cos(angle), 0, sin(angle))) t = V((-sin(angle), 0, cos(angle))) h = y_top - y0 local = [(r0, y0), (r_mid, y0 + h * .38), (r_mid * .86 + r_top * .14, y0 + h * .72), (r_top, y_top)] if bent: local[-1] = (r_top + bent, y_top - bent * .6) points = [d * r + V((0, y, 0)) for r, y in local] return L.sweep(name, points, [width, width * .9, width * .6, width * .12], [thick, thick * .9, thick * .7, thick * .2], mat, up_hint=tuple(t), steps=20, lens=.35) def slits(prefix, radius, ys, angles, mat, size=(.13, .44)): out = [] for j, y in enumerate(ys): for k, a in enumerate(angles): out.append(L.box('%s_%d_%d' % (prefix, j, k), (cos(a) * radius, y, sin(a) * radius), (size[0], size[1], .08), mat, yaw=-a + pi / 2, bev=.0)) return out def door(prefix, radius, mat_frame, mat_leaf, angle=-pi / 2, height=1.5, width=.76): """Arched door on a flat octagon face; `radius` is the face (apothem) distance.""" d = V((cos(angle), 0, sin(angle))) t = V((-sin(angle), 0, cos(angle))) c = d * radius yaw = -angle + pi / 2 parts = [L.box(prefix + '_Leaf', (c.x, height / 2 + .55, c.z), (width, height, .1), mat_leaf, yaw=yaw, bev=.01)] arch = [] for k in range(13): a = pi * k / 12 arch.append(tuple(c + d * .06 + t * cos(a) * width * .56 + V((0, .55 + height - .12 + sin(a) * width * .42, 0)))) parts.append(L.sweep(prefix + '_Arch', arch, [.13] * 3, [.14] * 3, mat_frame, up_hint=tuple(d), steps=14, smooth=False)) for sx in (-1, 1): post = c + d * .06 + t * sx * width * .56 parts.append(L.box('%s_Jamb_%d' % (prefix, sx), (post.x, .55 + (height - .12) / 2, post.z), (.14, height - .12, .14), mat_frame, yaw=yaw, bev=.015)) return parts def jagged_stump(name, radius_bottom, radius_top, y0, y_top, mat, seed, sides=8, bite=.55): """Octagonal shaft whose top ring is broken irregularly (ruin stump).""" verts, faces = [], [] levels = 5 for j in range(levels + 1): t = j / levels for k in range(sides): a = 2 * pi * k / sides + OCT r = radius_bottom * (1 - t) + radius_top * t y = y0 + (y_top - y0) * t if j == levels: y -= bite * (.5 + .5 * noise.noise(V((k * .9, seed, 1.7)))) + .25 * (1 if k % 3 == 0 else 0) r *= .9 + .12 * noise.noise(V((k, seed, 3))) elif j == levels - 1: y -= bite * .25 * abs(noise.noise(V((k * .9, seed, 2.3)))) verts.append((cos(a) * r, y, sin(a) * r)) for j in range(levels): for k in range(sides): a0, b0 = j * sides + k, j * sides + (k + 1) % sides faces.append((a0, b0, b0 + sides, a0 + sides)) faces.append(tuple(reversed(range(sides)))) faces.append(tuple(range(levels * sides, levels * sides + sides))) ob = L.make(name, verts, faces, mat) L._outward(ob) return ob def rubble(prefix, center, spread, count, size, mat, seed, y=0.0, outward_bias=None): """Scattered broken masonry chunks (ruin and damage debris), biased to one side.""" out = [] for k in range(count): a = 2 * pi * ((k * .618 + seed * .13) % 1.0) if outward_bias is not None: a = outward_bias + (a - pi) * .55 r = spread * (.35 + .65 * ((k * .377 + seed * .21) % 1.0)) s = size * (.55 + .6 * ((k * .733 + seed) % 1.0)) cx = center[0] + cos(a) * r cz = center[2] + sin(a) * r out.append(L.chunk('%s_%d' % (prefix, k), (cx, y + s * .22, cz), (s * .75, s * .42, s * .58), mat, seed=seed * 3.1 + k, yaw=a * 1.7 + seed, tilt=.3 * noise.noise(V((k, seed, 0))))) return out def shards(prefix, center, count, radius, mat, seed, spread=.9): out = [] for k in range(count): a = 2 * pi * ((k * .618 + seed * .07) % 1.0) r = spread * (.3 + .7 * ((k * .41 + seed) % 1.0)) c = (center[0] + cos(a) * r, center[1] + radius * .3, center[2] + sin(a) * r) out.append(L.crystal('%s_%d' % (prefix, k), c, radius * (.5 + .5 * ((k * .29) % 1.0)), radius * 1.6, radius * .5, mat, sides=5, tilt=(1.1 + .4 * noise.noise(V((k, seed, 0))), a), seed=k + seed)) return out # ================================================================ Grenzwacht (Tower 1) def grenzwacht(): L.reset() split = 2.2 lower = [ octa('Plinth', [(1.48, 0), (1.48, .26), (1.36, .32), (1.36, .46), (1.22, .52)], 'LN_Stone_Dark', jitter=.015, seed=1), octa('ShaftLow', [(1.02, .5), (.99, 1.2), (.955, split + .02)], 'LN_Stone_Pale'), octa('Course', [(1.07, .5), (1.07, .66), (1.02, .7)], 'LN_Stone_Dark', cap_bottom=False), ] lower += slits('SlitLow', .905, [1.55], [0.0, pi], 'LN_Stone_Dark') lower += door('Door', .9, 'LN_Stone_Dark', 'LN_Wood') upper = [ octa('ShaftHigh', [(.955, split - .02), (.9, 2.9), (.85, 3.7), (.87, 3.74)], 'LN_Stone_Pale'), octa('Band', [(.975, 2.34), (.975, 2.5)], 'LN_Metal_Team', cap_top=False, cap_bottom=False), octa('Runes', [(.905, 3.02), (.89, 3.3)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.34), octa('Corbel', [(.86, 3.7), (1.18, 3.96), (1.23, 4.2), (1.15, 4.26)], 'LN_Stone_Pale'), octa('Floor', [(1.0, 4.18), (1.0, 4.24)], 'LN_Stone_Dark'), ] upper += rivets('Rivet', .99, 2.42, 8, 'LN_Metal_Iron') upper += slits('SlitHigh', .84, [2.75], [0, pi / 2, pi, 3 * pi / 2], 'LN_Stone_Dark') upper += merlons('Merlon', 1.07, 4.22, 8, (.36, .34, .24), 'LN_Stone_Pale') horns = [horn('Horn_%d' % i, a, .6, 4.2, .82, 6.05, .24, .24, .15, 'LN_Metal_Team') for i, a in enumerate((0.0, pi))] collar = L.ring('Collar', (0, 4.62, 0), .38, .07, .08, 'LN_Metal_Team', sides=24) pole = L.box('PennantPole', (0, 5.0, -1.02), (.07, 1.7, .07), 'LN_Wood', bev=.01) upper += horns + [collar, pole] damaged = [ octa('D_ShaftHigh', [(.955, split - .02), (.9, 2.9), (.85, 3.7), (.87, 3.74)], 'LN_Stone_Pale'), octa('D_Band', [(.975, 2.34), (.975, 2.5)], 'LN_Metal_Team', cap_top=False, cap_bottom=False), octa('D_Runes', [(.905, 3.02), (.89, 3.3)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.34), octa('D_Corbel', [(.86, 3.7), (1.18, 3.96), (1.23, 4.2), (1.15, 4.26)], 'LN_Stone_Pale', jitter=.04, seed=5), octa('D_Floor', [(1.0, 4.18), (1.0, 4.24)], 'LN_Stone_Dark'), L.box('D_PennantPole', (0.12, 4.9, -1.0), (.07, 1.5, .07), 'LN_Wood', roll=.35, bev=.01), horn('D_Horn_0', 0.0, .6, 4.2, .82, 6.05, .24, .24, .15, 'LN_Metal_Team'), horn('D_Horn_1', pi, .6, 4.2, .86, 5.55, .62, .24, .15, 'LN_Metal_Team', bent=.35), L.ring('D_Collar', (0, 4.6, 0), .38, .07, .08, 'LN_Metal_Team', sides=24, axis_tilt=(.18, .1)), ] damaged += rivets('D_Rivet', .99, 2.42, 8, 'LN_Metal_Iron') damaged += merlons('D_Merlon', 1.07, 4.22, 8, (.36, .34, .24), 'LN_Stone_Pale', skip=(1, 2, 5), jitter=.25, seed=3) debris = rubble('Debris', (0, 0, 0), 1.9, 7, .34, 'LN_Stone_Pale', 3.0, outward_bias=-pi / 2) crystal = L.crystal('Crystal', (0, 5.2, 0), .34, .64, .5, 'LN_Crystal_Team', sides=6, seed=2) pennant = L.banner('Pennant', .62, 1.0, 'LN_Cloth_Team', top=(-.37, 5.72, -1.02), segments=(4, 8)) ruin = [ octa('R_Plinth', [(1.48, 0), (1.48, .26), (1.36, .32), (1.36, .46), (1.22, .52)], 'LN_Stone_Dark', jitter=.03, seed=1), jagged_stump('R_Stump', 1.0, .96, .5, 1.95, 'LN_Stone_Pale', 4.0), octa('R_Course', [(1.07, .5), (1.07, .66), (1.02, .7)], 'LN_Stone_Dark', cap_bottom=False), horn('R_Horn', 2.4, .3, .05, .5, .5, 1.6, .24, .15, 'LN_Metal_Team'), ] ruin += rubble('R_Rubble', (0, 0, 0), 2.4, 10, .62, 'LN_Stone_Pale', 7.0, outward_bias=-pi / 2) ruin += rubble('R_Dark', (0, 0, 0), 2.2, 6, .5, 'LN_Stone_Dark', 17.0, outward_bias=-pi / 2) ruin += rubble('R_Merlon', (0, 0, 0), 2.0, 4, .42, 'LN_Stone_Pale', 11.0, outward_bias=pi * .1) ruin += shards('R_Shard', (0, .35, -.9), 5, .2, 'LN_Crystal_Team', 5.0) return finish('grenzwacht', 'Grenzwacht', split, lower, upper, damaged, debris, [crystal], [pennant], ruin, 5.2, {}) # ================================================================ Bastion (Tower 2) def buttress(name, angle, r_in, r_out, h_in, h_out, width, mat): d = V((cos(angle), 0, sin(angle))) t = V((-sin(angle), 0, cos(angle))) verts = [] for r, h in ((r_in, h_in), (r_out, h_out)): for s in (-1, 1): base = d * r + t * s * width / 2 verts.append((base.x, 0.0, base.z)) verts.append((base.x, h, base.z)) # verts: 0 in-left-bottom, 1 in-left-top, 2 in-right-bottom, 3 in-right-top, 4..7 outer. faces = [(0, 2, 6, 4), (1, 5, 7, 3), (0, 4, 5, 1), (2, 3, 7, 6), (0, 1, 3, 2), (4, 6, 7, 5)] ob = L.make(name, verts, faces, mat) L._outward(ob) L.bevel(ob, .04, 1, 30.0) return ob def shield(name, angle, radius, y, mat, tilt=0.0, drop=0.0): d = V((cos(angle), 0, sin(angle))) outline = [(-.34, .42), (.34, .42), (.3, -.18), (0, -.52), (-.3, -.18)] verts, faces = [], [] t = V((-sin(angle), 0, cos(angle))) for depth in (0.0, .07): for x, yy in outline: p = d * (radius + depth) + t * x + V((0, y + yy - drop, 0)) verts.append(tuple(p)) n = len(outline) faces = [(i, (i + 1) % n, n + (i + 1) % n, n + i) for i in range(n)] + [tuple(reversed(range(n))), tuple(range(n, 2 * n))] ob = L.make(name, verts, faces, mat) L._outward(ob) L.bevel(ob, .025, 2, 30.0) if tilt: ob.rotation_euler = (0, tilt, 0) return ob def bastion(): L.reset() split = 2.6 back = -pi / 2 lower = [ octa('Plinth', [(1.78, 0), (1.78, .34), (1.66, .4), (1.66, .62), (1.52, .7)], 'LN_Stone_Dark', jitter=.015, seed=2), octa('Talus', [(1.5, .68), (1.43, 1.25), (1.31, 1.42)], 'LN_Stone_Dark', cap_bottom=False), octa('ShaftLow', [(1.31, 1.4), (1.28, 2.0), (1.26, split + .02)], 'LN_Stone_Pale'), octa('BandLow', [(1.35, 1.52), (1.35, 1.72)], 'LN_Metal_Team', cap_top=False, cap_bottom=False), ] lower += rivets('RivetLow', 1.36, 1.62, 8, 'LN_Metal_Iron') for i, a in enumerate((back - pi / 4, back, back + pi / 4)): lower.append(buttress('Buttress_%d' % i, a, 1.1, 2.25, 2.7, .75, .5, 'LN_Stone_Dark')) def upper_parts(prefix, broken=False): parts = [ octa(prefix + 'ShaftHigh', [(1.26, split - .02), (1.22, 3.5), (1.18, 4.42)], 'LN_Stone_Pale'), octa(prefix + 'BandHigh', [(1.29, 3.28), (1.29, 3.46)], 'LN_Metal_Team', cap_top=False, cap_bottom=False), octa(prefix + 'Runes', [(1.23, 3.84), (1.21, 4.12)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.36), octa(prefix + 'Parapet', [(1.18, 4.4), (1.56, 4.62), (1.6, 5.0), (1.5, 5.06)], 'LN_Stone_Pale', jitter=.05 if broken else 0.0, seed=8), octa(prefix + 'Deck', [(1.36, 4.96), (1.36, 5.03)], 'LN_Stone_Dark'), L.lathe(prefix + 'Plate', [(.62, 5.0), (.58, 5.18), (.44, 5.24)], 16, 'LN_Metal_Iron'), ] parts += rivets(prefix + 'RivetHigh', 1.3, 3.37, 8, 'LN_Metal_Iron') for k in range(8): a = 2 * pi * (k + .5) / 8 parts.append(L.box('%sCorbel_%d' % (prefix, k), (cos(a) * 1.36, 4.48, sin(a) * 1.36), (.3, .28, .36), 'LN_Stone_Dark', yaw=-a + pi / 2, bev=.03)) parts += merlons(prefix + 'Merlon', 1.42, 5.0, 8, (.5, .5, .3), 'LN_Stone_Pale', skip=(0, 3, 4) if broken else (), jitter=.2 if broken else 0.0, seed=4) lane = pi / 2 for i, a in enumerate((lane - pi / 4, lane, lane + pi / 4)): loose = broken and i == 2 parts.append(shield('%sShield_%d' % (prefix, i), a, 1.17, 2.95, 'LN_Metal_Team', drop=.0 if not loose else .12)) for i, a in enumerate((pi / 6, pi / 6 + 2 * pi / 3, pi / 6 + 4 * pi / 3)): if broken and i == 1: parts.append(horn(prefix + 'Horn_%d' % i, a, .52, 5.0, .78, 6.2, .5, .26, .17, 'LN_Metal_Team', bent=.42)) else: parts.append(horn(prefix + 'Horn_%d' % i, a, .52, 5.0, .82, 7.15, .2, .26, .17, 'LN_Metal_Team')) parts.append(L.box(prefix + 'PennantPole', (.0, 5.9, -1.46), (.08, 1.9, .08), 'LN_Wood', roll=.3 if broken else 0.0, bev=.01)) return parts upper = upper_parts('') damaged = upper_parts('D_', True) debris = rubble('Debris', (0, 0, 0), 2.3, 9, .42, 'LN_Stone_Pale', 5.0, outward_bias=-pi / 2) debris += [shield('Debris_Shield', 1.2, 1.9, .12, 'LN_Metal_Team', drop=0.0)] crystal = L.crystal('Crystal', (0, 6.2, 0), .48, .86, .66, 'LN_Crystal_Team', sides=6, seed=6) ring = L.ring('Ring', (0, 6.2, 0), .82, .09, .1, 'LN_Metal_Team', sides=40, uv_cell=.3) shard_objs = [] for i in range(3): a = 2 * pi * i / 3 shard_objs.append(L.crystal('Shard_%d' % i, (cos(a) * 1.02, 6.35, sin(a) * 1.02), .11, .26, .18, 'LN_Crystal_Team', sides=5, seed=10 + i)) pennant = L.banner('Pennant', .7, 1.15, 'LN_Cloth_Team', top=(-.41, 6.8, -1.46), segments=(4, 8)) ruin = [ octa('R_Plinth', [(1.78, 0), (1.78, .34), (1.66, .4), (1.66, .62), (1.52, .7)], 'LN_Stone_Dark', jitter=.03, seed=2), octa('R_Talus', [(1.5, .68), (1.43, 1.25), (1.31, 1.42)], 'LN_Stone_Dark', cap_bottom=False, jitter=.03), jagged_stump('R_Stump', 1.3, 1.24, 1.4, 2.9, 'LN_Stone_Pale', 6.0, bite=.7), buttress('R_Buttress_0', back - pi / 4, 1.1, 2.25, 2.1, .75, .5, 'LN_Stone_Dark'), buttress('R_Buttress_1', back, 1.1, 2.25, 1.5, .6, .5, 'LN_Stone_Dark'), shield('R_Shield', 2.1, 2.0, .14, 'LN_Metal_Team'), horn('R_Horn', .5, .5, .05, .6, .45, 1.9, .26, .17, 'LN_Metal_Team'), ] ruin += rubble('R_Rubble', (0, 0, 0), 2.7, 12, .7, 'LN_Stone_Pale', 9.0, outward_bias=-pi / 2) ruin += rubble('R_Dark', (0, 0, 0), 2.4, 7, .6, 'LN_Stone_Dark', 19.0, outward_bias=-pi / 2) ruin += rubble('R_Merlon', (0, 0, 0), 2.3, 5, .5, 'LN_Stone_Pale', 13.0, outward_bias=pi * .15) ruin += shards('R_Shard', (0, .4, -1.0), 7, .24, 'LN_Crystal_Team', 8.0) return finish('bastion', 'Bastion', split, lower, upper, damaged, debris, [crystal, ring] + shard_objs, [pennant], ruin, 6.2, {}) # ================================================================ Kernwacht (base tower) BLADES = (pi / 2, pi / 2 + 2 * pi / 3, pi / 2 + 4 * pi / 3) def blade(name, angle, mat, y0=2.62, broken=False, lower_only=False): """One of three crescent blades: from the drum up and out, curling in to the shared apex. Lies in the vertical plane at `angle`; the broad face shows tangentially.""" d = V((cos(angle), 0, sin(angle))) t = V((-sin(angle), 0, cos(angle))) local = [(.72, y0), (1.3, 4.1), (1.42, 5.3), (1.04, 6.7), (.2, 7.66)] if lower_only: local = local[:3] if broken: local = [(.72, y0), (1.3, 4.1), (1.42, 5.3), (1.3, 5.9)] widths = [.46, .44, .38, .28, .16][:len(local)] depths = [.66, .6, .52, .42, .3][:len(local)] return L.sweep(name, [d * r + V((0, y, 0)) for r, y in local], widths, depths, mat, up_hint=tuple(t), steps=26, lens=.25) def edge_strip(name, angle, mat, broken=False): d = V((cos(angle), 0, sin(angle))) t = V((-sin(angle), 0, cos(angle))) local = [(.96, 2.72), (1.56, 4.1), (1.66, 5.3), (1.24, 6.75), (.28, 7.8)] if broken: local = local[:3] + [(1.52, 5.92)] return L.sweep(name, [d * r + V((0, y, 0)) for r, y in local], [.1] * len(local), [.46, .42, .36, .28, .2][:len(local)], mat, up_hint=tuple(t), steps=26, lens=.1) def kernwacht(): L.reset() split = 2.7 lower = [ octa('Plinth', [(1.95, 0), (1.95, .3), (1.8, .36), (1.8, .62), (1.62, .7)], 'LN_Stone_Dark', jitter=.012, seed=3), octa('Drum', [(1.28, .68), (1.24, 1.4), (1.12, 2.4), (1.06, split + .02)], 'LN_Stone_Pale'), octa('DrumCap', [(1.14, 2.55), (1.2, 2.66), (1.1, split + .06)], 'LN_Stone_Dark'), octa('Runes', [(1.272, 1.02), (1.192, 1.9)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.46), ] for k in range(8): a = 2 * pi * k / 8 lower.append(L.box('Pilaster_%d' % k, (cos(a) * 1.19, 1.45, sin(a) * 1.19), (.2, 1.5, .14), 'LN_Stone_Dark', yaw=-a + pi / 2, bev=.02)) def upper_parts(prefix, broken=False): parts = [ L.ring(prefix + 'Crown', (0, 7.5, 0), .42, .12, .2, 'LN_Metal_Team', sides=32, uv_cell=.25), L.crystal(prefix + 'Apex', (0, 7.95, 0), .16, .38, .14, 'LN_Crystal_Team', sides=5, seed=4), L.lathe(prefix + 'Seat', [(.6, split - .02), (.72, split + .12), (.5, split + .2)], 16, 'LN_Metal_Team'), ] for i, a in enumerate(BLADES): snapped = broken and i == 2 parts.append(blade('%sBlade_%d' % (prefix, i), a, 'LN_Stone_Pale', broken=snapped)) parts.append(edge_strip('%sEdge_%d' % (prefix, i), a, 'LN_Metal_Team', broken=snapped)) for i in (1, 2): a = BLADES[i] d = V((cos(a), 0, sin(a))) t = V((-sin(a), 0, cos(a))) at = d * 1.72 + V((0, 5.55, 0)) parts.append(L.box('%sBannerArm_%d' % (prefix, i), tuple(at), (.08, .08, .5), 'LN_Metal_Iron', yaw=-a, bev=.01)) return parts upper = upper_parts('') damaged = upper_parts('D_', True) debris = rubble('Debris', (0, 0, 0), 2.4, 8, .4, 'LN_Stone_Pale', 2.0, outward_bias=-pi / 2) crystal = L.crystal('Crystal', (0, 5.25, 0), .56, 1.0, .74, 'LN_Crystal_Team', sides=6, seed=9) ring = L.ring('Ring', (0, 5.25, 0), .98, .1, .11, 'LN_Metal_Team', sides=40, uv_cell=.3) banners = [] for i, key in ((1, 'Banner_L'), (2, 'Banner_R')): a = BLADES[i] d = V((cos(a), 0, sin(a))) top = d * 1.98 + V((0, 5.5, 0)) banners.append(L.banner(key, .62, 1.6, 'LN_Cloth_Team', top=tuple(top), segments=(4, 10), yaw=-(a + pi / 2))) ruin = [ octa('R_Plinth', [(1.95, 0), (1.95, .3), (1.8, .36), (1.8, .62), (1.62, .7)], 'LN_Stone_Dark', jitter=.03, seed=3), jagged_stump('R_Drum', 1.28, 1.14, .68, 2.2, 'LN_Stone_Pale', 3.0, bite=.5), octa('R_Runes', [(1.272, 1.02), (1.225, 1.6)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.46), ] fallen = blade('R_Blade', pi / 2, 'LN_Stone_Pale') fallen.data.transform(Matrix.Rotation(pi / 2 - .12, 4, 'Y')) fallen.location = L.G(-1.6, -2.3, -1.2) ruin.append(fallen) ruin += rubble('R_Rubble', (0, 0, 0), 2.8, 12, .66, 'LN_Stone_Pale', 5.0, outward_bias=-pi / 2) ruin += rubble('R_Dark', (0, 0, 0), 2.4, 7, .58, 'LN_Stone_Dark', 15.0, outward_bias=-pi / 2) ruin += shards('R_Shard', (0, .4, -1.0), 7, .26, 'LN_Crystal_Team', 3.0) return finish('kernwacht', 'Kernwacht', split, lower, upper, damaged, debris, [crystal, ring], banners, ruin, 5.25, {}) # ================================================================ assembly def finish(key, title, split, lower, upper, damaged, debris, energy, cloth, ruin, crystal_y, extra): for ob in lower + upper + damaged + debris + ruin: ob.data.update() lower_ob = L.join(lower, 'Lower') upper_ob = L.join(upper, 'Upper', origin=(0, split, 0)) damaged_ob = L.join(damaged, 'Upper_Damaged', origin=(0, split, 0)) debris_ob = L.join(debris, 'Debris') ruin_ob = L.join(ruin, 'Ruin') named = [] for ob in energy: name = ob.name c = sum((ob.matrix_world @ v.co for v in ob.data.vertices), V()) / len(ob.data.vertices) g = L.to_godot(c) if name.startswith('Ring'): g = (0.0, crystal_y, 0.0) named.append(L.join([ob], name, origin=g)) cloth_obs = [L.join([ob], ob.name) for ob in cloth] intact = [lower_ob, upper_ob] + named L.finalize(intact, occluders=intact) L.finalize([damaged_ob], occluders=[lower_ob, damaged_ob] + named) L.finalize([debris_ob], occluders=[lower_ob, debris_ob]) L.finalize([ruin_ob], occluders=[ruin_ob]) L.finalize(cloth_obs, ao=False) objects = intact + [damaged_ob, debris_ob, ruin_ob] + cloth_obs for ob in objects: L.triangulate(ob) doc, raw = L.export(objects, key + '.glb', key + '.blend') entry = L.record(key, key + '.glb', key + '.blend', 'build_towers.py', [lower_ob, upper_ob] + named, doc, raw, title=title, split_height=split, crystal_height=crystal_y, triangles_intact=L.triangles(intact + cloth_obs), triangles_ruin=L.triangles([ruin_ob]), **extra) print('TOWER_BUILD', key, entry['triangles'], 'triangles (intact %d, ruin %d)' % (entry['triangles_intact'], entry['triangles_ruin']), 'height %.2f' % entry['bounds_godot']['max'][1], flush=True) return entry only = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else [] entries = [] for key, fn in (('grenzwacht', grenzwacht), ('bastion', bastion), ('kernwacht', kernwacht)): if only and key not in only: continue entries.append(fn()) L.write_manifest(entries)