Wireframe To 3d
Convert 2D orthographic wireframe PNG drawings to 3D Blender models exported as glTF/GLB. Use this skill whenever the user provides wireframe images (technical drawings, line drawings, orthographic views, side/front/back panels) and wants to generate a 3D model, mesh, or .glb file. Triggers on phrases like "convert this wireframe to 3D", "make a 3D model from these drawings", "build a model from this wireframe", "generate GLB from these views", or any image-to-3D-mesh request involving line drawings. Make sure to use this skill even if the user does not explicitly say "wireframe" — also covers "orthographic views", "technical drawings", "line drawings of objects", "front and side views". Requires the Blender MCP addon to be running (port 9876) and Python with opencv-python, numpy, scipy installed.
- Skill ID
- roble3/cc-blender-skill/wireframe-to-3d
- Publisher
- roble3
- Repository
- cc-blender-skill
- Installs
- 0
- Files
- 6
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
roble3/cc-blender-skill/wireframe-to-3dInstalls these files- SKILL.md
- evals/evals.json
- references/algorithms.md
- references/best-practices.md
- references/blender-patterns.md
- scripts/wireframe_analyzer.py
What this skill tells the agent
Wireframe-to-3D Conversion
Convert 2D orthographic wireframe images to parametric 3D Blender models, exported as glTF 2.0 binary (.glb).
Overview
The skill drives a four-stage pipeline:
- Analyze wireframe images locally with
scripts/wireframe_analyzer.py(OpenCV → Bezier control points in JSON). - Generate Blender Python code that recreates the contours as parametric Bezier curves.
- Execute code in Blender via
mcp__blender__execute_blender_code, converting curves to meshes with PBR materials. - Export as optimized GLB (≤ 15 MB), validating size and topology.
You (Claude) are the orchestrator. The scripts/ directory contains the only standalone code (wireframe_analyzer.py); everything else is patterns you emit and run via MCP.
Prerequisites — check first
Before any wireframe work, verify the environment:
- Blender MCP is reachable. Call
mcp__blender__get_scene_info. If it errors with "Could not connect to Blender", stop and tell the user: > "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>&1`If it errors, runpip install opencv-python numpy scipy Pillow` (or instruct the user to).
- Image input. Confirm the user provided at least one PNG. Reasonable bounds: ≥ 400×400 px, black-on-white or white-on-black line art.
Decision flow
Q1: How many views?
- 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.
Q2: Detail level?
- `preview` — RDP epsilon = 4.0, target ~1–2k tris, < 1 MB GLB.
- `production` — RDP epsilon = 2.0, target ~5–8k tris, 2–4 MB GLB. Default.
- `high` — RDP epsilon = 1.0, target ~10–20k tris, may need Decimate to stay under 15 MB.
Q3: Geometry type?
- `wires` — frames, arms, hinges. Use
bevel_depthon curves. - `surfaces` — lenses, domes. Use lofted profiles or fill caps.
- `hybrid` — both. Default for glasses-like objects.
Q4: Real-world scale?
- 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.
Mandatory correction loop for reference/texture-driven subjects
When user feedback says the model does not match the reference/texture, stop the normal "generate from primitives" loop and switch to reference-locked modeling:
- Count visible design parts first from the provided texture/wireframe and write the count into stdout/notes before modeling. Do not infer a radial or repeated count from symmetry; the visual design count in the source manifest is the contract.
- Use the front view as canonical. Create an image-empty/plane/reference overlay in the same front orthographic camera used for validation. Align scale, centerline, and bounding circle before adding depth.
- Trace or define 2D silhouettes in front-view X/Z first. Build mesh surfaces from those silhouettes; do not place generic ellipses and then try to texture them.
- Lock the front projection. Add depth only on the view axis after the front silhouette matches. Side/back views refine thickness and stacking but must not alter the front outline.
- Use Project-from-View-style UVs for any texture that is meant to match the front drawing. One front-facing surface should map to the corresponding texture crop 1:1.
- Render an overlay validation: reference/wireframe behind or over the model, plus a printed checklist: expected count, actual object count, face position, outer silhouette bounds, and side/back depth.
- If an iteration repeats the same error twice, consult
references/best-practices.mdand add a short durable note before another attempt.
For repeated motif subjects, create exactly the primary structural components declared by the source manifest. Name them by source-visible position or semantic role, and validate the count before export.
Stage 1 — Run the analyzer
Run the bundled analyzer once per view:
python3 ${CLAUDE_SKILL_DIR}/scripts/wireframe_analyzer.py <input.png> <output.json>The script outputs JSON with this shape:
{
"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
--rdp-epsilon(or edit the call in the script).
Read the JSON with Read. Do not pass huge JSON blobs to Blender — extract what you need first.
Stage 2 — Generate Blender code
Build code in small, self-contained chunks (each execute_blender_code call gets a fresh Python namespace; only bpy.data persists between calls). Always re-import what you need.
Pattern: create a Bezier curve from control points
import bpy
# Identify by stable name; bpy.data persists between calls.
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)
# Control points come from the analyzer JSON (px → mm scaling done client-side).
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 stdoutPixel → 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.0Pattern: convert curves to mesh + cleanup
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)
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-metal—(0.08, 0.08, 0.10)base, metallic=1.0, roughness=0.25 (brushed steel)MAT-lens-mirror—(0.05, 0.08, 0.15)base, metallic=0.8, roughness=0.05, IOR=1.5 (mirror glass)
More from roble3
Use this skill in a project
Open any RiverX project, search the Skills panel for roble3/cc-blender-skill/wireframe-to-3d, and install it in one click.
