Files
OnebotCatalog/onebot-data/tools/simplify-stl.py

94 lines
3.5 KiB
Python

# -*- coding: utf-8 -*-
"""
simplify-stl.py —— 简化过大的 STL 网格 (窗口内 3D 预览用, 纯 numpy 顶点聚类, 零额外依赖)
用途: 屏幕预览不需要几十 MB 的精细网格; 本脚本把 mesh\\ 里 > 阈值的 STL
按网格边长 (GRID_MM) 做顶点聚类, 体积缩小 3~10 倍, 外形基本不变。
用法: python simplify-stl.py (处理 mesh\\ 下所有 > 5MB 的 .stl, 就地覆盖)
"""
import os
import re
import struct
import numpy as np
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MESH_DIR = os.path.join(BASE, 'catalog', 'mesh')
SIZE_MIN = 2 * 1024 * 1024 # 只处理 2MB 以上的网格 (控制窗口内渲染内存)
GRID_MM = 0.75 # 聚类网格边长 (mm): 越大越简 (0.5~1.0 预览合适)
def read_stl(path):
"""自动识别二进制/ASCII STL, 返回 (N,3,3) 三角顶点数组"""
with open(path, 'rb') as f:
head = f.read(5)
if head[:5] == b'solid':
# ASCII STL: 正则抽出所有 vertex 行
with open(path, 'r', errors='ignore') as f:
txt = f.read()
arr = np.array(re.findall(r'vertex\s+([-\deE+.]+)\s+([-\deE+.]+)\s+([-\deE+.]+)', txt),
dtype=np.float32)
return arr.reshape(-1, 3, 3)
with open(path, 'rb') as f:
f.read(80)
(n,) = struct.unpack('<I', f.read(4))
raw = np.frombuffer(f.read(n * 50), dtype=np.uint8).reshape(n, 50)
# 每条记录 50 字节: 12 个 float (48B) + 2B 属性; 截取前 48B 再按 float 视图
f32 = np.ascontiguousarray(raw[:, :48]).view('<f4').reshape(n, 12)
return f32[:, 3:].reshape(n, 3, 3)
def write_binary_stl(path, tris):
n = len(tris)
with open(path, 'wb') as f:
f.write(b'ONEBOT simplified mesh'.ljust(80, b'\0'))
f.write(struct.pack('<I', n))
for tri in tris:
p0, p1, p2 = tri[0], tri[1], tri[2]
nrm = np.cross(p1 - p0, p2 - p0)
norm = np.linalg.norm(nrm)
nrm = nrm / norm if norm > 1e-12 else np.array([0.0, 0.0, 1.0])
f.write(struct.pack('<12f', nrm[0], nrm[1], nrm[2],
p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]))
f.write(struct.pack('<H', 0))
def simplify(path):
tris = read_stl(path)
n_in = len(tris)
verts = tris.reshape(-1, 3)
# 顶点聚类: 落到同一网格单元的顶点合并为均值点
cell = np.floor(verts / GRID_MM).astype(np.int64)
_, inv, counts = np.unique(cell, axis=0, return_inverse=True, return_counts=True)
sums = np.zeros((counts.shape[0], 3))
np.add.at(sums, inv, verts)
new_verts = sums / counts[:, None]
# 重映射三角形, 丢弃退化 (两顶点落入同一单元)
idx = inv.reshape(-1, 3)
keep = (idx[:, 0] != idx[:, 1]) & (idx[:, 1] != idx[:, 2]) & (idx[:, 0] != idx[:, 2])
out = new_verts[idx[keep]]
write_binary_stl(path, out)
return n_in, len(out)
def main():
if not os.path.isdir(MESH_DIR):
print('mesh 目录不存在')
return
for name in sorted(os.listdir(MESH_DIR)):
if not name.lower().endswith('.stl'):
continue
p = os.path.join(MESH_DIR, name)
size_in = os.path.getsize(p)
if size_in < SIZE_MIN:
continue
n_in, n_out = simplify(p)
print('%s: %d%d 三角面 (%.1fMB → %.1fMB)' % (
name, n_in, n_out, size_in / 1048576.0, os.path.getsize(p) / 1048576.0))
print('完成')
if __name__ == '__main__':
main()