Processing Intermediate Article 7

3D with P3D

Unlock Processing's P3D renderer to build, light, texture, and animate a 3D scene with a spinning planet, orbiting moon, and OBJ model loading.

⏱ 19 min 3d p3d lighting texture pshape

Processing’s default renderer is a 2D canvas. Passing P3D as the third argument to size() switches to OpenGL-backed 3D rendering and unlocks a completely different set of drawing primitives, camera controls, and lighting APIs. This tutorial covers every major 3D feature, ending with a textured spinning planet and orbiting moon.

Enabling P3D

void setup() {
  size(900, 600, P3D);
}

That one change gives you a Z axis, depth sorting, perspective projection, and hardware-accelerated rendering. Everything you already know — fill(), stroke(), translate(), rotate(), pushMatrix() — works the same way but now moves in three dimensions.

Primitive Shapes

void draw() {
  background(0);
  lights();                 // default lighting — needed for 3D shading

  translate(width/2, height/2, 0);

  // Box: width, height, depth
  pushMatrix();
  translate(-160, 0, 0);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.013);
  fill(180, 100, 80);
  noStroke();
  box(100, 80, 60);
  popMatrix();

  // Sphere: radius
  pushMatrix();
  translate(160, 0, 0);
  rotateY(frameCount * 0.02);
  fill(80, 140, 200);
  noStroke();
  sphereDetail(40);    // set polygon count before drawing
  sphere(70);
  popMatrix();
}

sphereDetail(n) sets the number of longitudinal and latitudinal segments. Higher values give smoother spheres at the cost of performance.

Transforms: rotateX, rotateY, rotateZ

The three rotation functions take an angle in radians and rotate around the corresponding world axis at the current matrix position. Always wrap related transforms in pushMatrix() / popMatrix() so they do not bleed into other objects:

pushMatrix();
translate(x, y, z);      // move to object position
rotateY(angle);           // spin around vertical axis
scale(2, 1, 1);           // stretch along X
box(50);
popMatrix();              // restore matrix

Lighting

Without lights in P3D, every surface is flat-shaded. Processing provides four light types:

// Soft uniform fill
ambientLight(60, 60, 70);

// Directional (like the sun) — direction vector, not position
directionalLight(255, 240, 200,   // colour
                 -1, 0.5, -1);    // direction (pointing toward neg-X, down, neg-Z)

// Point light — radiates from a position
pointLight(255, 200, 100,         // colour
           mouseX, mouseY, 200);  // position in world space

// Spot light — cone
spotLight(200, 220, 255,          // colour
          width/2, 0, 400,        // position
          0, 1, -1,               // direction
          PI / 4, 2);             // cone angle, concentration

Mix multiple lights for realistic illumination. Call them all before drawing geometry; Processing accumulates all active lights into the current frame.

Camera and Perspective

The default camera sits at (width/2, height/2, cameraZ) looking at the centre, where cameraZ = height / (2 * tan(PI/6)). Override it with camera():

camera(
  eyeX, eyeY, eyeZ,           // camera position
  centerX, centerY, centerZ,  // look-at point
  upX, upY, upZ               // up direction (usually 0, 1, 0)
);

Change the field of view and near/far clip planes with perspective():

perspective(
  PI / 3,                         // 60-degree field of view
  (float) width / height,         // aspect ratio
  0.1,                            // near clip
  10000                           // far clip
);

For 2D-style flat rendering without perspective, use ortho() instead.

PShape and OBJ Files

loadShape() imports Wavefront OBJ files (and SVG in 2D mode). Place the .obj and its .mtl alongside it in the data/ folder:

PShape model;

void setup() {
  size(800, 600, P3D);
  model = loadShape("spaceship.obj");
}

void draw() {
  background(5, 5, 20);
  lights();
  translate(width/2, height/2);
  rotateY(frameCount * 0.01);
  scale(0.5);
  shape(model);
}

You can also build PShape objects programmatically with createShape() for reusable, pre-compiled geometry — much faster than calling beginShape() every frame when geometry does not change.

Texturing

Load an image and apply it to geometry using texture() inside a beginShape() block. You must also call noStroke() or texture seams will show.

PImage tex;

void setup() {
  size(800, 600, P3D);
  tex = loadImage("earth.jpg");
}

