"""WL 프로젝트 이펙트(VFX) → 이 게임으로 옮기기 (파일 작업만 · GUID 유지 · 원본 무수정). PD 2026-11-09: 「이 게임에 활용 가능한 리소스를 WL 프로젝트에서 체크해보고 이 프로젝트에도 그대로 쓸 수 있도록 옮기고」. WL = URP · 이 게임 = 빌트인 렌더 파이프라인 → 재질 셰이더가 빌트인에서 도는 프리팹만 옮긴다. 판정(재질 1장씩 · 프리팹 = 의존 재질이 전부 통과해야 통과): 통과 = 빌트인 셰이더 · 셰이더 그래프에 Built-In 타깃이 있음(이 게임에 com.unity.shadergraph 필요) · .shader 가 URP 인클루드 없이 CG/빌트인 경로를 가짐 탈락 = URP 패키지 셰이더·재질 · URP 전용 .shader · URP 타깃만 있는 셰이더 그래프 · 셰이더 파일 없음 · 프리팹이 쓰는 스크립트가 이 게임에 없는 네임스페이스(URP · HDRP · Input System · DOTween …)를 씀 복사 = 프리팹 + 의존 파일(guid 참조 · 셰이더 #include · .meta 의 외부 재질 참조) 을 .meta 째 복사(GUID 그대로 → 참조 유지). 경로 = WL Assets/ → 이 게임 /(앞의 ResWork/VFX/ · ResWork/ 는 뗌) · 같은 GUID 가 이미 있으면 건너뜀. 실행(어느 python 이든 · 표준 라이브러리만): python -I -B -X utf8 Tools/wl_vfx_import.py scan ... # 판정 + 크기 표(복사 없음) python -I -B -X utf8 Tools/wl_vfx_import.py copy ... # 통과 프리팹만 복사 옵션: --wl (없으면 환경 변수 WL_PROJECT_ROOT) · --dest Assets/VFX · --list(프리팹별 판정 줄) · --report 복사 뒤 = Unity 에디터 Assets 새로고침(자동) → 메뉴 TKS/VFX/Check Imported VFX (분홍 재질 검사). 다른 Unity 프로젝트도 같은 방식(2026-11-10 PD 「IB(쿠로네코) 이펙트를 그대로 써서 확인」): python -I -B -X utf8 Tools/wl_vfx_import.py copy AssetResources/FX/ef_booster.prefab … --wl E:/NerdNavis/Kuroneko/KuronekoClient --dest Assets/VFX/IB --skip-scripts --skip-scripts = 프리팹이 붙인 그 게임 전용 스크립트(.cs)는 판정·복사에서 뺀다(사본 Resources/Fx/TKS_Fx_* 에서 빠진 스크립트 부품을 지움). """ import argparse import collections import csv import os import re import shutil import sys HERE = os.path.dirname(os.path.abspath(__file__)) TKS_ROOT = os.path.dirname(HERE) GUID_YAML = re.compile(r"guid:\s*([0-9a-f]{32})") HEX32 = re.compile(r"(?= subs: res = "shader_urp_tag" else: res = "shader_builtin" else: res = "other" self.shader_cache[guid] = res return res def script_risk(self, p): if p in self.script_cache: return self.script_cache[p] bad = [] for ns in USING.findall(read_text(p)): if ns.startswith(BAD_NS) or not ns.startswith(SAFE_NS): bad.append(ns) self.script_cache[p] = bad return bad def judge(self, prefab): files = self.closure(prefab) why = [] for f in files: low = f.lower() if self.in_pkg(f): why.append("패키지 의존 " + os.path.basename(f)) continue if low.endswith(".mat"): m = SHADER_LINE.search(read_text(f, 6000)) cls = "noshader" if not m else self.classify_shader(m.group(1), m.group(2)) if cls not in OK_CLASSES: why.append("%s:%s" % (cls, os.path.basename(f))) elif low.endswith(".cs"): if self.skip_scripts: continue bad = self.script_risk(f) if bad: why.append("스크립트 %s(%s)" % (os.path.basename(f), ",".join(sorted(set(bad))))) size = 0 for f in files: if not self.in_pkg(f): try: size += os.path.getsize(f) except OSError: pass return files, why, size def collect_prefabs(sc, targets): out = [] for t in targets: p = os.path.normpath(os.path.join(sc.assets, t)) if os.path.isfile(p) and p.lower().endswith(".prefab"): out.append(p) elif os.path.isdir(p): for dp, dn, fn in os.walk(p): # 데모 · 씬 폴더 프리팹은 제외(데모 연출용 · 카메라 · 조명) if re.search(r"(^|[\\/])(Demo|Demos|Scene|Scenes|Showcase)([\\/]|$)", os.path.relpath(dp, p), re.I): continue for f in fn: if f.lower().endswith(".prefab"): out.append(os.path.join(dp, f)) else: print(" 없음:", t) return out def unpack(pkg_path, dest, skip_scenes): """빌트인용 .unitypackage(WL 폴더 안 · 예: GAP UniqueToonProjectiles BIRP)를 GUID 째 /<경로>(앞 Assets/ 뗌)로 풀기. 같은 GUID 가 이미 있으면 건너뜀 · 경로는 Assets/ 로 시작하고 .. 가 없을 때만.""" import tarfile tks = Index(TKS_ROOT, False) entries = {} with tarfile.open(pkg_path, "r:gz") as t: for m in t.getmembers(): if not m.isfile(): continue g, _, kind = m.name.partition("/") entries.setdefault(g, {})[kind] = m done = skip = 0 for g, e in entries.items(): if "pathname" not in e or "asset" not in e: continue path = t.extractfile(e["pathname"]).read().decode("utf-8").splitlines()[0].strip() if not path.startswith("Assets/") or ".." in path.split("/"): continue if skip_scenes and path.lower().endswith(".unity"): continue if g in tks.g2p: skip += 1 continue dst = os.path.normpath(os.path.join(TKS_ROOT, dest, path[len("Assets/"):])) os.makedirs(os.path.dirname(dst), exist_ok=True) with open(dst, "wb") as fh: fh.write(t.extractfile(e["asset"]).read()) if "asset.meta" in e: with open(dst + ".meta", "wb") as fh: fh.write(t.extractfile(e["asset.meta"]).read()) done += 1 print("풀기 %d · 이미 있음 %d → %s" % (done, skip, os.path.join(TKS_ROOT, dest))) def main(): ap = argparse.ArgumentParser() ap.add_argument("cmd", choices=["scan", "copy", "unpack"]) ap.add_argument("targets", nargs="+") ap.add_argument("--wl", default=os.environ.get("WL_PROJECT_ROOT", "")) ap.add_argument("--dest", default="Assets/VFX") ap.add_argument("--list", action="store_true") ap.add_argument("--report", default="") ap.add_argument("--with-scenes", action="store_true", help="unpack: 데모 씬도 풀기") ap.add_argument("--skip-scripts", action="store_true", help="프리팹이 쓰는 .cs 스크립트는 판정·복사에서 제외") a = ap.parse_args() if not a.wl or not os.path.isdir(os.path.join(a.wl, "Assets")): sys.exit("WL 루트를 --wl 또는 환경 변수 WL_PROJECT_ROOT 로 주세요") if a.cmd == "unpack": for t in a.targets: unpack(os.path.join(a.wl, "Assets", t), a.dest, not a.with_scenes) return sc = Scanner(a.wl) sc.skip_scripts = a.skip_scripts prefabs = collect_prefabs(sc, a.targets) print("프리팹 %d" % len(prefabs), flush=True) ok_files = set() rows = [] per_pack = collections.OrderedDict() for pf in prefabs: files, why, size = sc.judge(pf) rel = os.path.relpath(pf, sc.assets).replace("\\", "/") pack = next((t for t in a.targets if rel.startswith(t.replace("\\", "/"))), "?") st = per_pack.setdefault(pack, {"ok": 0, "ng": 0, "reasons": collections.Counter()}) if why: st["ng"] += 1 for w in why: st["reasons"][w.split(":")[0].split("(")[0]] += 1 else: st["ok"] += 1 ok_files.update(f for f in files if not sc.in_pkg(f)) rows.append((rel, "OK" if not why else "NG", round(size / 1048576, 2), "; ".join(sorted(set(why)))[:300])) if a.list: print(" %s %s %.1fMB %s" % ("OK" if not why else "NG", rel, size / 1048576, "; ".join(sorted(set(why)))[:160])) total = 0 for f in ok_files: try: total += os.path.getsize(f) except OSError: pass print("\n팩별 판정") for pk, st in per_pack.items(): print(" %-70s 통과 %4d · 탈락 %4d %s" % (pk, st["ok"], st["ng"], dict(st["reasons"].most_common(4)))) print("통과 프리팹 의존 파일 %d · %.1f MB" % (len(ok_files), total / 1048576)) if a.report: os.makedirs(os.path.dirname(os.path.abspath(a.report)), exist_ok=True) with open(a.report, "w", encoding="utf-8-sig", newline="") as fh: w = csv.writer(fh) w.writerow(["s_prefab", "e_result", "f_mb", "Memo"]) w.writerows(rows) print("판정 표 →", a.report) if a.cmd != "copy": return tks = Index(TKS_ROOT, False) copied = skipped = 0 for src in sorted(ok_files): rel = os.path.relpath(src, sc.assets).replace("\\", "/") if rel.startswith("..") or (a.skip_scripts and src.lower().endswith((".cs", ".dll"))): continue dst = os.path.normpath(os.path.join(TKS_ROOT, a.dest, strip_rel(rel))) meta = src + ".meta" g = GUID_YAML.search(read_text(meta, 400)) g = g.group(1) if g else None if g and g in tks.g2p: skipped += 1 continue os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy2(src, dst) if os.path.isfile(meta): shutil.copy2(meta, dst + ".meta") copied += 1 print("복사 %d · 이미 있음 %d → %s" % (copied, skipped, os.path.join(TKS_ROOT, a.dest))) if __name__ == "__main__": main()