最先让我决定写这篇文章的,是一次让我极其狼狈的内部需求:市场部让我把一个老工程师电脑里的STEP模型放到公司网页上,说“转换一下就行”。我当时也以为STEP转GLB就是类似Office文档另存为的操作,结果连试了好几个工具,模型要么所有零件焊成了一坨,要么从毫米直接变成米,网页上打开后视角里根本找不到模型。后来我才彻底弄明白,CAD和Web之间这条转换链,本质上不是在“换格式”,而是在把精确的数学曲面重新解释成浏览器能渲染的三角形网格。这篇文章就是我从那次踩坑之后梳理出的完整实践流程,适合正在做Web端CAD预览、产品展示或三维图纸归档的工程师参考。
1. STEP/IGES里的“精确”和GLB里的“好看”根本是两套体系
1.1 STEP/IGES本质上是数学曲面,不是网格
很多刚接触CAD数据的人会默认STEP/IGES是一种“三维模型格式”,这个说法不算错,但它和GLB完全是两个维度的东西。
STEP文件保存的是边界表示(B-rep)数据。所谓边界表示,意思是模型不是由一个个三角形拼出来的,而是由曲面方程、曲线方程、顶点、边和面之间的拓扑关系构成。举个例子,一个圆柱孔在STEP文件里记录的是“这是一个半径5mm、轴线方向为X轴、深度20mm的圆柱面”,而不是圆周上几百个三角形顶点的坐标。IGES更老一些,它最初的设计目标偏向于曲面和曲线交换,很多早期IGES文件里甚至只有曲面片,没有实体的概念。这两种格式都追求“精确”,目标是让SolidWorks、CATIA、NX这些CAD软件能完整还原设计意图。
GLB则是glTF的二进制容器,glTF被称作“3D界的JPEG”,它的核心服务对象是渲染器和游戏引擎。GLB文件里装的是场景树、三角形网格、顶点坐标、法线、UV、材质参数、贴图,还有可选的动画和节点变换。WebGL和Three.js读取GLB时,不需要也不可能去解析NURBS曲面方程,它只需要拿到一堆三角形,然后按顶点索引画出来就行。
1.2 转换的三层含义:曲面离散化、拓扑重组、数据简化
既然STEP/IGES和GLB底层逻辑不同,那它们之间的“转换”就必然包含三个动作。
第一层是曲面离散化。把NURBS曲面、圆柱面、圆锥面按一定的精度切成三角形网格。这里有个核心参数叫“线性偏差”,它决定了三角形边到原始曲面的最大距离。偏差越小,网格越贴近原始几何,但三角形数量会指数级上升。
第二层是拓扑重组。STEP文件里的装配树包含零件层级、实例引用、坐标系变换,GLB也有节点层级,但两者并不是一一对应。尤其是在处理子装配、多实例零件时,如果转换工具不认真处理装配树,很容易把所有零件拍扁到一个层级里,甚至直接合并成一个网格。
第三层是数据简化。STEP文件可以不携带任何渲染信息,也可以携带颜色、图层、产品属性、GD&T标注。到了GLB里,这些元数据绝大部分都用不上,我们需要做的是把“有用的”保住,把“没用的”丢掉。
提示:判断一个转换工具靠不靠谱,不要只看它能不能生成GLB,还要看它在上述三个环节里给了你多少控制权。
2. 工具链选型:为什么我默认走FreeCAD + Blender + gltfpack
2.1 在线转换器能用,但只有“能用”两个字
先说我为什么不推荐在线转换器。确实,现在不少网站提供STEP转GLB,拖进去点一下就能下载。优点是省事,缺点是太多东西不可控。
首先是精度不可控。在线服务为了保证响应速度,往往采用很粗糙的默认偏差值,小零件上的圆角、倒角、螺纹特征转完就变成多边形。其次是安全不可控。机械设计文件经常涉及未发布产品,把源文件传到别人服务器上,对很多公司来说是不可接受的操作风险。再次是文件大小限制,你传一个200MB的装配体上去,要么超时,要么被限制到必须注册。
我不否认在线工具适合快速验证一个模型能不能转,但如果你要做的是“高效转换”,也就是可控、可重复、可批量执行,那就必须把核心步骤放在本地工具链里。
2.2 主流程工具的分工与搭配逻辑
我推荐的默认组合是FreeCAD + Blender + gltfpack,三者的分工非常明确。
FreeCAD负责读取STEP/IGES并做曲面离散化。它底层用的是Open CASCADE(OCCT),这是工业级CAD内核,对STEP和IGES的兼容性远好于那些直接把模型丢给网格库的轻量工具。FreeCAD支持Python脚本,这意味着你可以把导入、网格化、导出整条链路写成脚本,扔进批处理流程里跑。
Blender负责GLB文件的前期修复和最终导出。它集成glTF导出器非常成熟,支持Draco压缩、材质扩展、节点层级导出,而且Blender也可以命令行方式运行,适合批处理。
gltfpack是个轻量命令行工具,来自meshoptimizer库,它能在不引入额外运行时依赖的情况下进一步压缩GLB。对Web端来说,这一步能把首屏加载时间砍掉一半甚至更多。
这套组合还有一个隐形优势:全部免费开源。团队内部使用没有任何授权合规负担,也不用担心试用期结束后流程突然断掉。
3. 完整转换流程:从STEP/IGES文件到可上Web的GLB
3.1 预处理:单位、装配结构和源文件检查
很多人拿到STEP文件就急着拖进转换工具,我不建议这样。按我的习惯,先做三件事。
第一件,确认单位。STEP标准用LENGTH_UNIT实体记录单位,绝大多数情况是毫米,但偶尔也有英寸。FreeCAD导入时会读取单位,可如果源文件本身单位定义混乱,后面所有尺寸都会对不上。一个快速检查办法:在FreeCAD里导入后测量一个已知特征的长度。如果实际应是100mm的边长显示成了2.54,那基本就是英寸被当毫米读了。
第二件,确认装配结构。STEP文件如果是单一零件,简单很多。如果是装配体,导入后要在FreeCAD的树形视图里检查:有几层子装配?哪些零件是重复实例?有没有外部引用(一般表现为文件里的只读参考)?外部引用零件经常导致转换后模型残缺,建议在CAD源文件里把外部引用做成普通零件再导出STEP。
第三件,查源文件有没有“隐藏杀手”。有些模型为了做布尔运算,内部有大量辅助曲面、隐藏实体,这些在CAD软件里看不见,但在转换时会变成一层一层重叠的碎面。遇到这种源文件,最好的办法是回到原始CAD软件里删掉辅助几何再导出STEP,比在转换后修复省力十倍。
3.2 FreeCAD导入与网格化参数设置
打开FreeCAD后新建一个文档,用文件菜单导入STEP。我这里更推荐直接跑脚本,因为脚本能记录参数,方便后续批处理复用。
import FreeCAD as App import Import import MeshPart import Mesh # 设定文档和导入 doc = App.newDocument("cad_conv") Import.insert("input_model.step", doc.Name) # 遍历文档内所有对象,做一个融合网格 mesh = Mesh.Mesh() for obj in doc.Objects: if hasattr(obj, "Shape") and obj.Shape.isNull() is False: part_mesh = MeshPart.meshFromShape( Shape=obj.Shape, LinearDeflection=0.5, AngularDeflection=0.5, Relative=False ) mesh.addPart(part_mesh) # 导出OBJ,比STL更适合后续进Blender操作 mesh.write("output_model.obj")两个参数是重点。
LinearDeflection是线性偏差,单位和你模型的单位一致。它表示网格顶点到原始曲面表面的最大允许距离。以毫米为单位的产品模型,我一般取0.1到0.5mm。如果模型包络尺寸超过1米,比如一个机床或汽车部件,取0.5到1mm就足够,肉眼根本看不出差别,但网格量会少一个数量级。
AngularDeflection是角度偏差,单位是度。它控制曲面在弯曲处的网格密度,默认0.5度效果很好,但网格很大。我的经验:如果模型以平面和圆柱面为主,角度偏差放到1到2度问题不大;如果全是自由曲面,建议保持0.5度以下,否则曲面边缘会出现明显的折痕。
3.3 导出OBJ后在Blender里做最后修补
从FreeCAD导出的OBJ通常只是“能看”的水平,离“能上线”还有距离。在Blender里,我有一套固定操作。
首先导入OBJ,勾选“拆分法线”选项,因为FreeCAD导出的OBJ法线大多是面法线,曲面会显得很硬。导入后按N键打开右侧面板,把视图变换里的缩放锁定打开,确认模型尺寸和真实尺寸一致。
然后做一次全局法线检查:进入编辑模式,开启“面朝向”叠加显示。蓝色的面是法线朝外的,红色的是法线朝内。STEP转换过来的网格偶尔会出现个别面法线反转,如果不修,Web端渲染时这些面会发黑或透明。修复方式简单粗暴,选中所有面,用快捷键Alt+N重算外侧。
接下来是去除重叠顶点。CAD转换的网格经常有大量重复顶点,在编辑模式下按M合并距离设为0.001mm即可。这个操作能把顶点数减少10%到20%,对渲染性能是实打实的提升。
材质这一步,如果原始STEP带颜色,FreeCAD导出OBJ时会一并生成MTL文件,导入Blender后材质会和对象绑定。如果原始文件没带颜色,那么只能在Blender里按对象批量指定材质了。我一般会把同一类零件选中,新建一个PBR材质,基础色设为原CAD颜色,粗糙度设为0.5左右,金属度看情况。机械零件我倾向于金属度0.3到0.8加一点粗糙度,观感比默认值好很多。
3.4 按Web标准导出GLB并压缩
Blender里确认完模型后,就可以导出GLB了。导出前必须检查两项设置。
第一项是坐标朝向。CAD软件普遍是Z轴向上,glTF标准是Y轴向上。Blender导出GLB时会自动处理这个转换,但前提是你在导出面板里选择“+Y up”。如果选错了,模型在Web端会躺倒90度。
第二项是压缩。Blender导出面板勾选“Draco压缩”,这能把大模型的几何数据压缩掉80%左右。注意使用Draco必须在网页加载时引入DRACOLoader,用Three.js的话还要把draco_decoder.wasm文件放到静态目录。如果不想引入额外的解码器,可以暂时不勾选Draco,改用下一步的gltfpack。
gltfpack -i model.glb -o model_compressed.glb -cc-cc参数表示对网格几何进行量化压缩,-tc参数则是对纹理进行处理。处理完之后的GLB比原始文件小很多,而且被大多数glTF加载器原生支持,不需要额外写代码。我的习惯是:小模型直接Draco,大模型先用gltfpack,因为gltfpack的网格优化算法对提取过重叠顶点的模型效果极佳。
导出后用Three.js的GLTFLoader跑一遍,再打开浏览器开发者工具的性能面板看一下DrawCall数量。机械模型如果零件数量多,建议在Blender里把静态的、永远不会动的零件合并成几个大网格,这能显著降低渲染压力,后面我会再详细说。
4. 实际项目里那些“转换完才发现”的问题
4.1 一次装配体转换的排查全过程
有一次我处理一个带8个子装配、400多个零件的设备模型。转换完GLB后,我用在线预览工具看整体外观没问题,结果客户在系统里点开零件清单,发现少了24个零件。
我第一反应是STEP文件没导全,但重新检查源文件,发现零件都在。后来我把GLB重新导入Blender,逐个零件对照FreeCAD里的树结构,才定位到问题:STEP文件里有几个零件是重复实例引用,FreeCAD在合并网格时默认折叠了重复实例。这个折叠对展示外观没有影响,但零件编号列表里就缺了。
解决办法不是改FreeCAD,而是在导出OBJ前把每个需要保留的实例单独提取出来。脚本里改为逐个零件导出,不合并,每个零件一个OBJ文件,或者用OBJ组的方式保留命名,再进Blender统一装配。
这个经历让我养成了一个习惯:任何装配体模型,转换后第一件事不是看外观,而是先核对零件数量、命名和层级。
4.2 网格密度和精度的平衡点
网格密度是CAD转Web里最容易被忽视的问题。很多人为了追求“精度”,把线性偏差设到0.01mm,结果一个零件就变成上千万个三角形,浏览器直接卡死。
我用倒角举例。一个半径2mm的倒角,如果你用0.5mm的线性偏差去离散化,在平面上看不出问题,但在倒角圆弧上会呈现多边形效果。这个效果在截图里能看出来,在旋转视角时尤其明显。如果你把这个模型投到网页上,客户看的是外观,他们不会用卡尺去量倒角半径。
所以实操上,我做两条线处理:
| 应用场景 | 线性偏差 | 角度偏差 | 说明 |
|---|---|---|---|
| 大型设备整体展示 | 1.0mm | 2.0° | 网格量小,载入快 |
| 中小零件的Web交互展示 | 0.3mm | 0.5° | 兼顾细节和性能 |
| 需要近距离精细浏览 | 0.1mm | 0.25° | 网格大,建议按需加载 |
| 测量或横截分析 | 不用网格 | 不用网格 | 保留STEP,不要转GLB |
首要原则是“目标导向”。模型在网页上用来干什么,决定了网格密度。如果只是为了选型和展示,就大胆牺牲精度换性能。如果客户可能会在网页上放大某个接口细节,那局部加密比全局加密更明智。
4.3 碎面、破孔和法线问题怎么处理
STEP/IGES转换出的网格并不总是闭合的,常见三类问题。
第一类是碎面。曲面离散化后,边缘位置经常出现细小长条三角形。它们不影响显示,但会拖慢加载。在Blender里做一次“合并距离0.01mm”的顶点合并,然后对单独小三角带来做一次Mesh清理,通常能清掉绝大多数。
第二类是破孔。两个曲面在理论上是相接的,但离散化后因为偏差不同,边缘对不上,形成一道缝隙。在Blender里的检查方式是进入编辑模式,选中全部边缘,Select > Select All by Trait > Non Manifold,把非流形边显示出来。如果是小缝隙,把相邻边桥接或填充面;如果缝隙很大,那就要回FreeCAD重新用更高的网格密度导出。
第三类是法线错误。IGES文件里出现法线翻转的概率比STEP高很多,因为早期IGES曲面本来就容易丢拓扑方向。修复法线不需要手工选面,用Blender的Alt+N > Recalculate Outside可以统一处理。处理完以后用Solidify修改器加极薄厚度或者直接用Mesh Analysis面板检查,彻底干净的模型在Web端不会有暗面。
这里我特别想说一句:不要试图在GLB阶段去修复CAD源文件的错误。源文件里如果某个曲面本来就是坏的,那你无论如何修网格都是治标不治本,最好的处理方式是回到源头,把该修的面在CAD里修好再重新走一遍流程。
5. 把转换流程工程化:命名规范、批处理与二次复用
5.1 用脚本把FreeCAD工作流自动化
一次两次手动操作没问题,但当一个项目里要转几十个STEP,手动效率就完全跟不上。FreeCAD的Python接口这时候就能派上大用场。
我写了一个简易转换脚本,放在项目目录下直接跑:
import os import sys import FreeCAD as App import Import import MeshPart import Mesh def convert_step(step_path, obj_path, linear=0.3, angular=0.5): doc_name = os.path.splitext(os.path.basename(step_path))[0] doc = App.newDocument(doc_name) Import.insert(step_path, doc.Name) combined = Mesh.Mesh() for obj in doc.Objects: if hasattr(obj, "Shape") and not obj.Shape.isNull(): shape_mesh = MeshPart.meshFromShape( Shape=obj.Shape, LinearDeflection=linear, AngularDeflection=angular, Relative=False ) # 保留对象名称,方便Blender里辨识零件 # 这里选择合并,若需逐个零件导出则拆分保存 combined.addPart(shape_mesh) combined.write(obj_path) App.closeDocument(doc.Name) if __name__ == "__main__": step_file = sys.argv[1] obj_file = sys.argv[2] convert_step(step_file, obj_file)命令行调用方式:
python convert_step.py input_model.step output_model.obj建议所有输入输出路径都用英文。中文字符路径不是不行,但一旦CI环境或某些命令行工具遇到编码问题,排查起来非常消耗时间。
5.2 用Blender命令行做批量导出
FreeCAD导出OBJ之后,原本需要在Blender手动做的工作也能用命令行批量完成。下面这个脚本用Blender的Python接口实现对OBJ批量化处理并导出GLB:
import bpy import sys import os argv = sys.argv[sys.argv.index("--") + 1:] input_dir, output_dir = argv[0], argv[1] for filename in os.listdir(input_dir): if not filename.endswith(".obj"): continue # 清空场景 bpy.ops.wm.read_factory_settings(use_empty=True) filepath = os.path.join(input_dir, filename) bpy.ops.wm.obj_import(filepath=filepath) # 合并重复顶点 bpy.ops.object.select_all(action='SELECT') bpy.ops.object.join() obj = bpy.context.active_object bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=0.0001) bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') # 导出GLB out_path = os.path.join(output_dir, filename.replace(".obj", ".glb")) bpy.ops.export_scene.gltf( filepath=out_path, export_format='GLB', use_selection=False, export_yup=True, export_draco_mesh_compression_enable=False ) print("batch export done")然后在命令行执行:
blender -b -P batch_export.py -- ./obj_dir ./glb_dir这条命令意味着Blender可以在无界面状态下批量处理所有OBJ,非常适合定时任务或CI流水线。
5.3 与前端加载代码对接的最后验证
模型转出来最终要进Web页面,所以在工程化流程最后一步,我建议写一个最简单的前端验证页面,挂到本地静态服务器上检查三个东西。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { margin: 0; overflow: hidden; background: #222; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 10000); camera.position.set(500, 400, 600); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); scene.add(new THREE.HemisphereLight(0xffffff, 0x333333, 1)); const loader = new GLTFLoader(); loader.load('model_compressed.glb', (gltf) => { scene.add(gltf.scene); const box = new THREE.Box3().setFromObject(gltf.scene); const center = box.getCenter(new THREE.Vector3()); gltf.scene.position.sub(center); }); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); </script> </body> </html>这个页面主要检查三件事:第一,模型能不能正常加载显示;第二,方向是否正确,CAD的Z轴是否变成了所预期的方向;第三,模型的包围盒中心是否合理,如果模型离原点很远,镜头定位会出问题。代码中的box.getCenter这一行就是把模型居中,这是Web端CAD展示的常用技巧。
6. 什么时候不该用“转换GLB”这条路
6.1 要看CAD原始属性的需求
GLB能保留节点命名、层级和部分自定义属性,但保留不了GD&T标注、几何尺寸、约束关系、设计历史树。如果你的Web端业务需求是“让客户查看STEP模型的所有原始数据和标注”,那转GLB就是错误方向。
这类需求我更建议直接对接Web端CAD查看器SDK,它读取原始STEP文件,在后端做解析,前端只负责交互。代价是需要处理许可证和服务器计算资源,但换来的是真正的CAD数据完整性。
6.2 超大装配的Web展示替代思路
动辄几个GB的超大型装配,你费劲转成一个GLB,浏览器加载大概率会直接卡崩。即使压缩,DrawCall数量也会让渲染性能惨不忍睹。
遇到超大模型,我的替代方案是分块处理,而不是一次转换。把装配体按子装配拆分成几十个GLB,然后在前端做视距加载:相机靠近时加载细节零件,远离时只加载整体轮廓。另外,给模型做LOD(多级细节)也很实用——远距离显示粗糙低面数模型,近距离切换高精度模型。这类架构比死磕“一步到位转GLB”靠谱得多。
6.3 对精度敏感的制造和测量场景
如果你的Web端要做剖切测量,或者要求用户能在网页里量出精确到0.01mm的尺寸,GLB就完全不合适。网格化必然带来精度丢失,测量结果会出现偏差,这在工程上是不可接受的。
正确做法是保留STEP文件接口,在前端只做展示,测量请求发送到后端,由后端用CAD内核计算精确结果再返回给前端。这也是现在很多PLM和图纸管理系统的标准做法。记住一个边界原则:GLB负责“看”,STEP负责“用”,两者不要混在一起。
说回流程本身,我在实际项目里反复用这套方案处理过几十个模型,从几MB的单零件到几百MB的装配体都有。做得越多越能感受到一个道理:转换流程的核心不是工具,而是你对“精度”和“用途”的判断。该精细的地方精细,该偷懒的地方偷懒,做出来的GLB才能在浏览器里既流畅又好看。
最后分享一个我自己用得很顺的小习惯:每次转完模型,我会顺手在导出的GLB目录下放一个convert_log.txt,记下源文件版本、FreeCAD参数、Blender版本和导出时间。后续模型出了问题,翻一下日志就能定位是哪一步的参数不对。这个小动作帮我省过好几次重复排查的时间。