2D colored Triangle in Elm with WebGL

2D colored box

For some time, I’ve been experimenting with WebGL and Elm lang, for my job and my personal projects.

Today I’ll show a typical graphic example: a 2D colored box (two triangles).

Packages required

  • elm-lang/core
  • elm-lang/html
  • elm-community/linear-algebra
  • elm-community/webgl

The linear-algebra package is a dependency required from WebGL package.

I’ll use the latest WebGL package (+2.0.0) announced here. In this version the package provides us an abstraction for an Entity in WebGL that specifies a full rendering pipeline to be run on the GPU.

This diagram illustrates the entire pipeline (source)

Triangles, Triangles, Triangles

Triangles are the most popular 3D drawing primitive because any 3 points in the 3D space are the vertices of a triangle.

Therefore to draw a box we will need two triangles, just like this:

Triangles forming a box

We can to see above two triangles (A and B) and their vertices. The marked vertices are shared between the two triangles (same coordinates).

Clipspace

In WebGL the data is typically uploaded to GPU with its own coordinate system and then in the vertex Shader this coordinates are transformed into a different coordinate system known as clipspace. All data outside from clipspace will not be rendered.

In the next picture we can to see how the clipspace is defined:

Clipspace (source)

The coordinates are normalized between -1 and 1.

Our mesh Box

Now, we going to define the mesh for our box in WebGL with Elm.

Mesh forms geometry from the specified vertices. A mesh is defined as a type union:

type Mesh attributes

“attributes” is a generic type (defined as a record) which represents all information that contains a vertex.

For our example we are going to define a record that stores the position and a color of vertex:

type alias Vertex =
{ position : Vec2
, color : Vec3
}

Position is a 2D vector (in our vertex shader let’s keep the coordinate z in 0) and the color is defined as 3D vector for the three channels of RGB.

Note: Vec3 and Vec2 are types from linear-algebra package.

Now, let’s to set our two triangles, using the triangles function from WebGL package, which is defined as:

triangles 
: List (attributes, attributes, attributes)
-> Mesh attributes

Our attributes type is the “type alias” Vertex defined above, therefore let’s set the two triangles that defines our box:

boxMesh : Mesh Vertex
boxMesh =
GL.triangles
[ ( (Vertex (vec2 -1 1) (vec3 1 0 0))
, (Vertex (vec2 1 1) (vec3 0 1 0))
, (Vertex (vec2 -1 -1) (vec3 0 0 1))
)
, ( (Vertex (vec2 -1 -1) (vec3 1 0 0))
, (Vertex (vec2 1 -1) (vec3 0 1 0))
, (Vertex (vec2 1 1) (vec3 0 0 1))
)
]

We define a vertex as follows:

Vertex (vec2 -1 1) (vec3 1 0 0)

The first argument is the position and the second is the color. In this case, we have define a vertex in position (-1, 1) and red color.

Important: we have put our data in the clipspace coordinate system directly. Normally the data has some coordinate system and is then transformed by use a matrix.

Our triangles whit positions and colors

In the last image, we are seeing two triangles and their vertex information. The result is a box filling clipspace.

Shaders

A shader is a program that tells a computer how to draw something in a specific and unique way. (source)

Generally we handle two types of shaders:

  • Vertex shader: its purpose is transforms each vertex’s 3D position to the 2D coordinate at which it appears on the screen. You can manipulate properties such as position, color and texture coordinate but you cannot create new vertices.
  • Fragment shader: also known as Pixel shader , compute color and other attributes of each “fragment” or pixel. Fragment shaders give you total control over the pixels rendered on the screen.

The shaders are written in a language called GLSL.

Vertex and Fragment shaders

Shaders in Elm

In the WebGL package in Elm, a shader is defined as follows:

type Shader attributes uniforms varyings

Where attributes, uniforms and varyings are generic types defined by a elm record. These types are used to send information:

  • Attribute: These variables represent a particular vertex in our mesh.
  • Uniform: These are global read-only variables that can be use in the vertex and fragment shaders.
  • Varying: These are variables you can write in your vertex shader which then is passed to the fragment shader, where they are read-only.

Note: A vertex shader can have attributes, uniforms and varyings and a fragment shader can only have uniforms and varyings.

Vertex shader

Let’s go to define our vertex shader, just like this:

vertexShader : Shader Vertex {} Varying
vertexShader =
[glsl|
precision mediump float;
attribute vec2 position;
attribute vec3 color;
varying vec3 vColor;
void main () {
gl_Position = vec4(position, 0.0, 1.0);
vColor = color;
}
|]

gl_Position expects a 4D vector. Three coordinates for the position into the clipspace and an extra coordinate that we will not explain in this post (by default we will set it to 1).

The first two coordinates correspond to the position of the vertex, and the third coordinate (z) we will put it to 0, because it’s a 2D program).

In this vertex shader we are also saving the color of vertex into vColor varying.

How does Elm WebGL pass variables to the shader?

In our vertex shader, the type annotation can also be written as follows:

vertexShader 
: Shader { position : Vec2, color : Vec3 } {} { vColor : Vec3 }

In GLSL, {position : Vec2, color : Vec3} is transformed into:

attribute vec2 position;
attribute vec3 color;

The same happens with the Varying and Uniform.

Fragment shader

It’s time for the fragment shader:

fragmentShader : Shader {} {} Varying
fragmentShader =
[glsl|
precision mediump float;
varying vec3 vColor;
void main () {
gl_FragColor = vec4(vColor, 1.);
}
|]

gl_FragColor expects a 4D vector corresponding to the four RGBA channels.

In our shader each fragment simply receives the interpolated color based on its position relative to the vertices.

Note: In Elm, when we write ‘{}’ means that we do not expect to receive that value. In this case, we do not expect Uniforms or Attributes.

Putting it all together

We already have a mesh and shaders. Now we are going to encapsulate all into one Entity.

In Elm WebGL we can to define an Entity by using entity function:

entity 
: Shader attributes uniforms varyings -- vertexShader
-> Shader { } uniforms varyings -- fragmentShader
-> Mesh attributes -- our box mesh
-> uniforms -- we haven't uniforms
-> Entity

Therefore we create our Entity like this:

boxEntity : Entity
boxEntity =
GL.entity
vertexShader
fragmentShader
boxMesh
{}

Rendering the scene

Now we render the scene (Entity set). The function to render the scene is called toHtml. The type annotation for this function is:

toHtml : List (Attribute msg) -> List Entity -> Html msg

Then, let’s to use as follows:

view : {} -> Html msg
view _ =
GL.toHtml
[]
[ boxEntity ]

We are going to create our main function and then we can to see the result:

main : Program Never {} {}
main =
Html.beginnerProgram
{ model = {} -- I have not model
, view = view
, update = (\_ _ -> {}) -- I update nothing
}

Final ideas

I’ve spent a lot of time working with elm-community/webgl and it’s a great package. I keep learning more every day about WebGL and Elm.

I hope this example will help you start with WebGL and Elm.

You can find all code in this Github repository.

If you liked this, click the 💚 below so other people will see this here on Medium.

My Twitter: @nithstong