Game engines · Godot

CAD files in Godot

A GDExtension for Godot 4.4 and later. Put a STEP, IFC, Rhino, IGES, ACIS, OCCT .brep or OpenSCAD file in a project and it imports as a scene, the way a .glb does. Or open files while the game runs, walk the file's tree and draw its bodies and edges. The blacksmith kernel is there too: build exact solids in GDScript and write them to STEP.

A flange with six bolt holes and a filleted boss, with its edges, in a Godot window, its face count and build time above it
Godot's own types

Nodes, meshes and scenes

The classes follow the Python module's object model, name for name: CadaclysmScene, CadaclysmNode, CadaclysmPlacement, and the kernel's CadaclysmSolid, CadaclysmProfile and CadaclysmFrame. Godot's own types stand in where Godot has one: a Transform3D for a placement, an AABB for bounds, a Color for paint and an ArrayMesh for triangles. One call, instantiate(), turns a whole file into MeshInstance3D nodes, and a body used many times shares one mesh.

The examples →

Install

Three steps, no compiler

  1. Godot 4.4 or later. The standard build for Windows, macOS or Linux; the .NET build is not needed.

  2. The addon. Each SDK release carries cadaclysm-<version>-godot.zip. Copy its addons/cadaclysm/ into your project's addons/. It holds the extension and the two cadaclysm libraries for Windows x64, macOS 11+ (universal) and Linux x64 and arm64: the same builds as the release's own archives.

    mygame/
      project.godot
      addons/cadaclysm/
        cadaclysm.gdextension  cadaclysm_orbit_camera.gd
        bin/windows-x64/  bin/macos-universal/  bin/linux-x64/  bin/linux-arm64/
  3. Open the project. There is no plugin to enable: CAD files in the project import as scenes, and the Cadaclysm… classes are there in GDScript. Godot finds a new extension on its first scan of a project, so CAD files already in it import on the next launch. When you export a game, the .gdextension file copies the two libraries beside the executable.

The examples below are the scenes in the SDK's godot/project/examples/, beside the extension's source. To build it yourself, with Rust 1.94 or later: python fetch.py in the SDK, then python build.py --release --libs ../lib in godot/. Run an example with godot --path project res://examples/part/part.tscn -- part.step, and the tests headless with GODOT=path/to/godot python build.py --test.

Example · in the editor

A STEP file is a scene

Drop a CAD file into the FileSystem dock and Godot imports it through the extension: a scene with a MeshInstance3D for each body, in the file's colours, Y up and in metres. Instance it, inherit from it, or load it from code like any other scene. The Import dock's cadaclysm/ options add the edges, nest the bodies under the file's own tree, draw faces from both sides, or add world-scale UVs. The imported scene is plain Godot data, so an exported game draws it with no cadaclysm library at all.

var nut: Node3D = load("res://models/nut.step").instantiate()
add_child(nut)
Example · at run time

A part on its stand

Files the game only meets while it runs — a player's own model, a download — open with CadaclysmScene.open. It reads res:// and user:// paths too, in an exported game as well. instantiate_with({"edges": true}) builds the bodies and their edges. The edges use a small shader that pulls them towards the camera, so they stay on top of the faces they lie on. Drop another file on the window to open it.

The cadaclysm nut, shaded with its edges, in Godot
The logo's nut, as the part example draws it.
# A CAD part turning on its stand: `godot --path project res://examples/part/part.tscn
# -- part.step`, or drop a STEP, IGES, IFC, SAT, 3DM or BREP file on the window.
extends Node3D

var model: Node3D

func _ready() -> void:
	var args := OS.get_cmdline_user_args()
	open(args[0] if args.size() > 0 else "res://examples/models/nut.step")
	get_window().files_dropped.connect(func(files): open(files[0]))

func open(path: String) -> void:
	var scene := CadaclysmScene.open(path)              # metres, Y up: Godot's own space
	if scene == null:
		push_error(Cadaclysm.last_error())
		return
	if model:
		model.queue_free()
	model = scene.instantiate_with({"edges": true})     # a MeshInstance3D per body
	add_child(model)
	$Camera.frame(scene.bounds)
	scene.close()                                       # the meshes are Godot's now
