Get Started with WebGL: Draw 2D Shape

Lan Wang
7 min readApr 21, 2023

--

WebGL

If you have learnt HTML, CSS, and JavaScript, maybe you will think: Can I do something more fancy than displaying texts and images? Of course YES! For example, you can use WebGL to generate interactive 2D or 3D graphics.

Sounds difficult and challenging? Let’s start with something simple: drawing a 2D shape.

Prerequisites: HTML, CSS, and JavaScript are must. If you know GLSL, it’s better, but I’ll explain the GLSL code.

Repository: https://github.com/yexiaosu/Intro-to-WebGL

Note: To see the result, you cannot just open the HTMl file. Instead, you need to launch a local server. If you use VS Code, you can just install an extension called Live Server and click “Go Live” at the bottom of the VS Code window.

What is WebGL?

WebGL (Web Graphics Library) is a JavaScript API which closely conforms to OpenGL that can be used in HTML <canvas> elements, so that we can render 2D or 3D graphics on the canvas. WebGL can take advantage of hardware graphics acceleration provided by the user’s device, which means that the data can be sent to GPU to render graphics.¹

Getting Started with WebGL

As we mentioned before, the API is used in HTML <canvas> elements, so first we need a HTML file called index.html with a <canvas> element:

index.html

Here the style is just used to make the canvas clearer. You can define the style of the canvas as you want, and you can also add more elements to the HTML file if you want. The JavaScript file we included here will be created later.

I’m going to draw the secondary mark of University of Michigan² in this tutorial.

Before drawing the shape, we need to know the coordinates of the shape’s border by modelling. We can find a paper with grids, and fit the mark to the grids and calculate the coordinates like this:

The Secondary Mark of University of Michigan and its Model

Notes:

  • The coordinates in WebGL has a range from -1 to 1, so we need to do some transformation;
  • You might notice that I divide the shape into several triangles. This is because WebGL renders shapes by drawing triangles or triangle strip. I will choose the mode of drawing triangles, which will be easier when modelling.

As we have modelled the mark, we can form the geometry information of it. The geometry information includes the coordinates of vertices, the color of each vertex, and the order to traverse the vertices when drawing triangles. We can gather these information in a JSON file:

geometry.json

Notes:

  • Here I put positions and colors in attributes, which is separated from triangles. But you can define the structure yourself.
  • triangles is a list of vertices' index in the position list. You can see that each three indexes are grouped, which means that the corresponding three vertices are the vertices of one triangle.
  • Since the mark only has one color, so each vertices are of the same color. The color is in RGBA percentage. When WebGL rendering the shape, it colors each vertex and all other pixels’ colors are calculated using interpolation³.

Vertex Shader and Fragment Shader

We also need to prepare two shaders for rendering the shape with the geometry information: vertex shader and fragment shader. As the names tells, they deal with vertices and the fragments between vertices. When a shape is rendered:

  1. The vertex shader will transform the positions to the coordinate used by WebGL. It will also do other transformations as required. For example, you can apply a transform matrix to each vertex to make an animation in the shader.
  2. The fragment shader will use the information shared by vertex shader to compute the color, light or any other things that rendering a pixel needs with the program provided in this shader. The shader will be called once for each pixel.

I know this is complex if you have no related knowledge before… But don’t worry! We are not doing complex things here! We have no animation, no color gradient, and no lighting. We just need two simple shaders to use our geometry information to render a static 2D shape:

vertex.glsl
fragment.glsl

The shaders are written by GLSL. If you know it well, you can skip the following explanation of the code above and go to the next section.

  • #version 300 es specifies the version of GLSL language.
  • precision highp float(in the fragment shader) specifies the precision of float type.
  • The keywords in and out in GLSL defines the inputs and outputs of the shader. vec4 declares a 4-component vector.
  • In the vertex shader, we have two inputs: position and color. position is the coordinate of a vertex, and color is the color of that vertex. These inputs will come from our geometry information.
  • In the vertex shader, we also have an output called vColor. This will be shared with the fragment shader, in which we can see an input also called vColor. In our example, since we don't need the color to change, we just use the original color information and pass it through to the output of the fragment shader. So each pixel is rendered as the same color of all the vertices.
  • In the vertex shader, we can see that the value of input position is assigned to gl_Position. This is a built-in variable, which contains the position information that will be used to render a vertex.

More information of GLSL can refer to the document: OpenGL Wiki

