"""A Freelancer interface icon (.3db) from a PNG -- like the NeuroNet icons (NN_info.3db ...). Retail icons are one textured quad: the mesh lives in DATA\\INTERFACE\\interface.generic.vms, the material (DcDtOcOt, Dt_flags 64) and a 32-bit TGA with three mips are inside the .3db. The texture is grey; the button tints it (white, yellow under the mouse). A new icon gets the same quad in its own embedded VMeshLibrary, its own material name (material ids are global, keyed by the name's CRC) and the PNG as grey + alpha, brightest pixel white. python tools/make_icon_3db.py [--color] becomes the material "UI_REM_", the texture "REM_.tga" and the mesh library "rem_icon_.lod0.vms". --color keeps the PNG's colours (not tinted like the originals). """ import os import struct import sys import zlib sys.path.insert(0, os.path.dirname(__file__)) import flcrc # noqa: E402 import utf # noqa: E402 # the retail NN_info quad (interface.generic-102.vms, start vertex 11026): x, y, z, u, v QUAD_HALF = 0.020543185994029045 QUAD_Z = -2.1847961306775687e-06 QUAD = [(-QUAD_HALF, QUAD_HALF, QUAD_Z, 0.0, 1.0), (-QUAD_HALF, -QUAD_HALF, QUAD_Z, 0.0, 0.0), (QUAD_HALF, QUAD_HALF, QUAD_Z, 1.0, 1.0), (QUAD_HALF, -QUAD_HALF, QUAD_Z, 1.0, 0.0)] QUAD_INDICES = [0, 1, 2, 1, 3, 2] TEX_SIZE = 128 def read_png(path): """RGBA rows (top first) of an 8-bit PNG: grey, grey+alpha, RGB, RGBA or palette.""" d = open(path, 'rb').read() assert d[:8] == b'\x89PNG\r\n\x1a\n', path pos = 8 idat = b'' palette = trns = None while pos < len(d): n, kind = struct.unpack('>I4s', d[pos:pos + 8]) body = d[pos + 8:pos + 8 + n] if kind == b'IHDR': w, h, depth, ctype, _c, _f, interlace = struct.unpack('>IIBBBBB', body) elif kind == b'PLTE': palette = body elif kind == b'tRNS': trns = body elif kind == b'IDAT': idat += body pos += 12 + n assert depth in (8, 16) and interlace == 0, path + ': only 8/16-bit non-interlaced PNG' assert depth == 8 or ctype != 3, path + ': 16-bit palette PNG' # a 16-bit sample is filtered byte-wise as two bytes; its high byte is kept below channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[ctype] * (depth // 8) raw = zlib.decompress(idat) stride = w * channels rows = [] prev = bytearray(stride) p = 0 for _ in range(h): f = raw[p] line = bytearray(raw[p + 1:p + 1 + stride]) p += 1 + stride for i in range(stride): a = line[i - channels] if i >= channels else 0 b = prev[i] c = prev[i - channels] if i >= channels else 0 if f == 1: line[i] = (line[i] + a) & 0xFF elif f == 2: line[i] = (line[i] + b) & 0xFF elif f == 3: line[i] = (line[i] + ((a + b) >> 1)) & 0xFF elif f == 4: pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c) pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c) line[i] = (line[i] + pr) & 0xFF prev = line rows.append(line if depth == 8 else line[0::2]) channels //= depth // 8 out = [] for line in rows: px = [] for x in range(w): s = line[x * channels:(x + 1) * channels] if ctype == 6: px.append(tuple(s)) elif ctype == 2: px.append((s[0], s[1], s[2], 255)) elif ctype == 4: px.append((s[0], s[0], s[0], s[1])) elif ctype == 0: px.append((s[0], s[0], s[0], 255)) else: i = s[0] a = trns[i] if trns is not None and i < len(trns) else 255 px.append((palette[3 * i], palette[3 * i + 1], palette[3 * i + 2], a)) out.append(px) return w, h, out def resample(w, h, rows, size): """Box filter to size x size, colour weighted by alpha (no dark fringes).""" out = [] for y in range(size): y0, y1 = y * h // size, max((y + 1) * h // size, y * h // size + 1) line = [] for x in range(size): x0, x1 = x * w // size, max((x + 1) * w // size, x * w // size + 1) r = g = b = a = n = 0 for yy in range(y0, y1): for xx in range(x0, x1): pr, pg, pb, pa = rows[yy][xx] r += pr * pa g += pg * pa b += pb * pa a += pa n += 1 line.append((r // a, g // a, b // a, a // n) if a else (0, 0, 0, 0)) out.append(line) return out def to_grey(rows, gamma=0.55): """Luminance, the brightest visible pixel white and the rest lifted (gamma < 1) -- the retail icons are near-white shapes the button tints.""" lum = [[(0.2126 * r + 0.7152 * g + 0.0722 * b, a) for (r, g, b, a) in line] for line in rows] peak = max([l for line in lum for (l, a) in line if a > 32] or [255.0]) out = [] for line in lum: out.append([(min(int(255.0 * (min(l / max(peak, 1.0), 1.0) ** gamma) + 0.5), 255),) * 3 + (a,) for (l, a) in line]) return out def crop_to_content(w, h, rows, margin=0.06): """The visible part (alpha > 8) as a centred square with a small margin, so the icon fills the quad like the retail ones.""" xs = [x for line in rows for x, p in enumerate(line) if p[3] > 8] ys = [y for y, line in enumerate(rows) if any(p[3] > 8 for p in line)] if not xs: return w, h, rows x0, x1, y0, y1 = min(xs), max(xs) + 1, min(ys), max(ys) + 1 side = int(max(x1 - x0, y1 - y0) * (1.0 + 2.0 * margin)) + 1 cx, cy = (x0 + x1) // 2, (y0 + y1) // 2 ox, oy = cx - side // 2, cy - side // 2 empty = (0, 0, 0, 0) out = [[rows[y][x] if 0 <= y < h and 0 <= x < w else empty for x in range(ox, ox + side)] for y in range(oy, oy + side)] return side, side, out def tga(rows): """32-bit uncompressed TGA, bottom row first, 8 alpha bits -- as the retail icons.""" size = len(rows) head = struct.pack('