Wireframe-to-3D Conversion

SkillFiles & storage

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 dsh Blender tools and Python with opencv-python, numpy, scipy installed.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Wireframe-to-3D Conversion skill

What this skill tells your AI

The instructions your AI receives, as published by cheshirejcat/blender in skills/create-3d-model/references/modules/wireframe-to-3d/SKILL.md and read by ahel’s review.

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:

  1. Analyze wireframe images locally with scripts/wireframe_analyzer.py (OpenCV → Bezier control points in JSON).
  2. Generate Blender Python code that recreates the contours as parametric Bezier curves.
  3. Execute code in Blender via blender_python, converting curves to meshes with PBR materials.
  4. Export with blender_export as optimized GLB (≤ 15 MB), validating size and topology.

The dsh agent is the orchestrator. The scripts/ directory contains the only standalone code (wireframe_analyzer.py); everything else is patterns emitted and run via blender_python.

Prerequisites — check first

Before any wireframe work, verify the environment:

  1. Blender is reachable. Call blender_status. If available is false, stop and report its version/error field and configured executable.

  2. Python deps for the analyzer. Run:

    python3 -c "import cv2, numpy, scipy" 2>&1
    

    If it errors, run pip install opencv-python numpy scipy Pillow (or instruct the user to).

  3. 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_depth on 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. If an iteration repeats the same error twice, consult references/best-practices.md and 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 /absolute/path/to/create-3d-model/references/modules/wireframe-to-3d/scripts/wireframe_analyzer.py \
  /absolute/workspace/path/input.png \
  /absolute/workspace/path/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 blender_python 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 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

Pattern: 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)
  • MAT-pad-silicone(0.65, 0.63, 0.60) base, metallic=0.0, roughness=0.7 (matte silicone)

Pattern: export to GLB

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 size_mb > 15: apply Decimate and re-export (see error recovery).

Stage 3 — Validate

After the full pipeline, validate before declaring success:

  1. blender_scene_info — confirm expected objects exist.
  2. For paired parts (left/right lens), compare the returned object dimensions. Tolerance: 1 mm.
  3. Triangle count: use the returned mesh polygon counts. If a part exceeds budget, plan Decimate.
  4. File size: must be ≤ 15 MB hard cap, ideally ≤ 8 MB.

Error recovery

SymptomLikely causeFix
Python traceback from blender_pythonBad Python in generated codeRe-emit code in smaller chunks; trace the line from the error message
blender_status.available is falseBlender executable unavailableReport the executable/configuration error
Blender timeoutCode chunk too large or slowBreak it into smaller blender_python calls
Variables undefined across callsEach call gets a fresh namespaceRe-import modules; refer to objects by bpy.data.objects['name']
Analyzer outputs 0 contoursImage too low contrastRe-run with --gaussian-kernel 7 --canny-t1 30
Asymmetric lensesOriginal drawing asymmetric, or contour detection inconsistentWarn the user; do not auto-mirror unless asked
GLB too largeHigh poly count or embedded texturesApply DECIMATE modifier with ratio 0.6–0.8; re-export
Mesh has holesCurve resolution too lowRaise curve_data.resolution_u to 24 or 32; reconvert
Material missing in GLBUsed non-Principled-BSDF nodesRebuild material using only Principled BSDF

Decimate code pattern (when GLB > 15 MB)

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 /tmp/wireframe_output.glb (2.4 MB) Triangles: 5 200 (3 parts: GEO-frame, GEO-lens-right, GEO-lens-left) Materials: MAT-frame-metal, MAT-lens-mirror Warnings: none

When to load deeper references

The body above covers the 80% case. For the long tail, load these on demand:

  • references/algorithms.md — 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/blender-patterns.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/best-practices.md — performance optimization (foreach_set, batch ops, context caching), naming conventions (Blender Studio standards), modifier stack ordering. Load when builds are slow or output topology is poor.

Constraints

  • Blender ≥ 4.0 (5.x preferred). The Principled BSDF node and glTF exporter are stable across these versions.
  • glTF embedded only (no .bin + textures sidecar; no KTX2/Draco compression — Three.js needs extra loaders we haven't vendored).
  • 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.

Tip

If the user just says "convert this wireframe", default to: view_type=auto-detect, detail_level=production, geometry_type=hybrid, world_width_mm=auto. Only ask for clarification if multiple interpretations are plausible.

Scope boundary — what wireframe-to-3d does and doesn't produce

This skill produces 2D outline tracing extruded to thin curves — a flat-in-Y wireframe representation of the input drawing. It does not produce:

  • Filled surfaces (e.g. lens glass between rim outlines)
  • True 3D depth from a single view (output is flat in Y)
  • Multi-view 3D reconstruction (front + side views combined volumetrically)
  • Sub-features not in the input lines (nose pads, articulated hinges, etc.)
  • Production-quality materials (placeholder only — real materials come from blender-materials)

For a "complete rendered and textured X" (e.g. Ray-Ban Aviator from a wireframe), chain this skill with others:

StepSkillWhat it adds
1wireframe-to-3dFrame outline as 3D curves — the foundation, not the deliverable
2blender-modelingFilled lens discs (UV spheres scaled to lens dimensions); temple arms (Bezier curves extending backward in Y); any 3D detail not in the wireframe
3blender-materialsGold/silver metal frame; mirror lens material with Metallic=0.9, Roughness=0.04; etc.
4blender-lightingsubject_class='metal' for product-shot lighting
5blender-cameras85-100mm focal length, shallow DoF for hero product shot
6blender-renderingCycles 256+ samples, denoise, AgX view transform

Treat wireframe extraction as the structural first pass, not the presentation result. Validate the current task with local before/after renders: the trace should establish the outline, while the chained modeling, material, lighting, and camera passes should produce the finished asset.

The orchestrator (create-3d-model) should always plan for the chain when the user asks for a "model of X" from a wireframe.

Signals

GitHub stars
26
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
wireframe-to-3d
Source
github.com/cheshirejcat/blender