3D films have always felt like a technical novelty to me. The fun factor of slapping on a pair of cheap red-blue 3D glasses to make stuff pop out of the screen has captured the minds of inventors, printers and filmmakers throughout the 19th and 20th centuries. Anaglyph 3D, the kind you need red-blue glasses for, is an outdated technology - but the novelty aesthetic makes it perfect for a shader effect!
Photo by Nesnad on Wikimedia Commons
An Extra Dimension
Many animals, including humans, are able to perceive depth through stereopsis - essentially, different information is received by each of two eyes and combined in the brain to detect depth. Anaglyph 3D exploits this by controlling which information from a 2D image will be seen by each eye, resulting in the brain perceiving depth where it doesnât truly exist. Out of the multitude of techniques for faking 3D - the Nintendo 3DS, for example, angles alternating columns of pixels towards each eye for true stereoscopic vision without glasses - anaglyph 3D is one of the easiest and cheapest to recreate because the only equipment required is a pair of cheap 3D glasses.
There are several ways of recreating the effect in Unity, and all of them involve separating two images and tinting them different colours. Weâll look at two approaches.
Please download the project repository from GitHub if youâd like to follow along!
Depth Sampling
The first approach is to render the scene once and send the image texture to a shader. The shader will then sample the depth texture once to figure out how far away a pixel is from the camera, then will sample the image texture twice, moving the sample UVs horizontally based on the depth - the further away the pixel, the further the UVs move. The two samples are tinted red or blue accordingly and then combined to obtain an anaglyphed image.
Letâs look at the script first. It can be found at Scripts/Image Effects/AnaglyphEffect.cs. This script only needs to pass a strength
parameter to the shader inside OnCreate
- the rest of the class is our boilerplate Render
function and the CreateAssetMenu
attribute.
using UnityEngine;
[CreateAssetMenu(menuName = "Image Effects Ultra/Anaglyph 3D", order = 1)]
public class AnaglyphEffect : BaseEffect
{
[SerializeField]
private float strength = 0.01f;
// Find the Anaglyph shader source.
public override void OnCreate()
{
baseMaterial = new Material(Resources.Load<Shader>("Shaders/Anaglyph3D"));
baseMaterial.SetFloat("_Strength", strength);
}
public override void Render(RenderTexture src, RenderTexture dst)
{
Graphics.Blit(src, dst, baseMaterial);
}
}
For this approach, the interesting bit is inside the shader. Letâs look at Resources/Shaders/Anaglyph3D.shader. Weâll start off including the strength property that we sent to the shader in AnaglyphEffect.cs.
// In Properties.
_Strength("Strength", Float) = 0.1
// In shader code.
uniform float _Strength;
Now we need to figure out a strategy for separating two images using UVs. In Part 2 of this series, when we created the underwater fog effect, we sampled the depth texture in order to determine how far pixels were from the screen - weâll do the same here inside the fragment shader. First, we must include the depth texture variable above the fragment shader.
uniform sampler2D _CameraDepthTexture;
Then weâll use the SAMPLE_DEPTH_TEXTURE
and Linear01Depth
functions weâre familiar with. Remember that the first will retrieve depth values from the texture sample and the second will normalise the value between 0 and 1.
fixed4 frag (v2f i) : SV_Target
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = Linear01Depth(depth);
...
return col;
}
Once we have a depth value, we will calculate the offset weâll use for our UVs. Weâre going to sample _MainTex
twice - once using a set of UVs offset to the left, and once with UVs offset to the right. The âred textureâ shifts the image to the right and the âblue textureâ shifts it to the left.
float2 offset = float2(depth * _Strength, 0.0f);
float2 redUV = i.uv - offset;
float2 blueUV = i.uv + offset;
fixed4 redCol = tex2D(_MainTex, redUV);
fixed4 blueCol = tex2D(_MainTex, blueUV);
We have two texture samples. Now all we need to do is combine them - the final pixel colour takes its red component from redCol
and its green and blue components from blueCol
.
float4 col;
col.r = redCol.r;
col.gb = blueCol.gb;
col.a = 1.0f;
return col;
Our shader is complete! If we create an Anaglyph
effect asset and give it a strength of 0.5, youâll get an output similar to this:
The effect works, although there are some problems involving colours bleeding between each eye. Since weâre just moving image UVs horizontally, the effect is more like an approximation than a true anaglyph - the real thing relies on angling two viewpoints so this version will always be slightly incorrect, although it is efficient since the screen only needs to be rendered once. Results may vary depending on the glasses youâre using - but this effect can be improved a little.
Multiple Exposure
The second approach involves angling two cameras in world space horizontally and rendering the scene twice. As with the first approach, each image is tinted red or blue and combined inside a shader - although there is a lot less work going on inside the shader here and a lot more work going on inside a script.
So letâs look at the shader first. It can be found at Resources/Shaders/Anaglyph3DPro.shader. Weâll look at the Properties first - the one major thing weâll do differently is to not include _MainTex
- weâll pass in two textures called _LeftTex
and _RightTex
instead.
// In Properties.
_LeftTex ("Left Eye Image", 2D) = "white" {}
_RightTex ("Right Eye Image", 2D) = "white" {}
// In shader code.
uniform sampler2D _LeftTex;
uniform sampler2D _RightTex;
It might be confusing why weâre not including _MainTex
- but that will be explained when we look at the script. Now letâs look at the fragment shader. Itâs much shorter than the previous one.
fixed4 frag (v2f i) : SV_Target
{
float4 col;
float4 leftCol = tex2D(_LeftTex, i.uv);
float4 rightCol = tex2D(_RightTex, i.uv);
col.r = rightCol.r;
col.gb = leftCol.gb;
col.a = 1.0f;
return col;
}
Since we donât need to do any processing within the fragment shader to separate out two coloured textures, itâs as easy as just sampling both _LeftTex
and _RightTex
, then outputting a pixel colour based on different colour channels in those images like we did for the UV separation approach.
Weâll move onto the script that goes with this shader. Open up Scripts/Image Effects/AnaglyphProEffect.cs. Inside the Render
function, weâll take a secondary camera with the same parameters as our main camera, angle it slightly to the left and take a snapshot - the resulting image belongs to our right eye. Weâll then angle the camera slightly to the right, take a snapshot and send the output to the left eye. The heavy lifting here is done outside of the shader and both of those textures will be sent to the shader - _LeftTex
and _RightTex
as we discussed before.
In order to achieve this, we need a rotation
member variable to denote how far the camera rotates. Also, since we wonât be able to take snapshot using the main camera on Render
because it is already rendering the scene, we will include a camera prefab and Instantiate
a new camera during OnCreate
. The prefab is a GameObject
containing only a Transform
and a Camera
script, and it is set inactive - this allows us to position it and call Camera.Render
to take a snapshot without interfering with the main image.
[SerializeField]
private float rotation = 1.0f;
[SerializeField]
private Camera cameraPrefab;
private Camera camera;
// Find the Anaglyph Pro shader source.
public override void OnCreate()
{
baseMaterial = new Material(Resources.Load<Shader>("Shaders/Anaglyph3DPro"));
camera = Instantiate(cameraPrefab);
}
Now we have a camera
and an amount to rotate it by. The heavy lifting is done inside Render
. The first step is to copy the main cameraâs transform, aspect ratio, field of view and so on to the temporary camera - Unity provides the CopyFrom
method for this purpose. Then, weâll rotate it to the right by the amount specified by the rotation
variable.
public override void Render(RenderTexture src, RenderTexture dst)
{
// Render camera at pos 1.
camera.CopyFrom(Camera.main);
camera.transform.Rotate(camera.transform.up, rotation);
...
}
Next, weâll need to set up a location for the camera to render to. A temporary render texture seems like a good choice - weâll copy the format of the src
texture to create the new one. Then, weâll assign the new texture as the cameraâs render target.
var leftTexture = RenderTexture.GetTemporary(src.descriptor);
camera.targetTexture = leftTexture;
Now weâll render the camera. The render target is set to a texture - if it had been null
, the output would get displayed on screen instead of sent to the texture. The final step is to send the image to the shader.
camera.Render();
baseMaterial.SetTexture("_LeftTex", leftTexture);
Thatâs one texture handled! Weâll repeat that process exactly for the right-eye texture, except weâll rotate to the left and send the image data to the shader under the _RightTex
name.
// Render camera at pos 2.
camera.CopyFrom(Camera.main);
camera.transform.Rotate(camera.transform.up, -rotation);
var rightTexture = RenderTexture.GetTemporary(src.descriptor);
camera.targetTexture = rightTexture;
camera.Render();
baseMaterial.SetTexture("_RightTex", rightTexture);
The shader has all the data it needs! Weâll use Graphics.Blit
to finish up the processing, then release both those temporary images we used.
Graphics.Blit(src, dst, baseMaterial);
// Release images.
RenderTexture.ReleaseTemporary(leftTexture);
RenderTexture.ReleaseTemporary(rightTexture);
Our effect should be complete now! Letâs see what it looks like.
Conclusion
Weâve seen how to create an anaglyph 3D effect using two approaches. Depending on whether youâd like to prioritise efficiency or accuracy, you could pick one or the other. In the next tutorial, weâll be looking at a cinematic shader aiming to recreate a âfilmicâ look!
Acknowledgements
Iâd like to thank my Patreon supporters for making this content possible. Become a Patron for $1+ to receive PDF versions of all articles, or $5+ to get certain articles early!
This tutorial series uses the following asset packs - available on the Unity Asset Store:
Forest - Low Poly Toon Battle Arena / Tower Defense Pack | AurynSky |
$20 Backers
Special thanks to my $20 backers:
- Gemma Louise Ilett