Add new game assets and update asset paths in code

This commit is contained in:
zwitschi committed 2026-10-31 15:20:08 +01:00
1 parent 53e98cf3fe
commit 69484866db
20 files changed
+228 -36

No files matched your search

+96 -11
View File
@@ -12,6 +12,12 @@ import generate_textures
ROOT = Path(__file__).resolve().parent.parent
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}
DEFAULT_ROLE_IMAGE_MODEL = "openai/gpt-5-image-mini"
API_ASPECTS = {
"texture": "1:1", "tile": "1:1", "icon": "1:1", "app_icon": "1:1",
"overlay": "1:1", "card": "3:2", "banner": "3:2", "backdrop": "3:2",
"background": "3:2",
}
PROFILES = {
"texture": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024),
@@ -24,15 +30,20 @@ PROFILES = {
"app_icon": {"target": (1024, 1024), "min": (512, 512), "max": (1024, 1024),
"aspect": 1.0, "alpha": None, "view": "square, centered composition"},
"overlay": {"target": (512, 512), "min": (256, 256), "max": (1024, 1024),
"aspect": 1.0, "alpha": None, "view": "screen-space overlay"},
"aspect": 1.0, "alpha": None, "view": "screen-space overlay",
"min_contrast": 5.0},
"card": {"target": (256, 128), "min": (128, 64), "max": (1024, 512),
"aspect": 2.0, "alpha": None, "view": "flat, front-facing card"},
"banner": {"target": (1024, 256), "min": (512, 128), "max": (2048, 512),
"aspect": 4.0, "alpha": None, "view": "flat, front-facing banner"},
"backdrop": {"target": (1024, 256), "min": (1024, 256), "max": (8192, 2048),
"aspect": 4.0, "alpha": None, "view": "straight-on wide backdrop"},
"aspect": 4.0, "alpha": True,
"view": "straight-on wide backdrop on transparency"},
"background": {"target": (1920, 1080), "min": (1280, 720), "max": (8192, 4320),
"aspect": 16 / 9, "alpha": None, "view": "intended game camera and horizon"},
"screenshot": {"target": (1280, 720), "min": (1280, 720), "max": (3840, 2160),
"aspect": 16 / 9, "alpha": None, "view": "gameplay frame, venue, and UI state",
"min_contrast": 5.0},
}
ROLE_GUIDANCE = {
@@ -41,10 +52,10 @@ ROLE_GUIDANCE = {
"icon": "single centered flat UI glyph, clean silhouette, transparent background, no mockup or perspective",
"app_icon": "square game application icon, centered composition, no perspective",
"overlay": "screen-space overlay texture with transparent edges, no perspective",
"card": "single front-facing flat game UI card, no perspective, no surrounding mockup",
"banner": "wide front-facing game banner texture, no perspective, no surrounding scene",
"backdrop": "wide seamless-looking game environment backdrop, straight-on view, no foreground objects",
"background": "wide 16:9 game background environment with a coherent horizon and straight camera view",
"card": "single wide landscape game UI card filling the frame, front-facing and flat, no perspective, no surrounding mockup or whitespace",
"banner": "wide front-facing game banner texture, keep important details in a narrow horizontal band through the image center for final cropping, no perspective, no surrounding scene",
"backdrop": "wide isolated skyline silhouette across a low horizon in a narrow horizontal band through the image center, transparent background, no foreground objects",
"background": "wide 16:9 game background with a coherent horizon and straight camera view, keep important details in the central horizontal band for final cropping",
}
@@ -137,6 +148,8 @@ def resize_image(source, output, width, height, fit="contain", background=(0, 0,
def infer_role(path):
parts = [part.lower() for part in path.parts]
stem = path.stem.lower()
if "screenshots" in parts or "screenshot" in stem:
return "screenshot"
if "icons" in parts:
return "icon"
if "cards" in parts:
@@ -193,6 +206,8 @@ def audit_image(path, role="auto", seam_threshold=18.0):
f"wrong aspect ratio: {actual_aspect:.3f}; expected about {aspect:.3f}")
if profile.get("alpha") is True and not alpha:
errors.append("transparency required but image is opaque")
if alpha and image.convert("RGBA").getchannel("A").getbbox() is None:
errors.append("image is fully transparent")
elif profile.get("alpha") is False and alpha:
warnings.append(
"image has an alpha channel although this role is normally opaque")
@@ -204,20 +219,27 @@ def audit_image(path, role="auto", seam_threshold=18.0):
if edge_difference > seam_threshold:
errors.append(
f"tile edges differ by {edge_difference:.1f} (limit {seam_threshold:.1f})")
contrast = None
if profile.get("min_contrast") is not None:
contrast = ImageStat.Stat(image.convert("L")).stddev[0]
if contrast < profile["min_contrast"]:
errors.append(
f"near-blank frame: contrast {contrast:.1f} is below {profile['min_contrast']:.1f}")
manual_review = [
f"Confirm {profile.get('view', 'intended')} perspective/projection visually; "
"automated checks cannot infer it reliably."
]
return {
"path": str(path), "role": chosen_role, "size": [width, height],
"alpha": alpha, "edge_difference": edge_difference,
"alpha": alpha, "edge_difference": edge_difference, "contrast": contrast,
"quality": "bad" if errors else "good",
"errors": errors, "warnings": warnings, "manual_review": manual_review,
}
except (OSError, ValueError) as exc:
return {
"path": str(path), "role": chosen_role, "size": None, "alpha": None,
"edge_difference": None, "quality": "bad", "errors": [f"cannot read image: {exc}"],
"edge_difference": None, "contrast": None, "quality": "bad",
"errors": [f"cannot read image: {exc}"],
"warnings": [], "manual_review": [],
}
@@ -246,13 +268,19 @@ def _generate(args):
"alpha") is True
prompt = f"{args.prompt.strip()}. {ROLE_GUIDANCE[args.role]}."
image = generate_textures.generate_image(
prompt, args.model, args.request_size, transparent)
prompt, args.model, args.request_size, transparent,
aspect_ratio=API_ASPECTS[args.role])
foreground = getattr(args, "foreground", None)
if foreground:
image = colorize_alpha(image, foreground)
if args.role == "icon" and has_alpha(image):
image = resize_image_from_image(
image, width, height, "contain", (0, 0, 0, 0))
else:
image = ImageOps.fit(image, (width, height),
method=Image.Resampling.LANCZOS)
if args.role == "tile":
image = make_seamless(image)
save_image(image, resolve_path(args.output))
@@ -273,6 +301,53 @@ def resize_image_from_image(image, width, height, fit, background):
return result
def colorize_alpha(image, color):
if not has_alpha(image):
raise ValueError(
"foreground color requires an image with transparency")
result = Image.new("RGBA", image.size, color[:3])
result.putalpha(image.convert("RGBA").getchannel("A"))
return result
def make_seamless(image, band=32):
original_mode = image.mode
result = image.convert("RGBA")
width, height = result.size
pixels = result.load()
band = min(band, width // 4, height // 4)
def blend_edges(first_index, last_index, length, across_length, horizontal):
for offset in range(band):
weight = 1.0 - offset / band
first = first_index(offset)
last = last_index(offset)
for across in range(across_length):
first_pos = (first, across) if horizontal else (across, first)
last_pos = (last, across) if horizontal else (across, last)
first_color = pixels[first_pos]
last_color = pixels[last_pos]
average = tuple((a + b) / 2 for a,
b in zip(first_color, last_color))
pixels[first_pos] = tuple(round(a * (1 - weight) + b * weight)
for a, b in zip(first_color, average))
pixels[last_pos] = tuple(round(a * (1 - weight) + b * weight)
for a, b in zip(last_color, average))
blend_edges(lambda offset: offset, lambda offset: width - 1 - offset,
width, height, True)
blend_edges(lambda offset: offset, lambda offset: height - 1 - offset,
height, width, False)
return result if has_alpha(image) else result.convert(original_mode)
def seamless_image(source, output):
with Image.open(source) as image:
image.load()
result = make_seamless(image)
save_image(result, output)
def build_parser():
parser = argparse.ArgumentParser(
description="Create and validate game image assets.")
@@ -283,13 +358,16 @@ def build_parser():
generate.add_argument("--prompt", required=True)
generate.add_argument("--output", required=True,
help="output path, relative to the project root or absolute")
generate.add_argument("--role", choices=PROFILES, required=True)
generate.add_argument("--role", choices=API_ASPECTS, required=True)
generate.add_argument(
"--model", default=generate_textures.DEFAULT_IMAGE_MODEL)
"--model", default=DEFAULT_ROLE_IMAGE_MODEL)
generate.add_argument("--request-size", type=int,
default=generate_textures.GEN_SIZE)
generate.add_argument("--width", type=int)
generate.add_argument("--height", type=int)
generate.add_argument(
"--foreground", type=parse_color,
help="replace visible RGB with this color while preserving alpha")
alpha = generate.add_mutually_exclusive_group()
alpha.add_argument("--transparent", dest="transparent",
action="store_true")
@@ -320,6 +398,13 @@ def build_parser():
resize.set_defaults(handler=lambda args: resize_image(resolve_path(args.input), resolve_path(args.output),
args.width, args.height, args.fit, args.background))
seamless = commands.add_parser(
"seamless", help="blend opposite edges of a texture for tiling")
seamless.add_argument("input")
seamless.add_argument("output")
seamless.set_defaults(handler=lambda args: seamless_image(
resolve_path(args.input), resolve_path(args.output)))
audit = commands.add_parser(
"audit", help="classify images as good/bad using their game-asset role")
audit.add_argument("paths", nargs="+", help="image files or directories")