I made this VHS Video shader in unreal as part of the first shader challange for the Technically Speaking discord. Our theme was ‘Retro’ and I may have been toying with ideas relating to an FMV game, so decided to throw the two ideas together.
In the spirit of sharing knowledge that the server is built on, I’ve written a tutorial below on how I set it up.
For anyone who prefers to see the source, the project files can be found here. Unzip these folders and copy them into the content folder of your project.
Feel free to comment on here or message me on twitter if you have any questions about the setup or how to do things. 🙂
I’ve also set up a ko-fi now, so if you get some use out of this and have pennies to spare, a tip would be much appreciated!
Start off by importing a video texture and creating a basic unlit material that uses it. Then make a blueprint that opens your imported media source on level start.
Instructions on how to import, setup and use a video texture in level can be found on the unreal documentation page, so I’m not going to cover it here, but feel free to reach out with any questions!
The video was just a silly horror-esqe clip I took with my phone, looking at the creepiest things I could find in my house. The quality of the video doesn’t matter – not only is the video going to be down-ressed in further steps, but low quality makes it more old-school!
In order to really give the video an old school feel, we’re going to add some loss of focus that takes the sharpness out of the image. (Thanks to Simon for this suggestion!)
This one is a little bit of a cheat – rather than writing your own function, grab the SpiralBlur-Texture node. If this doesn’t accept an external texture sample as an input, grab the custom node from inside and use that directly in your graph.
Convert the inputs to this node into scalar parameters. I found the values below worked well. A very small distance, but very large number of distance steps gives us a substantial amount of blur but keeps the coherence of the image together.
This blur looks great, but we don’t want our camera out of focus the whole time! To fix this, we’re going to lerp the original image and the blurred image, using a modulo operator to switch between them.
Modulo (the Fmod node in unreal) returns the remainder of a division. So the modulo of 4 and 2 is 0, where the modulo of 5 and 2 is 1.
By getting the modulo of Time and a scalar parameter, we create an uneven oscillation of values. This produces a more natural look than a wave function. By rounding this, we create a jarring switch between the two which suits the VHS look well.
You can now use the scalar parameter to control how often you see each version of the image. A value like 2 will evaluate to 0 often, whereas a value like 5 will be more likely to have a remainder so will come out as 1 or above. You can see how the output scales in the gif above.
I put the normal image in my A slot and the blurred image in my B slot and then, because I was seeing the blurred image more often than the non-blurred one, stuck in a one minus to flip the values around (one minus returns 1 – the input value). This was mostly put in there from a graph neatness perspective so feel free to skip this or to change the order of your inputs in your own graph!
The dark patches appearing on the gif above are where the result of our modulo is a negative number. Personally I think it adds to the effect, but if you’d rather get rid of it, a saturate node before the lerp will do the trick. (Saturate clamps between 0-1 in a single ALU instruction, where the Clamp node may represent more than one instruction on some hardware.)
Now that we have a nice blurry image, we’re going to make some edits to the UVs to add stretching, chromatic aberration and an era appropriate resolution!
The resolution of a VHS is 333×430, so our video should reflect this. I’ve explained this down-res technique in a previous post, however that was in hlsl, so I’ll go over it again for unreal.
Create a scalar parameter or constant for your X and Y resolutions and set them to 333 and 430 respectively. Append them to create a float2 value, then floor this.
Multiply your texture coord with this to create a grid of your image. Once the grid is created, floor it, then divide by the resolution. This will take your tiling back down, but because of the floor, it will be clamped to the grid we initially created.
This can then be used as as the UV input for your video texture.
The next thing we’re going to add is an occasional stretch of the image.
This is created by multiplying the Y resolution with a lerp between a sine wave and 1.
We use a sine wave so that every time we see the stretch it will be in a slightly different position. Create a parameter for stretch speed, then multiply this with time. Get the sine of this, and we have a basic wave.
Next, multiply this by 0.5, then add 0.5. This takes our wave from -1 -> 1 space into 0 -> 1 space ( -1 * 0.5 = – 0. 5, + 0.5 = 0 then 1 * 0.5 = 0.5, + 0.5 = 1).
Finally, multiply this with another scalar parameter to control the strength of our wave.
For the interpolator, we’re going to use the same modulo logic we used for the blurring. Create a new scalar parameter for how often we want to see the stretched version, modulo that with time, then round it.
Our final uv tweak is chromatic aberration. This is when we get red and blue around the edges of our image as the channels are offset from one another.
To do this, we are going to replace our single sample for the non-blurred version of the texture with three new samples, two of which are offset.
Take the UVs we made as a result of resolution changes, and add a scalar parameter to it for the offset. Append this with a 1 in the y, then input this as the uv for the first texture.
Take the UVs as they are for the second texture.
For the third, multiply the offset parameter by -1, then add it to the UVs and append this with 1 in the y.
Once these are setup, take the R from the first texture, the G from the second and the B from the third and append these to make a final color. This can then be input to the lerp with the blur.
Now that we’ve finished playing around with our UVs and base image, we can start to add the static and other image effects that will make the shader instantly recognisable as being a VHS style video.
For these effects you’ll need a channel packed texture with scanlines, a block of white and the date/time. I recommend putting the date/time on G, scanlines on R and white block on B, to take advantage of differing compression quality between channels.
If you’re using Photoshop, I’d recommend using a scatter brush, then motion blur, then add noise to create the scanlines effect.
The first thing we’re going to do is take the output of the lerp between the blurred and normal video and add some adjustments. Add a power and multiply node, then create parameters for these. The power lets us adjust the gamma of our image and the multiply gives us a color tint.
The next thing we’re going to add is a grain effect. This will be multiplied with the output of our adjustments.
Rather than a texture, grab the simplex noise node. We’re going to lerp between noise and one minus noise so that the position of the visible noise changes. (As we’re multiplying, only black areas will be visible.)
The interpolator is a rounded sine wave, so add Time multiplied by a new scalar parameter for the speed, then get this sine of this. Multiply and add by 0.5 as we covered above, then round it so we only get -1, 0 or 1 values.
After this, add another parameter for strength, reverse it using one minus and add it to the lerp. We reverse this because the grain becomes stronger as there is less white in it, but this doesn’t make a whole lot of sense to a user, so this is there to make the inputs more clean. This can then be multiplied by our adjustment output.
The next thing you want to add are scanlines. This is where the texture we made earlier comes in. Take the white block channel of your texture, multiply it by a constant float 2 of 1, 75 in order to create a number of thinner lines. Then use the panner node to move this in the y direction. I left these as constants, but feel free to add parameters for speed and tiling!
Multiply this with the grain and adjustments.
The next effect we’ll add is static. These are the long static bands that really sell a VHS feel.
Like the scanlines, we’re using our texture with a panner, but we have a bit of logic on the coordinates and speed to vary things a little.
Start by taking the scanline channel of your texture and plugging a panner node into it.
For the coordinate, we’re going to create a sin wave that is used to scale the texture up and down. Multiply Time by a scalar parameter for your speed, then get the sine of this and multiply it with another parameter for the amplitude. I’ve called the amp ‘variation’ because it changes how large and therefore how different from the original the texture becomes. Multiply this by your texture coordinate and you have a scanline that scales up and down!
For the speed, we want to pan in the y but not the x, so put down an append node with a constant of 0 in x. Multiply the wave by a new speed parameter (not the same as the scaling speed – coherence creates a less glitchy look), then put this in the y slot. This gives us an ociliating speed.
Add the texture sample to your grain and scanline multiplication.
This looks cool, but its no good if it says on screen forever. Multiply this with yet another variation on the modulo that we’ve used for other effects.
The last thing we’re going to do is add a date/time overlay. Just take the date/time channel from your texture and add it to the previous effects.
After this, you can add a multiplier to the whole effect for a stronger emissive glow.
And we’re done! Here’s a screenshot of the parameters I had and the settings I used in the final video above.
While this wasn’t really intended to be particularly performant or made with a games application in mind, I’d encourage everyone to be aware of how their shaders affect GPU timing and memory!
This is fairly low on instructions and samples, even with with number of times we sampled the video texture.
Looking at the stats, it took ~0.3ms at runtime, which in my opinion is acceptable even if the video wasn’t the main focus of the scene. If we were using this in a game aiming for 60fps I’d perhaps have some reservation, with 0.3ms being
Video textures cannot be streamed like regular textures. You can generate mips for them, but they don’t have the same behaviour, so we need to consider that our video will be loaded at all times.
My runtime non streaming memory with this in the scene was 130 MB. That’s a huge amount of memory for a single texture. If this is the singular focus of the scene, it might be okay, but if we’re having this in a larger level it starts to become a concern.
There are a couple of options for making this cheaper if you want to use this technique in a game:
- Make the video shorter – my video was fairly long
- Make the video smaller – I just used whatever the default res on my phone was then changed it in the shader, your source video could be low res.
- If you can’t afford video at all – make an atlas texture out of stills and run through them in the shader.
That’s it! Thanks for reading, and have fun making spooky VHS effects – ’tis the Halloween season after all!