d

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.

15 St Margarets, NY 10033
(+381) 11 123 4567
ouroffice@aware.com

 

KMF

Art Generator With Javascript and WebGL

Modern web design often makes use of large introduction images. These large images frame a product or service at the top of the website.

This got me thinking of how we could use WebGL to make an art effect similar to The Starry Night by Van Gogh. What I created was something similar, which nicely transforms into liquid if you want it to.

Step 1. Three.JS 

Three.js is a Javascript library that allows you to create WebGL 3d objects with ease. To accomplish our effect we will be following a few key steps:

  • 1. Create the geometry (shape) with Three.JS
  • 2. Render this onto an HTML5 canvas
  • 3. Pass this geometry data to shaders
  • 4. Animate this using Javascript
  • 5. Manipulate the geometry with the shaders

So with that in mind, let’s take a look at a snippet of our Javascript, which covers the first 3 points.

    // Please view github or codepen demo for entire code
    const noise = await loader('./shaders/noise.glsl');
    const fragment = await loader('./shaders/fragment.glsl');
    const vertex = await loader('./shaders/vertex.glsl');

    const renderer = new THREE.WebGLRenderer({
        powerPreference: "high-performance",
        antialias: true, 
        alpha: true,
        canvas: canvas // canvas is the Id for our HTML5 canvas. Remove this line and Three will auto create a canvas.
    });
    
    // Get el width and height
    let elWidth = window.innerWidth;
    let elHeight = window.innerHeight
    
    // Set sizes and set scene/camera
    renderer.setSize( elWidth, elHeight );
    document.body.appendChild( renderer.domElement )
    renderer.setPixelRatio( elWidth/elHeight );
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 75, elWidth / elHeight, 0.1, 1000 );
    
    let i = 2;
    // Check on colors to use
    let high = config.colors[i].high; 
    let low = config.colors[i].low;

    // Create a plane, and pass that through to our shaders
    let geometry = new THREE.PlaneGeometry(600, 600, 100, 100);
    let material = new THREE.ShaderMaterial({
        uniforms: {
            // All of these variables are passed to our shaders
            // which are then passed to the GPU
            u_lowColor: {type: 'v3', value: low },
            u_highColor: {type: 'v3', value: high },
            u_time: {type: 'f', value: 0},
            u_resolution: {type: 'v2', value: new THREE.Vector2(elWidth, elHeight) },
            u_mouse: {type: 'v2', value: new THREE.Vector2(0, 0) },
            u_height: {type: 'f', value: 1},
            u_manipulate: {type: 'f', value: 1 },
            u_veinDefinition: {type: 'f', value: 20 },
            u_goCrazy: { type: 't', value: 1 },
            u_inputTexture: {type: 't', value: lion},
            u_scale: {type: 'f', value: 0.85 },
            u_clickLength: { type: 'f', value: 1},
            u_rand: { type: 'f', value: randomInteger(0, 10) },
            u_rand: {type: 'f', value: new THREE.Vector2(randomInteger(6, 10), randomInteger(8, 10)) }
        },
        fragmentShader: noise + fragment,
        vertexShader: noise + vertex,
    });
    // Create the mesh and position appropriately
    let mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(0, 0, -300);
    scene.add(mesh);

    // This function when run will animate the renderer
    // Meaning for every animation frame the 3d model
    // will be rerendered onto the canvas.
    const animate = function () {
        requestAnimationFrame( animate );
        renderer.render( scene, camera );
        document.body.appendChild(renderer.domElement);
        mesh.material.uniforms.u_time.value = t;
        if(t < 10 && backtrack == false) {
            t = t + 0.005;
        } else {
            backtrack = true;
            t = t - 0.005;
            if(t < 0) {
                backtrack = false;
            }
        }
    };

I haven’t included all the details, but here are some of the key points:

  • The uniform variables in uniforms: {} are passed directly to the shader code we will write. When we update these, it will allow us to live to update our 3d object.
  • The shaders locations are mentioned in the ShaderMaterial() function
  • We then request an animation frame to re-render the 3d object. Notice in the animate() function we increase t (time) and the uniform variable, creating the animation

Step 2. Shaders 

If you are unfamiliar with what shaders are, they essentially allow you to manipulate the color of geometry and the position of a geometry. There are two types of shaders, fragments (for colors) and vertex (for positions). These alter the shape before it is rendered.

For this tutorial, our main focus is the fragment shader. We will be using noise to generate the liquid effect, and the specific noise we will be using is called fractal brownian noise (FBM). An example of the noise created by FBM is shown below:

Fractal brownian noise (FBM) image

Don’t worry, you don’t need to know how to create these noise effects, and in fact, one won an oscar. The full code for these noise functions is available online and you can find them in the GitHub Repo in the shaders/ folder. Our vertex shader is the default vertex shader, but we see the code below to see how the fragment shader works:

