Meshy-Figur Brunhild (28.706 Dreiecke mit Lane-Fassung und Speer) mit Ultimate-Clip Jump Attack, 113 Credits. Skin Eiswalküre über skin_variant.py (Farbband über Rot, Mindestsättigung schützt Hauttöne). Heldenwahl bei fünf Helden kompakter; Tests auf fünf Helden angehoben. Kern 619/619 (h2 22), EditMode 34/34, PlayMode 19/19, Verify 718/0, Selbsttest 30/30, Golden 0, Fingerabdruck unverändert; A/B mit Sylvara: Karte gleich, Mid nah -4 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
"""Variant skins of a Meshy figure (H1, docs/luneraria/CHAMPIONS-KONZEPT.md: skin stage "Variante").
|
|
|
|
Reads the albedo maps build_figure.py wrote (Assets/Luneraria/Art/Textures/<from>-albedo-2k.png,
|
|
glow mask in alpha) and the ORM map beside them (B = metalness), and writes a recoloured albedo
|
|
per skin (<to>-albedo-2k.png): texels in the hue band (the blue cloth, runes and moonstone) move to
|
|
the skin's hue with more saturation and less brightness, metal texels get a tint and darken, skin,
|
|
hair and leather stay. The glow mask in alpha is kept, so the runes glow in the new colour.
|
|
LuneMaterialFactory makes a Surface material AI_<Figure>_<Skin> from it (normal and ORM of the
|
|
standard maps) and LuneMaterialSet.Dress swaps it in for heroes that carry the skin.
|
|
|
|
blender --background --python tools/meshy/skin_variant.py -- tools/meshy/sylvara/skins.json
|
|
"""
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from build_figure import pixels, save_png # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
TEXTURES = ROOT / 'Assets/Luneraria/Art/Textures'
|
|
|
|
|
|
def hsv(rgb):
|
|
value = rgb.max(-1)
|
|
low = rgb.min(-1)
|
|
delta = value - low
|
|
saturation = np.where(value > 1e-6, delta / np.maximum(value, 1e-6), 0.0)
|
|
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
|
safe = np.maximum(delta, 1e-6)
|
|
hue = np.where(value == r, ((g - b) / safe) % 6.0, np.where(value == g, (b - r) / safe + 2.0, (r - g) / safe + 4.0)) * 60.0
|
|
return np.where(delta > 1e-6, hue, 0.0), saturation, value
|
|
|
|
|
|
def rgb_of(hue, saturation, value):
|
|
c = value * saturation
|
|
h = (hue % 360.0) / 60.0
|
|
x = c * (1.0 - np.abs(h % 2.0 - 1.0))
|
|
zero = np.zeros_like(c)
|
|
sector = np.floor(h).astype(int) % 6
|
|
table = [(c, x, zero), (x, c, zero), (zero, c, x), (zero, x, c), (x, zero, c), (c, zero, x)]
|
|
out = np.zeros(hue.shape + (3,), dtype=np.float32)
|
|
for index, (r, g, b) in enumerate(table):
|
|
mask = sector == index
|
|
out[mask] = np.stack([r[mask], g[mask], b[mask]], -1)
|
|
return out + (value - c)[..., None]
|
|
|
|
|
|
def recolour(albedo, metal, rule):
|
|
rgb = albedo[..., :3]
|
|
hue, saturation, value = hsv(rgb)
|
|
low, high = rule['hue_from']
|
|
# A band may wrap around red (e.g. [330, 25]).
|
|
in_hue = ((hue >= low) & (hue <= high)) if low <= high else ((hue >= low) | (hue <= high))
|
|
band = in_hue & (saturation > rule.get('min_saturation', 0.06))
|
|
shifted = rgb_of(np.full_like(hue, float(rule['hue_to'])), np.clip(saturation * rule['saturation_gain'], 0, 1), value * rule['value_gain'])
|
|
out = np.where(band[..., None], shifted, rgb)
|
|
metallic = (metal > rule['metal_from']) & ~band
|
|
tint = np.array(rule['metal_tint'], dtype=np.float32) * rule['metal_value']
|
|
out = np.where(metallic[..., None], out * tint, out)
|
|
albedo[..., :3] = np.clip(out, 0.0, 1.0)
|
|
return float(band.mean()), float(metallic.mean())
|
|
|
|
|
|
def main():
|
|
spec = json.loads(Path(sys.argv[sys.argv.index('--') + 1]).read_text())
|
|
report = {}
|
|
for skin in spec['skins']:
|
|
for pair in skin['maps']:
|
|
albedo_image = bpy.data.images.load(str(TEXTURES / (pair['from'] + '-albedo-2k.png')))
|
|
orm_image = bpy.data.images.load(str(TEXTURES / (pair['from'] + '-orm-2k.png')))
|
|
albedo_image.colorspace_settings.name = 'Non-Color'
|
|
orm_image.colorspace_settings.name = 'Non-Color'
|
|
albedo = pixels(albedo_image)
|
|
orm = pixels(orm_image)
|
|
band, metal = recolour(albedo, orm[..., 2], skin['recolor'])
|
|
save_png(albedo, TEXTURES / (pair['to'] + '-albedo-2k.png'), 4)
|
|
report[pair['to']] = {'hue_band': round(band, 4), 'metal': round(metal, 4)}
|
|
print('LUNE_SKINS', json.dumps(report))
|
|
|
|
|
|
main()
|