scale_part.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env python3
  2. """按"查看器标签"缩放 GLB 中某个零件的几何体(绕其包围盒中心)。
  3. 默认:长度 = 平面内最长的轴,宽度 = 第二长的轴。缩放以中心为基准,
  4. 所以 width=3 表示两边各 +100%(总宽变 3 倍),length=1.5 表示两边各 +25%。
  5. 用法:
  6. python3 3d/scale_part.py --input a.glb --label 区域_143 --width 3 --length 1.5 --inplace
  7. """
  8. import argparse, os, sys
  9. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  10. from split_glb import read_glb, write_glb, NCOMP, COMP_DTYPE, COMP_SIZE
  11. import numpy as np
  12. def viewer_labels(gltf):
  13. used, labels = {}, {}
  14. for i, n in enumerate(gltf["nodes"]):
  15. nm = n.get("name", "")
  16. if not nm:
  17. continue
  18. if nm in used:
  19. used[nm] += 1
  20. labels[i] = f"{nm}_{used[nm]}"
  21. else:
  22. used[nm] = 0
  23. labels[i] = nm
  24. return labels
  25. def main():
  26. ap = argparse.ArgumentParser()
  27. ap.add_argument("--input", required=True)
  28. ap.add_argument("--label", required=True, help="查看器标签,如 区域_143")
  29. ap.add_argument("--width", type=float, default=1.0, help="宽度方向缩放倍数(最短的平面轴)")
  30. ap.add_argument("--length", type=float, default=1.0, help="长度方向缩放倍数(最长的平面轴)")
  31. ap.add_argument("--output")
  32. ap.add_argument("--inplace", action="store_true")
  33. args = ap.parse_args()
  34. gltf, bin_ = read_glb(args.input)
  35. labels = viewer_labels(gltf)
  36. node_idx = next((i for i, l in labels.items() if l == args.label), None)
  37. if node_idx is None:
  38. print(f"未找到标签 {args.label!r}")
  39. return
  40. node = gltf["nodes"][node_idx]
  41. mesh = gltf["meshes"][node["mesh"]]
  42. changed = 0
  43. for pr in mesh["primitives"]:
  44. ai = pr["attributes"]["POSITION"]
  45. acc = gltf["accessors"][ai]
  46. if acc["componentType"] != 5126 or acc["type"] != "VEC3":
  47. print("仅支持 float VEC3 的 POSITION")
  48. return
  49. bv = gltf["bufferViews"][acc["bufferView"]]
  50. base = bv.get("byteOffset", 0) + acc.get("byteOffset", 0)
  51. n = acc["count"]
  52. arr = np.frombuffer(bin_, dtype="<f4", count=n * 3, offset=base).reshape(n, 3).copy()
  53. lo, hi = arr.min(0), arr.max(0)
  54. center = (lo + hi) / 2
  55. ext = hi - lo
  56. order = np.argsort(ext)[::-1] # 最长的两个平面轴
  57. length_ax, width_ax = int(order[0]), int(order[1])
  58. print(f"尺寸 {ext[0]:.2f} x {ext[1]:.2f} x {ext[2]:.2f}")
  59. print(f"长度轴={'XYZ'[length_ax]} (x{args.length}) 宽度轴={'XYZ'[width_ax]} (x{args.width})")
  60. arr[:, length_ax] = center[length_ax] + (arr[:, length_ax] - center[length_ax]) * args.length
  61. arr[:, width_ax] = center[width_ax] + (arr[:, width_ax] - center[width_ax]) * args.width
  62. # 回写字节
  63. buf = bytearray(bin_)
  64. buf[base:base + arr.nbytes] = np.ascontiguousarray(arr, dtype="<f4").tobytes()
  65. bin_ = bytes(buf)
  66. acc["min"] = arr.min(0).tolist()
  67. acc["max"] = arr.max(0).tolist()
  68. changed += 1
  69. print(f"新尺寸 {arr.max(0)[0]-arr.min(0)[0]:.2f} x {arr.max(0)[1]-arr.min(0)[1]:.2f} "
  70. f"x {arr.max(0)[2]-arr.min(0)[2]:.2f}")
  71. if not changed:
  72. print("没有可缩放的图元")
  73. return
  74. out = args.output or (args.input if args.inplace else args.input + ".scaled.glb")
  75. if args.inplace:
  76. tmp = out + ".tmp"
  77. write_glb(tmp, gltf, bin_)
  78. os.replace(tmp, out)
  79. else:
  80. write_glb(out, gltf, bin_)
  81. print(f"已写入 {out} ({os.path.getsize(out)/1048576:.1f} MB)")
  82. if __name__ == "__main__":
  83. main()