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.