OpenGL Cone Function


How do you manually draw a 3D cone in OpenGL immediate mode? The OpenGL utility library (GLU) provides a cone function, but there may be various reasons for preferring to not use it, including reduced dependencies and finer control over the cone.

This article explains one way to manually draw a cone. There are two sections, first a verbal and mathematical explication of the main function, and second a listing of relevant code snippets. Snippets are embedded Gists from Github, which means that you are welcome to make them more concise or pretty if you like. If you want to skip directly to the code, click here.

The Math

Cone terminology

A cone could be specified in a number of ways, but I chose the following parameters as input because they were the most expressive given the context of my program:

Parameter Set Description
\mVc{d} \mathbb{R}^3 Axis defined as a normalized vector from base to apex.
\mPt{a} \mathbb{R}^3 Position of apex.
h \mathbb{R} Height of cone.
r_d \mathbb{R} Radius of directrix.
n \mathbb{Z}^+ Number of radial "slices."

The cone is constructed from two triangle fans, emanating first from the apex and second from the base centroid, both connecting with a discrete set of n points sampled from the directrix. The primary challenge here is the calculation of these sample points. A common strategy for this type of problem is to work in terms of a different coordinate system that allows for more natural expressions; when dealing with a circle in a plane, polar coordinates is often a good bet. Specifically, we can formulate each point as a tuple (\theta, r) \in \mathbb{R}^2 , then map these values into 3D world coordinates using a modification of the polar-to-rectangular conversion.

The initial calculation of points is now very straightforward. Inside of a loop whose index i \in \mathbb{N} ranges from 0, 1, \dotsc, n, the polar angle \theta of each successive point is given by 2i\pi/n, and the radius is always the parameter r_{d}. In order to convert each of these values, we start by looking at the traditional formulae, x = r\cos \theta, y = r\sin\theta, in a new way. In particular, let’s embed the plane in \mathbb{R}^3 and view the calculation as a linear combination of the standard basis vectors, \mVc{i}, \mVc{j}, and \mVc{k}: (x, y, z) = r\cos\theta\mVc{i} + r\sin\theta\mVc{j} + 0\mVc{k}. Before we can apply this transformation, we must correct for orientation and positioning.

Orientation can be accounted for by swapping out  \BA{i}, \BA{j}, and \BA{k} for a new set of basis vectors that are aligned to the cone’s base plane instead of the xy plane, i.e., by performing a change of basis. We’ll begin by defining \BA{e_{2}} = \BA{d} to be our new \BA{k}, since it is analogously normal to the base plane. In my application, it did not matter exactly how the cone was rotated around the axis, so my only requirement for the new basis was that the other two vectors lie somewhere within the base plane. Thus, I define an abstract function \mathrm{perp}(\BA{v}) that returns a vector lying somewhere within the plane perpendicular to \BA{v}, and use this function to generate a second vector \BA{e_{0}} = \mathrm{perp}(\BA{e_{2}}), which can be thought of as \BA{i}. The last vector \BA{e_{1}} is formed from the cross product of  \BA{e_{2}} and  \BA{e_{0}}, and will correspond to \BA{j}.

Before dealing with position, it’s appropriate to comment about the eventual drawing mechanics in relation to the mathematics. Although the rotation of the cone around its axis does not matter to me, the orientation of the two basis vectors that lie within the base plane does. Since OpenGL by default culls polygons that are specified in a clockwise (CW) order, we want to list the vertices of both triangle fans counter-clockwise (CCW) relative to the viewer. The way we are calculating \BA{e_{1}} = \BA{e_{2}} \times \BA{e_{0}} will always place  \BA{e_{1}} 90 degrees CCW from  \BA{e_{0}} when viewed from the half-space pointed at by  \BA{e_{2}}. This means that we can draw the “top” of the cone directly, but we will have to somehow reverse the vertex ordering when we are drawing the base.

The last piece of the conversion puzzle is to account for position. This is easily accomplished by adding the base centroid, \mPt{c} = \mPt{a} + (-\BA{d}h), to the expression. Putting everything together, we come up with

    \begin{align*}(x, y, z) &= \mPt{c} + r\cos\theta\mVc{e_{0}} + r\sin\theta\mVc{e_{1}} + 0\mVc{e_{2}} \\&= \mPt{c} + [((\mVc{e_{0}}\cos\theta) + (\mVc{e_{1}}\sin\theta))r]. \end{align*}

The Code

Concrete implementation of \mathrm{perp}(\BA{v}):

Vector3f perp(const Vector3f &v) {
    float min = fabsf(v.x());
    Vector3f cardinalAxis(1, 0, 0);

    if (fabsf(v.y()) < min) {
        min = fabsf(v.y());
        cardinalAxis = Vector3f(0, 1, 0);
    }

    if (fabsf(v.z()) < min) {
        cardinalAxis = Vector3f(0, 0, 1);
    }

    return CrossProduct(v, cardinalAxis);
}

Cone drawing function:

void drawCone(const Vector3f &d, const Vector3f &a,
              const float h, const float rd, const int n) {
    Vector3f c = a + (-d * h);
    Vector3f e0 = perp(d);
    Vector3f e1 = CrossProduct(e0, d);
    float angInc = 360.0 / n * M_PI_DIV180;

    // calculate points around directrix
    std::vector<Vector3f> pts;
    for (int i = 0; i < n; ++i) {
        float rad = angInc * i;
        Vector3f p = c + (((e0 * cos(rad)) + (e1 * sin(rad))) * rd);
        pts.push_back(p);
    }

    // draw cone top
    glBegin(GL_TRIANGLE_FAN);
    glVertex3f(a.x(), a.y(), a.z());
    for (int i = 0; i < n; ++i) {
        glVertex3f(pts[i].x(), pts[i].y(), pts[i].z());
    }
    glEnd();

    // draw cone bottom
    glBegin(GL_TRIANGLE_FAN);
    glVertex3f(c.x(), c.y(), c.z());
    for (int i = n-1; i >= 0; --i) {
        glVertex3f(pts[i].x(), pts[i].y(), pts[i].z());
    }
    glEnd();
}

2 thoughts on “OpenGL Cone Function

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.