From our sponsor: Meco is a distraction-free space for reading and discovering newsletters, separate from the inbox.
WebGL is becoming quite popular these days as it allows us to create unique interactive graphics for the web. You might have seen the recent text distortion effects using Blotter.js or the animated WebGL lines created with the THREE.MeshLine library. Today you’ll see how to quickly create an interactive “fake” 3D effect for images with plain WebGL.
If you use Facebook, you might have seen the update of 3D photos for the news feed and VR. With special phone cameras that capture the distance between the subject in the foreground and the background, 3D photos bring scenes to life with depth and movement. We can recreate this kind of effect with any photo, some image editing and a little bit of coding.
Usually, these kind of effects would rely on either Three.js or Pixi.js, the powerful libraries that come with many useful features and simplifications when coding. Today we won’t use any libraries but go with the native WebGL API.
So let’s dig in.
Getting started
So, for this effect we’ll go with the native WebGL API. A great place to help you get started with WebGL is webglfundamentals.org. WebGL is usually being berated for its verboseness. And there is a reason for that. The foundation of all fullcreen shader effects (even if they are 2D) is some sort of plane or mesh, or so called quad, which is stretched over the whole screen. So, speaking of being verbose, while we would simply write THREE.PlaneGeometry(1,1)
in three.js which creates the 1×1 plane, here is what we need in plain WebGL:
let vertices = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, 1,
])
let buffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
gl.bufferData( gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW );
Now that we have our plane, we can apply vertex and fragment shaders to it.
Preparing the image
For our effect to work, we need to create a depth map of the image. The main principle for building a depth map is that we’ve got to separate some parts of the image depending on their Z position, i.e. being far or close, hence isolate the foreground from the background.
For that, we can open the image in Photoshop and paint gray areas over the original photo in the following way:
This image shows some mountains where you can see that the closer the objects are to the camera, the brighter the area is painted in the depth map. Let’s see in the next section why this kind of shading makes sense.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
Shaders
The rendering logic is mostly happening in shaders. As described in the MDN web docs:
A shader is a program, written using the OpenGL ES Shading Language (GLSL), that takes information about the vertices that make up a shape and generates the data needed to render the pixels onto the screen: namely, the positions of the pixels and their colors. There are two shader functions run when drawing WebGL content: the vertex shader and the fragment shader.
A great resource to learn more about shaders is The Book Of Shaders.
The vertex shader will not do much; it just shows the vertices:
attribute vec2 position;
void main() {
gl_Position = vec4( position, 0, 1 );
}
The most interesting part will happen in a fragment shader. Let’s load the two images there:
void main(){
vec4 depth = texture2D(depthImage, uv);
gl_FragColor = texture2D(originalImage, uv); // just showing original photo
}
Remember, the depth map image is black and white. For shaders, color is just a number: 1 is white and 0 is pitch black. The uv
variable is a two dimensional map storing information on which pixel to show. With these two things we can use the depth information to move the pixels of the original photo a little bit.
Let’s start with a mouse movement:
vec4 depth = texture2D(depthImage, uv);
gl_FragColor = texture2D(originalImage, uv + mouse);
Here is how it looks like:
Now let’s add the depth:
vec4 depth = texture2D(depthImage, uv);
gl_FragColor = texture2D(originalImage, uv + mouse*depth.r);
And here we are:
Because the texture is black and white, we can just take the red channel (depth.r
), and multiply it to the mouse position value on the screen. That means, the brighter the pixel is, the more it will move with the mouse. On the other hand, dark pixels will just stay in place. It’s so simple, yet, it results in such a nice 3D illusion of an image.
Of course, shaders are capable of doing all kinds of other crazy things, but I hope you like this small experiment of “faking” a 3D movement. Let me know what you think about it, and I hope to see your creations with this!
References and Credits
- Gyronorm library by Doruk Eker
- Photo by Cosmic Timetraveler
- Photo by Chelsea Ferenando
- Photo by Rio Syhputra
- Phoyo by Jonatan Pie
This is awesome! I downloaded the source to have a play but I get an error when I try to view the files locally in a browser:
“Uncaught DOMException: Failed to execute ‘texImage2D’ on ‘WebGLRenderingContext’: The image element contains cross-origin data, and may not be loaded.”
ANy tips to get it working?
You need to run it on a localhost and not just open the HTML file from the folder.
U must run it on a web-server, because of CORS
Thank you for this great article. I’ve always thought that this kind of effect would be extremely complicated, but you really simplified it.
Great article. But I have one questions. How to create the depth map image in Photoshop? Is there any plugin to generate that or we need to paint it manually?
in my photoshop its called gradient map, and its under image/adjustments tab
Its under Image/adjustments/gradient map on your top toolbar
Great article!
What are you using to compile src.js?
Sorry a bit late with a response, i just used Webpack, but you can get away with any bundler, like Parcel for example.
Awesome stuff, thanks so much for sharing!
Very nice effect. It’s kind of like using a displacement map in Photoshop. Didn’t know this could be done with WebGL. Thanks for the article. =)
Thank you very much for this article. Tried to replicate the effect already few months back when there was a codepen with a dog but without success, so it’s really appreciated 🙂
Thanks a lot for a great article, Yuriy! ?
Just applied this effect to my newly purchased domain (as a under construction page): https://alexweb.dev/ . The effect looks simply marvelous!