Rounding All The Corners With OpenGL

Hi friends! I wrote another one of these OpenGL tutorials just for you. I guess this’ll be the seventh one. Here are the previous six:

If you’ve been following along you already know that to run this code, you’ll need to have a folder on your computer called opengl_examples with the three.js library in the js subfolder. Go back to the first tutorial if you’d like a better explanation.

Anyway, today I thought I’d show you a trick for rounding the corners on certain meshes. We’ll start with an updated implementation of the regular polygon mesh we made in the second tutorial, and from there I’ll show you how to make a rectangle with rounded corners and even a regular polygon with rounded corners. This is how they look in the end:

Three Shapes

Open File, Copy Code

We’ll call this file rounding_corners.html. It goes in opengl_examples, along with all the other tutorials. Here’s the code to copy in:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Rounding All The Corners With OpenGL</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <style>
      body {
        color: #ffffff;
        background-color: #000000;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>
  <body>

    <div id="container"></div>

    <script src="js/three.js"></script>
    <script src="js/Detector.js"></script>

    <script>

      var webGlSupported = Detector.webgl;
      if (!webGlSupported) {
        Detector.addGetWebGLMessage();
      }

      const BRIGHT_GREEN = new THREE.Color(0.0, 1.0, 0.0);
      const DARK_GREEN = new THREE.Color(0.0, 0.3, 0.0);

      const VERTEX_COLORS = [DARK_GREEN, BRIGHT_GREEN, BRIGHT_GREEN];

      var container = undefined;
      var camera = undefined;
      var material = undefined;
      var scene = undefined;
      var renderer = undefined;

      initializeCamera();
      initializeMaterial();
      initializeScene();
      initializeRenderer();
      renderScene();

      function initializeMaterial() {
        material = new THREE.MeshBasicMaterial({
          vertexColors: THREE.VertexColors,
          side: THREE.DoubleSide
        });
      }

      function initializeScene() {
        scene = new THREE.Scene();

        addRegularPolygonToScene(0.0, 75.0);
        addRoundedRectangleToScene(0.0, 0.0);
        addRoundedRegularPolygonToScene(0.0, -75.0);
      }

      function addMeshToScene(geometry, material, x, y) {
        var mesh = new THREE.Mesh(geometry, material);
        mesh.position.set(x, y, 0.0);
        scene.add(mesh);
      }

      ////////////////////////////////////////
      // Regular Polygon
      ////////////////////////////////////////

      function addRegularPolygonToScene(x, y) {
        var radius = 40.0;
        var sidesCount = 6;
        var geometry = getRegularPolygonGeometry(radius, sidesCount);

        addMeshToScene(geometry, material, x, y);
      }

      function getRegularPolygonGeometry(radius, sidesCount) {
        var geometry = new THREE.Geometry();

        geometry.vertices = getRegularPolygonVertices(radius, sidesCount);
        geometry.faces = getRadialFaces(sidesCount);

        return geometry;
      }

      // 1
      function getRegularPolygonVertices(radius, sidesCount) {
        var vertices = [];

        var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
        vertices.push(centerPoint);

        for (sideIndex = 0; sideIndex < sidesCount; sideIndex++) {
          var sideProgress = sideIndex / sidesCount;
          var point = getRadialPoint(sideProgress, radius);
          vertices.push(point);
        }

        return vertices;
      }

      ////////////////////////////////////////
      // Rounded Rectangle
      ////////////////////////////////////////

      function addRoundedRectangleToScene(x, y) {
        var width = 180.0;
        var height = 50.0;
        var roundingRadius = 15.0;
        var sidesCountPerRounding = 20;
        var geometry = getRoundedRectangleGeometry(width, height, roundingRadius, sidesCountPerRounding);

        addMeshToScene(geometry, material, x, y);
      }

      function getRoundedRectangleGeometry(width, height, roundingRadius, sidesCountPerRounding) {
        var geometry = new THREE.Geometry();

        geometry.vertices = getRoundedRectangleVertices(width, height, roundingRadius, sidesCountPerRounding);

        // 4
        var cornersCount = 4;
        var facesCount = sidesCountPerRounding * cornersCount + cornersCount;
        geometry.faces = getRadialFaces(facesCount);

        return geometry;
      }

      // 3
      function getRoundedRectangleVertices(width, height, roundingRadius, sidesCountPerRounding) {
        var vertices = [];

        var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
        vertices.push(centerPoint);

        var quadrantPositivities = [
          {x: 1.0, y: 1.0},
          {x: -1.0, y: 1.0},
          {x: -1.0, y: -1.0},
          {x: 1.0, y: -1.0}
        ];

        var cornersCount = 4;

        for (var cornerIndex = 0; cornerIndex < cornersCount; cornerIndex++) {
          var startProgress = cornerIndex / cornersCount;
          var endProgress = (cornerIndex + 1) / cornersCount;

          var quadrantPositivity = quadrantPositivities[cornerIndex];

          var cornerOuterX = quadrantPositivity.x * 0.5 * width;
          var cornerRoundingCenterX = cornerOuterX - quadrantPositivity.x * roundingRadius;

          var cornerOuterY = quadrantPositivity.y * 0.5 * height;
          var cornerRoundingCenterY = cornerOuterY - quadrantPositivity.y * roundingRadius;

          var cornerVertices = getRoundingVertices(
            startProgress,
            endProgress,
            roundingRadius,
            sidesCountPerRounding,
            cornerRoundingCenterX,
            cornerRoundingCenterY
          );

          vertices = vertices.concat(cornerVertices);
        }

        return vertices;
      }

      ////////////////////////////////////////
      // Rounded Regular Polygon
      ////////////////////////////////////////

      function addRoundedRegularPolygonToScene(x, y) {
        var radius = 40.0;
        var roundingRadius = 10.0;
        var sidesCount = 6;
        var sidesCountPerRounding = 20;
        var geometry = getRoundedRegularPolygonGeometry(radius, roundingRadius, sidesCount, sidesCountPerRounding);

        addMeshToScene(geometry, material, x, y);
      }

      function getRoundedRegularPolygonGeometry(radius, roundingRadius, sidesCount, sidesCountPerRounding) {
        var geometry = new THREE.Geometry();

        geometry.vertices = getRoundedRegularPolygonVertices(radius, roundingRadius, sidesCount, sidesCountPerRounding);

        var facesCount = sidesCountPerRounding * sidesCount + sidesCount;
        geometry.faces = getRadialFaces(facesCount);

        return geometry;
      }

      // 5
      function getRoundedRegularPolygonVertices(radius, roundingRadius, sidesCount, sidesCountPerRounding) {
        var vertices = [];

        var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
        vertices.push(centerPoint);

        var roundingCenterRadius = radius - roundingRadius;
        var progressPerRounding = 1.0 / sidesCount;

        for (sideIndex = 0; sideIndex < sidesCount; sideIndex++) {
          var roundingCenterProgress = sideIndex / sidesCount;
          var roundingStartProgress = roundingCenterProgress - 0.5 * progressPerRounding;
          var roundingEndProgress = roundingCenterProgress + 0.5 * progressPerRounding;

          var roundingCenterPoint = getRadialPoint(roundingCenterProgress, roundingCenterRadius);

          var roundingVertices = getRoundingVertices(
            roundingStartProgress,
            roundingEndProgress,
            roundingRadius,
            sidesCountPerRounding,
            roundingCenterPoint.x,
            roundingCenterPoint.y
          );

          vertices = vertices.concat(roundingVertices);
        }

        return vertices;
      }

      ////////////////////////////////////////

      // 2
      function getRadialFaces(facesCount) {
        var faces = [];

        var centerPointIndex = 0;

        for (faceIndex = 0; faceIndex < facesCount; faceIndex++) {
          var face = new THREE.Face3();

          var trailingOuterPointIndex = faceIndex + 1;

          var leadingOuterPointIndex = faceIndex + 2;
          if (leadingOuterPointIndex > facesCount) {
            leadingOuterPointIndex -= facesCount;
          }

          var normal = undefined;
          var face = new THREE.Face3(centerPointIndex, trailingOuterPointIndex, leadingOuterPointIndex, normal, VERTEX_COLORS);

          faces.push(face);
        }

        return faces;
      }

      function getRoundingVertices(startProgress, endProgress, radius, sidesCount, centerX = 0.0, centerY = 0.0) {
        var vertices = [];

        var progressRange = endProgress - startProgress;

        for (vertexIndex = 0; vertexIndex <= sidesCount; vertexIndex++) {
          var vertexProgress = startProgress + progressRange * (vertexIndex / sidesCount);

          var vertex = getRadialPoint(vertexProgress, radius, centerX, centerY);
          vertices.push(vertex);
        }

        return vertices;
      }

      function getRadialPoint(progress, radius, centerX = 0.0, centerY = 0.0) {
        var angle = progress * 2.0 * Math.PI;

        var x = centerX + Math.cos(angle) * radius;
        var y = centerY + Math.sin(angle) * radius;

        return new THREE.Vector3(x, y, 0.0);
      }

      function initializeCamera() {
        var aspectRatio = window.innerWidth / window.innerHeight;
        var screenWidth = undefined;
        var screenHeight = undefined;
        if (aspectRatio > 1.0) {
          screenWidth = 320.0 * aspectRatio;
          screenHeight = 320.0;
        } else {
          screenWidth = 320.0;
          screenHeight = 320.0 / aspectRatio;
        }

        var nearPlane = 1.0;
        var farPlane = 1000.0;
        camera = new THREE.OrthographicCamera(
          -0.5 * screenWidth,
          0.5 * screenWidth,
          0.5 * screenHeight,
          -0.5 * screenHeight,

          nearPlane,
          farPlane
        );

        var distanceFromScene = 500.0;
        camera.position.set(0.0, 0.0, distanceFromScene);
      }

      function initializeRenderer() {
        renderer = new THREE.WebGLRenderer();
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);

        container = document.getElementById("container");
        container.appendChild(renderer.domElement);
      }

      function renderScene() {
        renderer.render(scene, camera);
      }

    </script>

  </body>
