151 lines
7.4 KiB
Python
151 lines
7.4 KiB
Python
|
|
"""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")))
|