Files
airdef/tools/gen_models.py
T

940 lines
40 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Генерирует контент мода AirDef (airdef-mod/assets и data): текстуры, модели частей, предметы.
Каждая часть — блок со своей моделью в одной ориентации: нос смотрит на север (-z), центр — (8, 8, 8).
Поворачивает и масштабирует их движок форка (render.set-models), так что ракурсы больше не нужны.
Координаты элементов — в пределах -16..32 (иначе Horizon отбрасывает модель).
Запуск: python3 tools/gen_models.py
"""
import json
import os
import random
import shutil
from PIL import Image
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "airdef-mod"))
NS = "airdef"
# ---------------------------------------------------------------- текстуры
def clamp(v):
return max(0, min(255, int(v)))
def noise(base, amp, seed, size=16):
rnd = random.Random(seed)
im = Image.new("RGBA", (size, size))
for y in range(size):
for x in range(size):
d = rnd.randint(-amp, amp)
im.putpixel((x, y), tuple(clamp(c + d) for c in base) + (255,))
return im
def panels(base, seed, size=16, every=8):
im = noise(base, 4, seed, size)
line = tuple(clamp(c - 16) for c in base) + (255,)
lite = tuple(clamp(c + 10) for c in base) + (255,)
for i in range(size):
for k in range(0, size, every):
im.putpixel((i, k), line)
im.putpixel((k, i), line)
if k + 1 < size:
im.putpixel((i, k + 1), lite)
rnd = random.Random(seed + 99)
for _ in range(size // 2): # заклёпки
x, y = rnd.randrange(size), rnd.randrange(size)
im.putpixel((x, y), lite)
return im
def camo(colors, seed, size=32, blobs=7):
rnd = random.Random(seed)
im = noise(colors[0], 4, seed, size)
for col in colors[1:]:
for _ in range(blobs):
cx, cy = rnd.randrange(size), rnd.randrange(size)
rx, ry = rnd.randint(2, 6), rnd.randint(1, 4)
for y in range(cy - ry, cy + ry + 1):
for x in range(cx - rx, cx + rx + 1):
if ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 + rnd.random() * 0.3:
im.putpixel((x % size, y % size), tuple(clamp(c + rnd.randint(-5, 5)) for c in col) + (255,))
return im
def stripes(base, line, step, seed, size=16):
im = noise(base, 3, seed, size)
for y in range(0, size, step):
for x in range(size):
im.putpixel((x, y), line + (255,))
return im
def gradient(top, bottom, size=16):
im = Image.new("RGBA", (size, size))
for y in range(size):
t = y / (size - 1)
c = tuple(clamp(top[i] + (bottom[i] - top[i]) * t) for i in range(3))
for x in range(size):
im.putpixel((x, y), c + (255,))
return im
def canister_front():
im = noise((92, 100, 72), 3, 11)
for cx in (4, 11):
for cy in (4, 11):
for y in range(16):
for x in range(16):
d = (x - cx + 0.5) ** 2 + (y - cy + 0.5) ** 2
if d <= 9:
im.putpixel((x, y), (36, 38, 30, 255))
elif d <= 12:
im.putpixel((x, y), (120, 124, 100, 255))
return im
def radar_face():
im = noise((70, 76, 56), 3, 17)
for y in range(1, 15):
for x in range(1, 15):
if (x + y) % 3 == 0:
im.putpixel((x, y), (54, 58, 44, 255))
return im
def glass():
im = noise((40, 58, 78), 5, 31)
for i in range(16):
im.putpixel((i, 15 - i), (130, 160, 180, 255))
if i < 15:
im.putpixel((i + 1, 15 - i), (100, 130, 150, 255))
return im
def missile():
im = noise((232, 232, 226), 3, 21)
for x in range(16):
for y in (3, 4):
im.putpixel((x, y), (40, 40, 40, 255))
return im
def target():
im = Image.new("RGBA", (16, 16))
for y in range(16):
for x in range(16):
d = abs(x - 7.5) + abs(y - 7.5)
im.putpixel((x, y), (255, clamp(40 + d * 12), 40, 255))
return im
def fire(seed):
rnd = random.Random(seed)
im = Image.new("RGBA", (16, 16))
for y in range(16):
for x in range(16):
t = rnd.random()
im.putpixel((x, y), (255, clamp(140 + 110 * t), clamp(30 + 120 * t * t), 255))
return im
def missile_ru():
im = noise((226, 228, 222), 3, 49)
for x in range(16):
im.putpixel((x, 5), (180, 40, 40, 255))
im.putpixel((x, 11), (40, 40, 40, 255))
return im
def tube_cap():
im = noise((80, 88, 60), 3, 50)
for y in range(16):
for x in range(16):
d = (x - 7.5) ** 2 + (y - 7.5) ** 2
if d <= 36:
im.putpixel((x, y), (44, 48, 36, 255))
elif d <= 48:
im.putpixel((x, y), (120, 128, 96, 255))
return im
def tubes6():
im = noise((82, 96, 60), 3, 51)
for cx in (3, 8, 13):
for cy in (4, 11):
for y in range(16):
for x in range(16):
if (x - cx) ** 2 + (y - cy) ** 2 <= 4:
im.putpixel((x, y), (30, 32, 26, 255))
return im
def radar_big():
im = noise((92, 100, 70), 3, 52)
for y in range(16):
for x in range(16):
if x % 2 == 0 and y % 2 == 0:
im.putpixel((x, y), (60, 66, 46, 255))
return im
TEXTURES = {
"shahed": lambda: panels((66, 68, 70), 1),
"shahed_nose": lambda: noise((104, 106, 108), 4, 2),
"geran": lambda: panels((28, 28, 31), 3),
"geran_engine": lambda: panels((60, 60, 64), 4),
"intake": lambda: noise((14, 14, 14), 2, 5),
"prop": lambda: noise((22, 22, 22), 3, 6),
"missile": missile,
"nozzle": lambda: noise((44, 42, 40), 4, 7),
"flame": lambda: gradient((255, 250, 210), (255, 120, 20)),
"fire": lambda: fire(8),
"nato_green": lambda: camo([(76, 86, 58), (58, 68, 44), (44, 48, 34)], 9),
"flecktarn": lambda: camo([(92, 100, 62), (62, 74, 44), (104, 84, 54), (30, 32, 26)], 10),
"track": lambda: stripes((36, 36, 34), (74, 74, 70), 3, 11),
"tire": lambda: stripes((24, 24, 24), (40, 40, 40), 4, 12),
"canister": lambda: panels((94, 102, 74), 13),
"canister_front": canister_front,
"radar_face": radar_face,
"glass": glass,
"metal": lambda: panels((118, 120, 122), 14),
"dark_metal": lambda: panels((52, 54, 52), 15),
"barrel": lambda: noise((40, 42, 38), 3, 16),
"dish": lambda: noise((176, 178, 174), 5, 18),
"target": target,
"foam": lambda: panels((196, 180, 150), 40),
"kh101": lambda: panels((84, 88, 92), 41),
"kh101_belly": lambda: noise((150, 152, 150), 4, 42),
"lancet": lambda: panels((70, 78, 66), 43),
"ru_green": lambda: camo([(82, 96, 60), (54, 70, 40), (110, 96, 64), (34, 36, 28)], 44, blobs=6),
"ru_khaki": lambda: camo([(108, 110, 80), (84, 90, 60), (60, 64, 44)], 45, blobs=5),
"missile_ru": missile_ru,
"missile_grey": lambda: panels((150, 154, 156), 46),
"booster": lambda: noise((60, 62, 58), 3, 47),
"tube": lambda: stripes((96, 106, 74), (70, 78, 54), 4, 48),
"tube_cap": tube_cap,
"tubes6": tubes6,
"radar_big": radar_big,
"tracer": lambda: noise((255, 40, 20), 6, 60),
"tracer_hot": lambda: noise((255, 190, 150), 6, 61),
"white": lambda: noise((250, 250, 246), 2, 19),
"nato_tan": lambda: camo([(150, 130, 96), (124, 106, 76), (96, 84, 62)], 62, blobs=5),
"crate": lambda: stripes((92, 78, 48), (70, 58, 34), 5, 63),
"concrete": lambda: panels((132, 130, 124), 64),
"lamp": lambda: gradient((255, 255, 240), (255, 230, 170)),
"beam": lambda: gradient((255, 250, 225), (255, 240, 200)),
"antenna": lambda: noise((70, 72, 70), 3, 65),
"nose_dark": lambda: noise((58, 60, 62), 3, 66),
}
# ---------------------------------------------------------------- модели
def el(frm, to, tex, faces=None, rot=None):
"""Элемент; rot = (ось, угол) — поворот вокруг центра элемента."""
return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex, "faces": faces, "rot": rot}
def mx(e):
"""Зеркальная копия относительно x = 8."""
rot = e.get("rot")
if rot and rot[0] in ("y", "z"):
rot = (rot[0], -rot[1])
return {**e, "from": [16 - e["to"][0], e["from"][1], e["from"][2]],
"to": [16 - e["from"][0], e["to"][1], e["to"][2]], "rot": rot}
def both(e):
return [e, mx(e)]
def face_uv(frm, to, face):
dx, dy, dz = (abs(to[i] - frm[i]) for i in range(3))
w, h = {"north": (dx, dy), "south": (dx, dy), "east": (dz, dy), "west": (dz, dy),
"up": (dx, dz), "down": (dx, dz)}[face]
return [0, 0, max(0.25, min(16, w)), max(0.25, min(16, h))]
def model(parts):
for p in parts:
for i in range(3):
assert -16 <= p["from"][i] <= 32 and -16 <= p["to"][i] <= 32, p
textures, elements = {}, []
for p in parts:
textures[p["tex"]] = f"{NS}:block/{p['tex']}"
faces = {}
for f in ("north", "south", "east", "west", "up", "down"):
tex = (p["faces"] or {}).get(f, p["tex"])
textures[tex] = f"{NS}:block/{tex}"
faces[f] = {"uv": face_uv(p["from"], p["to"], f), "texture": "#" + tex}
e = {"from": p["from"], "to": p["to"], "faces": faces}
if p.get("rot"):
axis, angle = p["rot"]
origin = [(p["from"][i] + p["to"][i]) / 2 for i in range(3)]
e["rotation"] = {"angle": angle, "axis": axis, "origin": origin}
elements.append(e)
textures["particle"] = f"{NS}:block/{parts[0]['tex']}"
return {"ambientocclusion": False, "textures": textures, "elements": elements}
def taper(tex, z0, z1, w0, w1, h0, h1, steps, y=8.0):
"""Корпус-веретено лесенкой: ширина w0→w1 и высота h0→h1 вдоль z0→z1."""
out = []
for i in range(steps):
za = z0 + (z1 - z0) * i / steps
zb = z0 + (z1 - z0) * (i + 1) / steps
t = (i + 0.5) / steps
w, h = w0 + (w1 - w0) * t, h0 + (h1 - h0) * t
out.append(el((8 - w / 2, y - h / 2, za), (8 + w / 2, y + h / 2, zb), tex))
return out
def delta(tex, z_root, z_tip, z_trail, half, steps, y0, y1):
"""Дельта-крыло: передняя кромка от корня (z_root) к законцовке (half, z_tip), задняя кромка — z_trail."""
out = []
for i in range(steps):
za = z_root + (z_tip - z_root) * i / steps
s = half * (i + 1) / steps
out.append(el((8 - s, y0, za), (8 + s, y1, z_trail), tex))
return out
def shahed():
p = taper("shahed_nose", -16, -10, 2.5, 5, 2.5, 5, 3)
p += [el((5.5, 5.5, -10), (10.5, 10.5, 24), "shahed")]
p += [el((6, 10.5, -6), (10, 11.5, 4), "dark_metal")] # люк ГСН/антенны
p += delta("shahed", -2, 21, 26, 21, 9, 7.2, 8.4)
p += both(el((-13.6, 6.5, 17), (-12.6, 14, 26), "shahed")) # законцовки-кили
p += [el((6, 6, 24), (10, 10, 29), "dark_metal")] # двигатель
p += both(el((6.5, 4, 25), (7.5, 6, 28), "dark_metal")) # выхлопы
return p
def shahed_prop():
return [el((7.6, -4, 7.5), (8.4, 20, 8.5), "prop"), el((-4, 7.6, 7.5), (20, 8.4, 8.5), "prop"),
el((7, 7, 7), (9, 9, 9.5), "dark_metal")]
def geran3():
p = taper("geran", -16, -9, 2.5, 5.5, 2.5, 5.5, 3)
p += [el((5, 5, -9), (11, 11, 26), "geran")]
p += delta("geran", 0, 20, 26, 18, 8, 7.2, 8.4)
p += both(el((-10.6, 6.5, 18), (-9.6, 13, 26), "geran"))
p += [el((4.5, 11, 6), (11.5, 16, 28), "geran_engine"), # реактивный двигатель сверху
el((5, 11.5, 5), (11, 15.5, 6), "intake"),
el((5.5, 11.5, 28), (10.5, 15.5, 30), "nozzle")]
return p
def geran3_flame():
return taper("flame", 6, 24, 4, 1, 4, 1, 5)
def pac3():
p = taper("missile", -16, -11, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -11), (9.5, 9.5, 26), "missile")]
p += both(el((1.5, 7.6, 20), (6.5, 8.4, 26), "missile"))
p += [el((7.6, 1.5, 20), (8.4, 6.5, 26), "missile"), el((7.6, 9.5, 20), (8.4, 14.5, 26), "missile")]
p += both(el((4.5, 7.7, -6), (6.5, 8.3, -2), "missile")) # рули ГСН
p += [el((7, 7, 26), (9, 9, 28), "nozzle")]
return p
def irist_missile():
"""IRIS-T SLM: тонкая белая ракета, две пары малых канардов у носа и четыре хвостовых киля."""
p = taper("white", -16, -10, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -10), (9.5, 9.5, 24), "white")]
p += both(el((3, 7.6, -9), (6.5, 8.4, -5), "missile_grey")) # канарды у носа
p += [el((7.6, 3, -9), (8.4, 6.5, -5), "missile_grey"), el((7.6, 9.5, -9), (8.4, 13, -5), "missile_grey")]
p += both(el((2, 7.6, 18), (6.5, 8.4, 26), "white")) # хвостовые киля
p += [el((7.6, 2, 18), (8.4, 6.5, 26), "white"), el((7.6, 9.5, 18), (8.4, 14, 26), "white")]
p += [el((7, 7, 24), (9, 9, 28), "nozzle")]
return p
def amraam_missile():
"""AMRAAM: белое тело с тёмным носом, четыре крыла на середине корпуса и четыре хвостовых киля."""
p = taper("nose_dark", -16, -9, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -9), (9.5, 9.5, 26), "white")]
p += both(el((1, 7.6, 2), (6.5, 8.4, 9), "white")) # среднекорпусные крылья
p += [el((7.6, 1, 2), (8.4, 6.5, 9), "white"), el((7.6, 9.5, 2), (8.4, 15, 9), "white")]
p += both(el((4, 7.6, 19), (6.5, 8.4, 25), "white")) # хвостовые киля
p += [el((7.6, 4, 19), (8.4, 6.5, 25), "white"), el((7.6, 9.5, 19), (8.4, 12, 25), "white")]
p += [el((7, 7, 26), (9, 9, 28), "nozzle")]
return p
def flame():
"""Факел двигателя: светящаяся «капля», хвост — на юг (+z)."""
return taper("flame", -8, 26, 5, 1, 5, 1, 8)
def patriot_launcher():
"""Пакет 4 ТПК M903. Ось качания — задняя нижняя кромка (z = 30, y = 0)."""
p = [el((-6, 1, -14), (22, 19, 30), "canister", {"north": "canister_front"})]
p += [el((-6.5, 9.5, -14), (22.5, 10.5, 30), "dark_metal"),
el((7.5, 1, -14), (8.5, 19, 30), "dark_metal"),
el((-7, 0, 24), (23, 2, 30), "metal")]
p += both(el((-8, 2, -10), (-6, 17, -6), "metal")) + both(el((-8, 2, 20), (-6, 17, 24), "metal"))
return p
def patriot_truck():
"""Тягач (HEMTT): кабина, рама, колёса. Длина 3 блока при масштабе 1."""
p = [el((-6, 5, -16), (22, 9, 32), "nato_green"),
el((-6, 9, -15), (22, 24, -1), "nato_green"),
el((-5, 16, -16), (21, 23, -15), "glass"),
el((-6.2, 16, -13), (-6, 23, -4), "glass"), el((22, 16, -13), (22.2, 23, -4), "glass"),
el((-6, 9, 2), (22, 15, 16), "nato_green"),
el((-2, 24, -12), (18, 25, -4), "dark_metal"),
el((2, 9, 20), (14, 13, 30), "metal")]
for z in (-12, 14, 23):
p += both(el((-9, 0, z), (-5.5, 9, z + 8), "tire"))
return p
def patriot_trailer():
p = [el((-7, 6, -16), (23, 11, 32), "nato_green"),
el((-1, 11, 14), (17, 16, 30), "metal"),
el((-8, 3, -16), (24, 7, -12), "dark_metal")]
for z in (14, 23):
p += both(el((-10, 0, z), (-6.5, 9, z + 8), "tire"))
p += both(el((-9, 0, -6), (-7, 6, -4), "metal")) # опоры
return p
def patriot_radar():
"""РЛС AN/MPQ-65: фазированная решётка на прицепе."""
p = [el((-6, 6, -10), (22, 10, 26), "nato_green"),
el((-4, 10, -6), (20, 26, 8), "nato_green"),
el((-5, 12, -7), (21, 30, -6), "radar_face"),
el((-2, 10, 10), (18, 18, 24), "metal")]
for z in (14,):
p += both(el((-9, 0, z), (-5.5, 9, z + 8), "tire"))
return p
def gepard_hull():
p = [el((-10, 4, -12), (26, 17, 30), "flecktarn"),
el((-10, 4, -16), (26, 12, -12), "flecktarn"),
el((-8, 17, 18), (24, 18, 30), "dark_metal"),
el((-6, 17, -10), (4, 18, -4), "metal")]
p += both(el((-14, 0, -14), (-10, 10, 31), "track"))
p += both(el((-14.5, 6, -14), (-10, 11, 31), "flecktarn")) # надгусеничные полки
return p
def gepard_turret():
p = [el((-4, 0, -6), (20, 12, 16), "flecktarn"),
el((-2, 12, -2), (18, 14, 14), "flecktarn"),
el((3, 4, -10), (13, 12, -6), "radar_face"), # РЛС сопровождения
el((7, 14, 10), (9, 18, 12), "metal")]
return p
def gepard_guns():
"""Две 35-мм пушки по бортам башни; ось качания — центр модели."""
p = both(el((-8, 5, -16), (-5.5, 7.5, 12), "barrel"))
p += both(el((-9, 3.5, 2), (-4, 9, 14), "flecktarn"))
p += both(el((-8.5, 4.5, -16), (-5, 8, -13), "dark_metal")) # дульные тормоза
return p
def gepard_dish():
"""Обзорная РЛС: вращается вокруг вертикальной оси."""
return [el((-4, 6, 6), (20, 14, 8), "dish"), el((7, 0, 6), (9, 8, 8), "metal")]
def target_marker():
return [el((7, -8, 7), (9, 24, 9), "target"),
el((2, 7.5, 2), (14, 8.5, 14), "target"),
el((5, 3, 5), (11, 13, 11), "target")]
def fireball():
return [el((2, 2, 2), (14, 14, 14), "fire"), el((0, 4, 4), (16, 12, 12), "fire"),
el((4, 0, 4), (12, 16, 12), "fire"), el((4, 4, 0), (12, 12, 16), "fire")]
def flash():
return [el((7, 7, -4), (9, 9, 20), "white"), el((-4, 7, 7), (20, 9, 9), "white"),
el((7, -4, 7), (9, 20, 9), "white"), el((5, 5, 5), (11, 11, 11), "flame")]
def debris():
return [el((5, 6, 3), (11, 9, 13), "dark_metal"), el((2, 7, 6), (14, 8, 10), "shahed")]
def lancet():
p = taper("lancet", -16, -10, 2, 3.5, 2, 3.5, 3)
p += [el((6.2, 6.2, -10), (9.8, 9.8, 20), "lancet"),
el((6.8, 4.8, -9), (9.2, 6.2, -5), "intake")] # оптика
for z0, z1, half in ((-5, 1, 13), (12, 18, 11)): # Х-образные крылья
for ang in (45, -45):
p.append(el((8 - half, 7.7, z0), (8 + half, 8.3, z1), "lancet", rot=("z", ang)))
p += [el((6.8, 6.8, 20), (9.2, 9.2, 22), "dark_metal")]
return p
def gerbera():
p = taper("foam", -15, -10, 2.5, 4.5, 2.5, 4.5, 3)
p += [el((5.8, 5.8, -10), (10.2, 10.2, 20), "foam")]
p += delta("foam", 0, 18, 22, 17, 7, 7.3, 8.5)
p += both(el((-9.6, 6.5, 15), (-8.6, 13, 22), "foam"))
p += [el((6.5, 6.5, 20), (9.5, 9.5, 24), "dark_metal")]
return p
def kh101():
p = taper("kh101", -16, -9, 2, 6, 2, 4.5, 4, y=7.5)
p += [el((5, 5.5, -9), (11, 8, 30), "kh101", {"down": "kh101_belly"}),
el((6, 8, -6), (10, 10.5, 28), "kh101")]
p += [el((-12, 7.1, 4), (28, 7.7, 10), "kh101")] # крылья
p += [el((7.6, 10.5, 24), (8.4, 15, 30), "kh101")] # киль
p += both(el((1, 7.2, 25), (6, 7.8, 30), "kh101"))
p += [el((6, 2, 15), (10, 5.5, 28), "dark_metal"), # двигатель под хвостом
el((6.5, 2.5, 14), (9.5, 5, 15), "intake"),
el((6.5, 2.5, 28), (9.5, 5, 29.5), "nozzle")]
return p
def wheels(zs, x0=-9, x1=-5.5, tex="tire"):
out = []
for z in zs:
out += both(el((x0, 0, z), (x1, 9, z + 7), tex))
return out
def pantsir_truck():
p = [el((-6, 5, -16), (22, 9, 32), "ru_green"),
el((-6, 9, -16), (22, 21, -6), "ru_green", {"north": "glass"}),
el((-5, 13, -16), (21, 20, -15.9), "glass"),
el((-5, 9, -5), (21, 18, 6), "ru_green"),
el((-4, 9, 8), (20, 12, 30), "dark_metal"),
el((-6.5, 3, 26), (22.5, 6, 32), "dark_metal")]
return p + wheels((-13, -4, 16, 24))
def pantsir_turret():
return [el((-2, 0, -6), (18, 10, 16), "ru_green"),
el((1, 1, -11), (15, 11, -6), "radar_face"),
el((7, 10, 4), (9, 17, 6), "metal"),
el((0, 10, 8), (16, 12, 16), "ru_green")]
def pantsir_pack():
"""Пакеты ракет 57Э6 по бортам и спаренные 30-мм пушки над ними; ось качания — центр."""
p = both(el((-11, 2, -16), (-3, 12, 12), "ru_green", {"north": "tubes6"}))
p += both(el((-9.5, 12.5, -16), (-8, 14, 6), "barrel"))
p += both(el((-6.5, 12.5, -16), (-5, 14, 6), "barrel"))
p += both(el((-10, 12, 2), (-4, 15, 10), "dark_metal"))
return p
def flat_radar(tex="radar_face"):
"""Плоская вращающаяся антенна (ось — вертикаль через центр модели)."""
return [el((-6, 2, 7), (22, 16, 9), tex), el((-6.5, 1.5, 9), (22.5, 16.5, 10), "dark_metal"),
el((7, -8, 8), (9, 2, 10), "metal")]
def maz_chassis(tex):
"""8×8 шасси МАЗ-543/БАЗ с раздельными кабинами; рама под пусковой контейнер."""
p = [el((-6, 5, -16), (22, 10, 32), tex),
el((-6, 10, -16), (3, 19, -7), tex, {"north": "glass"}),
el((13, 10, -16), (22, 19, -7), tex, {"north": "glass"}),
el((3, 10, -16), (13, 15, -6), "dark_metal"),
el((-5, 10, -6), (21, 13, 4), tex),
el((-3, 10, 26), (19, 14, 32), "dark_metal")]
p += both(el((-8, 0, 28), (-6, 6, 31), "metal")) # опоры
return p + wheels((-14, -5, 13, 22))
def tubes(n_wide, n_high, size, tex="tube"):
"""Пакет ТПК: n_wide × n_high труб сечением size, от z=-16 до 32. Ось подъёма — задняя нижняя кромка."""
p = []
w = n_wide * size + (n_wide - 1)
x0 = 8 - w / 2
for i in range(n_wide):
for j in range(n_high):
x = x0 + i * (size + 1)
y = j * (size + 1)
p.append(el((x, y, -16), (x + size, y + size, 32), tex, {"north": "tube_cap", "south": "tube_cap"}))
p += [el((x0 - 1, 0, 26), (x0 + w + 1, n_high * (size + 1) - 1, 28), "dark_metal"),
el((x0 - 1, 0, -2), (x0 + w + 1, n_high * (size + 1) - 1, 0), "dark_metal")]
return p
def radar_trailer():
p = [el((-6, 5, -12), (22, 10, 26), "ru_khaki"),
el((2, 10, 2), (14, 16, 14), "dark_metal"),
el((-5, 10, 16), (21, 18, 26), "ru_khaki")]
return p + wheels((-9, 17))
def big_radar_panel():
return [el((-10, 0, 7), (26, 26, 9), "radar_big"), el((-10.5, -0.5, 9), (26.5, 26.5, 10), "dark_metal"),
el((6, -6, 8), (10, 0, 12), "metal")]
def missile_57e6():
p = taper("missile_ru", -16, -12, 0.5, 2, 0.5, 2, 2)
p += [el((7, 7, -12), (9, 9, 4), "missile_ru"), el((6.4, 6.4, 4), (9.6, 9.6, 28), "booster")]
p += both(el((3.5, 7.7, 22), (6.4, 8.3, 28), "booster"))
p += [el((7.7, 3.5, 22), (8.3, 6.4, 28), "booster"), el((7.7, 9.6, 22), (8.3, 12.5, 28), "booster")]
return p
def missile_48n6():
p = taper("missile_ru", -16, -11, 1, 3.5, 1, 3.5, 3)
p += [el((6.25, 6.25, -11), (9.75, 9.75, 28), "missile_ru"),
el((6.5, 6.5, 28), (9.5, 9.5, 30), "nozzle")]
p += both(el((1.5, 7.7, 20), (6.25, 8.3, 28), "missile_ru"))
p += [el((7.7, 1.5, 20), (8.3, 6.25, 28), "missile_ru"), el((7.7, 9.75, 20), (8.3, 14.5, 28), "missile_ru")]
return p
def missile_77n6():
p = taper("missile_grey", -16, -10, 1, 4, 1, 4, 3)
p += [el((6, 6, -10), (10, 10, 30), "missile_grey"),
el((6, 6, 4), (10, 10, 5), "booster"),
el((6.5, 6.5, 30), (9.5, 9.5, 32), "nozzle")]
p += both(el((3, 7.7, 24), (6, 8.3, 30), "missile_grey"))
p += [el((7.7, 3, 24), (8.3, 6, 30), "missile_grey"), el((7.7, 10, 24), (8.3, 13, 30), "missile_grey")]
return p
def kalibr():
p = taper("missile_grey", -16, -11, 1.5, 4.5, 1.5, 4.5, 3)
p += [el((5.75, 5.75, -11), (10.25, 10.25, 30), "missile_grey"),
el((-6, 7.6, 6), (22, 8.4, 11), "missile_grey"),
el((7.6, 10.25, 25), (8.4, 14, 30), "missile_grey"),
el((6.5, 3, 18), (9.5, 5.75, 26), "dark_metal")]
p += both(el((2, 7.6, 26), (5.75, 8.4, 30), "missile_grey"))
return p
def kh22():
"""Х-22/Х-32: толстое тело, треугольные крылья, киль сверху и снизу."""
p = taper("missile_ru", -16, -8, 1.5, 6, 1.5, 6, 4)
p += [el((5, 5, -8), (11, 11, 30), "missile_ru"),
el((7.5, 11, 14), (8.5, 17, 30), "missile_ru"),
el((7.5, -1, 18), (8.5, 5, 30), "missile_ru"),
el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")]
p += delta("missile_ru", 4, 22, 28, 12, 5, 7.6, 8.4)
return p
def zircon():
p = taper("dark_metal", -16, -6, 1, 5, 1, 4, 4)
p += [el((5.5, 6, -6), (10.5, 10, 30), "kh101"),
el((6, 3.5, 0), (10, 6, 24), "intake"),
el((5.5, 6, 30), (10.5, 10, 32), "nozzle")]
p += both(el((1, 7.6, 22), (5.5, 8.4, 30), "kh101"))
p += [el((7.6, 10, 22), (8.4, 14, 30), "kh101")]
return p
def iskander():
"""9М723: толстая ракета с конической головной частью и решётчатыми рулями."""
p = taper("ru_khaki", -16, -6, 1.5, 7, 1.5, 7, 5)
p += [el((4.5, 4.5, -6), (11.5, 11.5, 30), "ru_khaki"),
el((4.4, 4.4, 4), (11.6, 11.6, 5), "dark_metal"),
el((5, 5, 30), (11, 11, 32), "nozzle")]
p += both(el((1, 7.5, 24), (4.5, 8.5, 30), "ru_khaki"))
p += [el((7.5, 1, 24), (8.5, 4.5, 30), "ru_khaki"), el((7.5, 11.5, 24), (8.5, 15, 30), "ru_khaki")]
return p
def kinzhal():
p = taper("missile_grey", -16, -8, 1.5, 6.5, 1.5, 6.5, 4)
p += [el((4.75, 4.75, -8), (11.25, 11.25, 30), "dark_metal"),
el((4.6, 4.6, -4), (11.4, 11.4, -3), "missile_grey"),
el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")]
p += both(el((0, 7.6, 22), (4.75, 8.4, 30), "dark_metal"))
p += [el((7.6, 0, 22), (8.4, 4.75, 30), "dark_metal"), el((7.6, 11.25, 22), (8.4, 16, 30), "dark_metal")]
return p
def tracked(tex):
"""Гусеничное шасси (Бук, Тор): корпус, гусеницы и надгусеничные полки."""
p = [el((-10, 4, -14), (26, 15, 32), tex),
el((-10, 4, -16), (26, 10, -14), tex),
el((-8, 15, 22), (24, 16, 32), "dark_metal")]
p += both(el((-14, 0, -15), (-10, 9, 31), "track"))
p += both(el((-14.5, 5, -15), (-10, 10, 31), tex))
return p
def buk_turret():
return [el((-4, 0, -4), (20, 9, 20), "ru_green"),
el((1, 2, -10), (15, 14, -4), "radar_face"), # РЛС подсвета на башне
el((-2, 9, 6), (18, 11, 18), "ru_green")]
def buk_rails():
"""Четыре ракеты 9М317 на направляющих; ось качания — центр."""
p = []
for x in (-9, -3, 13, 19):
p += [el((x - 1.5, 4, -16), (x + 1.5, 7, 26), "missile_ru"),
el((x - 1.8, 3.5, 20), (x + 1.8, 7.5, 27), "booster"),
el((x - 0.5, 2, -6), (x + 0.5, 4, 16), "dark_metal")]
p += [el((-12, 0, -2), (28, 2, 14), "ru_green")]
return p
def tor_turret():
"""Башня Тора: блок вертикального пуска (8 ячеек сверху) и РЛС спереди; вращается целиком."""
p = [el((-4, 0, -6), (20, 16, 22), "ru_khaki"),
el((-2, 2, -11), (18, 15, -6), "radar_face"),
el((-3, 16, 2), (19, 17, 20), "dark_metal")]
for i in range(4):
for j in range(2):
x, z = -1 + i * 5, 4 + j * 8
p.append(el((x, 16.5, z), (x + 4, 17.5, z + 6), "tube_cap"))
return p
def nasams_trailer():
p = [el((-6, 5, -10), (22, 9, 26), "nato_green"),
el((-4, 9, 10), (20, 13, 24), "dark_metal"),
el((6, 3, -16), (10, 6, -10), "metal")]
return p + wheels((4, 14))
def p1sun():
"""Дрон-перехватчик: короткий корпус, X-образные лучи с моторами; нос — север."""
p = [el((6.5, 6.5, -10), (9.5, 9.5, 10), "dark_metal"),
el((7, 7, -14), (9, 9, -10), "missile")]
for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)):
x, y = 8 + dx * 6, 8 + dy * 6
p += [el((min(8, x) , min(8, y), 0), (max(8, x), max(8, y), 1.5), "dark_metal", rot=None),
el((x - 2.5, y - 2.5, 0), (x + 2.5, y + 2.5, 0.6), "prop")]
return p
def radar_truck():
return maz_chassis("ru_khaki")
def depot():
"""Склад боеприпасов: бетонная площадка, штабели ящиков и контейнеров ракет."""
p = [el((-14, 0, -14), (30, 2, 30), "concrete")]
for x, z in ((-12, -12), (-12, 4), (4, -12)):
p.append(el((x, 2, z), (x + 12, 10, z + 12), "crate"))
for k in range(3):
p.append(el((6, 2 + k * 5, 6), (28, 7 + k * 5, 10), "tube", {"north": "tube_cap", "south": "tube_cap"}))
return p
def tzm_truck():
"""Транспортно-заряжающая машина: кабина, кузов с ракетами и кран."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_green"),
el((-6, 9, -16), (22, 20, -6), "ru_green", {"north": "glass"}),
el((-4, 9, -2), (20, 12, 30), "dark_metal")]
for x in (-3, 5, 13):
p.append(el((x, 12, -1), (x + 6, 18, 29), "tube", {"north": "tube_cap", "south": "tube_cap"}))
p += [el((18, 12, -4), (21, 26, -1), "metal"), el((6, 24, -4), (21, 26, -1), "metal")]
return p + wheels((-12, -3, 16, 24))
def ew_truck():
"""Станция РЭБ: кузов-фургон, мачта с антенными решётками."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_khaki"),
el((-6, 9, -16), (22, 19, -6), "ru_khaki", {"north": "glass"}),
el((-5, 9, -4), (21, 22, 30), "ru_khaki"),
el((7, 22, 10), (9, 32, 12), "antenna")]
for y in (24, 28):
p += both(el((-4, y, 9), (7, y + 3, 13), "antenna"))
return p + wheels((-12, -3, 16, 24))
def searchlight():
"""Прожектор на тумбе: тумба и голова отдельно (голова — `searchlight_head`)."""
return [el((2, 0, 2), (14, 3, 14), "dark_metal"), el((6, 3, 6), (10, 12, 10), "dark_metal")]
def searchlight_head():
"""Голова прожектора: нос (стекло) смотрит на север; ось — центр."""
return [el((3, 3, -2), (13, 13, 14), "dark_metal"), el((3.5, 3.5, -2.5), (12.5, 12.5, -2), "lamp")]
def beam():
"""Луч прожектора: длинная светящаяся трубка вдоль z, от z=32 к -16 (масштабируется по длине)."""
return [el((6.5, 6.5, -16), (9.5, 9.5, 32), "beam")]
def flare():
return [el((6.5, 6.5, 6.5), (9.5, 9.5, 9.5), "lamp"), el((7.6, 9.5, 7.6), (8.4, 16, 8.4), "antenna")]
MODELS = {
"shahed": (shahed, "Шахед-136"),
"shahed_prop": (shahed_prop, "Винт"),
"geran3": (geran3, "Герань-3"),
"geran3_flame": (geran3_flame, "Факел"),
"pac3": (pac3, "PAC-3 MSE"),
"irist_missile": (irist_missile, "IRIS-T SLM"),
"amraam_missile": (amraam_missile, "AMRAAM"),
"flame": (flame, "Факел"),
"patriot_launcher": (patriot_launcher, "ПУ M903"),
"patriot_truck": (patriot_truck, "Тягач"),
"patriot_trailer": (patriot_trailer, "Полуприцеп"),
"patriot_radar": (patriot_radar, "РЛС AN/MPQ-65"),
"gepard_hull": (gepard_hull, "Корпус Гепарда"),
"gepard_turret": (gepard_turret, "Башня Гепарда"),
"gepard_guns": (gepard_guns, "Пушки Гепарда"),
"gepard_dish": (gepard_dish, "РЛС Гепарда"),
"target": (target_marker, "Цель"),
"fireball": (fireball, "Огненный шар"),
"flash": (flash, "Вспышка"),
"debris": (debris, "Обломки"),
"lancet": (lancet, "Ланцет-3"),
"gerbera": (gerbera, "Гербера"),
"kh101": (kh101, "Х-101"),
"pantsir_truck": (pantsir_truck, "Шасси Панциря"),
"pantsir_turret": (pantsir_turret, "Модуль Панциря"),
"pantsir_pack": (pantsir_pack, "Ракеты и пушки Панциря"),
"pantsir_dish": (flat_radar, "РЛС Панциря"),
"s400_chassis": (lambda: maz_chassis("ru_green"), "Шасси С-400"),
"s400_tubes": (lambda: tubes(2, 2, 7), "ТПК 48Н6"),
"s500_chassis": (lambda: maz_chassis("ru_khaki"), "Шасси С-500"),
"s500_tubes": (lambda: tubes(2, 1, 9.5), "ТПК 77Н6"),
"radar_trailer": (radar_trailer, "Прицеп РЛС"),
"radar_panel": (big_radar_panel, "РЛС обзора"),
"m57e6": (missile_57e6, "57Э6"),
"m48n6": (missile_48n6, "48Н6"),
"m77n6": (missile_77n6, "77Н6"),
"kalibr": (kalibr, "Калибр"),
"kh22": (kh22, "Х-22"),
"zircon": (zircon, "Циркон"),
"iskander": (iskander, "Искандер-М"),
"kinzhal": (kinzhal, "Кинжал"),
"tracer": (lambda: [el((7.6, 7.6, -16), (8.4, 8.4, 32), "tracer"), el((7.3, 7.3, -16), (8.7, 8.7, -4), "tracer_hot")], "Трассер"),
# Боевой блок «Орешника»: раскалённое ядро и длинный плазменный хвост.
"irist_chassis": (lambda: maz_chassis("nato_green"), "Шасси IRIS-T SLM"),
"irist_tubes": (lambda: tubes(4, 2, 4, "canister"), "Пакет IRIS-T"),
"nasams_trailer": (nasams_trailer, "Прицеп NASAMS"),
"nasams_launcher": (lambda: tubes(3, 2, 4, "canister"), "Пусковая NASAMS"),
"buk_chassis": (lambda: tracked("ru_green"), "Шасси Бука"),
"buk_turret": (buk_turret, "Башня Бука"),
"buk_rails": (buk_rails, "Ракеты Бука"),
"tor_chassis": (lambda: tracked("ru_khaki"), "Шасси Тора"),
"tor_turret": (tor_turret, "Башня Тора"),
"p1sun_launcher": (lambda: tubes(4, 2, 3, "dark_metal"), "Пусковая P1-SUN"),
"p1sun": (p1sun, "Дрон-перехватчик P1-SUN"),
"radar_truck": (radar_truck, "Шасси РЛС"),
"depot": (depot, "Склад боеприпасов"),
"tzm_truck": (tzm_truck, "Транспортно-заряжающая машина"),
"ew_truck": (ew_truck, "Станция РЭБ"),
"searchlight": (searchlight, "Прожектор"),
"searchlight_head": (searchlight_head, "Голова прожектора"),
"beam": (beam, "Луч прожектора"),
"flare": (flare, "Осветительная ракета"),
"reentry": (lambda: [el((6.5, 6.5, -16), (9.5, 9.5, -8), "white"), el((7, 7, -8), (9, 9, 12), "tracer_hot"),
el((7.5, 7.5, 12), (8.5, 8.5, 32), "flame")], "Боевой блок"),
}
CAM_MODELS = ["shahed", "geran3", "lancet", "gerbera", "kh101", "kalibr", "kh22", "zircon", "iskander", "kinzhal", "reentry"]
# Предметы: (id, название, цвет иконки)
ITEMS = [
("designator", "Целеуказатель", (220, 50, 50)),
("launch_shahed", "Пуск: Шахед-136", (90, 92, 96)),
("launch_geran3", "Пуск: Герань-3", (30, 30, 34)),
("deploy_patriot", "Развернуть Patriot", (76, 86, 58)),
("deploy_gepard", "Развернуть Гепард", (104, 84, 54)),
("deploy_pantsir", "Развернуть Панцирь-С1", (82, 96, 60)),
("deploy_s400", "Развернуть С-400", (60, 80, 50)),
("deploy_s500", "Развернуть С-500", (108, 110, 80)),
("launch_lancet", "Пуск: Ланцет-3", (70, 78, 66)),
("launch_kh101", "Пуск: Х-101", (84, 88, 92)),
("launch_kalibr", "Пуск: Калибр", (150, 154, 156)),
("launch_kh22", "Пуск: Х-22", (226, 228, 222)),
("launch_zircon", "Пуск: Циркон", (52, 54, 52)),
("launch_iskander", "Пуск: Искандер-М", (108, 110, 80)),
("launch_kinzhal", "Пуск: Кинжал", (60, 62, 60)),
("launch_oreshnik", "Пуск: Орешник", (230, 120, 40)),
("deploy_irist", "Развернуть IRIS-T SLM", (76, 86, 58)),
("deploy_nasams", "Развернуть NASAMS", (76, 86, 58)),
("deploy_buk", "Развернуть Бук-М2", (82, 96, 60)),
("deploy_tor", "Развернуть Тор-М2", (108, 110, 80)),
("deploy_p1sun", "Развернуть P1-SUN", (52, 54, 52)),
("deploy_radar", "Развернуть РЛС дальнего обнаружения", (108, 110, 80)),
("deploy_depot", "Развернуть склад боеприпасов", (92, 78, 48)),
("deploy_tzm", "Развернуть ТЗМ", (82, 96, 60)),
("deploy_ew", "Развернуть станцию РЭБ", (108, 110, 80)),
("deploy_light", "Развернуть прожектор", (52, 54, 52)),
("mover", "Пульт перемещения", (60, 140, 220)),
("flare_gun", "Осветительная ракета", (255, 240, 180)),
("remover", "Свернуть ПВО", (200, 200, 200)),
]
def item_icon(color, seed):
rnd = random.Random(seed)
im = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
for y in range(2, 14):
for x in range(4, 12):
im.putpixel((x, y), tuple(clamp(c + rnd.randint(-10, 10)) for c in color) + (255,))
for x in range(5, 11):
im.putpixel((x, 3), (230, 230, 230, 255))
im.putpixel((7, 6), (255, 60, 60, 255))
im.putpixel((8, 6), (255, 60, 60, 255))
for y in range(13, 16):
im.putpixel((7, y), (40, 40, 40, 255))
im.putpixel((8, y), (40, 40, 40, 255))
return im
def write_json(path, obj):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=1)
def main():
# Только своё: звуки (tools/gen_sounds.py) и sounds.json не трогаем.
for sub in ("blockstates", "items", "lang", "models", "textures"):
shutil.rmtree(os.path.join(ROOT, "assets", NS, sub), ignore_errors=True)
shutil.rmtree(os.path.join(ROOT, "data"), ignore_errors=True)
a = os.path.join(ROOT, "assets", NS)
d = os.path.join(ROOT, "data", NS, "horizon")
os.makedirs(os.path.join(a, "textures", "block"))
os.makedirs(os.path.join(a, "textures", "item"))
for name, fn in TEXTURES.items():
fn().save(os.path.join(a, "textures", "block", name + ".png"))
lang = {}
for name, (fn, title) in MODELS.items():
write_json(os.path.join(a, "models", "block", name + ".json"), model(fn()))
write_json(os.path.join(a, "blockstates", name + ".json"), {"variants": {"": {"model": f"{NS}:block/{name}"}}})
write_json(os.path.join(d, "block", name + ".json"),
{"template": "minecraft:stone", "item": False})
lang[f"block.{NS}.{name}"] = title
for i, (name, title, color) in enumerate(ITEMS):
item_icon(color, i).save(os.path.join(a, "textures", "item", name + ".png"))
write_json(os.path.join(a, "models", "item", name + ".json"),
{"parent": "minecraft:item/generated", "textures": {"layer0": f"{NS}:item/{name}"}})
write_json(os.path.join(a, "items", name + ".json"),
{"model": {"type": "minecraft:model", "model": f"{NS}:item/{name}"}})
write_json(os.path.join(d, "item", name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1})
lang[f"item.{NS}.{name}"] = title
# Предметы-иконки для камеры монитора: 3D-модель цели в интерфейсе.
for name in CAM_MODELS:
title = MODELS[name][1]
write_json(os.path.join(a, "models", "item", "cam_" + name + ".json"),
{"parent": f"{NS}:block/{name}",
"display": {"gui": {"rotation": [20, 145, 0], "translation": [0, 0, 0], "scale": [0.36, 0.36, 0.36]}}})
write_json(os.path.join(a, "items", "cam_" + name + ".json"),
{"model": {"type": "minecraft:model", "model": f"{NS}:item/cam_{name}"}})
write_json(os.path.join(d, "item", "cam_" + name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1})
lang[f"item.{NS}.cam_{name}"] = title
write_json(os.path.join(a, "lang", "en_us.json"), lang)
write_json(os.path.join(a, "lang", "ru_ru.json"), lang)
print(f"{len(MODELS)} моделей, {len(ITEMS)} предметов, {len(TEXTURES)} текстур -> {ROOT}")
if __name__ == "__main__":
main()