67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
"""Convert the original GoldSrc additive explosion into a portable PNG atlas.
|
|
|
|
Read-only on the Half-Life installation. No native SPR importer needed at runtime.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def extract(source: Path, destination: Path) -> dict:
|
|
raw = source.read_bytes()
|
|
ident, version, orientation, fmt, radius, width, height, count, beam, sync = (
|
|
struct.unpack_from('<4siiifiiifi', raw)
|
|
)
|
|
assert (ident, version, fmt) == (b'IDSP', 2, 1), 'Expected GoldSrc additive SPR v2'
|
|
palette_count = struct.unpack_from('<H', raw, 40)[0]
|
|
assert 1 <= palette_count <= 256 and count == 15 and (width, height) == (64, 64)
|
|
palette = raw[42:42 + palette_count * 3]
|
|
offset = 42 + len(palette)
|
|
columns, rows, gutter = 5, 3, 2
|
|
tile_w, tile_h = width + 2 * gutter, height + 2 * gutter
|
|
atlas = Image.new('RGB', (columns * tile_w, rows * tile_h))
|
|
origins = []
|
|
for index in range(count):
|
|
kind, x, y, w, h = struct.unpack_from('<5i', raw, offset)
|
|
assert kind == 0 and (w, h) == (width, height), 'Unsupported frame/group'
|
|
offset += 20
|
|
pixels = raw[offset:offset + w * h]
|
|
assert len(pixels) == w * h and max(pixels) < palette_count
|
|
frame = Image.frombytes('P', (w, h), pixels)
|
|
frame.putpalette(palette)
|
|
# Preserve RGB: black contributes zero under additive blending, NOT alphatest.
|
|
atlas.paste(frame.convert('RGB'),
|
|
((index % columns) * tile_w + gutter, (index // columns) * tile_h + gutter))
|
|
origins.append([x, y])
|
|
offset += w * h
|
|
assert offset == len(raw), 'Trailing or truncated sprite data'
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
output = destination / 'zerogxplode_atlas.png'
|
|
atlas.save(output, optimize=True)
|
|
manifest = {
|
|
'source': 'Half-Life/valve/sprites/zerogxplode.spr',
|
|
'source_sha256': hashlib.sha256(raw).hexdigest(),
|
|
'destination': str(output.relative_to(ROOT)),
|
|
'destination_sha256': hashlib.sha256(output.read_bytes()).hexdigest(),
|
|
'format': 'IDSP v2 / SPR_ADDITIVE', 'frame_size': [width, height],
|
|
'frame_count': count, 'columns': columns, 'rows': rows, 'gutter': gutter,
|
|
'origins': origins, 'fps': 15,
|
|
'fps_source': 'https://github.com/ValveSoftware/halflife/blob/master/dlls/ggrenade.cpp',
|
|
}
|
|
(destination / 'source_manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
|
return manifest
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--source', type=Path, default=Path.home() /
|
|
'.steam/steam/steamapps/common/Half-Life/valve/sprites/zerogxplode.spr')
|
|
args = parser.parse_args()
|
|
print(json.dumps(extract(args.source, ROOT / 'assets/effects/explosion'), indent=2))
|