vkgs-sim / scripts /normalize_vehicle_gltf.py
kimjunhan's picture
Add files using upload-large-folder tool
4c75661 verified
Raw History Blame Contribute Delete
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()