Example · 2D

A drawing sheet

Every edge of the model seen from the front, from above and from the left, hidden ones included. The views are laid out in ISO first angle, with the overall sizes in millimetres and a title block. drawing() flattens the edges of the whole file into one array of point pairs, and one draw_multiline call draws each view.

A drawing sheet with three views of a hollow box with a window, its sizes and a title block
A hollow box with a window, from the Examples page, as the drawing example lays it out.
# A drawing sheet from a CAD file: three views (ISO first angle), the overall sizes in
# millimetres and a title block. `godot --path project res://examples/drawing/drawing.tscn
# -- part.step`, or drop a file on the window.
extends Control

const INK := Color(0.11, 0.105, 0.10)
const PAPER := Color(0.955, 0.945, 0.915)

var sheet := {}

func _ready() -> void:
	var args := OS.get_cmdline_user_args()
	open(args[0] if args.size() > 0 else "res://examples/models/nut.step")
	get_window().files_dropped.connect(func(files): open(files[0]))
	resized.connect(queue_redraw)

func open(path: String) -> void:
	var scene := CadaclysmScene.open(path)              # metres, Y up
	if scene == null:
		push_error(Cadaclysm.last_error())
		return
	sheet = {
		"name": path.get_file(),
		"size": scene.bounds.size,                     # metres
		"front": scene.drawing("front"),               # every edge, seen from the front
		"top": scene.drawing("top"),
		"left": scene.drawing("left"),
	}
	scene.close()
	queue_redraw()

func _draw() -> void:
	var w := size.x
	var h := size.y
	draw_rect(Rect2(Vector2.ZERO, size), PAPER)
	draw_rect(Rect2(24, 24, w - 48, h - 48), INK, false, 2.0)     # the border
	if sheet.is_empty() or sheet["front"]["segments"].is_empty():
		return
	var f: Dictionary = sheet["front"]
	var t: Dictionary = sheet["top"]
	var l: Dictionary = sheet["left"]
	var fw: float = f["hi"].x - f["lo"].x
	var fh: float = f["hi"].y - f["lo"].y
	var lw: float = l["hi"].x - l["lo"].x
	var th: float = t["hi"].y - t["lo"].y

	# One scale for all three views: the front view top left, the view from the left
	# to its right and the view from above below it -- ISO first angle.
	var gap := 0.18 * maxf(fw, fh)
	var s := minf((w - 200) / (fw + gap + lw), (h - 260) / (fh + gap + th))
	var x0 := 96 + (w - 200 - s * (fw + gap + lw)) / 2
	var y0 := 88.0
	view(f, Vector2(x0, y0), s)
	view(l, Vector2(x0 + (fw + gap) * s, y0), s)
	view(t, Vector2(x0, y0 + (fh + gap) * s), s)

	var metres: Vector3 = sheet["size"]
	dimension(Vector2(x0, y0 - 28), Vector2(x0 + fw * s, y0 - 28), metres.x)          # width
	dimension(Vector2(x0 - 32, y0), Vector2(x0 - 32, y0 + fh * s), metres.y)          # height
	var lx := x0 + (fw + gap) * s
	dimension(Vector2(lx, y0 - 28), Vector2(lx + lw * s, y0 - 28), metres.z)          # depth

	# The title block.
	var font := get_theme_default_font()
	var box := Rect2(w - 24 - 380, h - 24 - 112, 380, 112)
	draw_rect(box, INK, false, 2.0)
	draw_line(box.position + Vector2(0, 38), box.position + Vector2(box.size.x, 38), INK, 2.0)
	draw_line(box.position + Vector2(0, 75), box.position + Vector2(box.size.x, 75), INK, 2.0)
	draw_string(font, box.position + Vector2(12, 27), sheet["name"], HORIZONTAL_ALIGNMENT_LEFT, -1, 16, INK)
	draw_string(font, box.position + Vector2(12, 64), "%.1f x %.1f x %.1f mm" % [metres.x * 1000, metres.y * 1000, metres.z * 1000], HORIZONTAL_ALIGNMENT_LEFT, -1, 16, INK)
	draw_string(font, box.position + Vector2(12, 101), "ISO first angle  ·  cadaclysm + Godot", HORIZONTAL_ALIGNMENT_LEFT, -1, 16, INK)

