vkgs-sim / scripts /split_wheels_gltf.py
kimjunhan's picture
vkgs-sim ์†Œ์Šค (nusim hub upload code)
6b0231d verified
Raw History Blame Contribute Delete
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()