thi.ng/shadergraph

WebGL/GLSL shader library & dependency graph for ClojureScript



Documentation

thi.ng/shadergraph

Contents

About the project

Overview

This project provides a function-oriented approach to composing complex shaders. The library contains:

  • Dozens of pure GLSL functions, generally useful and often used in GLSL shaders for both OpenGL & WebGL projects (both Clojure & Clojurescript).
  • Automatic Clojure metadata generation for GLSL functions (incl. arg lists) to help with documentation in the REPL or for custom tool development.
  • A generic transitive dependency graph resolution mechanism for any shader functions defined (not just in this library).
  • An optional basic shader minifier (without name mangling)
  • GLSL source code can be specified as strings or read from files/resources

Several shader functions have been gathered from literature and other projects and been partially refactored as pure functions.

Leiningen coordinates

Latest stable

[thi.ng/shadergraph "0.2.0"]

Latest snapshot

[thi.ng/shadergraph "0.3.0-SNAPSHOT"]

Status

STABLE, in active development.

Usage example

The brief example below defines a shader pair with standard Lambert lighting. The defglsl macro is used to define a GLSL sourcecode snippet with (optional) dependencies. It also minifies the original sources at compile time (all snippets provided by this library are minified this way).

(ns example
  (:require
   [thi.ng.glsl.core :as glsl :include-macros true]
   [thi.ng.glsl.vertex]
   [thi.ng.glsl.lighting]))

(glsl/defglsl my-vertex-shader
  [thi.ng.glsl.vertex/mvp
   thi.ng.glsl.vertex/surface-normal]
  "void main() {
     vNormal = surfaceNormal(normal, normalMat);
     gl_Position = mvp(position, model, view, proj);
   }")

(glsl/defglsl my-fragment-shader
  [thi.ng.glsl.lighting/lambert]
  "void main() {
     float lam = lambert(normalize(vNormal), normalize(lightDir));
     gl_FragColor = vec4(ambientCol + diffuseCol * lightCol * lam, 1.0);
   }")

Apart from the defglsl macro, the assemble function is the other main tool in this library. It takes a map of GLSL dependencies and sourcecode and returns a transformed/expanded source with all transitive deps injected in the correct order. The my-vertex-shader spec above in assembled form will expand to:

(glsl/assemble my-vertex-shader)
;; "vec3 surfaceNormal(vec3 normal,mat4 normalMat){return normalize((normalMat*vec4(normal,.0)).xyz);}vec4 mvp(vec3 pos,mat4 model,mat4 view,mat4 proj){return proj*view*model*vec4(pos,1.);}void main(){vNormal=surfaceNormal(normal,normalMat);gl_Position=mvp(position,model,view,proj);}"

Using another library like, e.g. thi.ng/geom ‘s GL module, these shader sources can then be combined into a fully defined shader like this (the make-shader-from-spec function used below generates all attribute, varying & uniform definitions and setters automatically and returns a compiled shader program):