# One view's edges at `s` pixels per metre, its top left corner at `at`.
func view(d: Dictionary, at: Vector2, s: float) -> void:
	draw_set_transform(at - d["lo"] * s, 0.0, Vector2(s, s))
	draw_multiline(d["segments"], INK, 1.6 / s, true)
	draw_set_transform(Vector2.ZERO)

# A dimension line with end ticks from `a` to `b`, its length in millimetres beside it.
func dimension(a: Vector2, b: Vector2, metres: float) -> void:
	var vertical := a.x == b.x
	var tick := Vector2(6, 0) if vertical else Vector2(0, 6)
	draw_line(a, b, INK, 1.0, true)
	draw_line(a - tick, a + tick, INK, 1.0, true)
	draw_line(b - tick, b + tick, INK, 1.0, true)
	var label := "%.1f" % (metres * 1000)
	var font := get_theme_default_font()
	var width := font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15).x
	if vertical:
		# Read from the right, as a drawing's vertical dimensions are.
		draw_set_transform(Vector2(a.x - 8, (a.y + b.y + width) / 2), -PI / 2)
		draw_string(font, Vector2.ZERO, label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, INK)
		draw_set_transform(Vector2.ZERO)
	else:
		draw_string(font, Vector2((a.x + b.x - width) / 2, a.y - 8), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, INK)
Example · the kernel

Build it in GDScript

The blacksmith kernel in a game loop. A flange with a ring of bolt holes, a boss, a bore and a fillet where the boss meets the disc is rebuilt as an exact B-rep each time a key changes a parameter. S writes it to STEP. The kernel works in millimetres, so the part hangs under a node scaled to metres.

A flange with six bolt holes and a filleted boss in a Godot window
The forge example: the solid, its face count and how long the kernel took.
# A parametric flange, built by the blacksmith kernel while you watch:
# `godot --path project res://examples/forge/forge.tscn`. Up and down change the bolt
# holes, left and right the boss; S writes the exact solid to flange.stp.
extends Node3D

var holes := 6                  # the two parameters; the kernel works in millimetres
var boss := 14
var part: CadaclysmSolid
var bounds: AABB                # of the upright part, in millimetres

func flange() -> CadaclysmSolid:
	# A disc with a ring of bolt holes: one outline, extruded once.
	var outline := CadaclysmProfile.circle(40)
	for i in holes:
		var a := TAU * i / holes
		outline = outline.with_hole(CadaclysmProfile.circle(4).translate(30 * cos(a), 30 * sin(a)))
	var disc := CadaclysmSolid.extrude(outline, CadaclysmFrame.xy(), 8)
	# A boss on top, and a bore through both.
	var body := disc.join(CadaclysmSolid.cylinder(16, boss).translate(0, 0, 8)) \
		.cut(CadaclysmSolid.cylinder(9, boss + 40).translate(0, 0, -20))
	# Round the circle where the boss meets the disc: the one curved edge at z = 8, r = 16.
	var joint := []
	for edge in body.edges:
		var p: Vector3 = edge.segments[0]
		if not edge.is_line and absf(p.z - 8) < 1e-3 and absf(Vector2(p.x, p.y).length() - 16) < 1e-3:
			joint.append(edge)
	return body.fillet(joint, 3)

func rebuild() -> void:
	var started := Time.get_ticks_usec()
	part = flange()
	var took := (Time.get_ticks_usec() - started) / 1000.0
	var upright := part.rotate(Vector3.ZERO, Vector3.RIGHT, -PI / 2)   # Z up to Y up
	$Millimetres/Body.mesh = upright.array_mesh(0.02)
	$Millimetres/Edges.mesh = upright.edge_mesh(0.02)
	bounds = upright.bounds
	$Hud/Title.text = "%d holes, a %d mm boss: %d faces, built in %.0f ms" % [holes, boss, part.faces, took]

