Files
2026-07-04 10:34:46 +09:00

99 lines
4.7 KiB
Python

#!/usr/bin/env python
"""
rig_pivots_render.py — 풀캔버스 리그 파츠 검증 + 관절 피벗 자동 산출 + 배경춤 프리뷰 렌더.
사용법:
python rig_pivots_render.py <Profile_dir> <prefix>
예:
python rig_pivots_render.py "D:\\...\\Noeul_Profile" noeul_part_
하는 일:
1) <Profile>/03_Assets/Parts/Images/<prefix><bone>.png (마스터+16파츠) 검증
- 전부 520x900 / 32-bit alpha / 16파츠 스택 == 마스터(alpha union) missed/extra 출력
2) 관절 피벗 자동 산출: pivot(bone) = centroid(opaque(bone) & opaque(parent))
3) <Profile>/05_Animation/dance_idle.json 로 배경춤 프레임 렌더(풀캔버스 FK)
출력: <Profile>/04_Rig/_pivots.json (rig.json에 반영), <Profile>/04_Rig/_dance_preview.png
주의: PowerShell은 대소문자 무시 변수 충돌이 잦으니 리그 계산은 이 Python 도구를 쓸 것.
"""
import sys, os, json, math
import numpy as np
from PIL import Image
CW, CH, TH = 520, 900, 32
BONES = [ # (name, parent, z)
("pelvis",None,6),("chest","pelvis",8),("neck","chest",9),("head","neck",10),
("upperarm_r","chest",5),("forearm_r","upperarm_r",5),("hand_r","forearm_r",5),
("upperarm_l","chest",12),("forearm_l","upperarm_l",12),("hand_l","forearm_l",13),
("thigh_r","pelvis",4),("shin_r","thigh_r",3),("foot_r","shin_r",2),
("thigh_l","pelvis",4),("shin_l","thigh_l",3),("foot_l","shin_l",2)]
def main(profile, prefix):
pdir = os.path.join(profile, "03_Assets", "Parts", "Images")
anim_path = os.path.join(profile, "05_Animation", "dance_idle.json")
rigdir = os.path.join(profile, "04_Rig")
names = ["master_apose"] + [n for n,_,_ in BONES]
for nm in names:
p = os.path.join(pdir, f"{prefix}{nm}.png")
assert os.path.exists(p), f"MISSING {p}"
assert Image.open(p).size == (CW, CH), f"size!=520x900: {nm}"
print("all 17 present, all 520x900: OK")
img = {n: Image.open(os.path.join(pdir, f"{prefix}{n}.png")).convert("RGBA") for n,_,_ in BONES}
alpha = {n: (np.asarray(img[n])[:,:,3] > TH) for n,_,_ in BONES}
master = np.asarray(Image.open(os.path.join(pdir, f"{prefix}master_apose.png")).convert("RGBA"))[:,:,3] > TH
pivot = {}
for n,p,_ in BONES:
par = p if p else "chest"
ys,xs = np.nonzero(alpha[n] & alpha[par])
if len(xs): pivot[n] = (round(float(xs.mean()),1), round(float(ys.mean()),1))
else:
ys2,xs2 = np.nonzero(alpha[n]); pivot[n] = (round(float(xs2.mean()),1), round(float(ys2.min()),1))
print("== pivots (paste into rig.json) ==")
for n,_,_ in BONES: print(f" {n:12s} {pivot[n]}")
union = np.zeros_like(master)
for n,_,_ in BONES: union |= alpha[n]
mo = int(master.sum())
print(f"stack union vs master: opaque={mo} missed={int((master & ~union).sum())} extra={int((union & ~master).sum())}")
os.makedirs(rigdir, exist_ok=True)
json.dump({n:list(pivot[n]) for n,_,_ in BONES}, open(os.path.join(rigdir,"_pivots.json"),"w"), ensure_ascii=False, indent=2)
anim = json.load(open(anim_path, encoding="utf-8"))
ease = lambda u: -(math.cos(math.pi*u)-1)/2
def samp(keys,t):
if not keys: return 0.0
if t<=keys[0]["t"]: return float(keys[0]["v"])
if t>=keys[-1]["t"]: return float(keys[-1]["v"])
for i in range(len(keys)-1):
a,b=keys[i],keys[i+1]
if a["t"]<=t<=b["t"]:
sp=b["t"]-a["t"]
return float(a["v"]) if sp<=0 else float(a["v"])+(float(b["v"])-float(a["v"]))*ease((t-a["t"])/sp)
return 0.0
T=lambda x,y: np.array([[1,0,x],[0,1,y],[0,0,1]],float)
def R(d):
r=math.radians(d);c,s=math.cos(r),math.sin(r);return np.array([[c,-s,0],[s,c,0],[0,0,1]],float)
def render(t):
W={}
for n,p,_ in BONES:
tr=anim["tracks"].get(n,{}); rot=samp(tr.get("rot"),t);tx=samp(tr.get("tx"),t);ty=samp(tr.get("ty"),t)
jx,jy=pivot[n]; ml=T(tx,ty)@T(jx,jy)@R(rot)@T(-jx,-jy); W[n]=(W[p]@ml) if p else ml
fr=Image.new("RGBA",(CW,CH),(0,0,0,0))
for n,_,_ in sorted(BONES,key=lambda b:b[2]):
Mi=np.linalg.inv(W[n]); a,c,e=Mi[0]; b,d,f=Mi[1]
fr=Image.alpha_composite(fr,img[n].transform((CW,CH),Image.AFFINE,(a,c,e,b,d,f),resample=Image.BICUBIC))
return fr
times=[0.0,0.5,1.0,1.5]; sc=0.42; fw,fh=int(CW*sc),int(CH*sc)
mont=Image.new("RGBA",(fw*len(times),fh),(28,32,40,255))
for i,t in enumerate(times): mont.alpha_composite(render(t).resize((fw,fh),Image.BICUBIC),(fw*i,0))
out=os.path.join(rigdir,"_dance_preview.png"); mont.convert("RGB").save(out)
print("saved preview:", out)
if __name__ == "__main__":
if len(sys.argv) != 3:
print(__doc__); sys.exit(1)
main(sys.argv[1], sys.argv[2])