Use WebGL API in JavaScript to Render the Shape

The final step is to write the JavaScript! Let’s first create a file called webgl.js, which we included in our HTML file, and check the files we have:

.
├── fragment.glsl
├── geometry.json
├── index.html
├── vertex.glsl
└── webgl.js

In webgl.js, we need to set up the WebGL environment and load the resources we need first:

webgl.js: setup() and add load event listener

As you may noticed, actually vs and fs are strings. The shaders can actually defined in JavaScript files as strings like this:

const fs = `
#version 300 es
precision highp float;

in vec4 vColor;
out vec4 fragColor;

void main() {
fragColor = vColor;
}
`;

But if the shader is complex, this is not recommended.

After that, we need to compile the shaders:

webgl.js: createShader()

The shaders should also be linked to the program:

webgl.js: createShader()

Let’s call the compileAndLinkGLSL function after we load the sources:

webgl.js: setup()

Then we are going to deal with the geometry information. We need to load the information to a VAO (vertex array object), so that it can be used by our program later.

webgl.js: setupGeometry()

Note that gl.bindVertexArray() is NOT used to bind data. It actually declares which VAO s being used. For example, gl.bindVertexArray(triangleArray); means that we are using triangleArray, and later whenever we modify a VAO, we are actually modify triangleArray, until another VAO is bound.

After that, we need to create buffers and store the geometry information in them, because when GPU computing and rendering graphics, it actually reads data from buffers.

A VAO is atually like:

// pseudo code
// reference: https://webglfundamentals.org/webgl/lessons/webgl-attributes.html
vertexArray = {
attributes: [
{ enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0, },
// ... other attributes
],
elementArrayBuffer: null,
}

That’s why I put positions and colors in attributes of geometry.json. We actually store them in a buffer and assign it to an attribute of the VAO, so that the buffers can be accessed by the location of the attributes.

To do this, we can update the setupGeometry() function:

webgl.js: setupGeometry()

Note that gl.bindBuffer() is just like gl.bindVertexArray(), which only declares the buffer that will be used later. While gl.bufferData() is the function used to load data to a buffer.

If you catch up well, you may remember that we also have a triangle list in our geometry information, and we cannot draw a shape only with position and color. We must also create a buffer and tell WebGL how to draw the triangles. Recall the pseudo code of VAO, we are going to use the elementArrayBuffer in it this time. Add the following code after we iterate over attributes and bind buffers:

webgl.js: setupGeometry()

Note that this time when we bind buffer, we use gl.ELEMENT_ARRAY_BUFFER instead of gl.ARRAY_BUFFER as the binding point, which is always used for indices. Also, we add some other information to the object returned. These information will be used later to tell GPU how to read data from buffers when rendering shapes.

After that, let’s call the setupGeometry() function after we compile and link the shaders. To use the information and VAO later, we can assign the object returned by this function to a window variable:

webgl.js: setup()

The last step is drawing, which is really simple. All we need to do is to use our program and call the function gl.drawElements provided by WebGL:

webgl.js: draw()

And call the function in the setup() function:

webgl.js: setup()

Since we have already added this function to the window as a load event listener, when we start the server and open the webpage, we can see the mark on our canvas!

Result of Rendering the mark

Reference

[1] WebGL: 2D and 3D graphics for the web — web apis: MDN. Web APIs | MDN. (n.d.). Retrieved April 19, 2023, from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API

[2] U-M logo guidelines. Vice President for Communications, University of Michigan. (n.d.). Retrieved April 19, 2023, from https://brand.umich.edu/logos/u-m-logo/

[3] Using shaders to apply color in webgl — web apis: MDN. Web APIs | MDN. (n.d.). Retrieved April 20, 2023, from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_shaders_to_apply_color_in_WebGL

[4] OpenGL shading language. OpenGL Shading Language — OpenGL Wiki. (n.d.). Retrieved April 20, 2023, from https://www.khronos.org/opengl/wiki/OpenGL_Shading_Language

[5] Webgl attributes. WebGL Attributes. (n.d.). Retrieved April 20, 2023, from https://webglfundamentals.org/webgl/lessons/webgl-attributes.html

[6] WebGL indexed vertices. WebGL Indexed Vertices. (n.d.). Retrieved April 20, 2023, from https://webglfundamentals.org/webgl/lessons/webgl-indexed-vertices.html

--

--