Home

Awesome

cljsl

DISCLAIMER: CLJSL is pre-alpha software, and is subject to change in future. This document gives an introduction to using the current version of it at time of writing, but may become out of date.

CLJSL (the Clojure Shader Language) is a library for compiling a subset of Clojure code to compliant GLSL code for use with OpenGL, OpenGL ES, Vulkan (via a GLSL->SPIRV compiler), and WebGL. It provides facilities beyond simple translation of code as well, including dependency tracking, namespacing, and macros. CLJSL code is writtin as normal code inside of your existing Clojure namespaces, no resource files or similar are needed.

At the current stage, CLJSL is simplistic and does not emit errors from bad code to be compiled, instead leaving those errors to be picked up by the GLSL compiler. For example, CLJSL does not emit an error when using if in a part of the code besides a statement, but it does not support full if expressions (although a limited ternary ? operator is provided). Errors in usage of CLJSL functions and macros, however, are reported to the user via specs on macros and exceptions when calling compilation functions directly.

Installation

As CLJSL is pre-alpha software, no Clojars release has been made. You can depend on it as a git dependency when using the Clojure CLI, or you can manually build a jar and install it to your local maven repository (although CLJSL currently provides no build process for jars).

Usage

CLJSL provides a set of macros which can be used to build shaders. Each one registers a var that tracks which others it depends on to be included in the final shader source. To begin with, we'll make a simple pass-through vertex shader.

(require '[cljsl.compiler :as sl])

(sl/defparam vertex-position "vec3"
  :layout {:location 0})

(sl/defparam vertex-color "vec3"
  :layout {:location 1})

(sl/defparam geometry-color "vec3")

(sl/defshader vertex-shader
  {vertex-position :in
   vertex-color :in
   geometry-color :out}
  (set! gl_Position (vec4 vertex-position (float 1)))
  (set! geometry-color vertex-color))

To start with, we require the CLJSL compiler, and then we define the shader parameters. These are variables which can be used across different shaders to pass values between them.

The parameters vertex-position and vertex-color are given layout maps which specify a location because we will use them to get the per-vertex data from the application.

The shader itself includes a map to specify which parameters it will be using, as well as whether they will be used as input or output parameters for this shader. This is specified at the shader level because multiple shaders may refer to the same parameters with a different choice of input vs output, as will be shown shortly. Next it uses the set! operator to set the values of gl_Position, a variable provided by GLSL, to the return value of a call to vec4. When a symbol appears in the body of a shader that will be compiled and does not resolve to anything in the current namespace, it is assumed to be a GLSL builtin function or variable and will be interpolated into the resulting source code without modification.

You can get the source of the shader from the resulting var by looking at the ::sl/source key.

user> (println (::sl/source vertex-shader))
#version 400 core






    out vec3 NS_user_SYM_geometry_color ;
  layout(location=1)  in vec3 NS_user_SYM_vertex_color ;
  layout(location=0)  in vec3 NS_user_SYM_vertex_position ;






void main()
{
gl_Position=vec4(NS_user_SYM_vertex_position, 1.0f);
NS_user_SYM_geometry_color=NS_user_SYM_vertex_color;
}

;; => nil

You can ignore the extraneous whitespace, it's used to separate several sections which aren't included in this shader.

This example shows that the resulting identifiers are namespaced. This means you can define parameters, shaders, shader functions, uniforms, etc., all in different namespaces, and the shader will correctly identify them and include them in the code, even if the unqualified names of the vars conflict.

In addition, it shows that the GLSL version and extensions are included at the top of the shader file in the #version directive. You can change this to support OpenGL ES or other versions by adding metadata to your var on the :version and :extensions keys. The :version is a number, and :extensions is a list of strings.

To complete this simple pipeline, a vertex shader is produced below.

(sl/defparam fragment-color "vec4"
  :layout {:location 0})

(sl/defshader fragment-shader
  {geometry-color :in
   fragment-color :out}
  (set! fragment-color (vec4 geometry-color (float 1))))

This simple shader passes the input color directly to the output without modification, and sends this to the FBO color attachment 0.

In addition to parameters, uniforms are supported.

(sl/defuniform model-view-projection-matrix "mat4")

(sl/defshader camera-vertex-shader
  {vertex-position :in}
  (set! gl_Position (* model-view-projection-matrix (vec4 vertex-position (float 1)))))

When you set this uniform in the shader object, you can identify the uniform name to OpenGL or other querying mechanisms by using sl/sym->ident.

user> (sl/sym->ident `model-view-projection-matrix)
;; => "NS_user_SYM_model_view_projection_matrix"

CLJSL also supports basic flow control operators, if, cond, let, return, ternary (?), etc., as well as math operations, and n-ary comparators (where (< a b c) compiles to a < b && b < c). Casting is also supported in the form (cast value type).

In addition, support for for loops is included, with the following syntax:

(for [x 10 (incf x)]
     (> x 0)
  (do stuff))

The operators incf and decf are provided for post-increment, and post-decrement, respectively. Pre-* variants are provided with a *, i.e. incf*.

Functions can be defined with defshaderfn, and must have type hinted arguments and return.

(sl/defshaderfn square
  ^"float" [^"float" x]
  (return (* x x)))

No type-hinted return type is interpreted as a void return type, and explicit returns via return must be used.

Structs can also be constructed with defstruct.

(sl/defstruct directional-light
  {:strength "float"
   :direction "vec3"})

This can be used with type hints.

(sl/defshaderfn calc-light
  ^"float" [^directional-light light ^"vec3" normal]
  (return (* (dot (- (:direction light)) normal) strength)))

Additionally, regular clojure macros can be used, as long as they produce valid CLJSL code.

(defmacro square-macro-unsafe
  [x]
  `(* ~x ~x))

With the above macro, we can then compile the code using the compile function, passing it the form to compile and a map including the local variables (in this case just to prevent compile from attempting to resolve x).

user> (println (first (sl/compile '(square-macro-unsafe x) {'x 5})))
(x*x)
;; => nil

Since these macros are written in Clojure and not CLJSL, they have the full power of the language and the normal language semantics.

In addition, there are several functions provided for compiling CLJSL code. Their usage will not be explained here, but they are the basis on which the macros work, and provide ways to build your own macros if their syntax or semantics are unsatisfactory for your application.

License

Copyright © 2021 Joshua Suskalo

Distributed under the Eclipse Public License version 1.0.