Files
airdef/tools/gen_tex.py
T

392 lines
16 KiB
Python

"""Общая палитра и генератор текстур мода AirDef (используется tools/gen_models.py).
Все текстуры детерминированы (seed). Имя текстуры задаёт материал в HD (см. docs/HD-RENDERER.md, «Материалы»):
bare_* — голый металл / стекло (оверлей 42: гладко, полностью металлично): стволы, сопла, антенны, стёкла `bare_glass*`;
glow_* — светящиеся детали (оверлей 40): жар сопла, лампы;
прочее — крашеный металл (оверлей 41).
Имя `T__b` — обгоревший вариант текстуры T (остовы), `T@x3y1` — вариант для грани, растянутой в 3 раза по u
(в моделях грань длиннее 16 единиц тянет всю текстуру), картина заранее сжата, чтобы пятна камуфляжа не вытягивались.
"""
import random
from PIL import Image
# ------------------------------------------------------------------ палитра
PAL = {
"nato_green": [(70, 80, 54), (86, 74, 50), (34, 38, 30)], # NATO: зелёный, коричневый, чёрный
"flecktarn": [(88, 98, 62), (60, 74, 44), (112, 88, 56), (30, 32, 26)],
"ru_green": [(78, 94, 58), (52, 68, 40), (112, 94, 62), (32, 34, 26)],
"ru_khaki": [(112, 112, 82), (88, 92, 62), (64, 66, 46)],
"nato_tan": [(152, 132, 98), (126, 108, 78), (98, 86, 64)],
"olive": (74, 82, 58),
"rubber": (24, 24, 25),
"steel": (120, 122, 126),
"dark": (50, 52, 52),
"white": (236, 236, 230),
"grey": (150, 154, 158),
"red": (180, 40, 36),
}
def clamp(v):
return max(0, min(255, int(v)))
def col(c, d=0):
return tuple(clamp(v + d) for v in c[:3])
def mix(a, b, t):
return tuple(clamp(a[i] + (b[i] - a[i]) * t) for i in range(3))
class Canvas:
"""Полотно в «физических» пикселях: S*sx × S*sy, на выходе — S×S со сжатием (антиалиасинг по BOX)."""
def __init__(self, S, sx=1, sy=1, base=(0, 0, 0), seed=0):
self.S, self.sx, self.sy = S, sx, sy
self.W, self.H = S * sx, S * sy
self.im = Image.new("RGBA", (self.W, self.H), tuple(base) + (255,))
self.px = self.im.load()
self.rnd = random.Random(seed)
def put(self, x, y, c, a=1.0):
if 0 <= x < self.W and 0 <= y < self.H:
if a >= 1:
self.px[x, y] = tuple(c[:3]) + (255,)
else:
o = self.px[x, y]
self.px[x, y] = tuple(clamp(o[i] + (c[i] - o[i]) * a) for i in range(3)) + (255,)
def noise(self, amp):
r = self.rnd
for y in range(self.H):
for x in range(self.W):
o = self.px[x, y]
d = r.randint(-amp, amp)
self.px[x, y] = (clamp(o[0] + d), clamp(o[1] + d), clamp(o[2] + d), 255)
def rect(self, x0, y0, x1, y1, c, a=1.0):
for y in range(int(y0), int(y1)):
for x in range(int(x0), int(x1)):
self.put(x, y, c, a)
def disc(self, cx, cy, r, c, a=1.0):
for y in range(int(cy - r - 1), int(cy + r + 2)):
for x in range(int(cx - r - 1), int(cx + r + 2)):
if (x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2 <= r * r:
self.put(x, y, c, a)
def ellipse(self, cx, cy, rx, ry, c, wob=0.0):
r = self.rnd
for y in range(int(cy - ry - 2), int(cy + ry + 3)):
for x in range(int(cx - rx - 2), int(cx + rx + 3)):
if ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 + r.random() * wob:
self.put(x, y, tuple(clamp(v + r.randint(-4, 4)) for v in c))
def panels(self, px_w, px_h, c_line, c_lite, rivets=True):
"""Швы листов: сетка с неровными листами, блик под швом и заклёпки по краям."""
r = self.rnd
y = r.randrange(px_h)
while y < self.H:
x = r.randrange(px_w)
for yy in range(int(y), int(y) + 1):
for xx in range(self.W):
self.put(xx, yy, c_line, 0.55)
self.put(xx, yy + 1, c_lite, 0.3)
y += px_h * (0.8 + r.random() * 0.5)
x = r.randrange(px_w)
while x < self.W:
for yy in range(self.H):
self.put(int(x), yy, c_line, 0.45)
self.put(int(x) + 1, yy, c_lite, 0.25)
x += px_w * (0.8 + r.random() * 0.5)
if rivets:
for _ in range(self.W * self.H // 160):
self.put(r.randrange(self.W), r.randrange(self.H), c_lite, 0.7)
def dirt(self, amount, c=(30, 24, 18)):
"""Подтёки сверху вниз и грязь снизу."""
r = self.rnd
for _ in range(int(self.W * amount)):
x = r.randrange(self.W)
y0 = r.randrange(self.H)
for k in range(r.randint(3, max(4, self.H // 3))):
self.put(x, y0 + k, c, 0.16 * (1 - k / (self.H // 3 + 4)))
for y in range(self.H):
t = y / self.H
if t > 0.7:
for x in range(self.W):
self.put(x, y, c, (t - 0.7) * 0.5)
def wear(self, amount, c):
r = self.rnd
for _ in range(int(self.W * self.H * amount)):
self.put(r.randrange(self.W), r.randrange(self.H), c, 0.6)
def out(self):
im = self.im
if self.sx > 1 or self.sy > 1:
im = im.resize((self.S, self.S), Image.BOX)
return im
# ------------------------------------------------------------------ узоры
LIFT = 1.35 # краска светлее: средняя яркость камуфляжа ~85-100 sRGB (вживую выглядело почти чёрным)
def lifted(c, k=LIFT):
return tuple(clamp(v * k) for v in c[:3])
def painted(base, seed, S=32, sx=1, sy=1, panel=(14, 10), amp=3, dirt=0.1, wear=0.012, rivets=True, lift=LIFT):
"""Крашеный лист: шум, швы, заклёпки, подтёки, сколы."""
base = lifted(base, lift)
c = Canvas(S, sx, sy, base, seed)
c.noise(amp)
c.panels(panel[0] * 1, panel[1] * 1, col(base, -22), col(base, 16), rivets)
c.dirt(dirt)
c.wear(wear, col(base, 40))
return c.out()
def camo(kind, seed, S=64, sx=1, sy=1):
cols = [lifted(v) for v in PAL[kind]]
c = Canvas(S, sx, sy, cols[0], seed)
c.noise(3)
r = c.rnd
area = (c.W * c.H) / 1024.0
if kind == "flecktarn":
for k in range(1, len(cols)):
for _ in range(int(area * 20)):
x, y = r.randrange(c.W), r.randrange(c.H)
c.ellipse(x, y, r.uniform(1.5, 4.5), r.uniform(1.2, 3.2), cols[k], 0.5)
else:
for k in range(1, len(cols)):
for _ in range(int(area * (5 if k == 1 else 3))):
x, y = r.randrange(c.W), r.randrange(c.H)
c.ellipse(x, y, r.uniform(4, 11), r.uniform(2.5, 6), cols[k], 0.35)
c.panels(22, 16, col(cols[0], -34), col(cols[0], 12), True)
c.dirt(0.12)
c.wear(0.006, col(cols[0], 46))
return c.out()
def tire_tex(seed, S=32, sx=1, sy=1):
c = Canvas(S, sx, sy, PAL["rubber"], seed)
c.noise(3)
for x in range(0, c.W, 3): # протектор: ёлочка
for y in range(c.H):
if (y // 3) % 2 == 0:
c.put(x, y, (46, 46, 46), 0.9)
else:
c.put(x + 1, y, (46, 46, 46), 0.7)
for y in range(c.H):
c.put(0, y, (60, 60, 58), 0.8)
c.dirt(0.1, (60, 48, 36))
return c.out()
def hub_tex(seed, S=32):
c = Canvas(S, 1, 1, (70, 72, 70), seed)
c.noise(3)
c.disc(S / 2, S / 2, S * 0.46, (34, 34, 34)) # боковина шины
c.disc(S / 2, S / 2, S * 0.30, (118, 120, 122)) # диск
c.disc(S / 2, S / 2, S * 0.2, (84, 86, 88))
for k in range(8):
import math
a = k * math.pi / 4
c.disc(S / 2 + math.cos(a) * S * 0.25, S / 2 + math.sin(a) * S * 0.25, 1, (36, 36, 38))
c.disc(S / 2, S / 2, 2.4, (50, 52, 54))
return c.out()
def track_tex(seed, S=32, sx=1, sy=1):
"""Верх/низ гусеницы: траки поперёк."""
c = Canvas(S, sx, sy, (36, 36, 34), seed)
c.noise(3)
for y in range(0, c.H, 3):
for x in range(c.W):
c.put(x, y, (78, 76, 70), 0.9)
c.put(x, y + 1, (20, 20, 20), 0.5)
c.dirt(0.1, (70, 58, 44))
return c.out()
def track_side(seed, S=32, sx=1, sy=1):
"""Борт гусеничного блока: ленивец, 7 опорных катков, ведущее колесо и траки сверху и снизу."""
c = Canvas(S, sx, sy, (34, 34, 32), seed)
c.noise(3)
cy = 9
for i in range(8):
x = 10 + i * (c.W - 20) / 7
c.disc(x, cy, 6.2, (22, 22, 22))
c.disc(x, cy, 4.6, (68, 70, 66))
c.disc(x, cy, 1.8, (36, 38, 36))
for x in range(0, c.W, 2):
c.put(x, 0, (86, 84, 76))
c.put(x, 1, (26, 26, 24))
c.put(x, 17, (86, 84, 76))
c.dirt(0.15, (74, 60, 46))
return c.out()
def glass_tex(seed, S=32, tint=(36, 56, 74), refl=True):
c = Canvas(S, 1, 1, tint, seed)
c.noise(3)
for y in range(S): # небо отражается сверху вниз
for x in range(S):
t = 1 - y / S
c.put(x, y, (150, 180, 200), 0.28 * t * t)
if refl:
for i in range(S):
c.put(i, S - 1 - i, (190, 210, 224), 0.5)
c.put(min(S - 1, i + 3), S - 1 - i, (170, 196, 212), 0.3)
for i in range(S): # рама
for k in (0, S - 1):
c.put(i, k, (20, 22, 22))
c.put(k, i, (20, 22, 22))
return c.out()
def metal_tex(base, seed, S=32, brush=True, sx=1, sy=1):
"""Голый металл: шлифовка полосами, лёгкие царапины."""
c = Canvas(S, sx, sy, base, seed)
r = c.rnd
for y in range(c.H):
d = r.randint(-5, 5)
for x in range(c.W):
c.put(x, y, col(base, d + r.randint(-2, 2)))
for _ in range(c.W // 3):
y = r.randrange(c.H)
x0 = r.randrange(c.W)
for k in range(r.randint(3, 9)):
c.put(x0 + k, y, col(base, 32), 0.6)
return c.out()
def band_tex(base, bands, seed, S=32, transpose=False, text=None):
"""Ракета: полосы по длине (bands: (центр 0..1, ширина px, цвет)), трафарет. Для верха/низа — транспонировано."""
c = Canvas(S, 1, 1, base, seed)
c.noise(2)
for f, w, cc in bands:
x0 = int(f * S - w / 2)
for x in range(x0, x0 + int(w)):
for y in range(S):
c.put(x, y, cc)
c.panels(12, 99, col(base, -26), col(base, 12), True)
if text:
for (x0, y0, w) in text: # «надпись» — тёмные штрихи
for k in range(0, w, 2):
c.rect(x0 + k, y0, x0 + k + 1, y0 + 3, col(base, -90))
c.wear(0.01, col(base, 30))
im = c.out()
return im.transpose(Image.TRANSPOSE) if transpose else im
def lattice(seed, S=32, base=(70, 76, 56), dot=(150, 154, 130)):
"""Антенное полотно / ФАР: решётка элементов."""
c = Canvas(S, 1, 1, base, seed)
c.noise(2)
for y in range(1, S - 1, 2):
for x in range(1, S - 1, 2):
c.put(x, y, dot, 0.9)
c.put(x + 1, y, col(base, -26), 0.8)
for k in range(0, S, 8):
for i in range(S):
c.put(k, i, col(base, -30))
c.put(i, k, col(base, -30))
return c.out()
def ember(seed, S=32):
return Canvas(S, 1, 1, (40, 30, 24), seed).out()
def burn(im, seed, kind="paint"):
"""Обгоревший вариант: уголь, копоть сверху, ржавые пятна, прогары."""
S = im.size[0]
r = random.Random(seed)
out = Image.new("RGBA", im.size)
src, dst = im.load(), out.load()
for y in range(S):
for x in range(S):
p = src[x, y]
g = (p[0] * 0.3 + p[1] * 0.5 + p[2] * 0.2)
k = 0.45 if kind != "glass" else 0.15
c = mix(p, (g * 0.55, g * 0.5, g * 0.46), 0.75)
c = tuple(clamp(c[i] * (0.3 + 0.35 * k) + 6) for i in range(3))
# копоть: темнее сверху
c = tuple(clamp(v * (0.75 + 0.25 * y / S)) for v in c)
dst[x, y] = c + (255,)
for _ in range(S * S // 28): # ржавые и обожжённые пятна
x, y = r.randrange(S), r.randrange(S)
rad = r.uniform(1, 3)
tone = r.choice([(96, 52, 28), (70, 40, 26), (22, 20, 20), (110, 80, 52)])
for yy in range(int(y - rad), int(y + rad + 1)):
for xx in range(int(x - rad), int(x + rad + 1)):
if (xx - x) ** 2 + (yy - y) ** 2 <= rad * rad and 0 <= xx < S and 0 <= yy < S:
o = dst[xx, yy]
dst[xx, yy] = tuple(clamp(o[i] + (tone[i] - o[i]) * 0.6) for i in range(3)) + (255,)
if kind == "glass": # трещины
for _ in range(4):
x, y = r.randrange(S), r.randrange(S)
for k in range(r.randint(6, 14)):
x += r.choice([-1, 0, 1])
y += r.choice([0, 1])
if 0 <= x < S and 0 <= y < S:
dst[x, y] = (150, 160, 166, 255)
return out
FONT = {"0": "111101101101111", "1": "010110010010111", "2": "111001111100111", "3": "111001111001111",
"4": "101101111001001", "5": "111100111001111", "6": "111100111101111", "7": "111001001001001",
"8": "111101111101111", "9": "111101111001111", "-": "000000111000000"}
STAR = ["...#...", "...#...", "#######", ".#####.", "..###..", ".##.##.", ".#...#."]
def marking(kind, bg, seed, text="312", S=32):
"""Табличка-маркировка на борт (непрозрачная: фон — цвет камуфляжа). Рисунок в левом верхнем углу 16x8 пикселей
(грань 8x4 единицы модели показывает ровно эту часть текстуры)."""
c = Canvas(S, 1, 1, lifted(bg), seed)
c.noise(3)
ink = (232, 232, 224)
if kind == "digits":
for i, ch in enumerate(text):
for k, bit in enumerate(FONT[ch]):
if bit == "1":
c.put(2 + i * 4 + k % 3, 1 + k // 3, ink)
elif kind == "star":
for y, row in enumerate(STAR):
for x, ch in enumerate(row):
if ch == "#":
c.put(4 + x, y, (200, 36, 32))
for x in range(3, 12):
c.put(x, 7, ink)
elif kind == "cross":
for y in range(8):
for x in range(4, 12):
c.put(x, y, ink if (x in (4, 11) or y in (0, 7)) else (22, 22, 22))
for y in range(8):
for x in range(7, 9):
c.put(x, y, (22, 22, 22))
for x in range(4, 12):
for y in (3, 4):
c.put(x, y, (22, 22, 22))
c.dirt(0.05)
return c.out()
def cap_tex(seed, base=(92, 102, 74), S=32):
"""Торец ТПК: тёмный круг с кольцом, жёлто-чёрные полосы по краю."""
c = Canvas(S, 1, 1, lifted(base), seed)
c.noise(3)
c.disc(S / 2, S / 2, S * 0.42, (36, 38, 32))
c.disc(S / 2, S / 2, S * 0.30, (62, 66, 54))
c.disc(S / 2, S / 2, S * 0.12, (110, 114, 96))
for i in range(S):
if (i // 3) % 2 == 0:
c.put(i, 0, (200, 170, 40))
c.put(i, S - 1, (200, 170, 40))
return c.out()