Download scripts/normalize_vehicle_gltf.py from kimjunhan/vkgs-sim: direct link, hf CLI and curl.
- Browser
- Download file 9.74 kB
-
https://huggingface.co/kimjunhan/vkgs-sim/resolve/main/scripts/normalize_vehicle_gltf.py
- Command line
-
hf download hf://kimjunhan/vkgs-sim/scripts/normalize_vehicle_gltf.py
-
curl -L -o normalize_vehicle_gltf.py https://huggingface.co/kimjunhan/vkgs-sim/resolve/main/scripts/normalize_vehicle_gltf.py
9.74 kB
| """์ฐจ๋ glTF ๋ฅผ ์๋ฎฌ๋ ์ดํฐ ๊ด๋ก๋ก ์ ๋ฆฌํด .glb ํ ํ์ผ๋ก ๋ง๋ ๋ค. | |
| ์๋ฎฌ๋ ์ดํฐ(src/vkgs/src/viewer.cpp ์ gltf_to_box, gltf.cpp ์ ๋ฐํด ๊ฒ์ถ)๊ฐ ๊ธฐ๋ํ๋ ๊ฒ: | |
| - +Y ์, +Z ์, +X ์ผ์ชฝ, ๋ฐ๋ฅ(ํ์ด์ด ์๋)์ด y = 0 | |
| - ๋ฐํด ํ๋ = ์ด๋ฆ์ 'wheel' ์ด ๋ ๋ ธ๋ ํ๋, ๊ทธ ์๋์ ํ์ด์ดยท๋ฆผ ๋ฑ ๊ตฌ๋ฅด๋ ๋ถํ | |
| - ํธ๋ค ๊ฐ์ ๋ค๋ฅธ 'wheel' ์ด๋ฆ์ 'steering' ์ ํฌํจํด์ผ ๋ฐํด๋ก ์ค์ธ๋์ง ์๋๋ค | |
| Sketchfab ์์ ๋ฐ์ 3ds Max ๋ด๋ณด๋ด๊ธฐ๋ ์ฐจ๊ฐ ๋น์ค๋ฌํ ๋์ฌ ์๊ณ (์: Suzuki Carry 45ยฐ), ๋ฐํด ๋ถํ์ด ๋ฑ๊ฐ๋ก | |
| ํฉ์ด์ ธ ์๋ค. ์ด ์คํฌ๋ฆฝํธ๋ ์ ์ ยท๋ ธ๋ ๋ณํ์ ๊ทธ๋๋ก ๋๊ณ ๋งจ ์์ ํ์ ยท์ด๋ ๋ ธ๋ ํ๋๋ฅผ ์น๊ณ , ๋ฐํด ๋ถํ์ ๋ฌถ๋ | |
| ๋น(ํญ๋ฑ ๋ณํ) ๋ ธ๋๋ฅผ ๋ผ์ ๋ฃ๋๋ค โ ๊ทธ๋์ ๋ชจ์ยท์ฌ์ง์ ์๋ณธ๊ณผ ๋๊ฐ๋ค. | |
| python scripts/normalize_vehicle_gltf.py assets/gltf/suzuki_carry/scene.gltf assets/gltf/SuzukiCarry.glb \\ | |
| --tire '(?i)^tire' --parts '(?i)rim|logo|tire' --front '(?i)headlight' | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import struct | |
| from pathlib import Path | |
| import numpy as np | |
| def node_matrix(n: dict) -> np.ndarray: | |
| if "matrix" in n: | |
| return np.array(n["matrix"], dtype=np.float64).reshape(4, 4).T # glTF ๋ ์ด ์ฐ์ | |
| t = np.array(n.get("translation", [0, 0, 0]), dtype=np.float64) | |
| x, y, z, w = n.get("rotation", [0, 0, 0, 1]) | |
| s = np.array(n.get("scale", [1, 1, 1]), dtype=np.float64) | |
| r = np.array([[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], | |
| [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], | |
| [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)]]) | |
| m = np.eye(4) | |
| m[:3, :3] = r * s | |
| m[:3, 3] = t | |
| return m | |
| def world_matrices(j: dict) -> tuple[dict[int, np.ndarray], dict[int, int]]: | |
| world, parent = {}, {} | |
| stack = [(r, np.eye(4)) for r in j["scenes"][j.get("scene", 0)]["nodes"]] | |
| while stack: | |
| i, pm = stack.pop() | |
| world[i] = pm @ node_matrix(j["nodes"][i]) | |
| for c in j["nodes"][i].get("children", []): | |
| parent[c] = i | |
| stack.append((c, world[i])) | |
| return world, parent | |
| def positions(j: dict, gltf_dir: Path, acc: int, _cache: dict = {}) -> np.ndarray: | |
| """POSITION ์ ๊ทผ์ โ (N,3) float64 (float32 VEC3, byteStride ๊ณ ๋ ค).""" | |
| if acc in _cache: | |
| return _cache[acc] | |
| if "bin" not in _cache: | |
| _cache["bin"] = (gltf_dir / j["buffers"][0]["uri"]).read_bytes() | |
| a = j["accessors"][acc] | |
| bv = j["bufferViews"][a["bufferView"]] | |
| off = bv.get("byteOffset", 0) + a.get("byteOffset", 0) | |
| stride = bv.get("byteStride", 12) | |
| raw = np.frombuffer(_cache["bin"], dtype=np.uint8, count=stride * (a["count"] - 1) + 12, offset=off) | |
| pts = np.lib.stride_tricks.as_strided(raw.view(np.float32) if stride % 4 == 0 else raw, | |
| shape=(a["count"], 3), strides=(stride, 4)).astype(np.float64) | |
| _cache[acc] = pts | |
| return pts | |
| def subtree_bbox(j: dict, world: dict, i: int, gltf_dir: Path) -> tuple[np.ndarray, np.ndarray] | None: | |
| """๋ ธ๋ i ์๋ ๋ฉ์ ์ ์ ๋ค์ ์๋ ๊ฒฝ๊ณ ์์.""" | |
| lo, hi = np.full(3, np.inf), np.full(3, -np.inf) | |
| stack = [i] | |
| while stack: | |
| k = stack.pop() | |
| n = j["nodes"][k] | |
| if "mesh" in n: | |
| for p in j["meshes"][n["mesh"]]["primitives"]: | |
| v = positions(j, gltf_dir, p["attributes"]["POSITION"]) | |
| w = v @ world[k][:3, :3].T + world[k][:3, 3] | |
| lo, hi = np.minimum(lo, w.min(0)), np.maximum(hi, w.max(0)) | |
| stack += n.get("children", []) | |
| return (lo, hi) if np.isfinite(lo).all() else None | |
| def pack_glb(j: dict, gltf_dir: Path, out: Path) -> None: | |
| """์ธ๋ถ .binยท์ด๋ฏธ์ง๋ฅผ ๋ฒํผ ํ๋์ ๋ฃ์ด GLB ๋ก ์ด๋ค.""" | |
| assert len(j["buffers"]) == 1, "๋ฒํผ๊ฐ ์ฌ๋ฌ ๊ฐ์ธ glTF ๋ ์์ง ๋ค๋ฃจ์ง ์๋๋ค" | |
| blob = bytearray((gltf_dir / j["buffers"][0]["uri"]).read_bytes()) | |
| for img in j.get("images", []): | |
| if "uri" not in img: | |
| continue | |
| data = (gltf_dir / img.pop("uri")).read_bytes() | |
| while len(blob) % 4: | |
| blob.append(0) | |
| j["bufferViews"].append({"buffer": 0, "byteOffset": len(blob), "byteLength": len(data)}) | |
| blob += data | |
| img["bufferView"] = len(j["bufferViews"]) - 1 | |
| img["mimeType"] = "image/png" if data[:4] == b"\x89PNG" else "image/jpeg" | |
| while len(blob) % 4: | |
| blob.append(0) | |
| j["buffers"] = [{"byteLength": len(blob)}] | |
| js = json.dumps(j, separators=(",", ":")).encode() | |
| js += b" " * (-len(js) % 4) | |
| total = 12 + 8 + len(js) + 8 + len(blob) | |
| with open(out, "wb") as f: | |
| f.write(struct.pack("<III", 0x46546C67, 2, total)) | |
| f.write(struct.pack("<II", len(js), 0x4E4F534A) + js) | |
| f.write(struct.pack("<II", len(blob), 0x004E4942) + bytes(blob)) | |
| def main() -> None: | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("src", type=Path, help="์๋ณธ .gltf (๊ฐ์ ํด๋์ .bin, textures/)") | |
| ap.add_argument("out", type=Path, help="๊ฒฐ๊ณผ .glb") | |
| ap.add_argument("--tire", default=r"(?i)^tire", help="๋ฐํด๋ง๋ค ํ๋์ธ ํ์ด์ด ๋ ธ๋ ์ด๋ฆ (์ ๊ท์)") | |
| ap.add_argument("--parts", default=r"(?i)rim|logo|tire|hub", help="ํ์ด์ด์ ๊ฐ์ด ๊ตฌ๋ฅด๋ ๋ถํ ์ด๋ฆ (์ ๊ท์)") | |
| ap.add_argument("--front", default=r"(?i)headlight", help="์ฐจ ์์ชฝ์ ์๋ ๋ ธ๋ ์ด๋ฆ (์๋ค ํ์ ์ฉ)") | |
| a = ap.parse_args() | |
| j = json.loads(a.src.read_text()) | |
| world, parent = world_matrices(j) | |
| names = {i: (n.get("name") or "") for i, n in enumerate(j["nodes"])} | |
| center = {} | |
| for i in world: | |
| b = subtree_bbox(j, world, i, a.src.parent) | |
| if b is not None: | |
| center[i] = 0.5 * (b[0] + b[1]) | |
| tires = [i for i in world if re.search(a.tire, names[i].strip()) and i in center] | |
| if len(tires) < 4: | |
| raise SystemExit(f"ํ์ด์ด ๋ ธ๋๊ฐ {len(tires)}๊ฐ๋ฟ ({[names[i] for i in tires]}) โ --tire ๋ฅผ ํ์ธ") | |
| tc = np.array([center[i] for i in tires]) | |
| # ์ = ์๋ณธ ๋ฃจํธ๊ฐ ์ด๋ฏธ Y ์๋ก ๋๋ ค ๋์๋ค๊ณ ๊ฐ์ ํ๊ณ ํ์ธ: ํ์ด์ด ์ค์ฌ ๋์ด๊ฐ ๊ฑฐ์ ๊ฐ์์ผ ํ๋ค | |
| if np.ptp(tc[:, 1]) > 0.2 * np.ptp(tc[:, [0, 2]]): | |
| raise SystemExit(f"ํ์ด์ด ์ค์ฌ ๋์ด๊ฐ ์ ๊ฐ๊ฐ โ ์ ๋ฐฉํฅ์ด Y ๊ฐ ์๋ ๋ฏ: {tc}") | |
| # ์๋ค ์ถ: ํ์ด์ด ์ค์ฌ์ ์ํ๋ฉด์ ๋๊ณ , ์๋ก ๊ฐ์ฅ ๋จผ ๋ฐฉํฅ(์ถ๊ฑฐ)์ด ์๋ค | |
| hz = tc[:, [0, 2]] | |
| mid = hz.mean(0) | |
| cov = np.cov((hz - mid).T) | |
| evals, evecs = np.linalg.eigh(cov) | |
| fwd = evecs[:, np.argmax(evals)] # (x, z) | |
| fronts = [i for i in world if re.search(a.front, names[i].strip()) and i in center] | |
| if not fronts: | |
| raise SystemExit("์์ชฝ ๋ ธ๋๋ฅผ ๋ชป ์ฐพ์ โ --front ๋ฅผ ํ์ธ") | |
| if np.dot(center[fronts[0]][[0, 2]] - mid, fwd) < 0: | |
| fwd = -fwd | |
| yaw = np.arctan2(fwd[0], fwd[1]) # +Z ์์ +X ์ชฝ์ผ๋ก ์ฐ ๊ฐ | |
| # Y ์ถ์ผ๋ก -yaw ํ์ ํ๋ฉด fwd โ +Z | |
| c, s = np.cos(-yaw), np.sin(-yaw) | |
| R = np.array([[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]]) | |
| # ๋ฐ๋ฅ = ํ์ด์ด ์๋, ์ํ ์ค์ฌ = ์ฐจ์ฒด ์ ์ฒด ๊ฒฝ๊ณ ์์ ์ค์ | |
| rworld = {k: R @ m for k, m in world.items()} | |
| roots = j["scenes"][j.get("scene", 0)]["nodes"] | |
| lo, hi = np.full(3, np.inf), np.full(3, -np.inf) | |
| for r in roots: | |
| b = subtree_bbox(j, rworld, r, a.src.parent) | |
| lo, hi = np.minimum(lo, b[0]), np.maximum(hi, b[1]) | |
| tire_lo = min(subtree_bbox(j, rworld, i, a.src.parent)[0][1] for i in tires) | |
| T = np.eye(4) | |
| T[:3, 3] = [-(lo[0] + hi[0]) / 2, -tire_lo, -(lo[2] + hi[2]) / 2] | |
| M = T @ R | |
| size = hi - lo | |
| print(f"์ ๋ฐฉํฅ ํ์ {np.degrees(yaw):.1f}ยฐ, ํฌ๊ธฐ (ํญ x ๋์ด x ๊ธธ์ด) = {size[0]:.3f} x {size[1]:.3f} x {size[2]:.3f} (์๋ณธ ๋จ์)") | |
| # ๋ฐํด ๋ฌถ๊ธฐ: ํ์ด์ด๋ง๋ค, ๊ฐ์ ๋ถ๋ชจ ์๋์์ ์ด๋ฆ์ด ๋ถํ ํจํด์ด๊ณ ํ์ด์ด ์ค์ฌ์ ๊ฐ๊น์ด ๋ ธ๋๋ค | |
| taken: set[int] = set() | |
| radius = 0.5 * float(np.median([np.ptp(np.array(subtree_bbox(j, world, i, a.src.parent)), axis=0)[1] for i in tires])) | |
| for k, t in enumerate(tires): | |
| p = parent.get(t) | |
| sibs = j["nodes"][p]["children"] if p is not None else roots | |
| group = [n for n in sibs if n not in taken and re.search(a.parts, names.get(n, '').strip()) and n in center | |
| and np.linalg.norm(center[n] - center[t]) < radius] | |
| taken.update(group) | |
| gi = len(j["nodes"]) | |
| j["nodes"].append({"name": f"Wheel{k}", "children": group}) | |
| for n in group: | |
| sibs.remove(n) | |
| sibs.append(gi) | |
| print(f" Wheel{k}: {[names.get(n, '').strip() for n in group]}") | |
| # 'wheel' ์ด ๋ ๋ค๋ฅธ ๋ ธ๋(ํธ๋ค ๋ฑ)๋ ๋ฐํด๋ก ์ค์ธ๋์ง ์๊ฒ ์ด๋ฆ์ ๋ฐ๊พผ๋ค | |
| for i, n in enumerate(j["nodes"]): | |
| nm = n.get("name") or "" | |
| if "wheel" in nm.lower() and not re.fullmatch(r"Wheel\d+", nm) and "steering" not in nm.lower(): | |
| n["name"] = "Steering_" + nm.strip() | |
| print(f" ์ด๋ฆ ๋ณ๊ฒฝ: {nm!r} โ {n['name']!r} (๋ฐํด ์๋)") | |
| # ๋งจ ์์ ์ ๋ ฌ ๋ ธ๋ | |
| j["nodes"].append({"name": "SimAlign", "matrix": M.T.reshape(-1).tolist(), "children": list(roots)}) | |
| j["scenes"][j.get("scene", 0)]["nodes"] = [len(j["nodes"]) - 1] | |
| a.out.parent.mkdir(parents=True, exist_ok=True) | |
| pack_glb(j, a.src.parent, a.out) | |
| print(f"โ {a.out} ({a.out.stat().st_size / 2**20:.1f} MB)") | |
| if __name__ == "__main__": | |
| main() | |