ThreeKingdomsSlingshot/Tools/make_dash2d_textures.py

151 lines
7.4 KiB
Python
Raw Permalink Normal View History

스킬 이펙트 1차 + IB(쿠로네코) 연출 시험 — 돌진·조준 UI·타격감 (PD 2026-11-10) - 기본 공격(돌진) = IB 이펙트 그대로 시험: 앞 빛 물결 · 속도 꼬리(진영색 4색 · 멈춘 자리에서 0.12초 흐려짐) · WL 흙먼지 · 기준점 dash(아래로 갈수록 발 쪽) · 히트 이펙트 제거(PD 지정 대기) - 슬링샷 방향 결정 UI = IB GameTouchArrow 이식(IbAimArrow · IB 유닛 반지름 0.496 기준 비율) + 진행 중 캐릭터 연출(IbUnitMarks: 발밑 원 · 몸 빛 · 조준 중 속성 방울) · 값 표 Resources/Skills/ib_ui.csv - 기병: 복귀 영역(채운 원 · PreviewCavalryReturn) · 복귀 때 돌아서기(UnitView.TurnAround) - 타격감: HitFeel + CameraController.Shake · 표 hit_feel.csv(IB 흔들림 값은 디컴파일에 없어 추정값) - 액티브 스킬 1차 연출(Fable 제안 56 스킬 · 사본 Resources/Fx/TKS_Fx_Sk_*) · SkillVfx 확장(s_camp · f_fwd · f_drift · f_fade · s_over · b_flat · 트레일 페이드) · 셀화 셰이더 포트 - 2D 돌진 시험본(Dash2D · Aura2D test_aura) · 이전 시험 행은 skill_vfx.csv 에 # 주석 - 여포 크기: dz_boss_lvbu 몸 높이 819 → 718(깃털 제외 · 말 탄 유닛 머리 끝 상한 때문에 작아졌음) - 원본 = Assets/VFX/IB(쿠로네코 · GUID 유지 · Tools/wl_vfx_import.py --skip-scripts) · 사본 도구 Tools/make_ib_fx_copies.cs · make_ib_camp_textures.py · build_dash2d.cs - 테스트 PlayMode 118/118 통과 · 자동 재직렬화(png meta · UITheme · ProjectSettings) 제외 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 04:19:44 +00:00
"""2D 돌진 이펙트 그림 생성 — 2차(PD 2026-11-10 「초승달 공격 이펙트는 좋음 · 외곽선이 너무 짙어 검기처럼 안 보임 → 반투명 그러데이션 또는 제외 ·
캐릭터 밑 돌진 이펙트는 투명값 조정 + 더 자연스럽게 재구성」 · 참고 = 흰 바람 줄기 대쉬 · 흰 심 → 진영색 → 투명 번짐 검기).
1차(검정 외곽선 셀 그림) 백업 = 세션 scratchpad tks2d_v1 · 진영색 원본 = Tools/dash2d_camps.csv.
만드는 그림(Assets/VFX/TKS2D · 진행 방향 = +X):
dash2d_slash_<진영>.png 검기 초승달 — 앞 가장자리 흰 심 → 진영색 → 뒤(오목)로 갈수록 투명 · 바깥 번짐(glow) · 외곽선 없음 · 양끝 뿔은 옅게
dash2d_speedline.png 바람 줄기 — 둥근 머리(앞) · 꼬리로 가늘어지며 투명
dash2d_trail.png 부드러운 꼬리 띠 — 가운데 불투명 · 가장자리 투명(외곽선 없음)
dash2d_dot.png 작은 점(바람 알갱이)
dash2d_puff.png 흙먼지 — 부드러운 구름 덩이(외곽선 없음)
실행: python -I -X utf8 Tools/make_dash2d_textures.py
"""
import csv
import os
import numpy as np
from PIL import Image, ImageFilter
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(os.path.dirname(HERE), "Assets", "VFX", "TKS2D")
os.makedirs(OUT, exist_ok=True)
CAMPS = list(csv.DictReader(open(os.path.join(HERE, "dash2d_camps.csv"), encoding="utf-8-sig")))
def hex_rgb(h):
h = h.strip().lstrip("#")
return np.array([int(h[i:i + 2], 16) for i in (0, 2, 4)], float) / 255.0
def smooth(a, b, x):
t = np.clip((x - a) / (b - a), 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
def blur(a, r):
img = Image.fromarray((np.clip(a, 0, 1) * 255).astype(np.uint8), "L").filter(ImageFilter.GaussianBlur(r))
return np.asarray(img, float) / 255.0
def over(c_top, a_top, c_bot, a_bot):
a = a_top + a_bot * (1.0 - a_top)
c = (c_top * a_top[..., None] + c_bot * (a_bot * (1.0 - a_top))[..., None]) / np.maximum(a, 1e-6)[..., None]
return c, a
def save(rgb, a, name, size):
arr = np.dstack([np.clip(rgb, 0, 1), np.clip(a, 0, 1)[..., None]])
Image.fromarray((arr * 255 + 0.5).astype(np.uint8), "RGBA").resize(size, Image.LANCZOS).save(os.path.join(OUT, name))
# 1) 검기 초승달(진영별) — 1차와 같은 모양(PD 「초승달 형태는 좋음」) · 1024 → 512
N = 1024
k = N / 512.0
yy, xx = np.mgrid[0:N, 0:N].astype(float)
X, Y = xx - N / 2, yy - N / 2
rx, ry = 200 * k, 236 * k # 앞(볼록) 가장자리
cdx, rxc, ryc = -120 * k, 215 * k, 250 * k # 뒤(오목) 가장자리 = 잘라낸 타원
inside = (X / rx) ** 2 + (Y / ry) ** 2 <= 1.0
outside_cut = ((X - cdx) / rxc) ** 2 + (Y / ryc) ** 2 > 1.0
body = (inside & outside_cut).astype(float)
xf = rx * np.sqrt(np.clip(1 - (Y / ry) ** 2, 0, 1))
xb = cdx + rxc * np.sqrt(np.clip(1 - (Y / ryc) ** 2, 0, 1))
t = np.clip((X - xb) / np.maximum(xf - xb, 1e-3), 0, 1) # 0 = 뒤(오목) · 1 = 앞 가장자리
hy = np.abs(Y) / ry
body = blur(body, 1.5 * k) # 가장자리만 살짝 부드럽게
alpha = body * (0.18 + 0.82 * smooth(0.0, 0.8, t)) * (1.0 - 0.75 * smooth(0.55, 1.0, hy))
core = smooth(0.62, 0.93, t) * (1.0 - smooth(0.45, 0.9, hy)) # 흰 심 = 앞 가장자리 · 뿔 쪽은 진영색만
glow_a = blur(body * (1.0 - 0.6 * smooth(0.5, 1.0, hy)), 14 * k) * 0.42
for cp in CAMPS:
col = hex_rgb(cp["s_hex"])
light = col * 0.8 + 0.2
c_body = col[None, None, :] * (1 - core[..., None]) + core[..., None] * 1.0
c_glow = np.broadcast_to(light, (N, N, 3))
c, a = over(c_body, alpha, c_glow, glow_a)
save(c, a, "dash2d_slash_%s.png" % cp["s_camp"], (512, 512))
# 2) 바람 줄기(흰색 · 색은 입자 색으로) — 512×128 → 256×64
W, H = 512, 128
yy, xx = np.mgrid[0:H, 0:W].astype(float)
u = xx / W
v = (yy - H / 2) / (H / 2)
head_u, r = 0.86, 0.42 # 머리 중심 · 반지름(높이 비)
th = np.where(u < head_u, r * np.power(np.clip(u / head_u, 0, 1), 0.9),
r * np.sqrt(np.clip(1 - ((u - head_u) / (r * H / W)) ** 2, 0, 1)))
shape = (np.abs(v) <= th).astype(float)
shape = blur(shape, 2.5)
a = shape * smooth(0.0, 0.8, u)
save(np.ones((H, W, 3)), a, "dash2d_speedline.png", (256, 64))
# 3) 부드러운 꼬리 띠 — 256×64 · 가운데 불투명 → 가장자리 투명
W, H = 256, 64
v = np.abs((np.mgrid[0:H, 0:W][0].astype(float) + 0.5) - H / 2) / (H / 2)
a = np.power(np.clip(1 - v * v, 0, 1), 1.6)
save(np.ones((H, W, 3)), a, "dash2d_trail.png", (256, 64))
# 4) 작은 점 — 64×64
W = 64
yy, xx = np.mgrid[0:W, 0:W].astype(float)
rr = np.hypot(xx - W / 2 + 0.5, yy - W / 2 + 0.5) / (W / 2)
save(np.ones((W, W, 3)), 1 - smooth(0.55, 1.0, rr), "dash2d_dot.png", (64, 64))
# 5) 흙먼지 구름 — 256 · 원 3개 겹침 · 아래쪽 살짝 어둡게 · 외곽선 없음
W = 256
yy, xx = np.mgrid[0:W, 0:W].astype(float)
m = np.zeros((W, W))
for cx, cy, r in [(100, 150, 64), (162, 142, 56), (132, 104, 52)]:
m = np.maximum(m, (np.hypot(xx - cx, yy - cy) <= r).astype(float))
m = blur(m, 6)
shade = 1.0 - 0.12 * smooth(110, 200, yy)
save(np.dstack([shade] * 3), m * 0.95, "dash2d_puff.png", (256, 256))
# 6) 몸 뒤덮는 기운(시험 · PD 2026-11-10 「첨부 이미지와 같은 느낌이 나도록 몸을 뒤덮는 연출 · 스킬용」 · 참고 = 흰 심 앞머리 → 진영색 몸 → 투명 꼬리 · 물결 가장자리 · 결 무늬)
# aura_comet_<진영>.png 1024×512 → 512×256 · 앞(+X) 둥근 머리 · 뒤로 물결치며 가늘어짐
W, H = 1024, 512
yy, xx = np.mgrid[0:H, 0:W].astype(float)
u = xx / W
v = (yy - H / 2) / (H / 2)
hu, r = 0.76, 0.86 # 머리 중심(가로 비) · 반지름(세로 반폭 비)
ru = r * (H / 2) / W # 머리 반지름(가로 비)
tail = np.clip(u / hu, 0, 1)
wave = 1 + (0.16 * np.sin(u * 31.0) + 0.09 * np.sin(u * 67.0 + 1.3)) * (1 - tail)
th = np.where(u < hu, r * np.power(tail, 0.55) * wave, r * np.sqrt(np.clip(1 - ((u - hu) / ru) ** 2, 0, 1)))
mid = 0.10 * np.sin(u * 13.0) * (1 - tail) # 꼬리 쪽 중심선 흔들림
vn = np.abs(v - mid) / np.maximum(th, 1e-3)
inside = blur((vn <= 1.0).astype(float), 3)
front = smooth(hu - 0.30, hu + ru * 0.9, u) # 앞머리 쪽 1
core = front * (1 - smooth(0.25, 0.85, vn)) # 흰 심 = 앞머리 가운데
streak = 0.35 * smooth(0.55, 1.0, np.sin(v * 17.0 + u * 6.0)) * (1 - front) * tail # 몸통 결 무늬(밝은 줄)
a_comet = inside * (0.10 + 0.90 * smooth(0.0, hu * 0.85, u)) * (1 - 0.45 * smooth(0.55, 1.0, vn))
glow_c = blur((vn <= 1.0).astype(float), 18) * 0.38 * smooth(0.1, 0.9, u)
for cp in CAMPS:
col = hex_rgb(cp["s_hex"])
light = col * 0.75 + 0.25
white_mix = np.clip(core + streak, 0, 1)[..., None]
c_body = col[None, None, :] * (1 - white_mix) + white_mix
c, a = over(c_body, a_comet, np.broadcast_to(light, (H, W, 3)), glow_c)
save(c, a, "aura_comet_%s.png" % cp["s_camp"], (512, 256))
# aura_wisp.png 512×128 → 256×64 · 흰 불꽃 혀(색은 입자 색) · 앞 둥근 머리 · 꼬리 물결
W, H = 512, 128
yy, xx = np.mgrid[0:H, 0:W].astype(float)
u = xx / W
v = (yy - H / 2) / (H / 2)
hu, r = 0.84, 0.5
ru = r * (H / 2) / W
th = np.where(u < hu, r * np.power(np.clip(u / hu, 0, 1), 0.8), r * np.sqrt(np.clip(1 - ((u - hu) / ru) ** 2, 0, 1)))
mid = 0.28 * np.sin(u * 9.0 + 0.6) * (1 - u)
shape = blur((np.abs(v - mid) <= th).astype(float), 2.5)
save(np.ones((H, W, 3)), shape * smooth(0.0, 0.75, u), "aura_wisp.png", (256, 64))
print("ok", sorted(f for f in os.listdir(OUT) if f.endswith(".png")))