// Main function
void main() {
    // We have to adjust the effect to fit our resolution.
    // Heavily modified FBM function from https://thebookofshaders.com/13/
    vec2 res = (gl_FragCoord.xy + 100.) / (u_resolution.xy * u_scale);
    
    // Next lets get our colors
    vec3 highColor = rgb(u_highColor.r, u_highColor.g, u_highColor.b);
    vec3 lowColor = rgb(u_lowColor.r, u_lowColor.g, u_lowColor.b);
    
    // Set a random color
    vec3 color = vec3(23.0);

    // This is a randomised function based on fbm and some other variables
    // that we can adjust in our Javascript
    vec2 fbm1 = vec2(10.);
    fbm1.x = fbm( res + 0.05 * u_time) * snoise(res) * u_goCrazy;
    fbm1.y = fbm( res + vec2(3.0)) / (u_manipulate - snoise(res)) * 9. * u_goCrazy / u_veinDefinition * u_clickLength * 5.;

    // Next we adjust it all based on mouse position, time, and qfbm1
    vec2 r = vec2(0.);
    r.x = fbm( res + fbm1 * u_time * 0.1 ) + -sin(u_mouse.x) + 600.;
    r.y = fbm( res + fbm1 * u_time * 0.5 ) * -u_mouse.y;

    // And create a float of fbm, for use in the final color
    float f = fbm(res+r) * 1.;

    // Then we mix all our colors together
    color = mix(highColor*2., lowColor, f*3.);
    color = mix(color, lowColor, clamp(length(fbm1),0.0,2.0)); // * snoise(st) * 51.9
    color = mix(color, highColor, clamp(length(r.y),0.0,3.0));

    // And output them for render
    gl_FragColor = vec4((f*f*f*0.9*f*f+.5*f)*color,1.);
}
    

The cool thing about this particular fragment shader is that it renders as a cool noise effect for our demo, but if we increase the settings it can also double as a pretty simple liquid water fragment shader.

Our uniform variables are all defined at the top of the fragment shader but not listed above. They are listed in the form uniform float u_variableName;.

Step 3. Event Listeners

Finally, we add some typical event listeners to our code, to track mouse position, as well as the range selector positions. This allows us to adjust the effect and animate it as the user moves around.

This is perhaps the most straightforward vanilla JS part. For the most part, we are simply updating the uniform variables through the mesh we defined before – mesh.material.uniforms.... When these are updated, our animate() function rerenders the new object in our HTML5 canvas, so our cursor position and interaction with the canvas has real-time effects on the WebGL code.

    /* event listeners */
    document.getElementById('range').addEventListener('input', function(e) {
        // Update 'range' selector
        mesh.material.uniforms.u_manipulate.value = this.value;
    })
    document.getElementById('veins').addEventListener('input', function(e) {
        // Update 'veins' selector
        mesh.material.uniforms.u_veinDefinition.value = this.value;
    })
    document.getElementById('crazy').addEventListener('input', function(e) {
        // Update 'crazy' selector
        mesh.material.uniforms.u_goCrazy.value = this.value;
    })

    document.querySelectorAll('.color').forEach(function(item) {
        // Selector a color
        item.addEventListener('click', function(e) {
            let i = parseFloat(this.getAttribute('data-id'));
            mesh.material.uniforms.u_highColor.value = config.colors[i].high;
            mesh.material.uniforms.u_lowColor.value = config.colors[i].low;
        });
    });

    // Variables to track settings
    let reduceVector;
    let increasePressure;
    let reducePressure;
    let prevX = 0;
    let prevY = 0;
    let curValueX = 0;
    let curValueY = 0;
    let mouseEnterX = 0;
    let mouseEnterY = 0;

    // On move effect
    document.body.addEventListener('pointermove', function(e) {
        if(typeof reduceVector !== "undefined") {
            clearInterval(reduceVector);
            curValueX = 0;
            curValueY = 0;
        }
        let mouseMoveX = mouseEnterX - e.pageX;
        let mouseMoveY = mouseEnterY - e.pageY;
        mesh.material.uniforms.u_mouse.value = new THREE.Vector2(prevX + (mouseMoveX / elWidth), prevY + (mouseMoveY / elHeight));
    });
    
    // Animations on pointerdown and pointerup, using setInterval 60fps
    document.getElementById('canvas').addEventListener('pointerdown', function(e) {
        if(typeof reducePressure !== "undefined") clearInterval(reducePressure);
        increasePressure = setInterval(function() {
            if(mesh.material.uniforms.u_clickLength.value < 3) {
                mesh.material.uniforms.u_clickLength.value += 0.03;
            }
        },1000/60);
    });
    document.getElementById('canvas').addEventListener('pointerup', function(e) {
        if(typeof increasePressure !== "undefined") clearInterval(increasePressure);
        reducePressure = setInterval(function() {
            if(mesh.material.uniforms.u_clickLength.value > 1) {
                mesh.material.uniforms.u_clickLength.value -= 0.03;
            }
        },1000/60);
    });

Conclusion

And we’re done! After that, we’ll have most of the components needed to create the effect. Thanks for reading – you can find the resources including the full code for the effect below:

Credit: Source link

Previous Next
Close
Test Caption
Test Description goes like this