
如果你在刀具磨削行业待过大概率见过 NUMROTO 这个名字。它做 3D 仿真时会丢给你一个叫NUM_STLViewer.exe的小工具让你看看 STL 或 PLY 模型长什么样。这个程序不大5.19 MB2019 年 10 月 7 日构建典型的 Delphi / CBuilder VCL 应用PE32 i386跑在 Windows GUI 上。它内部同时挂着 OpenGL 和 Direct3D 两套渲染后端3D 层用 T3dStl、T3dLdiSolidPlus、TSolidOperationModel、TTriangle 这些库支持*.stl、*.stpack、*.ply甚至能把 STL 塞进 exe 里当自包含导出件。菜单树是 File / View / Mesh / Light / Measurement / Extras / Help十五种语言旋转动画和宏录制一应俱全。但这些东西都在一个二进制文件里。你想改一行代码加一个测量方式或者只是想知道它到底怎么画三角形的都无从下手。于是有了这个 Python 复现版。逆向笔记从字符串里挖出一棵菜单树逆向一个 Delphi 程序最直接的办法是翻字符串。miFile、miView、miMesh、miLight、miMeasurement、miExtras、miSpin*、miMovieMacro*、miLang*、miSTL*——这些菜单项名字就这么躺在二进制里。旋转动画、宏录制、语言切换、STL 格式相关命令一个都没少。原程序有 15 种语言EN、CH、CS、FR、GR、HU、IT、NL、PL、PO、RO、RU、SP、SW、TU。构建时间 2019 年 10 月 7 日体积 5.19 MBPE32 i386子系统 Windows GUI。渲染后端是 OpenGL / Direct3D 双套外面包了一层 NIRenderer3d 缓存。3D 库的名字也很具体T3dStl、T3dLdiSolidPlus、TSolidOperationModel、TTriangle。文件格式除了*.stl、*.stpack、*.ply还有*.exe——自包含导出件内嵌 STL。这些信息不是摆设。复现版的功能映射就是照着这棵菜单树来的File、View、Mesh、Light、Measurement、Extras、Help一个菜单对一个模块。逆向结论也不是一次性的写代码时遇到“原程序到底怎么处理这个操作”的疑问回头翻翻字符串和结构往往能找到线索。为什么不用 OpenGL也不碰 Pillow复现版最核心的技术选择是放弃 OpenGL、Direct3D、Pillow 和 Qt 渲染转而用纯 Python 加 numpy 实现软件光栅化。听起来像用自行车上高速但这么做有它的道理。OpenGL 和 Direct3D 是黑盒你把三角形丢进去它画出来中间发生了什么你看不见。Pillow 能读写 PNG但 PNG 的 zlib 和滤波细节被封装在 C 层。Qt 的渲染更不用说了整个图形栈都在背后。这个项目要的是透明。渲染管线每一行都能读、能改、能测。所以它自带 PNG 编解码器自己实现 zlib 和逐行滤波支持 8 位非隔行和全部滤波器。字体也是自带的font5x7.py里塞了一套 5×7 位图字形用来画标注和 HUD 文字。图形界面用 PyQt5但只是可选没有它命令行模式照样跑。整个项目只依赖 numpy版本要求numpy1.22。目录结构也反映了这种模块化的意图。num_stl_viewer主包有 13 个模块加一个入口。mathutil管向量、矩阵、几何工具stl_io负责 STL、PLY 和内嵌模型读写mesh做修复、统计、着色、剖切camera实现透视、正交和标准视图lighting管光照预设和着色模型renderer是软件光栅化核心measure提供测量工具scene组织多对象场景和图元i18n覆盖 15 种语言settings持久化 INI 设置macro处理旋转动画和宏录制png和font5x7解决图片和文字输出cli和viewer构成命令行和图形界面两条入口。配套的tests里核心测试 103 项图形界面测试 63 项还有拖拽对齐可视验证和示例生成脚本。samples下既有二进制和 ASCII 钻头毛坯也有实体、线框、分量着色、GUI 截图和拖拽降分辨率对比图。渲染器里的那些脏活软件光栅化在 Python 里最怕的就是慢。三角形填充用规范化重心坐标edge和深度都以c d·j的线性增量方式计算深度只在真正命中的像素上求值不再构造整幅中间数组。一批三角形共用一块矩形缓冲如果直接取批内最大宽高一个 31×170 的三角形会把同批全部撑成 31×170实测空洞率可达 8.0 倍改成按 bbox 边长的 2 次幂分桶后降到 2.0 倍光栅化耗时随之下降一半以上。不透明路径的遮挡由 Z 缓冲保证分桶不影响正确性透明混合路径仍保持全局“远到近”分批。命中像素解算用flatnonzero加divmod由线性下标反解批次、行、列再求像素下标只在少量元素里随机取值比按整块取快约 3 倍。颜色、深度、透明三块全尺寸数组按渲染尺寸缓存复用超采样时不再每帧分配释放数十 MB降采样直接对 uint8 视图分两步求和省掉全尺寸 float32 副本。这些优化单独看都不复杂但叠在一起才让 Python 软件渲染勉强跑进了可交互的范围。性能数据也说明了一些事情。在 932×707 画布、Windows、CPython 3.13 下1.4k 面模型拖拽 16 到 19 毫秒约 55fps静止精修 186 毫秒21k 面拖拽 26 到 31 毫秒精修 140 到 235 毫秒101k 面拖拽 110 到 130 毫秒精修 220 到 240 毫秒。大幅面模型的拖拽代价主要落在三角形数量而非分辨率上自适应降分辨率收益有限建议调低拖拽画质依赖或先用分割连通分量拆开看。STL 的三角汤问题STL 文件有个老毛病同一个几何点按三角形重复存储形成三角汤。不焊接时每个三角形都是独立连通分量体积算不出边界边满天飞。示例件从 4212 顶点、4212 条边界边焊接后变成 1027 顶点、640 条。载入时默认自动焊接重合顶点容差取包围盒对角线的1e-6可以用--no-weld或视图选项关闭。焊接之后平滑着色或设置里的插值法向才能让曲面呈现平滑过渡。坐标约定上Z 是刀具和旋转轴朝上和原程序 NUMROTO 一致。测量支持point3d、line3d、line2d、circle2p、circle3p、angle2d、rzpoint七种类型其中rzpoint输出柱坐标 (R, Z, θ)服务磨削场景。stl_io.extract_embedded_mesh还能从.exe或.stpack里扫描并解出内嵌 STL 或 zlib 流复原原程序自包含导出件的行为。命令行与图形界面命令行接口分成若干子命令。info统计三角形与顶点数量、包围盒、表面积、体积、连通分量、退化面、边界边、非流形边以及水密性支持 JSON 输出。render能把模型渲染成 PNG参数多到能覆盖尺寸、标准视图、偏航与俯仰、正交、线框、整件同色、单色平面、按分量着色、按曲率着色、法向彩虹、平滑着色、边线、网格、十字线、透明背景、透明对象、背景色、渐变、超采样、抗锯齿、双倍分辨率、焊接开关和 JSON 输出。convert用于转换与修复可在 ASCII 与二进制之间切换也可焊接顶点、修复法线、批量输出。components按连通分量拆分模型并可按最小三角形数过滤。measure覆盖两点距离、三点圆、两点圆、三点夹角、单点、RZ 点、指定 Z 高度最大回转半径等计算支持英寸单位和 JSON 输出。无图形环境或批处理场景python -m num_stl_viewer --cli --help能看全部用法。图形界面更贴近原程序的菜单树。文件菜单负责打开、追加、关闭、导出 STL、保存图片、剪贴板与透明背景版、最近文件与退出视图菜单提供标准视图、透视与正交切换、重置、适配窗口以及显示坐标轴、网格、十字线、边线、背景渐变、鼠标灵敏度、拖拽画质、抗锯齿和载入时焊接顶点等选项网格菜单处理焊接、移除退化面、修复法线、翻转法线、着色模式、统计、分割连通分量与删除选中光照菜单提供预设与环境光、漫反射、高光调节测量菜单可创建点、3D 直线、2D 直线、两点圆、三点圆、角度与 RZ 点标注显示在画布上并可清除附加菜单包含旋转动画、宏录制与播放、语言切换和单位切换帮助菜单提供关于对话框。着色模式有个小插曲。实体视图默认是整件同色加光照明暗也就是零件本来的样子。单色平面关闭逐顶点插值直接看到三角面片按分量让每个连通分量一种颜色按曲率显示曲率热力图按法向显示法向彩虹图。早期版本把默认的auto接到了法向彩虹图上实体视图看起来像花花的贴图这正是“模型显示失真”反馈的来源。后来拆出了独立的orientation模式默认回到整件同色要按分量、按曲率、按法向得自己切。交互层面也做了细致设计。左键拖拽轨道旋转Ctrl 加左键拖拽或开启 Rotate around Z 后绕 Z 轴旋转中键与右键拖拽平移Shift 加左键拖拽平移Alt 加左键拖拽缩放滚轮与触控板缩放左键单击在测量模式下取点并自动吸附最近顶点或边双击适配窗口方向键旋转Shift 加方向键平移Ctrl 加方向键绕 Z 或俯仰加号与减号缩放F 与 Home 适配窗口R 复位旋转中心数字 1 到 8 切换标准视图空格播放或暂停旋转动画Esc 取消当前测量。光标会随操作模式变化拖拽时状态栏同步显示当前操作与帧率。性能与画质的拉扯软件光栅化在 Python 里性能与画质的平衡是个绕不开的问题。复现版采用渐进式渲染任何一次视图变化都先立刻画出一张低分辨率快帧空闲 160 毫秒后再把画面精修成全分辨率加抗锯齿。整幅ss2渲染典型模型要一二百毫秒若同步画完会阻塞事件循环拖拽期间鼠标事件只能排队松手后模型会“跳”一大截。自适应分辨率则在拖拽旋转、平移、缩放以及旋转动画与宏播放期间降低画质默认降到视图选项设定的拖拽画质上限以下通常为 50%并按实测帧耗时向 40fps 收敛慢了降到 75%快了提到 115%范围在 15% 到 100% 之间上下阈值带有 1.35 倍与 0.6 倍迟滞避免画面忽清忽糊。鼠标灵敏度可调旋转默认 0.20 度每像素平移默认 1.0即模型严格跟随光标。之前硬编码 0.35拖 300 像素就转 105 度随手一拖模型就跑掉了。降分辨率位图由高度反推宽度保证放大铺满视口时不会把模型拉扁或拉长渲染视口在每次渲染时与目标位图尺寸同步因此降分辨率与全分辨率画面完全对齐不会偏移或裁切。测试与边界核心测试 103 项图形界面测试 63 项全部通过。覆盖数学、IO、网格、相机、渲染、视口、测量、宏、命令行以及界面构造、鼠标交互、渲染与导出。tests/make_sample.py能重新生成 samples 下的示例与预览图tests/check_drag_visual.py生成拖拽与静止对齐对比图与界面截图。这个项目是教学与研究性质的重实现不包含原程序任何二进制代码。原程序及相关商标归 NUM 所有。它的意义在于把逆向结论变成一套可运行、可验证、可扩展的 Python 实现让一个封闭的 Windows 可执行文件变成可以被阅读、测试和继续改造的工程样本。你可以用python run_viewer.py model.stl打开模型也可以用python -m num_stl_viewer --cli render model.stl -o out.png --double --antialias出图还可以钻进renderer.py改一改分桶策略看看帧率会怎么变。那个 5.19 MB 的 exe 不再是黑盒它变成了一堆可以阅读的 Python 代码和一堆可以复现的性能数字。