Reference

OpenSCAD transformations: translate, rotate, hull and minkowski

OpenSCAD transformations are operators that change the shape written after them: translate() moves it, rotate() turns it around the origin, scale() and resize() change its size, mirror() reflects it, and offset(), hull() and minkowski() reshape it. Every example on this page was compiled and measured, so the sizes in the comments are the sizes you get.

9 min read Updated October 2026 Beginner OpenSCAD reference · 5 of 8
In short
  1. A transformation applies to the one statement or { } block right after it.
  2. Read chains from the shape outward: the transformation nearest the object runs first.
  3. rotate(), scale() and mirror() all work around the origin, so build parts at the origin and move them last.
  4. minkowski() and offset(r=) grow the shape by r on every side. Shrink the input by 2r per axis to keep the outer size.

OpenSCAD transformations at a glance

TransformationWhat it doesExample
translate(v)Moves children by [x, y, z] mm (2D vectors work too)translate([10, 0, 0]) cube(5);
rotate([x, y, z])Rotates around the origin in degrees: X first, then Y, then Zrotate([0, 90, 0]) cylinder(h=10, d=3);
rotate(a, v)Rotates a degrees around the axis v; rotate(a) alone turns around Zrotate(45, [0, 0, 1]) cube(5);
scale(v)Multiplies sizes, measured from the origin; a negative factor mirrorsscale([2, 1, 0.5]) sphere(d=20);
resize(v, auto)Sets an absolute bounding size; 0 leaves that axis alone unless auto=trueresize([30, 0, 0], auto=true) sphere(d=20);
mirror(v)Reflects across the plane through the origin whose normal is vmirror([1, 0, 0]) cube(5);
multmatrix(m)Applies a 4 × 4 affine matrix: shears and combined transformsSee multmatrix
color(c, alpha)Sets the display colour: a name, hex string or RGBA values from 0 to 1color("orange", 0.5) cube(5);
offset(r | delta, chamfer)Grows or shrinks a 2D shape; r rounds corners, delta keeps them sharpoffset(r=2) square(10);
hull()The tightest convex shape around all childrenRounded plate
minkowski()Sweeps the second child around the first, growing it on every sideRounded box

How transformations work (and the semicolon trap)

A transformation is not a command that runs on its own. It wraps whatever comes next: either one statement ending in ;, or a { } block of several. You can stack as many as you like, put them inside for loops, or wrap them in your own modules.

The classic beginner bug is a semicolon straight after the transformation. That gives translate() an empty child, so the next shape is drawn at the origin, unmoved:

translate([20, 0, 0]);
cube(5);

The cube above lands at the origin because the ; ends the translate() statement.

Write translate([20, 0, 0]) cube(5); as one statement, or use braces for several shapes.

translate(): move objects in OpenSCAD

translate([x, y, z]) shifts its children by that many millimetres. Use braces to move a group together.

translate([10, 0, 0]) cube(5);
translate([0, 0, 5]) {
  cube(10);
  sphere(3);
}

rotate(): rotation order and rotating around a point

Angles are in degrees and positive angles follow the right-hand rule. With a vector, OpenSCAD rotates around X first, then Y, then Z. With an angle and an axis it turns around that axis, and a single number turns around Z.

rotate([0, 90, 0]) cylinder(h=10, d=3);           // lay a Z cylinder along +X
translate([0, 20, 0]) rotate(45, [0, 0, 1]) cube([10, 10, 2]);  // 45° around Z
translate([0, 40, 0]) rotate(30) cube([20, 5, 2]); // a single number = around Z

rotate() always pivots on the origin. To spin something around another point, move that point to the origin, rotate, then move it back:

// Rotate 30° around the point [10, 0, 0] instead of the origin
p = [10, 0, 0];
translate(p) rotate([0, 0, 30]) translate(-p) cube([20, 4, 4]);

Transformation order: read from the shape outward

When transformations are chained, the one written closest to the shape runs first. These two lines use the same two operations and put the cube in different places:

