A Simple 3D Renderer for the Web in C

Fletch
The Startup
Published in
6 min readDec 14, 2020

When it comes to rendering 3D content on the web there are quite a few options, the most popular being Three.js or the lesser-known p5.js. The problem being is that both of these are JavaScript frameworks, and when working with 3D usually you want the performance and portability of C, which in all respects is very similar to JavaScript being almost identical in syntax.

Web browsers support two types of OpenGL as stated on the official Wikipedia page; WebGL 1.0 which is based on OpenGL ES 2.0 and WebGL 2.0 which is based on OpenGL ES 3.0.

Generally if you desire to go for maximum compatibility you will go with the lesser of the two. In which case you will be working with GLSL ES 1.0 for your shader programs.

The key to a performant application is to minimise state changes on the GPU and to generally reduce any CPU to GPU communication possible. Additionally, if you are being cautious of what graphics hardware the end-user might have, to reduce your footprint and support a wider range of older hardware you can stick to simple Lambertian shaders and even using no textures, at most just simple vertex colours can help reduce load significantly.

So when it came to my desire to create some 3D web games, I wanted to target them for every platform from phone to pc but also have an incredibly low resource footprint so that they will play on the majority of budget devices, particularly knowing that as an indie developer I really have to cast my net far and wide to capture any possible attention my crude games might garner.

Luckily for me I never really liked textures in 3D graphics, seamless or not, they always just seemed a bit tacky to me, so going with the faster vertex colour alternative was a selling point in my books. It also simplified asset creation for me as I could quickly vertex paint 3D models in Blender using the Vertex Paint mode.

The rendering engine that I am presenting to you today is the same rendering engine that I use in the game Snowball.mobi, it’s an engine that I originally started making back in May of 2020 and between June and July of that year (this year at the time of writing) I made two games in it, the first one being too abstract to really release to the public but the second was Snowball.mobi.

The engine uses Emscripten to compile C code to WebAssembly / ASM.JS for the web. It features no texture support, as already alluded to, and is targeted at having a small footprint regarding performance, but primarily it is simple to set up and use.

The engine comes with a PLY to C Header File conversion program which is portable to any platform that can compile C code. This allows one to export 3D content from modelling programs such as Blender in the PLY format, those models can then be converted into C Header Files which can be directly included in the rendering engine project — meaning no loading of of external files at runtime. This program is called PTO and it’s usage is simply ./pto <file to convert> there is a shell script ready to go convert.sh which will convert the existing ncube.ply file.

One thing to keep in mind is that the PLY files must be exported in ASCII format, if you are using Blender this option can be found in the “Export to PLY” dialog in the top right corner (as of blender 2.9) as shown below.

Make sure the option highlighted in red is checked.

The main veg of the engine is located in esAux.h which provides all of your Vector, Matrix, and Shader operations. There is support for Quaternions too.

Here’s a quick breakdown of the esAux.h function set;

// structures
typedef struct
{
GLfloat m[4][4];
}
ESMatrix;

typedef struct
{
GLfloat x,y,z,w;
}
ESVector;

typedef struct
{
GLuint vid; // Vertex Array Buffer ID
GLuint iid; // Index Array Buffer ID
GLuint cid; // Colour Array Buffer ID
GLuint nid; // Normal Array Buffer ID
}
ESModel;

// defines
#define PI 3.1415926535897932384626433832795f // PI
#define x2PI 6.283185307178f // PI * 2
#define PI_2 1.570796326794f // PI / 2
#define DEGREE 57.295779513096f // 1 Radian as Degrees
#define RADIAN 0.017453293f // PI / 180 (1 Degree as Radians)

// vector
void vCross(ESVector* r, const ESVector v1, const ESVector v2);
GLfloat vDot(const ESVector v1, const ESVector v2);

void vNorm(ESVector* v);
GLfloat vDist(const ESVector v1, const ESVector v2);
GLfloat vModulus(const ESVector v);
void vInvert(ESVector* v);
void vCopy(ESVector* r, const ESVector v);

void vRotX(ESVector* v, const GLfloat radians);
void vRotY(ESVector* v, const GLfloat radians);
void vRotZ(ESVector* v, const GLfloat radians);

void vAdd(ESVector* r, const ESVector v1, const ESVector v2);
void vSub(ESVector* r, const ESVector v1, const ESVector v2);
void vDiv(ESVector* r, const ESVector denominator, const ESVector numerator);
void vMul(ESVector* r, const ESVector v1, const ESVector v2);

void vAddScalar(ESVector* r, const ESVector v1, const GLfloat v2);
void vSubScalar(ESVector* r, const ESVector v1, const GLfloat v2);
void vDivScalar(ESVector* r, const ESVector v1, const GLfloat v2);
void vMulScalar(ESVector* r, const ESVector v1, const GLfloat v2);