</html>

No Training Wheels

By this point you know the drill. I’m not going to write as much about the basics. Instead, I’ll just give you a high-level overview of the procedure I’m following. Now that you’ve seen a few examples, hopefully my code is clear enough that you can follow what’s going on.

Science Cat

Regular Old Polygons

Like I said, we’re making three shapes in this tutorial. The first one is the same kind of regular polygon that we’ve already been using to approximate circles in previous tutorials. Here’s the code to set up the vertices. Note that it gets called from getRegularPolygonGeometry, which is part of addRegularPolygonToScene, which is part of initializeScene.

// 1
function getRegularPolygonVertices(radius, sidesCount) {
  var vertices = [];

  var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
  vertices.push(centerPoint);

  for (sideIndex = 0; sideIndex < sidesCount; sideIndex++) {
    var sideProgress = sideIndex / sidesCount;
    var point = getRadialPoint(sideProgress, radius);
    vertices.push(point);
  }

  return vertices;
}

What’s new? This time around I’m relying on getRadialPoint to build the vertices array. In fact, I’m using it to build all the vertices in this tutorial. I’ve added optional arguments to the end of getRadialPoint’s signature so that it’s possible to provide a center point from which to plot the radial point. If you leave the arguments blank (as I’m doing in getRegularPolygonVertices), then (0.0, 0.0) will be used for the center point.

