Oh, I totally based the FFT example on the overtone.gui scope code. I'd have been lost without that... Also, adding "real" oscilloscope features like triggers and grids will be much easier to do in that overtone.gui code. Mine just makes the FFT a bit easier to understand via a call to pow(). 0-1 values are way easier than -infinity to +6 (ish)
Unfortunately, pointers for learning GLSL is a bit hard to explain in this context because most GLSL tutorials assume you are shading 3D models with normals, eye vectors, etc. The shadertoy model is much more limited than that and the GLSL tutorials could be confusing with a lot of background & explanation you just don't need (like setting up lights, materials, vertex shaders, etc).
I've got a start for trying to explain them in the README:
Writing Fragment Shaders
Fragment Shaders are code that is executed for every pixel displayed in the window. The output is the color of that pixel, which you store in gl_FragColor. To create different imagery, you take the current pixel's position (from gl_FragCoord) and use that as input. To have Overtone affect the color, you'll want to use inputs like iOvertoneVolume. The syntax is C-like and given the math-processing power of modern GPUs, it is somewhat astonishing what you can do in 1/60th of second.
If I knew what kinds of questions you had after reading this, I could try to address them--send 'em on. My problem is that I don't really know what people don't know...I've been doing this as a day job for a long time.
Since each pixel is running the same code, normally the first thing you need to do in one of these shaders is find out which pixel you are. To do that you need to read from the gl_FragCoord input and convert it to a range that is better suited for further processing. That's what this line does:
vec2 uv = getScreenUV(gl_FragCoord.xy);
which calls this function and hopefully the comments point out how the value is manipulated. The gl_FragCoord.x is an integer ranging from 0 to windowWidth-1. Dividing by iResolution.x will give you a [0,1) range output... The code below handles keeping your image "square" even when your window is rectangular.
// convert the fragment coordinates (fc.x,fc.y) to a screen u,v space.
vec2 getScreenUV(vec2 fc) {
vec2 uv = fc/iResolution.xy; // uv = [0,1)
uv = 2.0*uv-1.0; // uv = [-1,1)
float aspect_ratio = iResolution.x / iResolution.y;
if(aspect_ratio < 1.0) {
uv.x /= aspect_ratio; // u = [-ar,ar), v = [-1,1)
} else {
uv.y /= aspect_ratio; // u = [-1,1), v = [-ar,ar)
}
return(uv);
}
so each pixel now has a uv.xy vector that is in a known range (-1,1) and is useful for further processing. One way to see this would be to do
gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0);
Will just output these linear values uv.x -> a pixel's red color. uv.y -> a pixel's green color. (blue gets 0.0). Note that negative color values are going to be black when you see them on the screen.
I think playing with this very basic code to see how things change on the screen is where folks should start and be sure they understand the basics of what is happening.
Try multiplying uv by a constant. Try commenting out this line uv = 2.0*uv-1.0; Try doing an abs(uv). Try different window aspect ratios. Try removing the aspect ratio code. Etc...
Cheers,
--Roger