void drawTexturedSphere(float r) {
  int detail = 40;
  sphereDetail(detail);
  noStroke();
  textureMode(NORMAL);   // UV coordinates in 0.0–1.0 range

  // Processing's built-in sphere() does not support texturing directly.
  // Build a UV sphere manually:
  for (int lat = 0; lat < detail; lat++) {
    float theta1 = map(lat,     0, detail, -HALF_PI, HALF_PI);
    float theta2 = map(lat + 1, 0, detail, -HALF_PI, HALF_PI);

    beginShape(TRIANGLE_STRIP);
    texture(tex);
    for (int lon = 0; lon <= detail; lon++) {
      float phi = map(lon, 0, detail, 0, TWO_PI);
      float u   = (float) lon / detail;

      vertex(r*cos(theta1)*cos(phi), r*sin(theta1), r*cos(theta1)*sin(phi), u, (float)lat/detail);
      vertex(r*cos(theta2)*cos(phi), r*sin(theta2), r*cos(theta2)*sin(phi), u, (float)(lat+1)/detail);
    }
    endShape();
  }
}

Full Example: 3D Spinning Textured Planet with Orbiting Moon

Download or create earth.jpg and moon.jpg (any 2:1 ratio image works) and place them in data/.

PImage earthTex, moonTex;
float earthRot = 0;
float moonOrbit = 0;

void setup() {
  size(900, 600, P3D);
  earthTex = loadImage("earth.jpg");
  moonTex  = loadImage("moon.jpg");
  noStroke();
}

void draw() {
  background(2, 2, 12);

  // Star field (just random points drawn at setup would be better;
  // here we use a seeded approach)
  randomSeed(42);
  stroke(255, 180);
  strokeWeight(1.2);
  for (int i = 0; i < 300; i++) {
    point(random(width), random(height));
  }
  noStroke();

  // Lighting: sun from upper-right
  ambientLight(30, 30, 40);
  directionalLight(255, 240, 200, -1, 0.3, -1);

  perspective(PI / 3.5, (float) width / height, 1, 5000);
  camera(0, -100, 700, 0, 0, 0, 0, 1, 0);

  // ── Earth ────────────────────────────────────────────
  pushMatrix();
  rotateX(radians(23.5));          // axial tilt
  rotateY(earthRot);
  drawTexturedSphere(earthTex, 150, 40);
  popMatrix();

  // ── Moon ─────────────────────────────────────────────
  pushMatrix();
  float moonX = cos(moonOrbit) * 260;
  float moonZ = sin(moonOrbit) * 260;
  translate(moonX, 0, moonZ);
  rotateY(moonOrbit * 0.5);
  drawTexturedSphere(moonTex, 42, 24);
  popMatrix();

  // ── Orbit ring (wireframe) ────────────────────────────
  pushMatrix();
  rotateX(HALF_PI);
  stroke(80, 100, 140, 120);
  strokeWeight(1);
  noFill();
  ellipse(0, 0, 520, 520);
  noStroke();
  popMatrix();

  earthRot  += 0.006;
  moonOrbit += 0.008;
}

void drawTexturedSphere(PImage tex, float r, int detail) {
  textureMode(NORMAL);
  for (int lat = 0; lat < detail; lat++) {
    float t1 = map(lat,     0, detail, -HALF_PI, HALF_PI);
    float t2 = map(lat + 1, 0, detail, -HALF_PI, HALF_PI);
    beginShape(TRIANGLE_STRIP);
    texture(tex);
    for (int lon = 0; lon <= detail; lon++) {
      float phi = map(lon, 0, detail, 0, TWO_PI);
      float u   = (float) lon / detail;
      float v1  = (float)  lat      / detail;
      float v2  = (float) (lat + 1) / detail;
      vertex(r*cos(t1)*cos(phi), r*sin(t1), r*cos(t1)*sin(phi), u, v1);
      vertex(r*cos(t2)*cos(phi), r*sin(t2), r*cos(t2)*sin(phi), u, v2);
    }
    endShape();
  }
}

The moon’s orbit speed is independent of Earth’s rotation speed. The axial tilt of 23.5 degrees applied before the Y-axis rotation means the entire rotation happens around the tilted axis, just like the real Earth.

Key Takeaways

  • Pass P3D as the third argument to size() to enable OpenGL 3D rendering.
  • Wrap every object’s transforms in pushMatrix() / popMatrix() to prevent leakage.
  • Call lighting functions before drawing geometry; they accumulate per-frame.
  • camera() and perspective() override the default view frustum.
  • loadShape() imports OBJ files; build UV spheres manually with beginShape(TRIANGLE_STRIP) when you need texture mapping.