func _ready() -> void:
	rebuild()
	$Camera.frame($Millimetres.transform * bounds)     # the box in metres, as the scene is

func _unhandled_key_input(event: InputEvent) -> void:
	if not event.pressed:
		return
	match event.keycode:
		KEY_UP: holes = mini(holes + 1, 16)
		KEY_DOWN: holes = maxi(holes - 1, 3)
		KEY_RIGHT: boss = mini(boss + 2, 40)
		KEY_LEFT: boss = maxi(boss - 2, 4)
		KEY_S:
			part.step("flange.stp")
			return
		_: return
	rebuild()
Example · the kernel

The mark, modelled

The nut in the Cadaclysm logo, as an exact solid: a hexagon extruded and bored, with a 45-degree chamfer at each end that leaves a narrow ledge at the corners. One profile, drawn the way a lathe would cut it and turned about the axis, meets the hexagon: what both keep is the nut, its ledges true planes and its chamfers true cones. Thirteen faces, and S writes it to STEP.

The Cadaclysm logo's hex nut, built by the blacksmith kernel, shaded with its edges in Godot
The nut example: the logo's nut, built by the kernel as the scene loads.
# The cadaclysm nut, built by the blacksmith kernel: `godot --path project
# res://examples/nut/nut.tscn`. S writes the exact solid to nut.stp.
extends Node3D

const R := 60.0        # hexagon corner radius, mm
const BORE := 30.0     # bore radius
const THICK := 46.0    # thickness
const CONE := 50.0     # where each chamfer cone meets its ledge
const DEPTH := 5.0     # the chamfer, 45 degrees

var solid: CadaclysmSolid

func nut() -> CadaclysmSolid:
	# The hexagon, a corner on +X, extruded up Z.
	var hexagon := CadaclysmProfile.regular_polygon(Vector2.ZERO, R, 6)
	var hex := CadaclysmSolid.extrude(hexagon, CadaclysmFrame.xy(), THICK)
	# What a lathe would leave: drawn as (radius, height) in the XZ plane and turned
	# about Z -- the bore inside, and at each end a 45-degree chamfer out to a ledge.
	var profile := CadaclysmProfile.polygon([
		Vector2(BORE, 0), Vector2(CONE - DEPTH, 0), Vector2(CONE, DEPTH), Vector2(R + 10, DEPTH),
		Vector2(R + 10, THICK - DEPTH), Vector2(CONE, THICK - DEPTH), Vector2(CONE - DEPTH, THICK),
		Vector2(BORE, THICK)])
	var turned := CadaclysmSolid.revolve_in_plane(profile, CadaclysmFrame.xz(), Vector2(0, 0), Vector2(0, 1), TAU)
	# The nut is what both keep.
	return hex.common(turned)

func _ready() -> void:
	var started := Time.get_ticks_usec()
	solid = nut()
	var took := (Time.get_ticks_usec() - started) / 1000.0
	var upright := solid.rotate(Vector3.ZERO, Vector3.RIGHT, -PI / 2)    # Z up to Y up
	$Millimetres/Body.mesh = upright.array_mesh(0.02)
	$Millimetres/Edges.mesh = upright.edge_mesh(0.02)
	$Camera.frame($Millimetres.transform * upright.bounds)             # the box in metres
	$Hud/Title.text = "%d faces, built in %.0f ms" % [solid.faces, took]

func _unhandled_key_input(event: InputEvent) -> void:
	if event.pressed and event.keycode == KEY_S:
		var written := solid.step("nut.stp")
		$Hud/Keys.text = "wrote nut.stp" if written else Cadaclysm.last_error()
Notes

What to know

Status

The Godot extension is the SDK's godot/ folder, a Rust GDExtension over the SDK's Rust crate and the same two libraries every wrapper uses. Tested with Godot 4.7 on Windows.

Questions, or a game built on it: Discord or info@blitter.studio.