Using shaders with Laravel and Vite

Are you using Laravel with Vite? Here’s how you can include shaders in your build process to create cool looking 2D effects.

Let’s create a new Laravel project:

laravel new shaders-vite

You will need to install two NPM packages:

  • vite-plugin-glsl will allow you to import your fragment shaders in your JS files
  • glslCanvas will load your fragment shaders and display them in a canvas element
yarn add vite-plugin-glsl glslCanvas --dev

Let’s start by editing the vite.config.js file so that our glsl plugin is loaded:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import glsl from 'vite-plugin-glsl';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/js/app.js'
            ],
            refresh: true,
        }),
        glsl()
    ],
});

With this done, we can go ahead and add a canvas to our page. Let’s edit our welcome.blade.php view to add a canvas element and load a JS file:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Shaders 101</title>
    </head>
    <body>
        <canvas id="canvas-element" width="1000" height="500"></canvas>
        @vite('resources/js/app.js')
    </body>
</html>

Next, let’s create our shader. Fragment shaders need to have the .frag file extension, and can be placed anywhere you like. To keep the example simple, let’s use resources/js/my-first-shader.frag, so that it sits right next to the app.js file. Go ahead and add the following shader code to the file:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main() {
    vec2 pos = gl_FragCoord.xy / u_resolution.xy;

    gl_FragColor = vec4(pos.x, pos.y, 1.0, 1.0);
}

Next, we will update our app.js file to read the shader file and apply it onto our canvas element:

import GlslCanvas from 'glslCanvas';
import Shader from './my-first-shader.frag';

const canvasElement = document.getElementById('canvas-element');
const glsl = new GlslCanvas(canvasElement);

glsl.load(Shader);

Believe it or not, that’s it! Run yarn dev to start Vite, and load your project in your browser. You should see the shader. The glsl vite plugin also works with hot-reloading, so you can make changes to the shader file and see the changes immediately in your browser when you save.


PS. The shader at the top of this article was made by following this excellent guide by the talented Maxime Heckel.

Here’s how to do it. First, you need to load an image as a texture in your shader.

import Shader from './shader.frag';
import Canvas from '../canvas';
import Texture from './texture.webp';

let canvas = new Canvas(document.querySelector('#shaders-vite'));

canvas.setUniform('u_texture', Texture);
canvas.load(Shader);

Then use the following shader. Check the comments I added to show how it works!

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform sampler2D u_texture;

// Loop defines how many "layers" of chromatic aberration we should draw. High number = gradient effect.
const int LOOP = 8;

// This function boosts the saturation of colors by the given intensity value
vec3 sat(vec3 rgb, float intensity) {
    vec3 L = vec3(0.2125, 0.7154, 0.0721);
    vec3 grayscale = vec3(dot(rgb, L));
    return mix(grayscale, rgb, intensity);
}

void main() {
    // Maxime's shader has free camera movements, so the world normal and eye vectors can change.
    // In my case, I'm happy with a static point of view, as I'm rendering in 2D, so I fixed them to the center.
    vec3 worldNormal = vec3(0.5, 0.5, 0.5);
    vec3 eyeVector = vec3(0.5, 0.5, 0.5);

    // These are the indices of refraction, AKA the amount of refraction for each color
    float iorR = 1.15;
    float iorG = 1.18;
    float iorB = 1.22;

    // We set the amount of chromatic aberration and its direction 
    // (negative = left, positive = right) based on the cursor position
    float uChromaticAberration = 1.0 * (((u_mouse.x / u_resolution.x) - 0.5) / 3.0);

    // Set a few shortcut values to have an easier time later
    float iorRatioRed = 1.0 / iorR;
    float iorRatioGreen = 1.0 / iorG;
    float iorRatioBlue = 1.0 / iorB;

    vec2 uv = gl_FragCoord.xy / u_resolution.xy;
    vec3 color = vec3(0.0);

    Each iteration of the loop will draw a "layer" of color
    for (int i = 0; i < LOOP; i ++) {
        // The slide controls how far the layer of color will be drawn
        float slide = float(i) / float(LOOP) * 0.1;

        // We're using the refract function (it's built in) with our indices of refraction ratios
        vec3 refractVecR = refract(eyeVector, worldNormal, iorRatioRed);
        vec3 refractVecG = refract(eyeVector, worldNormal, iorRatioGreen);
        vec3 refractVecB = refract(eyeVector, worldNormal, iorRatioBlue);

        // And use these values along with the slide and the chromatic aberration multiplier to compute our color
        color.r += texture2D(u_texture, uv + refractVecR.xy * (slide * 1.0) * uChromaticAberration).r;
        color.g += texture2D(u_texture, uv + refractVecG.xy * (slide * 2.0) * uChromaticAberration).g;
        color.b += texture2D(u_texture, uv + refractVecB.xy * (slide * 3.0) * uChromaticAberration).b;
    }

    // Divide by the number of layers to normalize colors (rgb values can be worth up to the value of LOOP)
    color /= float(LOOP);

    // Add saturation to make it pop
    color = sat(color, 2.0);

    // Use clamp to make sure the RGB value is at least 0.1, which gives me a dark gray background instead of pure black.
    color = clamp(color, 0.1, 1.0);

    gl_FragColor = vec4(color, 1.0);
}