Cylinder

How to 3D

Chapter 3: Meshes

Cylinder

A cylinder is a grid that has been rolled up. It has a radius and length. As in a grid, the cylinder's vertex positions are functions of the latitude and longitude indices. The latitude determines the vertex's placement along the length of the cylinder, and the longitude determines the placement around the cylinder. The cylinder's orientation determines which of the latitude and longitude we use to compute the xyz-coordinates.

Let's generate a vertical cylinder, in which case the longitude measures how far around the cylinder a vertex is positioned. We turn the longitude into a number of radians with this assignment:

const radians = lon / longitudeCount * -2 * Math.PI;
const radians = lon / longitudeCount * -2 * Math.PI;

Note that the expression lon / longitudeCount yields a proportion in [0, 1). If we instead divide by longitudeCount - 1, the vertices on the right edge will have a proportion of 1 and will therefore sit at \(2 \pi\) radians. These rightmost vertices will perfectly duplicate the leftmost vertices at 0 radians. We want the right edge to connect to the left edge, not duplicate it.

We multiply by \(-2 \pi\) to make a negative angle. If we use a positive angle, the front faces of the triangles will be on the inside of the cylinder. We want them on the outside, because that's where the viewer is. Getting triangles to face the correct way generally requires experimenting in a renderer that has perspective and backface culling. If the model seems strangely inverted, the triangles are probably facing the wrong way.

This renderer shows a cube whose front faces point inside the cube:

Rotate just the cube by dragging on it, and rotate the whole scene by dragging on the background. The cube's back faces are culled, so we jarringly see the far faces rather than the near ones.

Together the radius and radians form a vertex's polar coordinates. We convert these polar coordinates into Cartesian xz-coordinates using the formulas we saw last chapter:

$$\begin{aligned} x &= r \cos a \\ z &= r \sin a \end{aligned}$$

As with the grid, a vertex's latitude is turned into a proportion and applied to the cylinder's length—its height, since the cylinder is vertical—to produce its y-coordinate. Putting all these ideas together, we have this code for generating the positions:

export class Prefab {
  static cylinder(radius: number, height: number, longitudeCount: number, latitudeCount: number) {
    const positions: Vector3[] = [];

    for (let lat = 0; lat < latitudeCount; ++lat) {
      const y = lat / (latitudeCount - 1) * height;
      for (let lon = 0; lon < longitudeCount; ++lon) {
        const radians = lon / longitudeCount * 2 * Math.PI;
        const x = radius * Math.cos(radians);
        const z = radius * Math.sin(radians);
        positions.push(new Vector3(x, y, z));
      }
    }

    // ...
  }
}
export class Prefab {
  static cylinder(radius: number, height: number, longitudeCount: number, latitudeCount: number) {
    const positions: Vector3[] = [];

    for (let lat = 0; lat < latitudeCount; ++lat) {
      const y = lat / (latitudeCount - 1) * height;
      for (let lon = 0; lon < longitudeCount; ++lon) {
        const radians = lon / longitudeCount * 2 * Math.PI;
        const x = radius * Math.cos(radians);
        const z = radius * Math.sin(radians);
        positions.push(new Vector3(x, y, z));
      }
    }

    // ...
  }
}

The vertices connect into triangles much as they did in the grid, but in a cylinder we want the vertices on the right edge to wrap around and connect to the vertices on the left edge. We let the loop visit the right edge and use modular arithmetic to identify the longitude to the right. Compared to the grid, only the for loop header and the assignment to nextLon are different:

class Prefab {
  static cylinder(radius: number, height: number, longitudeCount: number, latitudeCount: number) {
    // ...

    const faces: number[][] = [];
    for (let lat = 0; lat < latitudeCount - 1; ++lat) {
      for (let lon = 0; lon < longitudeCount; ++lon) {
        let nextLon = (lon + 1) % longitudeCount;
        let nextLat = lat + 1;

        faces.push([
          index(lon, lat),
          index(nextLon, lat),
          index(lon, nextLat),
        ]);

        faces.push([
          index(nextLon, lat),
          index(nextLon, nextLat),
          index(lon, nextLat),
        ]);
      }
    }

    return new Trimesh(positions, faces);
  }
}
class Prefab {
  static cylinder(radius: number, height: number, longitudeCount: number, latitudeCount: number) {
    // ...

    const faces: number[][] = [];
    for (let lat = 0; lat < latitudeCount - 1; ++lat) {
      for (let lon = 0; lon < longitudeCount; ++lon) {
        let nextLon = (lon + 1) % longitudeCount;
        let nextLat = lat + 1;

        faces.push([
          index(lon, lat),
          index(nextLon, lat),
          index(lon, nextLat),
        ]);

        faces.push([
          index(nextLon, lat),
          index(nextLon, nextLat),
          index(lon, nextLat),
        ]);
      }
    }

    return new Trimesh(positions, faces);
  }
}

We'll need to copy the local index function from grid. If we need a cylinder that's not oriented vertically, we rotate it with the tools we'll see in the next chapter.

← GridCone →