Triangle Waves

August 30, 2017

One of my favourite little shader techniques is to take continuous linear values, and turn them into oscillating triangle waves.

Oscillation is useful in all kinds of cases. It can create repetition in space, or repeating animation over time. The most common way to generate oscillation is with the trigonometry function sin, which creates the well known smooth sine wave shape.

But sin can sometimes be a little expensive, and other times that smoothness and non-linearity is not exactly what you want. In those cases, a triangle wave might be worth a look.

From line to triangle

We start out with a value - in this case called y - which increases at a constant rate. This could be a coordinate which changes from pixel to pixel, or a time value.

void main() { vec2 xy = (gl_FragCoord.xy - vec2(.5*size))/(.25*size); vec2 axes = step(.01,abs(xy)); float axescol = axes.x * axes.y; vec2 grid = step(0.,fract(xy*.5)*2.-1.); float gridcol = mix(axescol+.3,.6+.2*mod(grid.x + grid.y,2.),axescol); float y = xy.x; float a = y; //a = fract(y); //a = 2.*fract(y); //a = 2.*fract(y)-1.; //a = abs(2.0*fract(y)-1.); float graph = smoothstep(.0,.1,abs(a-xy.y)); vec3 col = mix(vec3(graph),vec3(gridcol),vec3(graph)); gl_FragColor = vec4( col,1.0 ); }
float a = y;

The first part of this trick is take only the fractional part of y. That already converts y into an oscillating value, but it is discontinuous. Every time the original value exceeds one, it wraps around to zero again.

void main() { vec2 xy = (gl_FragCoord.xy - vec2(.5*size))/(.25*size); vec2 axes = step(.01,abs(xy)); float axescol = axes.x * axes.y; vec2 grid = step(0.,fract(xy*.5)*2.-1.); float gridcol = mix(axescol+.3,.6+.2*mod(grid.x + grid.y,2.),axescol); float y = xy.x; float a = y; a = fract(y); //a = 2.*fract(y); //a = 2.*fract(y)-1.; //a = abs(2.0*fract(y)-1.); float graph = smoothstep(.0,.1,abs(a-xy.y)); vec3 col = mix(vec3(graph),vec3(gridcol),vec3(graph)); gl_FragColor = vec4( col,1.0 ); }
float a = fract(y);

Next we multiply - scale - the value by two, so that it oscillates between zero and two.

void main() { vec2 xy = (gl_FragCoord.xy - vec2(.5*size))/(.25*size); vec2 axes = step(.01,abs(xy)); float axescol = axes.x * axes.y; vec2 grid = step(0.,fract(xy*.5)*2.-1.); float gridcol = mix(axescol+.3,.6+.2*mod(grid.x + grid.y,2.),axescol); float y = xy.x; float a = y; //a = fract(y); a = 2.*fract(y); //a = 2.*fract(y)-1.; //a = abs(2.0*fract(y)-1.); float graph = smoothstep(.0,.1,abs(a-xy.y)); vec3 col = mix(vec3(graph),vec3(gridcol),vec3(graph)); gl_FragColor = vec4( col,1.0 ); }
float a = 2.*fract(y);

Now we subtract one, making it oscillate between minus one and plus one.

void main() { vec2 xy = (gl_FragCoord.xy - vec2(.5*size))/(.25*size); vec2 axes = step(.01,abs(xy)); float axescol = axes.x * axes.y; vec2 grid = step(0.,fract(xy*.5)*2.-1.); float gridcol = mix(axescol+.3,.6+.2*mod(grid.x + grid.y,2.),axescol); float y = xy.x; float a = y; //a = fract(y); //a = 2.*fract(y); a = 2.*fract(y)-1.; //a = abs(2.0*fract(y)-1.); float graph = smoothstep(.0,.1,abs(a-xy.y)); vec3 col = mix(vec3(graph),vec3(gridcol),vec3(graph)); gl_FragColor = vec4( col,1.0 ); }
float a = 2.*fract(y)-1.;

The final step of the trick is to take the absolute value. Now the negative parts of the wave are reflected, and the function becomes continuous again.

void main() { vec2 xy = (gl_FragCoord.xy - vec2(.5*size))/(.25*size); vec2 axes = step(.01,abs(xy)); float axescol = axes.x * axes.y; vec2 grid = step(0.,fract(xy*.5)*2.-1.); float gridcol = mix(axescol+.3,.6+.2*mod(grid.x + grid.y,2.),axescol); float y = xy.x; float a = y; //a = fract(y); //a = 2.*fract(y); //a = 2.*fract(y)-1.; a = abs(2.0*fract(y)-1.); float graph = smoothstep(.0,.1,abs(a-xy.y)); vec3 col = mix(vec3(graph),vec3(gridcol),vec3(graph)); gl_FragColor = vec4( col,1.0 ); }
float a = abs(2.*fract(y)-1.);

That’s it. Now you can make a triangle wave from any continuous value.