Assuming that you have read Drawing a 2d scene

Here is a WebGL demo showing what’s taking place in Sketch.h and Sketch.cpp from the TestingDrawing3d project’s source folder.

Sketch.h

We can see a new kind of stock shader: LambertShader which renders with a basic lighted apperance.

Sketch.cpp

Inside setup()

Sphere()
  .setFrontFace(CW)
  .setRadius(45)
  .setSectorCount(60)
  .setStackCount(30)
  .append(sunBatch, Matrix());

This creates a sphere mesh and adds it to sunBatch without any transformation. setFrontFace(CW) states that the vertex indices will be added in clockwise order, which is the default for OpenGL. Alternatively, we could use CCW for counter-clockwise order (more on this topic later.) setSectorCount() and setStackCount() are controlling the sphere’s resolution. Higher resolution means smoother apperance, at the cost of more vertices added.

Note that the sphere mesh vertices are of type XYZ.N. They contain information about position and normal. The normal will be used by the LambertShader to compute lighting.

pathBatch
  .setPrimitive(GL_LINE_STRIP)
  .setShader(colorShader)
  .setShaderColor(1, 1, 1, 0.25f);

This defines that pathBatch (representing the revolution of the earth around the sun) is drawn in GL_LINE_STRIP mode (the default mode for batches being GL_TRIANGLES.) Note that the same shader is used for drawing lines or basic colored triangles.

Then we have the createPath() method, which fills pathBatch with vertices describing a circle, using baisc trigonometry.

glEnable(GL_CULL_FACE);

This is an OpenGL setting used when working with 3d. It’s an optimization that makes sure that triangles which are not facing the camera won’t be rendered. The GPU will choose which triangles to keep based on the winding order (CW or CCW) discussed earlier.

glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);

These OpenGL settings are also used when working with 3d. Briefly, it is related to the z-buffer which controls the pixels that should be rendered. In order to allow z-buffering, you must define a parameter in main.cpp after the anti-aliasing parameter: here and there.

Inside draw()

glClearColor(0.125f, 0.125f, 0.125f, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

We fill the window with dark gray and clear the z-buffer.

auto projectionMatrix = glm::perspective(60 * D2R, windowInfo.aspectRatio(), 0.1f, 1000.0f);

Matrix viewMatrix;
viewMatrix
  .translate(0, 0, -400)
  .rotateX(22 * D2R);

This simulates a camera. We first create a perspective projection matrix with a vertical field-of-view of 60 degrees and clipping values of 0.1 for z-near and 1000 for z-far (pixels which are not within the clipping range won’t be rendered.) Then we create a view matrix which moves 400 pixels away on the z axis (the camera distance) and rotates by 22 degrees on the x axis, giving the impression that we look from above.

State()
  .setShaderMatrix<MVP>(viewMatrix * projectionMatrix)
  .setShaderMatrix<NORMAL>(viewMatrix.getNormalMatrix())
  .apply();

The LambertShader used for rendering the spheres requires that we pass a normal matrix, which can be obtained from our view matrix.

Then we render our sphere batches. The sun is static while the earth and the moon are dynamic, that is, generated each time we draw. This is not the most optimal technique (instancing could have been used instead, if there were many spheres to render.)

Note how the earth and the moon have lower resolutions, according to their radius.

And finally, we render our pathBatch, which is also static.