One Size Fits All

Did you want to know a secret? While I was writing this example, I realized that all three shapes that we’re making can rely on the same function to build their Face3s. That’s because they’re all sort of radial shapes. At least–they all have a center point, and all of their triangles fan out from it. Here’s the function I wrote up to connect all the dots:

// 2
function getRadialFaces(facesCount) {
  var faces = [];

  var centerPointIndex = 0;

  for (faceIndex = 0; faceIndex < facesCount; faceIndex++) {
    var face = new THREE.Face3();

    var trailingOuterPointIndex = faceIndex + 1;

    var leadingOuterPointIndex = faceIndex + 2;
    if (leadingOuterPointIndex > facesCount) {
      leadingOuterPointIndex -= facesCount;
    }

    var normal = undefined;
    var face = new THREE.Face3(centerPointIndex, trailingOuterPointIndex, leadingOuterPointIndex, normal, VERTEX_COLORS);

    faces.push(face);
  }

  return faces;
}

This time the VERTEX_COLORS are handled in constant definitions near the top of the main program. That helps shorten our function a little, which helps keep our code a little more tidy.

Grab Some Popcorn

But you’re not really interested in drawing the shapes we’ve already drawn, are you? Let’s move on to the rounded rectangle. This should be good. Take a look at how we build up its vertices.

