Download scripts/split_wheels_gltf.py from kimjunhan/vkgs-sim: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/kimjunhan/vkgs-sim/resolve/main/scripts/split_wheels_gltf.py
- Command line
-
hf download hf://kimjunhan/vkgs-sim/scripts/split_wheels_gltf.py
-
curl -L -o split_wheels_gltf.py https://huggingface.co/kimjunhan/vkgs-sim/resolve/main/scripts/split_wheels_gltf.py
12.9 kB
| """์ฌ์ง๋ณ๋ก ํฉ์ณ์ง ์ฐจ๋ glTF ์์ ๋ฐํด๋ฅผ ์ผ๊ฐํ ๋จ์๋ก ์๋ผ ๋ด ์๋ฎฌ๋ ์ดํฐ ๊ด๋ก์ .glb ๋ก ๋ง๋ ๋ค. | |
| normalize_vehicle_gltf.py ๋ ๋ฐํด ๋ถํ์ด ๋ฑ๊ฐ ๋ ธ๋๋ก ์๋ ๋ชจ๋ธ(3ds Max ๋ด๋ณด๋ด๊ธฐ ๋ฑ)์ ๋ฌถ๋๋ค. OBJ ์์ ๋ณํ๋ | |
| Sketchfab ๋ชจ๋ธ(์: Genesis GV80 2021)์ ๋ ธ๋๊ฐ 'Object_N' ์ด๊ณ ํ ๋ฉ์์ ๋ค ํ์ด์ด์ ์ฐจ์ฒด ์กฐ๊ฐ์ด ์์ฌ ์์ด ๋ ธ๋๋ก๋ | |
| ๋ฐํด๋ฅผ ๊ณ ๋ฅผ ์ ์๋ค. ์ฌ๊ธฐ์๋ ๋ฐํด ์ํต(์ถ = ์ข์ฐ, ์ค์ฌ = ์ฐจ์ถ ์์น, ๋ฐ์ง๋ฆ r, ๋ฐ๊นฅ์ชฝ |X| > inner) ์์ ๋ฌด๊ฒ์ค์ฌ์ด | |
| ๋ ์ผ๊ฐํ์ ๋ผ์ด ๋ฐํด๋ง๋ค 'Wheel<k>' ๋ ธ๋ ์๋ ์ ํ๋ฆฌ๋ฏธํฐ๋ธ๋ก ์ฎ๊ธด๋ค (์ ์ ๋ฒํผ๋ ๊ทธ๋๋ก ํจ๊ป ์ด๋ค). | |
| --repaint: ์ฌ์ง ์ด๋ฆ์ด ์ค์ ๋ถ์์ ๋ค์์ธ ๋ชจ๋ธ(GV80 ์๋ณธ์ Sketchfab ์์๋ ํ๋๊ฐ ๋ ธ๋, ๋ฒํผ๊ฐ ๋นจ๊ฐ)์ ์ฌ์ง์ ๋ฒ๋ฆฌ๊ณ | |
| ์ผ๊ฐํ ์์นยท๋ฒ์ ์ผ๋ก ๋ค์ ์น ํ๋ค โ ํ์ด์ด ยท ํ , ์ ๋ฆฌ(๋ฒจํธ๋ผ์ธ ์ ๊ฒฝ์ฌ๋ฉด), ๊ทธ๋ฆด, ํค๋๋จํ ยท ํ ์ผ๋จํ, ํ๋ถ ํธ๋ฆผ, ๋๋จธ์ง ์ฐจ์ฒด ๋์ฅ. | |
| ๊ตฌ์ญ ๊ฒฝ๊ณ๋ ์๋ REPAINT ์ ๊ฐ (๋ฐ๋ฅ ยท ์๋ค ๋ ๊ธฐ์ค ๋ฏธํฐ). | |
| ๊ฒฐ๊ณผ ๊ด๋ก (normalize_vehicle_gltf.py ์ ๊ฐ๋ค): +Y ์, +Z ์, +X ์ผ์ชฝ, ๋ฐ๋ฅ(ํ์ด์ด ์๋) y = 0, ๋ฏธํฐ, ์ํ ์ค์ฌ = ์ฐจ์ฒด ๊ฒฝ๊ณ ์์ ์ค์. | |
| python scripts/split_wheels_gltf.py assets/gltf/genesis_gv80/scene.gltf assets/gltf/GenesisGV80.glb \\ | |
| --axes x,-z,y --scale 0.01 --axles 1.574,-1.376 --axle-height 0.395 --radius 0.41 --inner 0.60 --repaint | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| from normalize_vehicle_gltf import pack_glb, positions # noqa: E402 | |
| _CT = {5121: np.uint8, 5123: np.uint16, 5125: np.uint32} | |
| # --repaint ์ฌ์ง (glTF PBR) ๊ณผ ๊ตฌ์ญ. ๋์ด y ๋ ๋ฐ๋ฅ์์, front/rear ๋ ์ฐจ ์๋ ยท ๋ค๋์์ ์์ชฝ์ผ๋ก ์ฐ ๋ฏธํฐ. | |
| MATERIALS = { | |
| "paint": {"baseColorFactor": [0.86, 0.87, 0.88, 1], "metallicFactor": 0.35, "roughnessFactor": 0.25}, # ํฐ ํ | |
| "glass": {"baseColorFactor": [0.02, 0.025, 0.03, 1], "metallicFactor": 0.0, "roughnessFactor": 0.05}, | |
| "trim": {"baseColorFactor": [0.03, 0.03, 0.03, 1], "metallicFactor": 0.0, "roughnessFactor": 0.6}, | |
| "grille": {"baseColorFactor": [0.015, 0.015, 0.015, 1], "metallicFactor": 0.6, "roughnessFactor": 0.3}, | |
| "headlight": {"baseColorFactor": [0.9, 0.92, 0.95, 1], "metallicFactor": 0.0, "roughnessFactor": 0.1, | |
| "emissive": [0.6, 0.62, 0.65]}, | |
| "taillight": {"baseColorFactor": [0.55, 0.02, 0.02, 1], "metallicFactor": 0.0, "roughnessFactor": 0.15, | |
| "emissive": [0.35, 0.0, 0.0]}, | |
| "tire": {"baseColorFactor": [0.025, 0.025, 0.025, 1], "metallicFactor": 0.0, "roughnessFactor": 0.85}, | |
| "rim": {"baseColorFactor": [0.6, 0.6, 0.62, 1], "metallicFactor": 1.0, "roughnessFactor": 0.3}, | |
| } | |
| REPAINT = { | |
| "belt_y": 1.02, # ์ด ๋์ด ์์ ๊ธฐ์ด ๋ฉด = ์ ๋ฆฌ, ๊ฑฐ์ ์ํ์ธ ๋ฉด(์ง๋ถ ยท ํ๋ ๋) = ๋์ฅ | |
| "roof_ny": 0.9, # |๋ฒ์ y| ๊ฐ ์ด๋ณด๋ค ํฌ๋ฉด ์ํ ๋ฉด | |
| "trim_y": 0.30, # ์ด ์๋ = ํ๋ถ ํธ๋ฆผ (์ฌ์ด๋์ค ยท ๋ฒํผ ์๋) | |
| "rim_r": 0.27, # ๋ฐํด ์ํต ์์์ ์ถ์ผ๋ก๋ถํฐ ์ด ๋ฐ์ง๋ฆ ์ = ํ , ๋ฐ = ํ์ด์ด | |
| "grille": {"front": 0.25, "half_x": 0.50, "y": [0.32, 0.85]}, | |
| "headlight": {"front": 0.35, "min_x": 0.55, "y": [0.72, 1.00]}, | |
| "taillight": {"rear": 0.25, "min_x": 0.45, "y": [0.86, 1.06]}, | |
| } | |
| def classify(c: np.ndarray, n: np.ndarray, zmin: float, zmax: float) -> np.ndarray: | |
| """์ฐจ์ฒด ์ผ๊ฐํ(๋ฌด๊ฒ์ค์ฌ c, ๋จ์ ๋ฒ์ n, ๋ฐ๋ฅ y=0 ๊ธฐ์ค)์ MATERIALS ์ด๋ฆ.""" | |
| R = REPAINT | |
| y, ax, z = c[:, 1], np.abs(c[:, 0]), c[:, 2] | |
| out = np.full(len(c), "paint", dtype=object) | |
| out[y < R["trim_y"]] = "trim" | |
| out[(y > R["belt_y"]) & (np.abs(n[:, 1]) < R["roof_ny"])] = "glass" | |
| g, h, t = R["grille"], R["headlight"], R["taillight"] | |
| out[(z > zmax - g["front"]) & (ax < g["half_x"]) & (y > g["y"][0]) & (y < g["y"][1])] = "grille" | |
| out[(z > zmax - h["front"]) & (ax > h["min_x"]) & (y > h["y"][0]) & (y < h["y"][1])] = "headlight" | |
| out[(z < zmin + t["rear"]) & (ax > t["min_x"]) & (y > t["y"][0]) & (y < t["y"][1])] = "taillight" | |
| return out | |
| def read_indices(j: dict, blob: bytes, acc: int, nverts: int) -> np.ndarray: | |
| if acc is None: | |
| return np.arange(nverts, dtype=np.uint32) | |
| a = j["accessors"][acc] | |
| bv = j["bufferViews"][a["bufferView"]] | |
| dt = _CT[a["componentType"]] | |
| off = bv.get("byteOffset", 0) + a.get("byteOffset", 0) | |
| return np.frombuffer(blob, dtype=dt, count=a["count"], offset=off).astype(np.uint32) | |
| def axes_matrix(spec: str) -> np.ndarray: | |
| """'x,-z,y' โ ์๋ณธ ์ขํ์์ (์๋ฎฌ X, Y, Z) ๋ฅผ ๊ณ ๋ฅด๋ 3x3.""" | |
| R = np.zeros((3, 3)) | |
| for row, s in enumerate(spec.split(",")): | |
| s = s.strip() | |
| R[row, "xyz".index(s[-1])] = -1.0 if s.startswith("-") else 1.0 | |
| if abs(np.linalg.det(R) - 1) > 1e-6: | |
| raise SystemExit(f"--axes {spec} ๋ ํ์ ์ด ์๋๋ค (det={np.linalg.det(R):.0f}) โ ์ข์ฐ๊ฐ ๋ค์งํ๋ค") | |
| return R | |
| def main() -> None: | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("src", type=Path, help="์๋ณธ .gltf (๊ฐ์ ํด๋์ .bin)") | |
| ap.add_argument("out", type=Path, help="๊ฒฐ๊ณผ .glb") | |
| ap.add_argument("--axes", default="x,y,z", help="์๋ฎฌ X(์ผ์ชฝ),Y(์),Z(์) ๊ฐ ์๋ณธ์ ์ด๋ ์ถ์ธ์ง, ์: x,-z,y") | |
| ap.add_argument("--scale", type=float, default=1.0, help="์๋ณธ ๋จ์ โ ๋ฏธํฐ") | |
| ap.add_argument("--axles", required=True, help="์ ์ฐจ์ถ Z, ๋ค ์ฐจ์ถ Z (์ถยท๋ฐฐ์จ ์ ์ฉ ํ, ์ค์ฌ ๋ง์ถ๊ธฐ ์ , ๋ฏธํฐ)") | |
| ap.add_argument("--axle-height", type=float, required=True, help="์ฐจ์ถ ๋์ด Y (์ถยท๋ฐฐ์จ ์ ์ฉ ํ, ๋ฏธํฐ)") | |
| ap.add_argument("--radius", type=float, required=True, help="๋ฐํด ์ํต ๋ฐ์ง๋ฆ (ํ์ด์ด ๋ฐ๊นฅ + ์ฌ์ , ๋ฏธํฐ)") | |
| ap.add_argument("--inner", type=float, required=True, help="๋ฐํด๋ก ๋ณผ |X| ์ต์ (๋ฏธํฐ, ๋ฐํด ์์ชฝ ๋ฉด)") | |
| ap.add_argument("--repaint", action="store_true", help="์๋ณธ ์ฌ์ง์ ๋ฒ๋ฆฌ๊ณ ์์น๋ก ๋ค์ ์น ํ๋ค (REPAINT)") | |
| a = ap.parse_args() | |
| j = json.loads(a.src.read_text()) | |
| blob = bytearray((a.src.parent / j["buffers"][0]["uri"]).read_bytes()) | |
| R = axes_matrix(a.axes) * a.scale | |
| zf, zr = (float(v) for v in a.axles.split(",")) | |
| centers = [(+1, zf), (-1, zf), (+1, zr), (-1, zr)] # (X ๋ถํธ, Z): ์์ผ, ์์ค, ๋ค์ผ, ๋ค์ค | |
| names = ["WheelFrontLeft", "WheelFrontRight", "WheelRearLeft", "WheelRearRight"] | |
| mesh_nodes = [i for i, n in enumerate(j["nodes"]) if "mesh" in n] | |
| for i in mesh_nodes: | |
| n = j["nodes"][i] | |
| if any(k in n for k in ("matrix", "translation", "rotation", "scale")): | |
| raise SystemExit(f"๋ฉ์ ๋ ธ๋ {n.get('name')} ์ ๋ณํ์ด ์๋ค โ ์ด ์คํฌ๋ฆฝํธ๋ ํญ๋ฑ ๋ณํ์ธ ๋ฉ์ ๋ ธ๋๋ง ๋ค๋ฃฌ๋ค") | |
| if len(j["buffers"]) != 1: | |
| raise SystemExit("๋ฒํผ๊ฐ ์ฌ๋ฌ ๊ฐ์ธ glTF ๋ ๋ค๋ฃจ์ง ์๋๋ค") | |
| def add_indices(idx: np.ndarray) -> int: | |
| while len(blob) % 4: | |
| blob.append(0) | |
| data = idx.astype(np.uint32).tobytes() | |
| j["bufferViews"].append({"buffer": 0, "byteOffset": len(blob), "byteLength": len(data), "target": 34963}) | |
| blob.extend(data) | |
| j["accessors"].append({"bufferView": len(j["bufferViews"]) - 1, "componentType": 5125, | |
| "count": int(idx.size), "type": "SCALAR"}) | |
| return len(j["accessors"]) - 1 | |
| prims = [] # (ํ๋ฆฌ๋ฏธํฐ๋ธ, ์๋ฎฌ ์ขํ ์ ์ (์ค์ฌ ๋ง์ถ๊ธฐ ์ ), ์ผ๊ฐํ) | |
| lo, hi = np.full(3, np.inf), np.full(3, -np.inf) | |
| for i in mesh_nodes: | |
| for p in j["meshes"][j["nodes"][i]["mesh"]]["primitives"]: | |
| if p.get("mode", 4) != 4: | |
| continue | |
| v = positions(j, a.src.parent, p["attributes"]["POSITION"]) @ R.T | |
| lo, hi = np.minimum(lo, v.min(0)), np.maximum(hi, v.max(0)) | |
| prims.append((p, v, read_indices(j, bytes(blob), p.get("indices"), len(v)).reshape(-1, 3))) | |
| mat_id = {} | |
| if a.repaint: | |
| for name, m in MATERIALS.items(): | |
| pbr = {k: m[k] for k in ("baseColorFactor", "metallicFactor", "roughnessFactor")} | |
| mm = {"name": f"sim_{name}", "pbrMetallicRoughness": pbr, "doubleSided": True} | |
| if "emissive" in m: | |
| mm["emissiveFactor"] = m["emissive"] | |
| j["materials"].append(mm) | |
| mat_id[name] = len(j["materials"]) - 1 | |
| def sub(p: dict, tris: np.ndarray, mat: str | None) -> dict: | |
| q = {kk: vv for kk, vv in p.items() if kk != "indices"} | |
| q["indices"] = add_indices(tris.reshape(-1)) | |
| if mat is not None: | |
| q["material"] = mat_id[mat] | |
| return q | |
| wheel_prims: list[list[dict]] = [[] for _ in centers] | |
| n_wheel = n_all = 0 | |
| counts: dict[str, int] = {} | |
| for p, v, tri in prims: | |
| c = v[tri].mean(1) | |
| owner = np.full(len(tri), -1) | |
| for k, (sx, zc) in enumerate(centers): | |
| inside = (sx * c[:, 0] > a.inner) & (np.hypot(c[:, 1] - a.axle_height, c[:, 2] - zc) < a.radius) | |
| owner[inside & (owner < 0)] = k | |
| n_all += len(tri) | |
| n_wheel += int((owner >= 0).sum()) | |
| if a.repaint: | |
| e = np.cross(v[tri[:, 1]] - v[tri[:, 0]], v[tri[:, 2]] - v[tri[:, 0]]) | |
| nrm = e / np.maximum(np.linalg.norm(e, axis=1, keepdims=True), 1e-12) | |
| cls = classify(c - [0, lo[1], 0], nrm, lo[2], hi[2]) | |
| for k, (_, zc) in enumerate(centers): | |
| w = owner == k | |
| cls[w] = np.where(np.hypot(c[w, 1] - a.axle_height, c[w, 2] - zc) < REPAINT["rim_r"], "rim", "tire") | |
| new_body = [] | |
| for k in range(len(centers)): | |
| sel = owner == k | |
| if not sel.any(): | |
| continue | |
| if a.repaint: | |
| for m in ("tire", "rim"): | |
| s2 = sel & (cls == m) | |
| if s2.any(): | |
| wheel_prims[k].append(sub(p, tri[s2], m)) | |
| counts[m] = counts.get(m, 0) + int(s2.sum()) | |
| else: | |
| wheel_prims[k].append(sub(p, tri[sel], None)) | |
| body = owner < 0 | |
| if a.repaint: | |
| for m in MATERIALS: | |
| s2 = body & (cls == m) | |
| if s2.any(): | |
| new_body.append(sub(p, tri[s2], m)) | |
| counts[m] = counts.get(m, 0) + int(s2.sum()) | |
| elif body.any(): | |
| new_body.append(sub(p, tri[body], None)) | |
| p["_split"] = new_body | |
| # ์ฐจ์ฒด ํ๋ฆฌ๋ฏธํฐ๋ธ๋ฅผ ์ฌ์ง๋ณ ์กฐ๊ฐ์ผ๋ก ๋ฐ๊พผ๋ค | |
| for i in mesh_nodes: | |
| mesh = j["meshes"][j["nodes"][i]["mesh"]] | |
| out = [] | |
| for p in mesh["primitives"]: | |
| out += p.pop("_split") if "_split" in p else [p] | |
| mesh["primitives"] = [q for q in out if j["accessors"][q["indices"]]["count"] > 0] | |
| if a.repaint: | |
| print("๋ค์ ์น ํ ์ผ๊ฐํ:", ", ".join(f"{m} {n:,}" for m, n in counts.items())) | |
| root_children = list(mesh_nodes) | |
| for k, prims in enumerate(wheel_prims): | |
| if not prims: | |
| raise SystemExit(f"{names[k]} ์ ์ผ๊ฐํ์ด ์๋ค โ --axles/--axle-height/--radius/--inner ํ์ธ") | |
| j["meshes"].append({"name": names[k], "primitives": prims}) | |
| j["nodes"].append({"name": names[k], "children": [len(j["nodes"]) + 1]}) | |
| j["nodes"].append({"name": names[k] + "_mesh", "mesh": len(j["meshes"]) - 1}) | |
| root_children.append(len(j["nodes"]) - 2) | |
| # ๋ฉ์ ๋ ธ๋๋ ์๋ ๋ถ๋ชจ์์ ๋ผ๊ณ ์ ๋ ฌ ๋ ธ๋ ํ๋ ์๋๋ก (์๋ณธ ๋ฃจํธ์ ์ถ ํ์ ์ ๋ฒ๋ฆฌ๊ณ --axes ๋ก ๋์ ํ๋ค) | |
| for n in j["nodes"]: | |
| if "children" in n and not n.get("name", "").startswith("Wheel"): | |
| n["children"] = [c for c in n["children"] if c not in mesh_nodes] | |
| M = np.eye(4) | |
| M[:3, :3] = R | |
| M[:3, 3] = [-(lo[0] + hi[0]) / 2, -lo[1], -(lo[2] + hi[2]) / 2] | |
| j["nodes"].append({"name": "SimAlign", "matrix": M.T.reshape(-1).tolist(), "children": root_children}) | |
| j["scenes"] = [{"nodes": [len(j["nodes"]) - 1]}] | |
| j["scene"] = 0 | |
| size = hi - lo | |
| print(f"ํฌ๊ธฐ ํญ {size[0]:.3f} x ๋์ด {size[1]:.3f} x ๊ธธ์ด {size[2]:.3f} m, ์ถ๊ฑฐ {zf - zr:.3f} m, " | |
| f"๋ฐํด ์ผ๊ฐํ {n_wheel:,} / {n_all:,} ({[sum(int(j['accessors'][q['indices']]['count']) // 3 for q in w) for w in wheel_prims]})") | |
| tmp = a.out.with_suffix(".bin.tmp") | |
| tmp.write_bytes(bytes(blob)) | |
| j["buffers"] = [{"uri": tmp.name, "byteLength": len(blob)}] | |
| a.out.parent.mkdir(parents=True, exist_ok=True) | |
| try: | |
| pack_glb(j, tmp.parent, a.out) | |
| finally: | |
| tmp.unlink() | |
| if __name__ == "__main__": | |
| main() | |