Skip to content
Alister Lee edited this page Aug 17, 2015 · 10 revisions
GLSL represented as Clojure maps. Generate maps with constructor functions.

The GLSL AST is represented as Clojure maps with certain keys.

Instead of entering the maps directly, use the constructor functions provided in gamma.api:

(require '[gamma.api :as g])
(g/sin 1)
=> {:tag :term, :head :sin, :id {:tag :id, :id 1}, :type :float,
      :body ({:tag :term, :head :literal, :value 1, :type :float, :id {:tag :id, :id 2}})}

Each GLSL operator, function, or type constructor has an equivalent function in gamma.api.

GLSL Input/Ouput variables are also maps with constructor functions.

The different species of GLSL input/output variables also have constructors:

;; attribute 
(g/attribute "a_Attr" :float)
=> {:tag :variable, :name "a_Attr", :type :float, :storage :attribute}
;; uniform 
(g/uniform "u_Uniform" :mat4)
=> {:tag :variable, :name "u_Uniform", :type :mat4, :storage :uniform}
;; varying 
(g/varying "v_Varying" :float :highp)
=> {:tag :variable, :name "v_Varying", :type :float, :storage :varying, :precision :highp} 

;; bult-in variables
(g/gl-position)
=> {:tag :variable, :name "gl_Position", :type :vec4}
(g/gl-frag-color)
=> {:tag :variable, :name "gl_FragColor", :type :vec4}
Compose constructor functions to buld the AST

Building the AST is just a matter of composing constructor functions, resulting in nested maps:

(g/clamp (g/sin 1.0) 0.25 0.5)
=> {:tag :term, 
    :head :clamp, 
    :body ({:tag :term, 
            :head :sin, 
            :body ({:tag :term, 
                    :head :literal, 
                    :value 1, 
                    :type :float, 
                    :id {:tag :id, :id 3}}), 
            :id {:tag :id, :id 2}, 
            :type :float} 
           {:tag :term, 
            :head :literal, 
            :value 0.25, 
            :type :float, 
            :id {:tag :id, :id 5}} 
           {:tag :term, 
            :head :literal, 
            :value 0.5, 
            :type :float, 
            :id {:tag :id, :id 6}}), 
    :id {:tag :id, :id 4}, 
    :type :float}

To refer to a input variable with the AST, simply create it and pass it to an AST constuctor:

(g/sin (g/attribute "a_Attr" :float))

If's are expressions, so we can nest if's inside of other expressions:

(g/sin (g/if (g/attribute "b_Bool" :bool) 1 2))
Use Clojure's binding forms

To reuse an expression in multiple places, use let, or any other binding form:

(let [x (g/sin (g/attribute "a_Attr" :float))]
  (g/vec3 x x x))
  
;; equivalent to 
(g/vec3 
  (g/sin (g/attribute "a_Attr" :float))
  (g/sin (g/attribute "a_Attr" :float))
  (g/sin (g/attribute "a_Attr" :float)))

Gamma's compiler will ensure that the (g/sin (g/attribute "a_Attr" :float)) expression will only be evaluated once. This frees you from having to think about intermediary variables within the AST and their impact on performance.

In general, Gamma disallows use of GLSL AST's binding forms. You never directly create assignments in GLSL yourself; use Clojure's binding to feed values where needed, and the compiler will insert an assignment to eliminate duplication. This restriction buys us an important property: referential transparency. This property is what allows easy metaprogramming and full use of Clojurescript's facilities.

Types are checked and inferred by constructor functions

Constructor functions typecheck their arguments and infer their own types:

(:type (g/sin 1.0))
=> :float
(:type (g/sin (g/vec3 0.0 0.0 1.0)))
=> :vec3

Passing the wrong type results in an exception:

(g/sin true)
=> Error: Wrong argument types for term sin: :bool

This is useful for debugging. Your code can also dispatch based on the GLSL type of the AST.

Factor your AST with functions and datastructures

It doesn't really matter how the AST comes together, just flow data to where it is needed.

We can create AST, put in in some datastructure, and write logic to flow it to a destination:

;; create some AST fragments and hang on to them
(def x {:partA (g/sin 1) :partB (g/cos 1)})
;; get AST fragments and put them where we want
(g/clamp (:partA x) 0 (:partB x))

Functions are an even more powerful abstraction. Use functions to factor out or parameterize subtrees:

;; start with 
(g/+ 1 (g/+ 2 3))

;; create helper
(defn my-helper [x] (g/+ x 3))

;; refactor tree using helper:
(g/+ 1 (my-helper 2))

Metaprogramming GLSL with higher-order functions:

(reduce g/+ 0 [1 2 3 4])

(apply g/vec4 (map #(g/clamp % 0 1) [0 0.5 1 2]))

Feel free to use whatever abstractions you want for building up the tree. Just remember that GLSL is a typed language, and its functions and operations have type signatures that need to be respected.

Create abstractions

For example, Gamma does not provide a for loop, since WebGL only supports unrollable for-loops.

Using Clojurescript, it is trivial to unroll loops ourselves:

(defn map-over-vec4 [f v]
 (apply g/vec4 (for [i (range 4)] (f (g/part v i)))))

A third party is free to develop a general-purpose library to cover common iteration pattens.