// 3
function getRoundedRectangleVertices(width, height, roundingRadius, sidesCountPerRounding) {
  var vertices = [];

  var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
  vertices.push(centerPoint);

  var quadrantPositivities = [
    {x: 1.0, y: 1.0},
    {x: -1.0, y: 1.0},
    {x: -1.0, y: -1.0},
    {x: 1.0, y: -1.0}
  ];

  var cornersCount = 4;

  for (var cornerIndex = 0; cornerIndex < cornersCount; cornerIndex++) {
    var startProgress = cornerIndex / cornersCount;
    var endProgress = (cornerIndex + 1) / cornersCount;

    var quadrantPositivity = quadrantPositivities[cornerIndex];

    var cornerOuterX = quadrantPositivity.x * 0.5 * width;
    var cornerRoundingCenterX = cornerOuterX - quadrantPositivity.x * roundingRadius;

    var cornerOuterY = quadrantPositivity.y * 0.5 * height;
    var cornerRoundingCenterY = cornerOuterY - quadrantPositivity.y * roundingRadius;

    var cornerVertices = getRoundingVertices(
      startProgress,
      endProgress,
      roundingRadius,
      sidesCountPerRounding,
      cornerRoundingCenterX,
      cornerRoundingCenterY
    );

    vertices = vertices.concat(cornerVertices);
  }

  return vertices;
}

What’s actually happening here? Well, we’re going to divide our rectangle up into four quadrants. quadrantPositivities is just recording whether x and y coordinates are positive or negative in each quadrant. For each quadrant, we use the corresponding quadrantPositivity to find the coordinates of a center point. From that center point, we use getRoundingVertices to plot points that make up one quarter of a circle. We save those points in our vertices array, but we discard the center point’s coordinate.

It's Just A Circle With Stuffing

If you think about it, you can form a rounded rectangle by starting with a circle. That circle’s radius is the corner radius (or roundingRadius in the case of our program). If you slice that circle into quadrants and move each slice so its outermost points are separated horizontally by the rectangle’s width and vertically by its height, you have your four corners. Then you just have to fill in the space between them. Which we do with our Face3s. Take a look at // 4.

function getRoundedRectangleGeometry(width, height, roundingRadius, sidesCountPerRounding) {
  var geometry = new THREE.Geometry();

  geometry.vertices = getRoundedRectangleVertices(width, height, roundingRadius, sidesCountPerRounding);

  // 4
  var cornersCount = 4;
  var facesCount = sidesCountPerRounding * cornersCount + cornersCount;
  geometry.faces = getRadialFaces(facesCount);

  return geometry;
}