// matrix
void esMatrixLoadIdentity(ESMatrix *result);
void esMatrixCopy(ESMatrix* r, const ESMatrix* v);
void esMatrixMultiply(<redacted for readability>);
void esMatrixMultiplyPoint(<redacted for readability>);
void esMatrixMultiplyVec(<redacted for readability>);
void esScale(<redacted for readability>);
void esTranslate(<redacted for readability>);
void esRotate(<redacted for readability>);
void esFrustum(<redacted for readability>);
void esPerspective(<redacted for readability>);
void esOrtho(<redacted for readability>);
void esLookAt(<redacted for readability>);
void esCramerInvert(GLfloat *dst, const GLfloat *mat);
void esTranspose(ESMatrix *r, const ESMatrix* m);
void esSetDirection(<redacted for readability>);
void esGetDirection(ESVector *result, const ESMatrix matrix);
void esGetDirectionX(ESVector *result, const ESMatrix matrix);
void esGetDirectionY(ESVector *result, const ESMatrix matrix);
void esGetDirectionZ(ESVector *result, const ESMatrix matrix);
void esGetViewDirection(ESVector *result, const ESMatrix matrix);

// utility functions
GLfloat esRandFloat(const GLfloat min, const GLfloat max);
void esBind(<redacted for readability>);

//quaternion
void qMatrix(ESMatrix* r, const ESVector q);
void qCovert(ESVector* r, const ESMatrix m);
void qAngle(<redacted for readability>);
void qDirection(ESVector* rq, const ESVector vn);
void qMul(ESVector* r, const ESVector lhs, const ESVector rhs);
GLfloat qDot(const ESVector q1, const ESVector q2);
void qNorm(ESVector* q);
void qInvert(ESVector* q);
void qRotV(ESVector* v, const ESVector q);

// shader
void makeAllShaders();

void makeFullbright();
void makeLambert();
void makeLambert1();
void makeLambert2();
void makeLambert3();
void makePhong();
void makePhong1();
void makePhong2();
void makePhong3();

void shadeFullbright(<redacted>); // solid color + no shading

void shadeLambert(<redacted>); // solid color + no normals
void shadeLambert1(<redacted>); // solid color + normals
void shadeLambert2(<redacted>); // colors + no normals
void shadeLambert3(<redacted>); // colors + normals

void shadePhong(<redacted>); // solid color + no normals
void shadePhong1(<redacted>); // solid color + normals
void shadePhong2(<redacted>); // colors + no normals
void shadePhong3(<redacted>); // colors + normals

Finally you have the Emscripten / SDL framework to tie it all together, it’s basically a main file with your Emscripten “main_loop” and SDL render context / window initialisation code.

Inside I also provide an orbit camera with support for both touch screen and mouse and a basic delta time function for animating and interpolation.

This is presented as the menger.c file which is a working example of the framework to render a simple L3 Menger cube with an animated light and two shading modes, shadeLambert1() and shadePhong1(). One thing to note is that the provided shaders are in camera space.

What is an L3 menger cube you may ask? Well, it is a simple 3D fractal iterated to 3 levels deep, here is a render of one from the menger.c output.

Check out the working demo here

A fun bit of trivia for you; the 2D counterpart, the Menger carpet pattern, is known to have been used in mobile phone ariels.

So there we have it, this is a simple 3 part renderer for C with Emscripten, it’s ready to roll you just need to install the dependencies required.

There are three working demos of this renderer online, one, two, and three. [?] key bind info.

Oh and not to forget, the GitHub project page with the demo and source code is here enjoy!

You may also be interested in my newer article “Vertex Color” which describes a similar process to this one focusing only on the use of Vertex Colours using a more recent code base, read it here.

OpenGL ES 2.0 Shader Specification 1.0 (the base version used by webgl)
https://www.khronos.org/registry/OpenGL/specs/es/2.0/GLSL_ES_Specification_1.00.pdf

WebGL 1.0 Reference Card:
https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf

WebGL 2.0 Reference Card:
https://www.khronos.org/files/webgl20-reference-guide.pdf

OpenGL ES 1.1 Reference Pages:
https://www.khronos.org/registry/OpenGL-Refpages/es1.1/xhtml/

OpenGL ES 2.0 Reference Card:
https://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf

OpenGL ES 2.0 Reference Pages:
https://www.khronos.org/registry/OpenGL-Refpages/es2.0/

Chrome Shader Editor Plugin:
https://chrome.google.com/webstore/detail/shader-editor/ggeaidddejpbakgafapihjbgdlbbbpob

--

--