translate([20, 0, 0]) rotate([0, 0, 45]) cube(10, center=true);   // spins in place, then slides to x = 20
rotate([0, 0, 45]) translate([20, 0, 0]) cube(10, center=true);   // slides out, then swings around the origin to (14.1, 14.1)

A handy habit: model every part centred on (or touching) the origin, do all rotations and scaling there, and make translate() the outermost step.

Where does the centre of the cube end up? rotate([0, 0, 90]) translate([10, 0, 0]) cube(2, center=true);

  1. At (10, 0, 0)
  2. At (0, 10, 0)
  3. At (0, -10, 0)

The translate runs first (it is closest to the cube), putting it at x = 10. The rotate then swings that point 90° counterclockwise around the origin, from the +X axis to the +Y axis.

scale() and resize(): change the size

scale() multiplies dimensions: pass one number for uniform scaling or a vector per axis. resize() sets the final bounding size in millimetres, which is handy for fitting a shape into a known envelope. In resize(), a 0 leaves that axis as it is, and auto=true scales the zero axes in proportion instead.

scale([2, 1, 0.5]) sphere(d=20);                           // 40 × 20 × 10 mm ellipsoid
translate([40, 0, 0]) scale(1.5) cube(10);                 // uniform: a 15 mm cube
translate([80, 0, 0]) resize([30, 0, 0], auto=true) sphere(d=20); // a 30 mm ball

Scaling also moves things. scale() measures from the origin, so an object that is not at the origin drifts as it grows:

scale(2) translate([10, 0, 0]) cube(5);   // ends up at x = 20 to 30, not 10 to 20

mirror(): flip across a plane

mirror(v) reflects its children across the plane that passes through the origin with normal v. [1, 0, 0] flips X (across the YZ plane), [0, 1, 0] flips Y and [0, 0, 1] flips Z. It does not keep the original, so draw both when you want a symmetric pair:

translate([5, 0, 0]) cube([10, 4, 4]);                // original: x = 5 to 15
mirror([1, 0, 0]) translate([5, 0, 0]) cube([10, 4, 4]); // copy: x = -15 to -5

multmatrix(): shear and custom matrices

multmatrix(m) applies a 4 × 4 affine matrix. The first three columns of the top three rows scale, rotate and shear; the fourth column translates. A value in row 1, column 3 shears X by Z, which leans a block like a parallelogram:

multmatrix([[1, 0, 0.5, 0],
            [0, 1, 0,   0],
            [0, 0, 1,   0],
            [0, 0, 0,   1]])
  cube([10, 10, 20]);   // top face shifts 10 mm: the block spans x = 0 to 20

color(): preview colours and 3MF

color("red") cube(5);
translate([10, 0, 0]) color("#1e40af") cube(5);
translate([22, 2.5, 2.5]) color([0.2, 0.6, 1.0, 0.8]) sphere(d=5);   // RGBA, 0 to 1
translate([30, 0, 0]) color("orange", 0.5) cube(5);                  // name + alpha

color() takes a CSS colour name, a hex string or an RGBA vector with values from 0 to 1, plus an optional alpha. In the last stable release, OpenSCAD 2021.01, colours only show in the F5 preview. Recent development snapshots keep colours through rendering and can write them into 3MF files. The PrintPal editor exports one uncoloured mesh, so assign colours in your slicer for multicolour prints.

offset(): round, chamfer and fillet 2D shapes

offset() works on 2D shapes, so it usually sits inside linear_extrude(). Positive values grow the outline, negative values shrink it.

CallResult
offset(r=3)Grows by 3 mm with rounded outside corners (radius 3)
offset(delta=3)Grows by 3 mm and keeps sharp corners
offset(delta=3, chamfer=true)Grows by 3 mm and cuts the corners at 45°
offset(r=3) offset(delta=-3)Rounds outside corners and keeps the original size
offset(r=-3) offset(delta=3)Fillets inside (concave) corners and keeps the original size
r = 3;
// Rounded plate that stays exactly 40 × 20 mm
linear_extrude(height=2)
  offset(r=r) offset(delta=-r) square([40, 20]);

