Naoto
Naoto Naoto Hieda

Thoughts on Hydra Modulate Function

Thoughts on Hydra Modulate Function

Hydra is an analog-synth-like coding environment for real-time visuals. It is created by Olivia Jack and is open-source.

One of the key function of Hydra is modulate. As referenced in Hydra README, Lumen is a commercial software that also uses modulation, and it has a documentation about modulation.

osc(20,0.1,2) .modulate(noise(), () => time%1*0.1, 0.1) .out()

I usually think modulation as a way to push pixels in x and y directions based on the red and green channels of the input texture. In the example above, the original texture (rainbow oscillator) is pushed by an input texture (noise). Let’s take a look another example.

shape(4).modulate(noise(), () => time%1*0.1, 0.1).out()

Here, the square shape is modulated in both x, y directions; in fact, noise returns grayscale values, so pixels are pushed equally in x and y directions. Effectively, pixels are pushed in a diagonal direction. To modulate only in x direction, green channel should be discarded (there is also modulateScrollX which specifically modulates in x direction, but currently there is a bug that the function is currently broken):

shape(4) .modulate(noise().color(1,0), () => time%1*0.1, 0.1) .out()

Notice that while the effect is similar, the top and bottom edges of the square are preserved because modulation only applies in the x (horizontal) direction. You might wonder why I used color(1,0) instead of color(1,0,0). The reason is that modulate only looks at the red and green channels, so the value in the blue channel does not matter. In modulate family, the following functions use only 1 channel (red):

  • modulateRepeatX
  • modulateRepeatY
  • modulateScrollX
  • modulateScrollY
  • modulateKaleid
  • modulateRotate

note that all the functions samples red channel; for example, modulateScrollY uses red channel to shift pixels in y direction while modulate uses red for x and green channel for y direction.

The following functions use 2 channels (red and green):

  • modulate
  • modulateRepeat
  • modulateScale
  • modulatePixelate

At last, modulateHue uses 3 channels (red, green and blue).

Therefore, blue channel will not affect the results of modulate. For example, gradient takes an argument to “animate” the texture.

gradient(2).out()

Using gradient animation as an input does not animate the texture because the animation only happens in the blue channel:

shape(4) .modulate(gradient(2), 0.1) .out()

How can we use the animation of gradient? Although swapping color channels can be confusing in Hydra, you can simply use hue to effectively shift values between channels:

shape(4) .modulate(gradient(2).hue(), 0.1) .out()

This is where things get confusing. hue is supposed to change color but in the code above, what is changed is the direction of pixels to push. Let’s look at an even more confusing example:

render() osc(30,0,2) .modulateScale(shape(99,0.5),0.5,1) .out(o0) osc(30,0,2) .modulateScale(shape(99,0.5).color(0.5,0.5),1,1) .out(o1) osc(30,0,2) .modulateScale(shape(99,0.5).brightness(2),0.5,0) .out(o2) osc(30,0,2) .modulateScale(shape(99,0.5).invert(),-0.5,1.5) .out(o3)

While they all have different operations using color, brightness and invert, with an appropriate arguments of modulateScale, they all yield the same texture. This is because that the color operations are simply arithmetic operations (color for *, brightness for + and invert for -).

By understanding such operations, animations can be coded without array objects or arrow functions:

shape(4) .modulateScale(gradient().r() .scrollX(0,1).pixelate(1,1),1,1) .out()

which is identical to

shape(4).scale(()=>time%1+1).out()

(r() copies the red channel to green and blue channels; thus scaling is applied equally to x and y directions). I prefer the color operations inside modulation because they give more freedom to work in the spatial domain:

shape(4) .modulateScale(gradient().r() .scrollX(0,1).pixelate(8,1),1,1) .out()

Nevertheless, I am hesitant to tell people about these tricks because simply they are not intuitive. As a design question, I would love to ask people what would be an alias of color inside modulation to actually multiply the values for modifying the texture coordinates? scale may sound like a good idea, but unfortunately it is already taken by real scaling function that stretches the texture in the spatial domain.

This is not totally a hydra geek talk. In the following example of naive uses of modulation family functions, the pairs look similar to each other:

render() osc(30,0,0) .modulate(noise(3)) .out(o0) osc(30,0,0) .modulateRotate(noise(3)) .out(o1) osc(30,0,0) .modulatePixelate(noise(3)) .out(o2) osc(30,0,0) .modulateKaleid(noise(3)) .out(o3)

Based on the observations above, these modulators become controllable by adding a few color operations and by picking the right parameters:

render() osc(30,0,0) .modulate(noise(3)) .out(o0) osc(30,0,0) .modulateRotate(noise(3).mult(shape(99,0.0,0.7)),Math.PI*2,0) .out(o1) osc(30,0,0) .modulatePixelate(noise(3).pixelate(8,8),1024,8) .out(o2) osc(30,0,0) .modulateKaleid(noise(3).color(0.1),4) .out(o3)

I would love to contribute to Hydra (or specifically hydra-synth) to make modulators more accessible. However, as written above, it is not a simple engineering problem but it needs to be closely discussed by community members to make it accessible.

comments powered by Disqus