mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
A SteamVR dashboard patch (vr-pet) drawing a baked 3D pet next to the windows: the Toon Cat in five coats, a Shiba Inu, a Fox and a Dachshund, plus models from steamFrame.pet.extraModels. It walks around, follows, sits, lies and sleeps, can be picked up by its grip bar, petted and switched in its menu; its spot, pose and model are saved state. Also the vr-pet command, "Pet" in the "+" menu with the current model's icon (steam-frame-nix-pet-icon), the flake outputs pet-models, pet-icons and pet-preview, and the check pet. A built-in model with "private": true fails evaluation; private models belong in extraModels.
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Recolour the Toon Cat's texture (bake.py's cat.png) into another coat.
|
|
|
|
usage: skin.py base.png model.json out.png
|
|
|
|
model.json (models/<id>/model.json, kind "recolor"):
|
|
{ "name": "Tuxedo", "kind": "recolor",
|
|
"fur": ["#18181c", "#55555e"], dark and light end of the coat
|
|
"white": ["chest", "muzzle", "paws"] } optional white markings
|
|
|
|
The texture is a gradient atlas (64 x 64): each vertex samples one texel,
|
|
and the body's texels are laid out like a side view of the cat (columns
|
|
along its length, rows by height), each column a dark-to-light ramp that
|
|
bakes the shading in. Fur texels are the ginger ones (hue 0.03-0.14,
|
|
saturation > 0.15); each is replaced by the new coat's ramp at the same
|
|
brightness (its place between the darkest and lightest fur texel), so the
|
|
baked shading stays. White markings are texel rectangles of that side view
|
|
(PATTERNS, checked against renders of the baked frames), painted from a
|
|
white ramp at the same brightness. Everything else (eyes, nose, mouth,
|
|
whisker and inner-ear swatches) is kept as it is.
|
|
"""
|
|
import json
|
|
import sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
HUE = (0.03, 0.14)
|
|
SAT = 0.15
|
|
# White markings, texel rectangles (x0, x1, y0, y1 inclusive) of the Toon
|
|
# Cat's 64 x 64 atlas: columns 0-4 face / throat, 5-6 chest and front legs,
|
|
# 9-11 hind legs; rows ~14 (floor) to ~40 (head top).
|
|
PATTERNS = {
|
|
'muzzle': [(0, 3, 24, 45)], # chin, mouth, muzzle and cheeks up between the eyes
|
|
'chest': [(4, 4, 22, 34), (5, 5, 20, 33), (6, 6, 20, 30), (7, 8, 22, 29)], # throat, chest, front legs' fronts, belly
|
|
'paws': [(5, 6, 13, 19), (9, 11, 13, 18)], # front and hind paws
|
|
}
|
|
WHITE = ('#dcdcdf', '#ffffff')
|
|
|
|
|
|
def rgb(c):
|
|
c = c.lstrip('#')
|
|
return np.array([int(c[i:i + 2], 16) for i in (0, 2, 4)], dtype=float) / 255
|
|
|
|
|
|
def hsv(a):
|
|
mx, mn = a.max(-1), a.min(-1)
|
|
d = mx - mn
|
|
s = np.where(mx > 0, d / np.maximum(mx, 1e-9), 0)
|
|
r, g, b = a[..., 0], a[..., 1], a[..., 2]
|
|
dd = np.maximum(d, 1e-9)
|
|
h = np.where(mx == r, ((g - b) / dd) % 6, np.where(mx == g, (b - r) / dd + 2, (r - g) / dd + 4)) / 6
|
|
return np.where(d > 0, h, 0), s, mx
|
|
|
|
|
|
def luma(a):
|
|
return a @ np.array([0.2126, 0.7152, 0.0722])
|
|
|
|
|
|
def fur_mask(a):
|
|
h, s, _ = hsv(a)
|
|
return (h >= HUE[0]) & (h <= HUE[1]) & (s > SAT)
|
|
|
|
|
|
def recolor(base, spec):
|
|
a = np.asarray(base.convert('RGB')).astype(float) / 255
|
|
fur = fur_mask(a)
|
|
lum = luma(a)
|
|
lo, hi = lum[fur].min(), lum[fur].max()
|
|
t = np.clip((lum - lo) / (hi - lo), 0, 1)[..., None]
|
|
dark, light = (rgb(c) for c in spec['fur'])
|
|
out = a.copy()
|
|
out[fur] = (dark + (light - dark) * t)[fur]
|
|
marks = np.zeros(fur.shape, bool)
|
|
for name in spec.get('white', []):
|
|
for x0, x1, y0, y1 in PATTERNS[name]:
|
|
marks[y0:y1 + 1, x0:x1 + 1] = True
|
|
marks &= fur
|
|
wd, wl = (rgb(c) for c in WHITE)
|
|
out[marks] = (wd + (wl - wd) * t)[marks]
|
|
return Image.fromarray((np.clip(out, 0, 1) * 255).round().astype(np.uint8), 'RGB'), fur
|
|
|
|
|
|
if __name__ == '__main__':
|
|
base, spec_path, out = sys.argv[1:4]
|
|
spec = json.load(open(spec_path))
|
|
img, _ = recolor(Image.open(base), spec)
|
|
img.save(out, optimize=True)
|