// Chamfered plate: the 20 mm square grows to 24 mm with 45° corners
translate([50, 0, 0]) linear_extrude(height=2)
  offset(delta=2, chamfer=true) square([20, 20]);

// Inside-corner fillet: rounds only the concave corner of an L
translate([0, 30, 0]) linear_extrude(height=2)
  offset(r=-2) offset(delta=2) union() {
    square([30, 8]);
    square([8, 30]);
  }

offset(r=3) on its own makes the shape bigger. It adds 3 mm on every side, so a 40 × 20 square becomes 46 × 26:

linear_extrude(height=2) offset(r=3) square([40, 20]);   // 46 × 26 mm, not 40 × 20

Use the shrink-then-grow pair from the table, or start from a square that is 2r smaller.

hull(): convex hull for rounded plates and organic shapes

hull() wraps all its children in the tightest convex shape, like shrink-wrap. A few well-placed cylinders or spheres become a smooth plate, capsule or tapered arm:

$fn = 48;
hull() {
  translate([0, 0, 0])   cylinder(h=2, d=10);
  translate([30, 0, 0])  cylinder(h=2, d=10);
  translate([15, 20, 0]) cylinder(h=2, d=10);
}   // rounded triangle plate

For a rounded rectangle with an exact outer size, put the corner cylinders r in from each edge:

module rounded_plate(size=[40, 20, 3], r=3) {
  hull()
    for (x = [r, size[0] - r], y = [r, size[1] - r])
      translate([x, y, 0]) cylinder(h=size[2], r=r, $fn=48);
}
rounded_plate();   // exactly 40 × 20 × 3 mm

hull() fills every dent. Hull an L-shape and you get a triangle. For bent or concave shapes, hull neighbouring pairs in a loop (a "chain hull") and union the pieces:

pts = [[0, 0], [30, 0], [30, 20], [50, 30]];
union()
  for (i = [0:len(pts) - 2])
    hull() {
      translate(pts[i])     cylinder(h=3, d=6, $fn=32);
      translate(pts[i + 1]) cylinder(h=3, d=6, $fn=32);
    }

minkowski(): rounded boxes with exact outer dimensions

minkowski() sweeps the second child around every point of the first. With a sphere of radius r, every face of a cube moves out by r and every edge and corner becomes round. That means the result is 2r bigger on every axis, and the sphere's origin decides where the growth goes.

Second childX and Y grow byZ grows byInner cube for an L × W × H result
sphere(r)2r2r[L - 2r, W - 2r, H - 2r]
cylinder(r=r, h=hc, center=true)2rhc[L - 2r, W - 2r, H - hc]
cylinder(r=r, h=hc) (not centred)2rhc, all upward[L - 2r, W - 2r, H - hc]

A box that really is 40 × 20 × 10 mm with every edge rounded to 3 mm:

size = [40, 20, 10];   // finished outer size
r = 3;                 // edge radius
minkowski() {
  cube([size[0] - 2*r, size[1] - 2*r, size[2] - 2*r], center=true);
  sphere(r=r, $fn=48);
}

Centring the inner cube keeps the result centred too. Without center=true the box would start at −3 mm on every axis. Expect 39.99 rather than 40.00 mm: a faceted sphere is a hair smaller than its nominal radius.

To round only the vertical edges and keep the top and bottom flat, use a cylinder:

size = [40, 20, 4];
r = 3;
minkowski() {
  cube([size[0] - 2*r, size[1] - 2*r, size[2] / 2], center=true);
  cylinder(r=r, h=size[2] / 2, center=true, $fn=48);
}   // exactly 40 × 20 × 4 mm

For a plate like that, the 2D route gives the same shape much faster:

size = [40, 20, 4];
r = 3;
linear_extrude(height=size[2], center=true)
  offset(r=r) square([size[0] - 2*r, size[1] - 2*r], center=true);