(require '[thi.ng.geom.gl.shaders :as shaders])

(def shader
  (shaders/make-shader-from-spec
   my-gl-context
   {:vs (glsl/assemble my-vertex-shader)
    :fs (glsl/assemble my-fragment-shader)
    :uniforms {:model      :mat4
               :view       :mat4
               :proj       :mat4
               :normalMat  :mat4
               :ambientCol :vec3
               :diffuseCol :vec3
               :lightCol   :vec3
               :lightDir   :vec3}
    :attribs  {:position   :vec3
               :normal     :vec3}
    :varying  {:vNormal    :vec3}}))

Module namespaces

Tests

Building & testing this project

Project definition

Building & testing this project

This project is written in a literate programming format and requires Emacs & Org-mode to generate usable source code. Assuming both tools are installed, the easiest way to generate a working project is via command line (make sure emacs is on your path or else edit its path in tangle.sh):

git clone https://github.com/thi-ng/shadergraph.git
cd shadergraph

# tangle selected files
./tangle.sh README.org src/*.org test/*.org

# ...or individual file
./tangle.sh src/core.org

Tangling is the process of extracting & combining source blocks from .org files into an actual working project/source tree.

Once tangling is complete, you can cd into the generated project directory (babel in this case) and then use lein as usual.

Testing

The project.clj file define an alias to trigger a complete build & tests for both CLJ & CLJS versions.

cd babel
lein cleantest # some tests currently fail due to still missing protocol impls

To only build the Clojurescript version simply run lein cljsbuild test from the same directory. A small HTML harness for the resulting JS file is also located in that folder (babel/index.html), allowing for further experimentation in the browser.

Working with the REPL

Editing code blocks / files in Org-mode, then re-loading & testing changes is quite trivial. Simply launch a REPL (via lein or Emacs) as usual. Everytime you’ve made changes to an .org file, re-tangle it from Emacs or tangle.sh, then reload the namespace in the REPL via (require 'thi.ng.glsl... :reload) or similar.

Injected properties

Dependencies

Runtime

Clojure

[org.clojure/clojure "1.8.0"]

ClojureScript

[org.clojure/clojurescript "1.8.40"]

Dependency graph

[com.postspectacular/dependency "0.1.2"]

Development

clojurescript.test

[com.cemerick/clojurescript.test "0.3.3"]

Cljsbuild

[lein-cljsbuild "1.1.3"]

Leiningen project file

(defproject <<project-name>> "<<version>>"
  :description  "WebGL/GLSL shader library & dependency graph for ClojureScript"
  :url          "<<project-url>>"
  :license      {:name "Apache Software License"
                 :url "http://www.apache.org/licenses/LICENSE-2.0"
                 :distribution :repo}
  :scm          {:name "git"
                 :url  "<<project-url>>"}

  :min-lein-version "2.4.0"

  :dependencies [<<dep-clj>>
                 <<dep-cljs>>
                 <<dep-dep>>]

  :profiles {:dev {:plugins [<<dep-cljsbuild>>
                             <<dep-cljs-test>>]
                   :aliases {"cleantest" ["do" "clean," "cljsbuild" "test"]}}}

  :cljsbuild {:builds [{:source-paths ["src" "test"]
                        :id "simple"
                        :compiler {:output-to "<<cljs-artefact-path>>"
                                   :optimizations :whitespace
                                   :pretty-print true}}
                       {:source-paths ["src" "test"]
                        :id "prod"
                        :compiler {:output-to "<<cljs-artefact-path>>"
                                   :optimizations :advanced
                                   :pretty-print false}}]
              :test-commands {"unit-tests" ["phantomjs" :runner "<<cljs-artefact-path>>"]}}

  :pom-addition [:developers [:developer
                              [:name "Karsten Schmidt"]
                              [:url "http://postspectacular.com"]
                              [:timezone "0"]]])

ClojureScript HTML harness

<!DOCTYPE html>
<html lang="en">
  <head>
    <title><<project-name>> <<version>> test</title>
  </head>
  <body>
    <canvas id="main" width="640" height="480"></canvas>
    <script type="text/javascript" src="<<cljs-artefact-path>>"></script>
  </body>
</html>

Accessing library version during runtime

The autogenerated namespace thi.ng.glsl.version contains a single symbol version holding the version string defined above:

(use 'thi.ng.glsl.version)

(prn version)
; "<<version>>"

Version namespace

(ns thi.ng.glsl.version)
(def version "<<version>>")

Release history

Version Released Description Lein coordinates Tagged Github URL
0.3.0 n/a CLJC refactor, metadata, ns additions [thi.ng/shadergraph "0.3.0-SNAPSHOT"]
0.2.0 2016-03-20 bugfix minify, update fn naming [thi.ng/shadergraph "0.2.0"] 0.2.0
0.1.1 2015-04-27 bugfix assemble [thi.ng/shadergraph "0.1.1"] 0.1.1
0.1.0 2015-02-25 1st release [thi.ng/shadergraph "0.1.0"] 0.1.0

Contributors

Name Role Website
Karsten Schmidt initiator & principal developer postspectacular.com
thi.ng
Tristan Strange bugfixes @triss