home top

LZ

Table of Contents

WebGL in ClojureScript - part one

This is a tutorial in using webGL in ClojureScript, mainly for my own reference as I go through it, but if it helps someone else then that's fab too.

What is it?

WebGL a graphics rendering API for JavaScript. It it based on OpenGL and it is GPU accelerated. Like OpenGL, webGL code is split between the shader code, which is generally run on the GPU and written in a dialect of C called GLSL, and the orchestration code. Below is a hello world-y basic project setup.

Getting set up

You have some CLJS project set up. I reccomend you have a running browser REPL connected to your editor so that you can have all the live coding, fast-feedback goodness of that.

Canvas

We need a canvas element in the body of the html document. Give it an id so we can grab it. I called it app out of habit.

<canvas id="app"></canvas>

Don't worry about the width and height for now. In clojure we are going to grab that and get a webgl context. We are just going to assume this is on a modern browser that supports it.

(def c (js/document.getElementById "app"))
(def gl (.getContext c "webgl"))  

Setting the dimensions

let's make a function to reset dimensions of both the canvas itself and the webgl context. Everything will be full window. We can call it whenever the window is resized.

(defn reset-dimensions []
  (let [width js/window.innerWidth
        height js/window.innerHeight]
    (g/set c "width" width)
    (g/set c "height" height)
    (.viewport gl 0 0 width height)))

Okay let's stick it in a -main:

(defn -main []
  (reset-dimensions)
  (.addEventListener js/window "resize" reset-dimensions))

Shader program

We will be making a vertex shader and a fragment shader. These two will be combined together into a program. We'll get into what these mean shortly. First we are going to make a convenience function that we can use to compile our shaders:

(defn create-shader [shader-type source]
  (let [shader (.createShader gl shader-type)]
    (.shaderSource gl shader source)
    (.compileShader gl shader)
    (js/console.log
     (str "compile shader: "
          (.getShaderParameter
           gl shader (g/get gl "COMPILE_STATUS"))))
    shader))

Vertex Shader

Our vertex shader's job is to set the the value of gl_position for each vertex that we want to draw. This one is basically just a pass-through, it doesn't do anything other than read an attribute and set gl_position with it.

We will just write the GLSL in a string. Could fetch it from a local file. This feels like less faff.

(def vert-shader-source
  "
attribute vec4 a_position; 

void main() {
  gl_Position = a_position;
}
")

(defn vert-shader []
  (create-shader (g/get gl "VERTEX_SHADER")
                 vert-shader-source))

Fragment Shader

The fragment shader sets the colour at each vertex. The space on a face in between the verticies will be interpolated between the vertex colours. In our case we are just going to set it to a fixed colour (r, g, b, a).

(def frag-shader-source
  "
precision mediump float;

void main() {
  gl_FragColor = vec4(1, 0, 0.5, 1);
}
")

(defn frag-shader []
  (create-shader
   (g/get gl "FRAGMENT_SHADER")
   frag-shader-source))

Program

Now we build a program from our shaders.

(defn create-prog [vert frag]
  (let [program (.createProgram gl)]
    (.attachShader gl program vert)
    (.attachShader gl program frag)
    (.linkProgram gl program)

    (js/console.log
     (str "link status: "
          (.getProgramParameter
           gl
           program
           (g/get gl "LINK_STATUS"))))

    ;; return
    program))

We can now add the program to the -main function:

(defn -main []
  (.addEventListener js/window
                     "resize"
                     reset-dimensions)
  (let [vs (vert-shader)
        fs (frag-shader)
        prog (create-prog vs fs)]
    (reset-dimensions)))

We are going to draw a triangle. To do this we need some verticies. x and y coordinates go from 1 (top and left) to -1 (bottom and right). Let's add an array of 3 verticies x and then y coordinates, like so:

(defn -main []
  (.addEventListener js/window
                     "resize"
                     reset-dimensions)
  (let [vs (vert-shader)
        fs (frag-shader)
        prog (create-prog vs fs)
        points [0 0 0 0.5 0.7 1]]
    (reset-dimensions)))

We need these points to go to the a_position attribute that we added to the vertex shader above. To do that we need an object to store the memory, this is called a buffer. We then need to bind this buffer toa target called ARRAY_BUFFER, used for vertex data. Once we have bound we will buffer the data. STATIC_DRAW is a hint to the compiler for optimizations based on what we are trying to do. Let's add some more helper functions to take care of this. We also want a function to clear the canvas.

(defn bind-pos-buff [pos-buff]
  (.bindBuffer gl
               (g/get gl "ARRAY_BUFFER")
               pos-buff))

(defn buffer-static-draw [points]
  (.bufferData gl
               (g/get gl "ARRAY_BUFFER")
               (js/Float32Array. points)
               (g/get gl "STATIC_DRAW")))

(defn set-vertex-attrib-pointer [pos-attr-loc]
  (let [size 2
        dtype (g/get gl "FLOAT")
        normalize? false
        stride 0
        offset 0]
    (.vertexAttribPointer gl
                          pos-attr-loc
                          size
                          dtype
                          normalize?
                          stride
                          offset)))

(defn draw-triangles []
  (let [prim-type (g/get gl "TRIANGLES")
        offset 0
        cnt 3]
    (.drawArrays gl prim-type offset cnt)))

(defn clear []
  (.clearColor gl 0 0 0 1)
  (.clear gl (g/get gl "COLOR_BUFFER_BIT")))

Let's call these from our -main fn.

(defn -main []
  (.addEventListener js/window
                     "resize"
                     reset-dimensions)
  (let [vs (vert-shader)
        fs (frag-shader)
        prog (create-prog vs fs)
        pos-attr-loc (.getAttribLocation
                      gl
                      prog
                      "a_position")
        pos-buff (.createBuffer gl)
        points [0 0 0 0.5 0.7 1.0]]   
    (reset-dimensions)
    (clear)
    (bind-pos-buff pos-buff)    ;; point pos-buff at ARRAY_BUFFER   
    (buffer-static-draw points) ;; buffer the data
    (.useProgram gl prog)       ;; envoke our shader program
    (.enableVertexAttribArray gl pos-attr-loc) ;; enable a_position attribute
    (set-vertex-attrib-pointer pos-attr-loc)   ;; tell webgl how to handle the points data
    (draw-triangles)))

This should give you a pink triangle.