You want a 30 × 30 × 10 mm box with every edge rounded to 2 mm, using minkowski() with sphere(r=2). What size should the inner cube be?

  1. 30 × 30 × 10
  2. 28 × 28 × 8
  3. 26 × 26 × 6
  4. 32 × 32 × 12

The sphere adds its radius on both sides of every axis, so subtract 2 × 2 = 4 mm from each dimension.

Speed and alternatives. A 3D minkowski() combines every facet of one child with every facet of the other, so high $fn values and non-convex first children get slow. Development builds with the Manifold geometry engine, which the PrintPal editor uses, are far faster than the old CGAL engine, but the 2D offset() route is still the quickest for plates. The bundled BOSL2 library also has cuboid([40, 20, 10], rounding=3), which takes the finished size directly. See OpenSCAD libraries.

Patterns: grids, polar arrays and mirrored copies

Rectangular grid

union() {
  cube([40, 40, 2]);
  for (x = [5:10:35], y = [5:10:35])
    translate([x, y, 0]) cylinder(h=6, d=4, $fn=24);
}

Polar (circular) array

n = 8;
for (i = [0:n - 1])
  rotate([0, 0, i * 360 / n])
    translate([20, 0, 0])
      cylinder(h=5, d=4, $fn=24);

Mirrored pair

module arm() {
  translate([15, 0, 0]) cylinder(h=3, d=8, $fn=32);
}

union() {
  translate([-15, -2, 0]) cube([30, 4, 3]);   // bar between the arms
  arm();
  mirror([1, 0, 0]) arm();
}

The grid and the mirrored pair sit inside union() on purpose. The PrintPal editor keeps separate top-level objects as separate bodies, so touching copies should be unioned to export as one solid. The boolean operations page explains why.

Ask the CAD Agent for these patterns

The CAD Agent writes the same OpenSCAD for you. State the finished size and say which edges are rounded, and it can do the 2r arithmetic.

Design a 60 x 40 x 4 mm mounting plate with 4 mm rounded vertical corners and a flat top, keeping the outer size exactly 60 x 40 mm. Add four 3.4 mm M3 clearance holes 5 mm in from each corner, and a 20 mm tall tab on each short end made by mirroring one tab. Make every dimension a parameter.

Make a round coaster exactly 90 mm in diameter and 5 mm thick with a 1 mm chamfer on the top edge, and a polar array of 12 radial grooves on top, each 2 mm wide and 1 mm deep, stopping 10 mm from the edge.

AI tool · free to tryCAD AgentDescribe a part in plain English and get editable OpenSCAD with sliders for every dimension.Open tool

Frequently asked questions

In what order does OpenSCAD apply transformations?

From the shape outward: the transformation written closest to the object runs first. In translate([20, 0, 0]) rotate([0, 0, 45]) cube(10) the cube is rotated in place and then moved. Swap the two and the cube swings around the origin instead.

How do I make a rounded box with minkowski() in OpenSCAD?

Shrink the inner cube by twice the rounding radius on every axis, then minkowski it with a sphere of that radius: cube([L - 2*r, W - 2*r, H - 2*r], center=true) plus sphere(r) gives exactly L x W x H. Use a cylinder instead of a sphere to round only the vertical edges.

What is the difference between hull() and minkowski()?

hull() wraps all its children in the tightest convex shape, like shrink-wrap, so it fills in any concave areas. minkowski() sweeps one shape around the surface of another, which grows the first shape by the second on every side and rounds its edges.

Does OpenSCAD rotate() spin an object around its centre?

No. rotate() always turns around the origin [0, 0, 0]. To rotate around another point p, use translate(p) rotate(a) translate(-p), or build the part centred on the origin and move it into place last.

Does mirror() keep the original object?

No. mirror() replaces its child with the reflected copy. To get both halves of a symmetric part, draw the child once normally and once inside mirror(), ideally inside a union().

Sources & further reading

  • OpenSCAD User Manual — Transformations (rotation order, resize, offset, minkowski, hull)
  • OpenSCAD — Downloads (2021.01 stable release and development snapshots)
  • BOSL2 wiki — shapes3d.scad (cuboid() with rounding)