6 min read

Things I have learned writing custom shaders for Hydra

Things I have learned writing custom shaders for Hydra

Most of these posts have a wide intended audience, but for this one – you know if you're in the market for it.

Don't start by reading this blog post

The places to learn about writing setFunction calls for Hydra are:

  • the documentation (obviously):
custom glsl
custom glsl # Using custom GLSL functions # Hydra is built using GLSL (a language for generating a program, or shader, that runs directly on the graphics card using WebGl). Each javascript function in hydra corresponds directly to a snippet of shader code. There are four possible types in hydra: src, coord (geometry), combine (blend), combineCoord (modulate). Each string of functions is composited based on its type into a single string of fragment shader code.
  • as the documentation suggests, looking over the built in shader sources:
hydra-synth/src/glsl/glsl-functions.js at main · hydra-synth/hydra-synth
Synth engine for hydra. Contribute to hydra-synth/hydra-synth development by creating an account on GitHub.
  • going onto the Hydra discord & searching for particular things:
Join the hydra video synth Discord Server!
Check out the hydra video synth community on Discord - hang out with 3221 other members and enjoy free voice and text chat.
  • looking at the sources of other people's custom Hydra functions. Here's a good place to start, although there's a bunch more scattered around here:
lib/all.js · main · Thomas Jourdan / Extra shaders for Hydra · GitLab
Additional fragment shaders for the live coding environment Hydra.
  • fucking around, trying things, seeing what happens. I do this too little, I think. It's especially informative to look at the generated shaders and see how they've been put together. See the next step for details.

How to tell if you've fucked up

The shader will start glitching back and forth between two frames. It'll look broken, but in a less exciting way than you were going for. You go to the inspector, switch to the console tab and then look for these two lines:

As the second line suggests, you'll only get this once until you refresh the page.

Probably you're wondering... okay, but how do I tell what I've fucked up. Here's my workflow, although it's not the most convenient:

1) Get the Hydra code that's calling your new function. Let's say it's something like this:

osc()
.coolFunc(17)
.out(o0);

You wanna edit it so it looks something like this:

console.log(
  osc()
  .coolFunc(17)
  .glsl()[0].frag
);

2) Run that code

3) Look in the console and you can see the generated shader source.

4) Now to figure out what's wrong with it. Maybe you can tell by looking? Looking and thinking, that's what coding is all about.

5) I personally paste it in here: https://evanw.github.io/glslx/ Probably I should set this up so it runs in a code editor & I can do some stuff there?

Types of input

So if you look at the docs or the built in functions, you will see a lot of functions which take float as an argument. Almost all, in fact! And this makes sense, there's lots of fun stuff that works when you're passing in floats - you have the array syntax which sequences the input, you can smooth it, it's easy to do maths on it...

But that's not all you can pass. Check this out:

setFunction({
    name: 'matExample',
    type: 'src',
    inputs: [
        { name: 'm', type: 'mat3', default: "mat3(1,1,1,1,1,1,1,1,1)"}
    ],
    glsl: `
    return vec4(m[0][0], m[0][1], m[0][2], 1);
`})

That's right, you can pass in a matrix (of course, we're not doing anything interesting with the matrix... that's your job).

How do we call this? If we try to pass in an array then we trigger the clever sequencing logic. Well, I'm not sure if there's a smarter way. But this works:

matExample("mat3(1,0,1,1,1,1,1,1,1)").out(o0);

What else might we wanna pass? Well, if we wanna do something where we're taking multiple texture samples, like a blur or whatever, then we're gonna want a sampler2d. There is of course an example of this in the default functions, it's good old src() . Let's have a look at that:

{
  name: 'src',
  type: 'src',
  inputs: [
    {
      type: 'sampler2D',
      name: 'tex',
      default: NaN,
    }
  ],
  glsl:
`   //  vec2 uv = gl_FragCoord.xy/vec2(1280., 720.);
   return texture2D(tex, fract(_st));`
},

Funny to see commented out code from an earlier version in there. Anyway, yeah, we can do the same thing ourselves! To make a function that looks like this: blur(s1) . We want this to be a src type of function so we know _st is available, and also just because it makes sense - it's always going to be at the start of a chain of functions.

I haven't experimented with the full range of possible inputs, but understanding how to pass in stuff that isn't a float took me some time, so hopefully this is helpful to others. I think other things should work similarly - falling back to passing the value as a string if needed.

Sampling

Of course, the other way to sample is to read from tex0 from somewhere else in the chain. This will, if the chain contains a texture read, read from that same sample. But it'll break if you're not doing any texture reads. So I don't recommend it, even if it makes the code a bit nicer in form. Thomas Jourdan's convolution shaders seem to do this, and this is why they sometimes don't work if you aren't also doing feedback stuff.

Also worth noticing when doing sampling is the resolution uniform. We can correct for the size of the screen! Make shaders which correct for the aspect ratio automatically (uh, although they will make assumptions about the lack of transformations earlier in the chain)

Although...

Hydra doesn't re-evaluate width & height? it should!

This is just a moan as I run down through my list of notes. Hydra doesn't seem to re-evaluate the builtin variables width and height! A pain if you wanna play with window size. Or maybe it makes sense given setResolution exists. Anyway, you can listen for the window resize event and set stuff yourself if you need to. But it was a surprise to me.

And yes, this affects the resolution uniform.

Helper functions

One thing I've spotted going through the built in function source is this bit of mangled code:

{
  name: 'sum',
  type: 'color',
  inputs: [
    {
      type: 'vec4',
      name: 'scale',
      default: 1,
    }
  ],
  glsl:
`   vec4 v = _c0 * s;
   return v.r + v.g + v.b + v.a;
   }
   float sum(vec2 _st, vec4 s) { // vec4 is not a typo, because argument type is not overloaded
   vec2 v = _st.xy * s.xy;
   return v.x + v.y;`
},

Can you see what's up? The glsl includes a close bracket and a new function declaration. If we try to use this, we find that the generated shader is invalid... but it doesn't need to be?

One limitation of the way you declare new functions is that you can include helper functions that you can call from your functions. When converting shaders I've written elsewhere, manually inlining these turns out to be a bunch of work. Can we take advantage of the textual mangling that Hydra is built on and sneak something in like this:

setFunction({
    name: 'withHelper',
    type: 'src',
    inputs: [
    ],
    glsl: `
    return otherFunc();
    }
    
    vec4 otherFunc() {
    	return vec4(1,0,0,1);
`})


withHelper().out(o0);

The answer is no. It generates something sensible:

  vec4 withHelper(vec2 _st) {
      
    return otherFunc();
    }
    
    vec4 otherFunc() {
    	return vec4(1,0,0,1);

  }

          

  void main () {
    vec4 c = vec4(1, 0, 0, 1);
    vec2 st = gl_FragCoord.xy/resolution.xy;
    gl_FragColor = withHelper(st);
  }

But alas, we need to declare otherFunc before we use it. So we'd have to rely on adding the helper function with a different setFunction call than the one we're using it with... which feels too fragile to actually use, for me. Maybe there's a way? Let me know if you figure it out. Or if you've fixed up Hydra so helper functions can get defined, that too.

Okay, I think that's the tidied up version of a bunch of my notes. Hope this is helpful to someone! Maybe just to me, later on!