Experiments in WebGL
Before this project, I’d never really experimented with WebGL. I had some experience with Three.js, which is a higher-level library that makes it easier to work with WebGL, but I wanted to try working directly with the WebGL API in order to gain a deeper understanding of how it works. I also wanted to learn how to use it to improve performance in, say, my IFS renderer, which is currently written in Three.js and uses the CPU to do all of the heavy lifting. I wanted to see if I could use WebGL to offload some of that work to the GPU, which is much better suited for this kind of task. If the world was perfect, I’d use a technology like WebGPU, which is a more modern API that is designed for GPGPU (General-Purpose computing on Graphics Processing Units) tasks, but support for it is patchy at best, especially with Firefox, so I decided to stick with WebGL for now.
Making it swirl
This project was largely just experimenting around with how to create programs and shaders in WebGL, and the result I ended up with was pretty simple. It simply uses a fragment shader to create a swirling effect that is animated over time.
What’s in the shader?
Here’s the fragment shader that I used to create the swirling effect. I’ve tried to annotate it as best I can, but if you have any questions about how it works, feel free to reach out to me. Also, yes, this absolutely is basically just ShaderToy, but I wanted to experiment writing my own raw WebGL code and shaders, and I wanted to see if I could get it to work in a SvelteKit project.
#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
uniform sampler2D u_state;
uniform vec2 u_res;
uniform float u_time;
float sampleCell(vec2 uv) {
return texture(u_state, uv).r;
}
float hash(vec2 p) {
//This is just some random noise, it doesn't have to be anything fancy
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
const float speed = 0.06;
void main() {
vec2 px = 1.0 / u_res;
vec2 warp = vec2(
hash(v_uv * u_res + u_time),
hash(v_uv * u_res - u_time)
) * 0.001;
//slightly warp the uv coordinates
//I'm honestly not sure how much this helps
vec2 uv_warped = v_uv + warp;
float center = sampleCell(uv_warped);
//get the values of the neighboring cells
float up = sampleCell(uv_warped + vec2(0.0, px.y));
float down = sampleCell(uv_warped - vec2(0.0, px.y));
float left = sampleCell(uv_warped - vec2(px.x, 0.0));
float right = sampleCell(uv_warped + vec2(px.x, 0.0));
float up_left = sampleCell(uv_warped + vec2( -px.x, px.y));
float up_right = sampleCell(uv_warped + vec2(px.x, px.y));
float down_left = sampleCell(uv_warped + vec2( -px.x, -px.y));
float down_right = sampleCell(uv_warped + vec2(px.x, -px.y));
//calculate the gradient using central differences
float derivative_x = (right - left + 0.25 * (up_right + down_right - up_left - down_left));
float derivative_y = (up - down + 0.25 * (up_left + up_right - down_left - down_right));
//calculate the gradient vector
vec2 gradient_v = vec2(derivative_x, derivative_y);
//calculate the magnitude and direction of the gradient
float gradient_magnitude = length(gradient_v);
float gradient_direction = atan(derivative_y, derivative_x);
//calculate the direction and distance from the center of the cell
float point_direction = atan(uv_warped.y - 0.5, uv_warped.x - 0.5);
float point_distance = length(uv_warped - vec2(0.5, 0.5));
//the gradient the direction of the gradient minus the direction of the point
float gradient = gradient_direction - point_direction;
//the new intensity is the current intensity plus
// the speed (how fast everything changes)
// times the magnitude of the gradient (how much the intensity changes)
// times the cosine of the gradient minus the point distance and the time
float new_intensity = center + speed * gradient_magnitude * cos( gradient - (sin(u_time * 0.1) * 6.2831853) + point_distance * 10.0);
// this means that how fast the gradient changes is affected by the distance from the center of the cell
//add a little bit of noise to to keep it interesting and not too uniform
new_intensity += 0.05 * (hash(uv_warped * u_res + u_time) - 0.5);
//this slightly slows how fast the intensity can get updated
new_intensity = mix(new_intensity, center, 0.2);
// finally, clamp the intensity to be between 0 and 1
new_intensity = clamp(new_intensity, 0.0, 1.0);
outColor = vec4(new_intensity,new_intensity,new_intensity, 1.0);
} Keeping the example in sync
Fun fact, the code here is directly embedded in the page from the source file, not duplicated here. This is possible due to Vite’s ?raw query parameter, which directly imports the shader code as a string. This is a really nice feature, and it makes it easy to keep the code in sync with the source file.