Like I said earlier, we’re using getRadialFaces to build the faces arrays for all three of our shapes. But I wanted to call your attention to these few lines because the formula for calculating the number of triangles you need to build varies from shape to shape. For the regular polygon, the number of faces happens to be the same as the number of sides, so we just passed in sidesCount. For the rounded rectangle, however, we need to do something a little more advanced. We’ll need sidesCountPerRounding triangles for each of the four corners, but what about the sides of the rectangle? That’s what the extra cornersCount that we add to our total is for.

Moar Slices Plz

Moar Plz

Now we’re moving on to our last shape of the day: the rounded regular polygon. The way we build this one is something of a combination of the previous two techniques. Once again, let’s look at the function for building the vertices.

// 5
function getRoundedRegularPolygonVertices(radius, roundingRadius, sidesCount, sidesCountPerRounding) {
  var vertices = [];

  var centerPoint = new THREE.Vector3(0.0, 0.0, 0.0);
  vertices.push(centerPoint);

  var roundingCenterRadius = radius - roundingRadius;
  var progressPerRounding = 1.0 / sidesCount;

  for (sideIndex = 0; sideIndex < sidesCount; sideIndex++) {
    var roundingCenterProgress = sideIndex / sidesCount;
    var roundingStartProgress = roundingCenterProgress - 0.5 * progressPerRounding;
    var roundingEndProgress = roundingCenterProgress + 0.5 * progressPerRounding;

    var roundingCenterPoint = getRadialPoint(roundingCenterProgress, roundingCenterRadius);

    var roundingVertices = getRoundingVertices(
      roundingStartProgress,
      roundingEndProgress,
      roundingRadius,
      sidesCountPerRounding,
      roundingCenterPoint.x,
      roundingCenterPoint.y
    );

    vertices = vertices.concat(roundingVertices);
  }

  return vertices;
}

If the rounded rectangle was formed by splitting a circle into four slices, then you can think of the rounded regular polygon as a circle split into n slices, where n is the number of corners on the polygon. And that’s precisely how we’re building up our vertices. Each iteration through our loop builds up the points for one of the polygon’s rounding sections–one of its corners. First, we have to find the center point for our rounding circle. We do that with exactly the same approach that we used to find the vertices of the original regular polygon. Except this time we use a radius that is smaller than the shape’s full radius. It’s smaller by roundingRadius units. Once we’ve found each rounding’s center point, we use it to find the roundingVertices with getRoundingVertices. Of course, we also have to calculate and pass in the roundingStartProgress and roundingEndProgress. Once again, we store all the roundingVertices and discard the roundingCenterPoint. It helps to visualize it. Here’s a horribly crude diagram.

Crude Diagram Of Rounded Regular Polygon

In this poorly-drawn example, we’re slicing our rounding circle up into five equal slices. That means each slice’s arc length will take up one fifth of the circle’s circumference. Then we push those slices outward to the borders of our shape. Each slice’s pointy end will be resting on one of the corners of the inner pentagon (colored red here). In our code, each corner of the red pentagon was a roundingCenterPoint. I’ve connected up the slices’ outside edges with yellow lines. The yellow lines and the exposed green section of each slice will form our final shape. Neato!

Not Bad

Three shapes. Not bad for a day’s work. As always, I encourage you to mess around with the code. Perhaps you can come up with some more funky shapes. Maybe invert some of the angles? Maybe increase some radii? As Meshes in 2D games go, this geometry is pretty advanced. And there are certainly easier ways to draw rounded shapes with OpenGL. We’ll cover some of those in future tutorials. But now that you’ve encountered some of the pain points of working with complex Meshes, you’ll appreciate the other methods I’m going to teach you that much more. 😉

Downdownloadables

Click here to run the finished product!

And here’s a link to a downloadable version of the source code: rounding_corners.html. Note that you’ll need to run it from inside the folder structure that we set up in the first tutorial.

Next Tutorial

Dirt Basic Animation With OpenGL

rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium vimeo