
WebGL Shader Background
After reading Alex Harri’s Flowing WebGL Gradient article, I wanted to add some WebGL to my page! I don’t really care for web dev, so anything I can do to make it feel more like game dev is a big improvement.
My site is written in Hugo, so I integrated the work into a Hugo shortcode.
I wanted an effect that wouldn’t be confused for a static scrolling image or a css animation, so I looked to shaderToy for inspiration. One by mklefrancois uses a commonly shared noise function in the shadertoy community, and I made some adaptations. I didn’t want it to ever blow out the color, and I wanted it to be a bit more subtle so you could conceivably read text on top of it. And I wanted a sense of overall motion, even if it was still ‘cloudlike’. I also tweaked it for performance.
You can inspect the source here if you’d like to see how the effect is made.
But I’ll also include the most relevant bits below, to be sure they are properly formatted for your viewing convenience.
The only part that I felt was missing from Alex Harri’s tutorial was the canvas setup. I found it to be pretty familiar since I’ve done lots of OpenGL. Setting uniforms was really easy, and I think there’s a lot of opportunity to make fun interactive JS content this way.
Sorry about the long unrolled loop!
// inspired from: https://www.shadertoy.com/view/W3S3zD
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
// A common random noise function from ShaderToy
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// also a borrowed noise function
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) +
(c - a) * u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
// Fractional Brownian motion
float fbm(vec2 st) {
float accum = 0.0;
float amplitude = 0.4;
float frequency = 1.1;
float lacunarity = 2.0;
float gain = 0.5;
// unroll loop by hand- Cause the javascript didn't like the less than in the for loop, which is weird cause it works
// other places. But maybe the hugo shortcode doesn't like it.
// it's fine though, the shader compiler will clean this up nicely.
int i = 0;
accum += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
i = 1;
accum += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
i = 2;
accum += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
i = 3;
accum += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
i = 4;
accum += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
return accum;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
uv.x *= u_resolution.x / u_resolution.y; // aspect ratio
float t = u_time * 0.6;
float firstNoise = fbm(uv + vec2(0.0, t));
float downwardSpeed = 1.5;
float dependentNoise = fbm(uv + firstNoise * downwardSpeed);
// --ctp-mocha-base-rgb: 30 30 46;
vec3 color1 = vec3(0.05, 0.0, 0.05);
vec3 color2 = vec3(0.11, 0.11, 0.18);
vec3 color3 = vec3(0.4, 0.5, 0.8);
vec3 color = mix(color1, color2, firstNoise);
color = mix(color, color3, dependentNoise);
gl_FragColor = vec4(color, 1.0);
}
Then the javascript to find the canvas, attach the shader programs, update the uniforms, and render it.
<script>
(function () {
const canvas = document.getElementById("artCanvas");
const gl = canvas.getContext("webgl");
function resizeCanvas() {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
resizeCanvas();
const vertexShaderSource = document.getElementById("vertex-shader").text;
const fragmentShaderSource = document.getElementById("fragment-shader").text;
function compileShader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}
const vertexShader = compileShader(gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Position buffer
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// vertex and texture coordinate data to draw. XYZ of each corner. Z = 1 for all faces.
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, 1, -1, -1, 1,
-1, 1, 1, -1, 1, 1
]), gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(program, "u_time");
const uResolution = gl.getUniformLocation(program, "u_resolution");
function render(time) {
resizeCanvas();
gl.uniform1f(uTime, time * 0.001);
gl.uniform2f(uResolution, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
render(0);
window.addEventListener("resize", resizeCanvas);
})();
</script>