All shader functions in this library are defined via simple maps stored in vars, each grouping source code and dependencies to other GLSL functions/snippets. The example spec below defines a new function with two dependencies.
Note:
- Since GLSL snippets are stored in normal ClojureScript vars,
namespaces of dependent GLSL snippets need to be
required
as usual. - It’s recommended, though optional, to use the
defglsl
,glsl-spec
orminified
macros for these GLSL source snippets to reduce boilerplate & final JS file size (see macro section below for more details). - This library is written using Emacs ORG-mode and all GLSL snippets here are defined using their own code blocks, which makes development easier.
(ns example
(:require
[thi.ng.glsl.core :include-macros true :refer-macros [defglsl minified]]
[other.namespace :as other]
[another.namespace :as another]))
(defglsl my-spec
[other/spec another/spec]
"vec3 myFunc(vec3 a, vec3 b) {
return ...;
}")
;; alternatively the same spec *without* using the defglsl macro...
(def my-spec
(glsl/glsl-spec-plain
[other/spec another/spec]
"vec3 myFunc(vec3 a, vec3 b) {
return ...;
}"))
;; ...or...
(def my-spec
{:deps [other/spec another/spec]
:src "vec3 myFunc(vec3 a, vec3 b) {
return ...;
}"})
In order to correctly concatenate the various shader source snippets,
a dependency graph is computed during assembly using Stuart Sierra’s
dependency library. The assemble
function below receives a spec and
returns an expanded GLSL source string with all dependencies in the
correct order. Note that cyclic dependencies are not supported.
(defn- build-graph
([spec] (build-graph (dep/graph) spec))
([g curr]
(reduce
(fn [g d] (build-graph (dep/depend g curr d) d))
g (:deps curr))))
(defn assemble
[spec]
(if (seq (:deps spec))
(->> spec
(build-graph)
(dep/topo-sort)
(mapv :src)
(apply str))
(:src spec)))
(defn glsl-spec-plain
[deps src] {:deps deps :src src})
So far the minifier only performs the following operations:
- comment removal (both single & multi line)
- whitespace removal
(defn- minify-floats
[src]
(str/replace
src #"(\d+)\.(\d+)"
(fn [[o d f]]
(cond
(= "0" d) (str \. f)
(every? #{\0} f) (str d \.)
:else o))))
(defn- minify-line
[src]
(let [src (-> src
(minify-floats)
(str/replace #"\s{2,}|\t" "")
(str/replace #"\s*(\{|\}|\=|\*|\,|\+|/|\>|\<|\&|\||\[|\]|\(|\)|\-|\!|\;)\s*" "$1"))]
(if (= \# (first src))
(str "\n" src "\n")
src)))
(defn- clean-line-breaks
[src] (str/replace src #"\n{2,}" "\n"))
(defn minify
[src]
(let [src (-> src
(str/replace #"//.*" "")
(str/replace #"/\*[\s\S]*?\*/" "")
(str/replace #"^\n+" ""))]
(->> (str/split src #"\n")
(map minify-line)
(apply str)
(clean-line-breaks))))
In addition to defining GLSL source code as Clojure strings, the
library also allows to read source from files via the defglsl-file
and glsl-file-spec
macros. For example, the following spec pulls in
the source from the file resources/glsl/foo.glsl
in the current
project dir.
(defglsl-file foo [dep1 dep2] "resources/glsl/foo.glsl")
#?(:clj
(defmacro minified
[src] `~(minify src)))
#?(:clj
(defmacro glsl-spec
[deps src] `{:deps ~deps :src (minified ~src)}))
#?(:clj
(defmacro glsl-file-spec
[deps path] `{:deps ~deps :src (minified ~(slurp path))}))
#?(:clj
(defmacro defglsl
[name deps src]
`(do (def ~name (glsl-spec ~deps ~src))
(alter-meta! (var ~name) merge ~(extract-glsl-meta src)))))
#?(:clj
(defmacro defglsl-file
[name deps path]
`(do (def ~name (glsl-file-spec ~deps ~path))
(alter-meta! (var ~name) merge ~(extract-glsl-meta (get name :src))))))
To improve the user experience and ease the development of custom
tooling (e.g. shader editors), all shader specs created via the
defglsl
or defglsl-file
macros attach GLSL metadata to their
generated Clojue vars. This metadata can then be retrieved via
Clojure’s meta
fn, like this:
(meta #'thi.ng.glsl.lighting/skylight-color)
;; {:line 165,
;; :column 1,
;; :file "thi/ng/glsl/lighting.cljc",
;; :name skylight-color,
;; :ns #object[clojure.lang.Namespace 0x5ded04ed "thi.ng.glsl.lighting"],
;; :glsl-return "vec3",
;; :glsl-name "skylightColor",
;; :glsl-args ["vec3 col1" "vec3 col2" "float height"]}
(def ^:private re-meta
#"((((highp|mediump|lowp)\s+)?(void|bool|float|int|(b|i)?vec\d|mat\d))\s+([\w_]+)\s*\(([A-Za-z0-9_, ]*)\)\s*\{)")
(defn extract-glsl-meta
"Attempts to extract function name, return type and args from given
GLSL source string. If successful, returns map w/ these
keys: :glsl-name :glsl-return :glsl-args"
[src]
(if-let [m (first (re-seq re-meta src))]
(let [return (nth m 2)
name (nth m 7)
args (vec (filter seq (str/split (nth m 8) #",\s*")))]
{:glsl-return return
:glsl-name name
:glsl-args args})))
(ns thi.ng.glsl.core
(:require
[clojure.string :as str]
[com.stuartsierra.dependency :as dep]))
<<minify>>
<<meta>>
<<macros>>
<<deps>>