wireframe-to-3d
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWireframe-to-3D Conversion
线框转3D转换
Convert 2D orthographic wireframe images to parametric 3D Blender models, exported as glTF 2.0 binary ().
.glb将2D正交线框图像转换为参数化3D Blender模型,并导出为glTF 2.0二进制格式()。
.glbOverview
概述
The skill drives a four-stage pipeline:
- Analyze wireframe images locally with (OpenCV → Bezier control points in JSON).
scripts/wireframe_analyzer.py - Generate Blender Python code that recreates the contours as parametric Bezier curves.
- Execute code in Blender via , converting curves to meshes with PBR materials.
mcp__blender__execute_blender_code - Export as optimized GLB (≤ 15 MB), validating size and topology.
You (Claude) are the orchestrator. The directory contains the only standalone code (); everything else is patterns you emit and run via MCP.
scripts/wireframe_analyzer.py该技能驱动一个四阶段流水线:
- 分析:使用本地的脚本(基于OpenCV)分析线框图像,输出包含贝塞尔曲线控制点的JSON文件。
scripts/wireframe_analyzer.py - 生成:生成Blender Python代码,将轮廓重新创建为参数化贝塞尔曲线。
- 执行:通过在Blender中执行代码,将曲线转换为带有PBR材质的网格。
mcp__blender__execute_blender_code - 导出:导出为优化后的GLB文件(≤15 MB),并验证文件大小和拓扑结构。
你(Claude)是该流程的协调器。目录包含唯一的独立代码;其余所有内容均为你通过MCP生成并运行的代码模式。
scripts/wireframe_analyzer.pyPrerequisites — check first
先决条件——先检查
Before any wireframe work, verify the environment:
-
Blender MCP is reachable. Call. If it errors with "Could not connect to Blender", stop and tell the user:
mcp__blender__get_scene_info"Blender's MCP addon isn't running. Start Blender, enable the BlenderMCP addon (port 9876), then re-run." -
Python deps for the analyzer. Run:
python3 -c "import cv2, numpy, scipy" 2>&1If it errors, run(or instruct the user to).pip install opencv-python numpy scipy Pillow -
Image input. Confirm the user provided at least one PNG. Reasonable bounds: ≥ 400×400 px, black-on-white or white-on-black line art.
在处理任何线框任务之前,请验证环境:
-
Blender MCP可访问:调用。如果报错“Could not connect to Blender”,请停止操作并告知用户:
mcp__blender__get_scene_info"Blender的MCP插件未运行。请启动Blender,启用BlenderMCP插件(端口9876),然后重新运行。" -
分析器的Python依赖:运行以下命令:
python3 -c "import cv2, numpy, scipy" 2>&1如果报错,请运行(或指导用户执行此操作)。pip install opencv-python numpy scipy Pillow -
图像输入:确认用户提供了至少一张PNG图像。合理的图像范围:≥400×400像素,黑底白线或白底黑线的线条画。
Decision flow
决策流程
Q1: How many views?
Q1:视图数量?
- Single view → flat 2D extrusion only (warn the user; depth must be supplied or assumed).
- Front + side → full 3D reconstruction (silhouette × depth profile).
- Front + side + back → use back view for symmetry validation.
- 单视图 → 仅进行平面2D挤出(需提醒用户;必须提供或假设深度值)。
- 正面+侧面 → 完整3D重建(轮廓×深度剖面)。
- 正面+侧面+背面 → 使用背面视图进行对称性验证。
Q2: Detail level?
Q2:细节级别?
- — RDP epsilon = 4.0, target ~1–2k tris, < 1 MB GLB.
preview - — RDP epsilon = 2.0, target ~5–8k tris, 2–4 MB GLB. Default.
production - — RDP epsilon = 1.0, target ~10–20k tris, may need Decimate to stay under 15 MB.
high
- — RDP epsilon=4.0,目标三角形数量约1–2k,GLB文件<1 MB。
preview - — RDP epsilon=2.0,目标三角形数量约5–8k,GLB文件2–4 MB。默认选项。
production - — RDP epsilon=1.0,目标三角形数量约10–20k,可能需要使用Decimate(减面)工具以保持文件大小在15 MB以内。
high
Q3: Geometry type?
Q3:几何类型?
- — frames, arms, hinges. Use
wireson curves.bevel_depth - — lenses, domes. Use lofted profiles or fill caps.
surfaces - — both. Default for glasses-like objects.
hybrid
- — 框架、支架、铰链。对曲线使用
wires参数。bevel_depth - — 镜片、穹顶。使用放样剖面或填充封口。
surfaces - — 同时包含上述两种类型。类似眼镜的物体默认选项。
hybrid
Q4: Real-world scale?
Q4:真实世界比例?
- If the user gave dimensions (e.g., "glasses are 140 mm wide"), use them.
- Otherwise infer from wireframe aspect ratio and assume a sensible default (140 mm width for glasses, 180 mm for helmets, etc.). Confirm with user if not obvious.
- 如果用户提供了尺寸(例如“眼镜宽140毫米”),请使用该尺寸。
- 否则根据线框的宽高比推断,并假设一个合理的默认值(眼镜宽140毫米,头盔宽180毫米等)。如果不明确,请与用户确认。
Stage 1 — Run the analyzer
阶段1 — 运行分析器
Run the bundled analyzer once per view:
bash
python3 ${CLAUDE_SKILL_DIR}/scripts/wireframe_analyzer.py <input.png> <output.json>The script outputs JSON with this shape:
json
{
"metadata": {"image_size": [W, H], "num_contours": N, "parameters": {...}},
"contours": [[[x, y], ...], ...],
"bezier_curves": [[[[P0], [P1], [P2], [P3]], ...], ...]
}Tuning RDP epsilon (only if defaults fail):
- Output has too few/jagged contours → lower epsilon to 1.0–1.5.
- Output has too many noisy points → raise epsilon to 3.0–4.0.
- Pass via (or edit the call in the script).
--rdp-epsilon
Read the JSON with . Do not pass huge JSON blobs to Blender — extract what you need first.
Read为每个视图运行一次捆绑的分析器:
bash
python3 ${CLAUDE_SKILL_DIR}/scripts/wireframe_analyzer.py <input.png> <output.json>脚本输出的JSON结构如下:
json
{
"metadata": {"image_size": [W, H], "num_contours": N, "parameters": {...}},
"contours": [[[x, y], ...], ...],
"bezier_curves": [[[[P0], [P1], [P2], [P3]], ...], ...]
}调整RDP epsilon参数(仅在默认值失效时使用):
- 输出轮廓过少/锯齿状 → 将epsilon降低至1.0–1.5。
- 输出包含过多噪点 → 将epsilon提高至3.0–4.0。
- 通过参数传递(或编辑脚本中的调用)。
--rdp-epsilon
使用读取JSON文件。不要将大型JSON blob传递给Blender — 先提取所需内容。
ReadStage 2 — Generate Blender code
阶段2 — 生成Blender代码
Build code in small, self-contained chunks (each call gets a fresh Python namespace; only persists between calls). Always re-import what you need.
execute_blender_codebpy.data以小型、独立的代码块构建代码(每次调用都会获得一个全新的Python命名空间;只有在调用之间持久化)。始终重新导入所需模块。
execute_blender_codebpy.dataPattern: create a Bezier curve from control points
模式:从控制点创建贝塞尔曲线
python
import bpypython
import bpyIdentify by stable name; bpy.data persists between calls.
使用稳定名称标识;bpy.data在调用之间持久化。
name = 'GEO-lens-right'
curve_data = bpy.data.curves.new(name=name, type='CURVE')
curve_data.dimensions = '3D'
curve_data.resolution_u = 16 # tessellation resolution
curve_data.bevel_depth = 0.001 # 1 mm wire thickness (adjust for surfaces)
curve_data.use_fill_caps = True
obj = bpy.data.objects.new(name, curve_data)
bpy.context.collection.objects.link(obj)
name = 'GEO-lens-right'
curve_data = bpy.data.curves.new(name=name, type='CURVE')
curve_data.dimensions = '3D'
curve_data.resolution_u = 16 # 细分分辨率
curve_data.bevel_depth = 0.001 # 1毫米线框厚度(针对曲面可调整)
curve_data.use_fill_caps = True
obj = bpy.data.objects.new(name, curve_data)
bpy.context.collection.objects.link(obj)
Control points come from the analyzer JSON (px → mm scaling done client-side).
控制点来自分析器的JSON文件(像素→毫米的缩放在客户端完成)。
control_points = [(0.0, 0.0, 0.0), (0.5, 1.0, 0.0), (1.5, 1.0, 0.0), (2.0, 0.0, 0.0)]
spline = curve_data.splines.new(type='BEZIER')
spline.bezier_points.add(len(control_points) - 1)
for i, (x, y, z) in enumerate(control_points):
pt = spline.bezier_points[i]
pt.co = (x, y, z)
pt.handle_left_type = 'ALIGNED' # C¹ smooth
pt.handle_right_type = 'ALIGNED'
print(f"created:{name}") # signal back via stdout
**Pixel → world conversion** (do this in the code you generate, before sending to Blender):norm_x = px_x / img_width
norm_y = 1.0 - (px_y / img_height) # flip Y; image origin is top-left
x_world = (norm_x - 0.5) * world_width_mm / 1000.0 # to metres
y_world = (norm_y - 0.5) * world_height_mm / 1000.0
undefinedcontrol_points = [(0.0, 0.0, 0.0), (0.5, 1.0, 0.0), (1.5, 1.0, 0.0), (2.0, 0.0, 0.0)]
spline = curve_data.splines.new(type='BEZIER')
spline.bezier_points.add(len(control_points) - 1)
for i, (x, y, z) in enumerate(control_points):
pt = spline.bezier_points[i]
pt.co = (x, y, z)
pt.handle_left_type = 'ALIGNED' # C¹平滑
pt.handle_right_type = 'ALIGNED'
print(f"created:{name}") # 通过标准输出返回信号
**像素→世界坐标转换**(在生成的代码中执行此操作,再发送到Blender):norm_x = px_x / img_width
norm_y = 1.0 - (px_y / img_height) # 翻转Y轴;图像原点为左上角
x_world = (norm_x - 0.5) * world_width_mm / 1000.0 # 转换为米
y_world = (norm_y - 0.5) * world_height_mm / 1000.0
undefinedPattern: convert curves to mesh + cleanup
模式:将曲线转换为网格并清理
python
import bpy
name = 'GEO-lens-right'
obj = bpy.data.objects[name]
bpy.context.view_layer.objects.active = obj
bpy.ops.object.convert(target='MESH')
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=0.0001)
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shade_smooth()
mesh = obj.data
print(f"mesh:{name} verts:{len(mesh.vertices)} polys:{len(mesh.polygons)}")python
import bpy
name = 'GEO-lens-right'
obj = bpy.data.objects[name]
bpy.context.view_layer.objects.active = obj
bpy.ops.object.convert(target='MESH')
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=0.0001)
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shade_smooth()
mesh = obj.data
print(f"mesh:{name} verts:{len(mesh.vertices)} polys:{len(mesh.polygons)}")Pattern: PBR material (Principled BSDF — the only shader glTF exports cleanly)
模式:PBR材质(Principled BSDF — 唯一能被glTF干净导出的着色器)
python
import bpy
mat = bpy.data.materials.get('MAT-frame-metal') or bpy.data.materials.new('MAT-frame-metal')
mat.use_nodes = True
bsdf = mat.node_tree.nodes['Principled BSDF']
bsdf.inputs['Base Color'].default_value = (0.08, 0.08, 0.10, 1.0)
bsdf.inputs['Metallic'].default_value = 1.0
bsdf.inputs['Roughness'].default_value = 0.25
obj = bpy.data.objects['GEO-frame']
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
print('material:assigned')Material presets (use these unless the user specifies):
- —
MAT-frame-metalbase, metallic=1.0, roughness=0.25 (brushed steel)(0.08, 0.08, 0.10) - —
MAT-lens-mirrorbase, metallic=0.8, roughness=0.05, IOR=1.5 (mirror glass)(0.05, 0.08, 0.15) - —
MAT-pad-siliconebase, metallic=0.0, roughness=0.7 (matte silicone)(0.65, 0.63, 0.60)
python
import bpy
mat = bpy.data.materials.get('MAT-frame-metal') or bpy.data.materials.new('MAT-frame-metal')
mat.use_nodes = True
bsdf = mat.node_tree.nodes['Principled BSDF']
bsdf.inputs['Base Color'].default_value = (0.08, 0.08, 0.10, 1.0)
bsdf.inputs['Metallic'].default_value = 1.0
bsdf.inputs['Roughness'].default_value = 0.25
obj = bpy.data.objects['GEO-frame']
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
print('material:assigned')材质预设(除非用户指定,否则使用这些预设):
- — 基础色
MAT-frame-metal,金属度=1.0,粗糙度=0.25(拉丝钢)(0.08, 0.08, 0.10) - — 基础色
MAT-lens-mirror,金属度=0.8,粗糙度=0.05,IOR=1.5(镜面玻璃)(0.05, 0.08, 0.15) - — 基础色
MAT-pad-silicone,金属度=0.0,粗糙度=0.7(哑光硅胶)(0.65, 0.63, 0.60)
Pattern: export to GLB
模式:导出为GLB
python
import bpy, os
filepath = '/tmp/wireframe_output.glb'
bpy.ops.export_scene.gltf(
filepath=filepath,
export_format='GLB',
export_materials='EXPORT',
export_uv=True,
export_normals=True,
export_animations=False,
export_yup=True,
)
size_mb = os.path.getsize(filepath) / (1024 * 1024)
print(f"export:{filepath} size_mb:{size_mb:.2f}")If : apply Decimate and re-export (see error recovery).
size_mb > 15python
import bpy, os
filepath = '/tmp/wireframe_output.glb'
bpy.ops.export_scene.gltf(
filepath=filepath,
export_format='GLB',
export_materials='EXPORT',
export_uv=True,
export_normals=True,
export_animations=False,
export_yup=True,
)
size_mb = os.path.getsize(filepath) / (1024 * 1024)
print(f"export:{filepath} size_mb:{size_mb:.2f}")如果:应用Decimate(减面)工具并重新导出(参见错误恢复部分)。
size_mb > 15Stage 3 — Validate
阶段3 — 验证
After the full pipeline, validate before declaring success:
- — confirm expected objects exist.
mcp__blender__get_scene_info - For paired parts (left/right lens), call on each and compare bounding box widths. Tolerance: 1 mm.
mcp__blender__get_object_info - Triangle count: get via . If a part exceeds budget, plan Decimate.
get_object_info - File size: must be ≤ 15 MB hard cap, ideally ≤ 8 MB.
完成整个流水线后,在宣布成功之前进行验证:
- — 确认预期对象存在。
mcp__blender__get_scene_info - 对于成对部件(左右镜片),调用获取每个部件的信息,并比较边界框宽度。容差:1毫米。
mcp__blender__get_object_info - 三角形数量:通过获取。如果某个部件超出预算,计划使用Decimate工具。
get_object_info - 文件大小:必须≤15 MB硬上限,理想情况下≤8 MB。
Error recovery
错误恢复
| Symptom | Likely cause | Fix |
|---|---|---|
| Bad Python in generated code | Re-emit code in smaller chunks; trace the line from the error message |
| Addon not running | Tell the user to start Blender + addon |
| Code chunk too large or slow | Break into smaller |
| Variables undefined across calls | Each call gets a fresh namespace | Re-import modules; refer to objects by |
| Analyzer outputs 0 contours | Image too low contrast | Re-run with |
| Asymmetric lenses | Original drawing asymmetric, or contour detection inconsistent | Warn the user; do not auto-mirror unless asked |
| GLB too large | High poly count or embedded textures | Apply |
| Mesh has holes | Curve resolution too low | Raise |
| Material missing in GLB | Used non-Principled-BSDF nodes | Rebuild material using only Principled BSDF |
| 症状 | 可能原因 | 修复方法 |
|---|---|---|
MCP返回 | 生成的代码存在Python语法错误 | 将代码拆分为更小的块重新生成;根据错误消息定位问题行 |
| 插件未运行 | 告知用户启动Blender和插件 |
| 代码块过大或运行缓慢 | 将代码拆分为更小的 |
| 调用之间变量未定义 | 每次调用都会获得全新的命名空间 | 重新导入模块;通过 |
| 分析器输出0个轮廓 | 图像对比度太低 | 使用 |
| 镜片不对称 | 原始图纸不对称,或轮廓检测不一致 | 提醒用户;除非用户要求,否则不要自动镜像 |
| GLB文件过大 | 多边形数量过高或嵌入纹理 | 应用 |
| 网格存在孔洞 | 曲线分辨率太低 | 将 |
| GLB中缺少材质 | 使用了非Principled-BSDF节点 | 仅使用Principled BSDF重建材质 |
Decimate code pattern (when GLB > 15 MB)
Decimate(减面)代码模式(当GLB>15 MB时)
python
import bpy
obj = bpy.data.objects['GEO-frame']
bpy.context.view_layer.objects.active = obj
mod = obj.modifiers.new(name='Decimate', type='DECIMATE')
mod.ratio = 0.7
mod.use_collapse_degenerate = True
bpy.ops.object.modifier_apply(modifier=mod.name)
print(f"decimated:{obj.name} verts:{len(obj.data.vertices)}")python
import bpy
obj = bpy.data.objects['GEO-frame']
bpy.context.view_layer.objects.active = obj
mod = obj.modifiers.new(name='Decimate', type='DECIMATE')
mod.ratio = 0.7
mod.use_collapse_degenerate = True
bpy.ops.object.modifier_apply(modifier=mod.name)
print(f"decimated:{obj.name} verts:{len(obj.data.vertices)}")Output to user
向用户输出结果
When done, report:
- Output path of the GLB file
- File size in MB (vs 15 MB cap)
- Triangle count per part (vs 30 000 cap)
- Material slots assigned
- Any warnings (asymmetry, decimation applied, fallbacks used)
Example:
✓ Exported(2.4 MB)/tmp/wireframe_output.glb
Triangles: 5 200 (3 parts: GEO-frame, GEO-lens-right, GEO-lens-left)
Materials: MAT-frame-metal, MAT-lens-mirror
Warnings: none
完成后,报告以下内容:
- GLB文件的输出路径
- 文件大小(MB)(与15 MB上限对比)
- 每个部件的三角形数量(与30000上限对比)
- 已分配的材质插槽
- 任何警告(不对称、已应用减面、使用了回退方案)
示例:
✓ 已导出(2.4 MB)/tmp/wireframe_output.glb
三角形数量:5200(3个部件:GEO-frame、GEO-lens-right、GEO-lens-left)
材质:MAT-frame-metal、MAT-lens-mirror
警告:无
When to load deeper references
何时加载深度参考资料
The body above covers the 80% case. For the long tail, load these on demand:
- — image-processing pipeline theory (Canny, RDP, least-squares Bezier fitting), 2D-to-3D reconstruction principles, ISO 128 orthographic standards. Load when the analyzer output looks wrong and you need to tune parameters.
references/algorithms.md - — exhaustive Blender Python patterns (lofting, surface revolution, custom modifier stacks). Load when the user requests non-standard geometry (curved surfaces, complex bridges, articulated parts).
references/blender-patterns.md - — performance optimization (
references/best-practices.md, batch ops, context caching), naming conventions (Blender Studio standards), modifier stack ordering. Load when builds are slow or output topology is poor.foreach_set
以上内容覆盖了80%的常见场景。对于长尾场景,按需加载以下资料:
- — 图像处理流水线理论(Canny边缘检测、RDP算法、最小二乘法贝塞尔拟合)、2D转3D重建原理、ISO 128正交标准。当分析器输出异常且需要调整参数时加载。
references/algorithms.md - — 详尽的Blender Python代码模式(放样、曲面旋转、自定义修改器堆栈)。当用户要求非标准几何形状(曲面、复杂桥接、铰接部件)时加载。
references/blender-patterns.md - — 性能优化(
references/best-practices.md、批量操作、上下文缓存)、命名规范(Blender Studio标准)、修改器堆栈顺序。当构建速度慢或输出拓扑结构不佳时加载。foreach_set
Constraints
约束条件
- Blender ≥ 4.0 (5.x preferred). The Principled BSDF node and glTF exporter are stable across these versions.
- glTF embedded only (no + textures sidecar; no KTX2/Draco compression — Three.js needs extra loaders we haven't vendored).
.bin - PNG textures only (max 1024×1024). Prefer flat PBR colours; textures only when essential.
- No bone animations in the GLB. Idle motion is driven in JS by the consumer site.
- Blender ≥4.0(推荐5.x版本)。Principled BSDF节点和glTF导出器在这些版本中稳定。
- 仅支持嵌入式glTF(不支持+纹理副文件;不支持KTX2/Draco压缩——Three.js需要额外的加载器,我们未提供)。
.bin - 仅支持PNG纹理(最大1024×1024像素)。优先使用纯色PBR材质;仅在必要时使用纹理。
- GLB中不包含骨骼动画。静态模型的动画由消费站点的JS驱动。
Tip
提示
If the user just says "convert this wireframe", default to: , , , . Only ask for clarification if multiple interpretations are plausible.
view_type=auto-detectdetail_level=productiongeometry_type=hybridworld_width_mm=auto如果用户仅说“转换这个线框”,默认使用:、、、。只有当存在多种合理解释时,才需要向用户确认。
view_type=自动检测detail_level=productiongeometry_type=hybridworld_width_mm=自动