i have pet chickens, i make drawings, and i write software of questionable usefulness. that's pretty much the extent of my personality. ask me about array programming, decker, or anything, really.


Anonymous Guest asked:

how did you learn enough glsl to write a compiler for it, and more importantly, what the fuck how does special-k/cathedral work please give a line-by-line explanation

To provide context for readers, Special-K is a transpiler I wrote which converts a (small) subset of the K programming language into a (large) subset of the OpenGL ES Shader Language, which is supported by most reasonably modern web browsers. K is a vector-oriented programming language I've worked with professionally at a few companies, and I thought it offered a much nicer and more compact notation for fragment shaders than the C-like syntax of GLSL.

One of the sample programs I include with Special-K is cathedral, which, as I note in the sample, I adapted from a shader program originally designed by the Twitter user Yosshin4004. My version looks a little less fancy, because the WebGL flavor of GLSL doesn't (or at least didn't at the time) support bitwise operations, but it's still pretty neat:

cathedral


It is perhaps not a very satisfying answer, but I learned GLSL by reading the reference manual I linked above. I was familiar with the general concepts of fragment shaders- parallel programs which yield a color for every pixel of a region of the screen- and I had done some prior experiments with signed distance fields in iKe, a web environment I built which supports a much richer dialect of K.

K is a dynamically typed language, while GLSL requires explicit, static type declarations, so a large portion of the Speciak-K compiler is the whole-program type-inference system. The entire compiler is about 400 lines of JavaScript, and in a skim you'll definitely notice the lookup tables describing types for all the GLSL intrinsic functions.


The cathedral example in its entirety looks like this:

t: frame*.005
d: (-.5+pos.xy%size),.8
q: 0 0 0
p: (0;(sin t*12)%200;t){
  x+:d*(.65-#fract[x+.5]-.5)&x.y+.2
  q:$[y~50;x       ;q]
  x:$[y~50;x-d*.01 ;x]
  d:$[y~50;.7 .7 .7;d]
  x
}/!99
g: .2*(3&1+#p-q)+.1*p.z-t
col: g,g,g,1

At a high level, this shader computes an rgba color (col) based on an auto-incrementing frame counter (frame), the position of the pixel on the screen (pos), and the viewport dimensions (size). These are all described briefly in The Special-K Manual.

The first three lines are straightforward: scale down the frame counter to a slower "time" value t, compute a 3d position d from pos and size which is centered in the viewport, and initialize an empty 3-vector named q:

t: frame*.005
d: (-.5+pos.xy%size),.8
q: 0 0 0

The next 7 lines repeatedly apply a function to another 3-vector based on our timer ((0;(sin t*12)%200;t)) while updating q as a side-effect. You can read the K notation

r:vec{func_body}/!iteration_count

as loosely similar to a JS expression like

r=range(iteration_count).reduce((x,y)=>{function_body},vec)

And the K notation $[x;y;z] within the function body as essentially x?y:z ternary expressions.

The last two lines compute a final grayscale value (g) for each pixel based on p, q, and t, and assemble the col output with an opaque alpha channel:

g: .2*(3&1+#p-q)+.1*p.z-t
col: g,g,g,1

It's a bit difficult to tease apart how every expression contributes to the final visuals, but you can gain some insight by changing or removing things.

The "seed" to our iterative calculation ((0;(sin t*12)%200;t)) can be thought of as an (x,y,z) camera position, with a subtle sinusoidal vertical wobble. We can substitute (0,0,t) to remove it.

You could try simplifying the calculation of g to g:p.x:

sort of like much harsher lighting

Or strip all those conditionals out of the loop:

t: frame*.005
d: (-.5+pos.xy%size),1
p: (0,0,t){y;x+:d*(.65-#fract[x+.5]-.5)&x.y+.2}/!99
g:p.x
col: g,g,g,1

one half of the superstructure

Observe how reducing the iteration count from 99 to 20 produces a softer image with fewer apparent "arches":

fuzzier beams

Or how the &x.y+.2 (& is "minimum" in K notation) part of that core expression is responsible for the "floor":

t: frame*.005
d: (-.5+pos.xy%size),1
p: (0,0,t){y;x+:d*(.65-#fract[x+.5]-.5)}/!99
g:p.x
col: g,g,g,1

endless holes

There's a very wide range of interesting programs to be found if you try more variations.


I hope that explanation is sufficient. I originally had some grander ambitions for Special-K, but a traumatic event left me unable to continue work on my creative projects, and when I recovered to some extent I decided to instead pursue a series of prototypes which would coalesce into Decker.


You must log in to comment.

in reply to @internet-janitor's post:

I really appreciate you taking the time to break all this down line by line. I've been exploring shaders on blender and seeing examples or ideas of how the same stuff is implemented in code is super cool.