ThreeKingdomsSlingshot/Tools/make_dash2d_textures.py

151 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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")))