---
title: "Experiments in WebGL"
description: "A simple WebGL experiment that uses a fragment shader to create a swirling effect, to learn more about WebGL and how to use it to offload work to the GPU."
tags:
  - "Computer Graphics"
  - "WebGL"
  - "GLSL"
canonical: "https://portfolio.rospo.dev/projects/webgl"
html: "https://portfolio.rospo.dev/projects/webgl"
---

# 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](https://portfolio.rospo.dev/projects/ifs/index.md), 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.

[Open the interactive demo](https://portfolio.rospo.dev/inline/swirl)

### 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](mailto:erik@rospo.dev). 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.

```glsl
#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.
