feat: Live2D outfit textures + expression system + canvas tweaks
- Generated 5 outfit texture variants via HSL recolor (saved skin tones) - Dynamic texture_02 swapping when outfit changes - Expression buttons (Normal, Smile, Sad, Angry, Surprised, Blushing) - Random idle expression changes every 8-15s - Responsive canvas sizing with devicePixelRatio support - Outfit generation script in scripts/gen_outfits.py - Smoother lip-sync with phase-based mouth animation
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate recolored outfit textures for Kira's Live2D model.
|
||||
|
||||
Takes the base texture_02.png (clothing layer) and creates
|
||||
color variants for each outfit using HSL shifts on non-skin areas.
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
import colorsys
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.expanduser(
|
||||
"~/Projects/ai-body-double/frontend/public/live2d/models/kira"
|
||||
)
|
||||
TEXTURE_DIR = os.path.join(BASE_DIR, "Epsilon.1024")
|
||||
OUT_DIR = os.path.join(BASE_DIR, "outfits")
|
||||
|
||||
# Outfit → HSL shift mapping (hue_shift_deg, sat_mult, light_mult)
|
||||
# Applied to non-white, non-skin pixels
|
||||
OUTFITS = {
|
||||
"cozy-hoodie": (10, 1.1, 1.0), # warm pink shift
|
||||
"girly-dress": (240, 1.2, 0.95), # lavender/purple
|
||||
"pajama-set": (140, 0.8, 1.1), # minty green
|
||||
"study-sweater": (30, 1.1, 1.0), # warm orange/amber
|
||||
"going-out": (320, 1.3, 1.05), # bright pink/magenta
|
||||
}
|
||||
|
||||
def is_skin(r, g, b):
|
||||
"""Roughly detect skin tones (avoid recoloring skin)."""
|
||||
return 180 < r < 250 and 120 < g < 200 and 80 < b < 170
|
||||
|
||||
def is_white_or_void(r, g, b, a):
|
||||
"""Detect transparent or near-white pixels."""
|
||||
if a < 10:
|
||||
return True
|
||||
return r > 240 and g > 240 and b > 240
|
||||
|
||||
def recolor_texture(src_path, dst_path, hue_shift, sat_mult, light_mult):
|
||||
"""Recolor a texture by shifting HSL on non-skin, non-white pixels."""
|
||||
img = Image.open(src_path).convert("RGBA")
|
||||
pixels = img.load()
|
||||
w, h = img.size
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if is_white_or_void(r, g, b, a) or is_skin(r, g, b):
|
||||
continue
|
||||
|
||||
# Convert to HSL
|
||||
h_val, l_val, s_val = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||
|
||||
# Apply shifts
|
||||
h_val = (h_val + hue_shift / 360) % 1.0
|
||||
s_val = min(1.0, s_val * sat_mult)
|
||||
l_val = max(0, min(1.0, l_val * light_mult))
|
||||
|
||||
# Convert back to RGB
|
||||
nr, ng, nb = colorsys.hls_to_rgb(h_val, l_val, s_val)
|
||||
pixels[x, y] = (int(nr * 255), int(ng * 255), int(nb * 255), a)
|
||||
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
img.save(dst_path)
|
||||
size = os.path.getsize(dst_path)
|
||||
print(f" Created {os.path.basename(dst_path)} ({size//1024}KB)")
|
||||
|
||||
|
||||
def main():
|
||||
src = os.path.join(TEXTURE_DIR, "texture_02.png")
|
||||
if not os.path.exists(src):
|
||||
print(f"Source not found: {src}")
|
||||
return 1
|
||||
|
||||
print(f"Generating outfit textures from {src}")
|
||||
print()
|
||||
|
||||
for outfit_name, (hue, sat, light) in OUTFITS.items():
|
||||
dst = os.path.join(OUT_DIR, f"{outfit_name}.png")
|
||||
print(f" {outfit_name}: hue={hue}° sat={sat:.1f}x light={light:.2f}x")
|
||||
recolor_texture(src, dst, hue, sat, light)
|
||||
|
||||
print()
|
||||
print("Done! Texture variants ready.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user