From our sponsor: Ready to show your plugin skills? Enter the Penpot Plugins Contest (Nov 15-Dec 15) to win cash prizes!
Today we are going to explore a playful animated landscape with a psychedelic look. The idea is to show how an experimentation on art and design with a generative process can lead to interesting interactive visuals which can be used in a variety of mediums like web, print, illustration, VJing, installations, games and many others. We made 3 variants of the landscape to show you how small changes in parameters can change a lot in visuals.
The demos are made with three.js and the animations and colors are controlled in a custom GLSL shader. For the letter animations we are using TweenMax.
The cool thing about doing this with WebGL is that it’s widely supported and with GLSL shaders we can animate thousands, even millions of vertices at 60 FPS on the major desktop and mobile web browsers.
If you’re not familiar with three.js and GLSL shaders, you can start by creating a scene and reading this introduction to Shaders.
Let’s go through the main build up of the demo.
Breaking down the demo
1. Creating terrain with a plane
Let’s make a basic three.js scene, place a plane with a nice amount of vertices, rotate it 90 degrees is the x-axis, and lift the camera a little bit:
Create custom vertex and fragment shaders and bind them to a ShaderMaterial. The objective is to displace vertices up in the vertex shader with a perlin noise and multiply it with a height value:
// pseudo-code for noise implementation
vec3 coord = vec3(uv, 1.0)*10.0;
float noise = 1 + pnoise( vec3( coord.x, coord.y + time, coord.z ));
float height = h * noise;
// we apply height to z because the plane is rotated on x-axis
vec4 pos = vec4( position.x, position.y, height, 1.0 );
// output the final position
gl_Position = projectionMatrix * modelViewMatrix * pos;
2. Create a road with some math
Now we’ll use a little bit of math. We’ll implement the formula below, where x is the vertex x-coordinate, h is the maximum height of terrain, c is the center of road and w is the width of road:
Playing with those variables, we can get different results, as we can see in the graphs:
Now, applied in vertex-shader code, multiplied by the previously calculated noise it looks as follows:
// pseudo-code for formula implementation
float height = h * pow( abs( cos( uv.x + c ) ), w ) * noise;
// we apply height to z because the plane is rotated on x-axis
vec4 pos = vec4( position.x, position.y, height, 1.0 );
// output the final position
gl_Position = projectionMatrix * modelViewMatrix * pos;
To make a curved road, we use uv.y as angle and take the sin of it to oscillate the center along the y-axis (the plane is rotated on the x-axis, remember?).
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
3. Adding color layers
Let’s colorize the terrain with a nice trick. First, create a color pallete image like this one:
And then we’ll use it as a lookup texture in the fragment shader, getting the color value from the height calculated in the vertex shader as texture uv.y coordinate:
// pseudo-code for getting the color
vec2 coord = vec2( 0.0, normalize( height ) );
vec4 color = texture2D( palleteTexture, coord );
gl_FragColor = color
4. Having fun adding interactivity
Now we’ve done the heaviest part, it’s easy to use mouse, touch or whatever input you want, to control the formula’s variables and get interesting forms of interactivity:
// JS pseudo-code in the render loop for uniforms manipulation with mouse
terrain.material.uniforms.c.value = (mouseX / window.innerWidth - 0.5) * 0.1;
terrain.material.uniforms.w.value = (mouseY / window.innerHeight - 0.5) * 4;
5. Final touches
Let’s adjust the camera position, add a nice color pallete, fog, a sky background, and we are done!
We hope you enjoy this walk-through and find the experiment inspirational!
References and Credits
- three.js
- WebGL noise by Stefan Gustavson
- sky + sun shader by @blurspline
- Desmos
- Coolors
- TweenMax