bvle-voxels/tools/prepare_textures.py

126 lines
4.4 KiB
Python
Raw Permalink Normal View History

"""
Prepare voxel textures from FreeStylized.com ZIPs.
Outputs per material:
- *_albedo.png : RGBA (RGB=albedo, A=heightmap)
- *_normal.png : RGB normal map (OpenGL convention, Y-up)
"""
import io
import os
import zipfile
from PIL import Image, ImageEnhance
# (zip_name, color_pattern, height_pattern, normal_pattern, brightness_factor)
# brightness_factor: <1 = darken, >1 = brighten, 1.0 = unchanged
MATERIALS = [
("grass_01_1k", "color", "height", "normal_gl", 1.0),
("ground_02_1k", "color", "height", "normal_gl", 0.75), # dirt: darkened
("ground_stones_01_1k", "baseColor", "height", "normal_gl", 1.0),
("sand_01_1k", "color", "height", "normal_gl", 1.0),
("snow_01_1k", "color", "height", "normal_gl", 1.0),
("rock_01_1k", "color", "height", "normal_gl", 1.0),
]
OUTPUT_NAMES = [
"grass",
"dirt",
"stone",
"sand",
"snow",
"smoothstone",
]
TARGET_SIZE = 512
RAW_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "raw")
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "voxel")
def find_file_in_zip(zf, pattern):
"""Find a file in the zip matching a pattern substring."""
for name in zf.namelist():
basename = os.path.basename(name).lower()
if pattern.lower() in basename and basename.endswith(".png"):
return name
return None
def load_image_from_zip(zf, filename, mode="RGB"):
data = zf.read(filename)
img = Image.open(io.BytesIO(data))
# Handle 16-bit heightmaps: Pillow's .convert("L") on I;16 images
# doesn't scale properly. We must manually scale 0-65535 → 0-255.
if img.mode in ("I;16", "I") and mode == "L":
# Convert to 32-bit int first, then scale down
img = img.convert("I")
img = img.point(lambda v: v / 256)
return img.convert("L")
return img.convert(mode)
def process_material(zip_path, color_pat, height_pat, normal_pat, brightness, out_name):
with zipfile.ZipFile(zip_path, "r") as zf:
color_file = find_file_in_zip(zf, color_pat)
height_file = find_file_in_zip(zf, height_pat)
normal_file = find_file_in_zip(zf, normal_pat)
if not color_file:
print(f" ERROR: no color file matching '{color_pat}' in {zip_path}")
return False
# ── Albedo + Heightmap → RGBA ──
color_img = load_image_from_zip(zf, color_file, "RGB")
if brightness != 1.0:
color_img = ImageEnhance.Brightness(color_img).enhance(brightness)
if height_file:
height_img = load_image_from_zip(zf, height_file, "L")
else:
print(f" WARNING: no height map, deriving from luminance")
height_img = color_img.convert("L")
color_img = color_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
height_img = height_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
r, g, b = color_img.split()
rgba = Image.merge("RGBA", (r, g, b, height_img))
albedo_path = os.path.join(OUT_DIR, f"{out_name}_albedo.png")
rgba.save(albedo_path, "PNG")
print(f" OK: {out_name}_albedo.png ({TARGET_SIZE}x{TARGET_SIZE})")
# ── Normal map → RGB ──
if normal_file:
normal_img = load_image_from_zip(zf, normal_file, "RGB")
normal_img = normal_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
normal_path = os.path.join(OUT_DIR, f"{out_name}_normal.png")
normal_img.save(normal_path, "PNG")
print(f" OK: {out_name}_normal.png ({TARGET_SIZE}x{TARGET_SIZE})")
else:
print(f" WARNING: no normal map found")
return True
def main():
os.makedirs(OUT_DIR, exist_ok=True)
print(f"Output directory: {os.path.abspath(OUT_DIR)}")
print()
success = 0
for i, (zip_name, color_pat, height_pat, normal_pat, brightness) in enumerate(MATERIALS):
zip_path = os.path.join(RAW_DIR, zip_name + ".zip")
print(f"[{i+1}/6] {OUTPUT_NAMES[i]} <- {zip_name}.zip")
if not os.path.exists(zip_path):
print(f" ERROR: {zip_path} not found")
continue
if process_material(zip_path, color_pat, height_pat, normal_pat, brightness, OUTPUT_NAMES[i]):
success += 1
print(f"\nDone: {success}/6 materials generated in {os.path.abspath(OUT_DIR)}")
if __name__ == "__main__":
main()