Add new game assets and update asset paths in code
This commit is contained in:
1 parent
53e98cf3fe
commit
69484866db
20 files changed
+228
-36
No files matched your search
+96
-11
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user