This post will guide you through the creation of a shader that reproduces the rainbow reflections that can be seen on CD-ROMs and DVDs. This tutorial is part of a longer series on physically based iridescence.

You can find the complete series here:

A link to download the Unity project used in this series is also provided at the end of the page.

Introduction

In a previous tutorial, The Mathematics of Diffraction Grating, we have derived the equations that capture the very nature of the iridescent reflections that certain surfaces exhibit. Iridescence occurs on materials featuring a repeating surface pattern which size is comparable to the wavelength of the light they reflect.

The optical effects we are interested in reproducing ultimately depends on three factors: the angle of the light source with the surface normal (light direction), the angle of the viewer (view direction) and the distance between the repeating gaps.

We want our shader to add iridescent reflections on top of the normal effects that the Standard material usually comes with. For this reason, we will extend the lighting function of a Standard Surface shader. If you are unfamiliar with the procedure, Physically Based Rendering and Lighting Models provides a good introduction.

Creating a Surface Shader

The first step is to create a new shader.Since we want to extend a shader that already supports physically based lighting, we will start with a Standard Surface Shader.

The newly created CD-ROM shader needs a new property: the distance used in the diffraction grating equation. Let’s add it to the Properties block, which should now look like this:

Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 _Distance ("Grating distance", Range(0,10000)) = 1600 // nm } 1 2 3 4 5 6 7 8 9 Properties { _Color ( "Color" , Color ) = ( 1 , 1 , 1 , 1 ) _MainTex ( "Albedo (RGB)" , 2D ) = "white" { } _Glossiness ( "Smoothness" , Range ( 0 , 1 ) ) = 0.5 _Metallic ( "Metallic" , Range ( 0 , 1 ) ) = 0.0 _Distance ( "Grating distance" , Range ( 0 , 10000 ) ) = 1600 // nm }

This will create a new slider in the Material Inspector. The _Distance property, however, still needs to be coupled with a variable in the CGPROGRAM section:

float _Distance; 1 float _Distance ;

We are now ready to start.

Customising the Lighting Function

The first step we need to take is to replace the lighting function of the CD-ROM shader with a custom one. We can do this by altering the #pragma directive from:

#pragma surface surf Standard fullforwardshadows 1 #pragma surface surf Standard fullforwardshadows

to:

#pragma surface surf Diffraction fullforwardshadows 1 #pragma surface surf Diffraction fullforwardshadows

This forces Unity to delegate the lighting calculation to a function called LightingDiffraction. It is important to understand that we want to extend the behaviour of this Surface shader, not override it. For this reason, out new lighting function will start by simply calling Unity’s Standard PBR lighting function:

#include "UnityPBSLighting.cginc" inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi) { // Original colour fixed4 pbr = LightingStandard(s, viewDir, gi); // <diffraction grating code here> return pbr; } 1 2 3 4 5 6 7 8 #include "UnityPBSLighting.cginc" inline fixed4 LightingDiffraction ( SurfaceOutputStandard s , fixed3 viewDir , UnityGI gi ) { // Original colour fixed4 pbr = LightingStandard ( s , viewDir , gi ) ; // <diffraction grating code here> return pbr ; }

As you can see from the snippet above, the new LightingDiffraction simply calls LightingStandard and returns its value. If we compile the shader now, we will see no difference in the way it renders materials.

Before continuing, however, we need to create an additional function to handle the Global Illumination. Since we are not interested in changing that behaviour, our new global illumination function will once be a proxy for Unity’s Standard PBR function:

void LightingDiffraction_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi) { LightingStandard_GI(s, data, gi); } 1 2 3 4 void LightingDiffraction_GI ( SurfaceOutputStandard s , UnityGIInput data , inout UnityGI gi ) { LightingStandard_GI ( s , data , gi ) ; }

Lastly, please note that since we are using LightingStandard and LightingDiffraction_GI directly, we will need to include UnityPBSLighting.cginc our shader.

⭐ Suggested Unity Assets ⭐ Unity is free, but you can upgrade to Unity Pro or Unity Plus subscriptions plans to get more functionality and training resources to power up your projects.

Implementing the Diffraction Grating

This is the core of our shader. We are finally ready to implement the diffraction grating equations seen in The Mathematics of Diffraction Grating. In that post, we concluded that the viewer sees an iridescent reflection which is a sum of all the wavelengths which satisfy the grating equation:

with being an integer number greater than .

Given a certain pixel, the values for (given by the light direction), (given by the view direction) and (the gap distance) are known. The unknown variables are and . The easiest thing to do is to loop over certain values of , to see which wavelengths satisfy the grating equation.

When we know which wavelengths contribute to the final iridescent reflection, we calculate their associated colours and add them together. The Improving the Rainbow discussed several approached to convert wavelengths from the visible spectrum into colours. for this tutorial, we will use spectral_zucconi6 as it provides the best approximation with the cheapest computational cost.

Let’s see a possible implementation below:

inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi) { // Original colour fixed4 pbr = LightingStandard(s, viewDir, gi); // Calculates the reflection color fixed3 color = 0; for (int n = 1; n <= 8; n++) { float wavelength = abs(sin_thetaL - sin_thetaV) * d / n; color += spectral_zucconi6(wavelength); } color = saturate(color); // Adds the refelection to the material colour pbr.rgb += color; return pbr; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 inline fixed4 LightingDiffraction ( SurfaceOutputStandard s , fixed3 viewDir , UnityGI gi ) { // Original colour fixed4 pbr = LightingStandard ( s , viewDir , gi ) ; // Calculates the reflection color fixed3 color = 0 ; for ( int n = 1 ; n < = 8 ; n ++ ) { float wavelength = abs ( sin_thetaL - sin_thetaV ) * d / n ; color += spectral_zucconi6 ( wavelength ) ; } color = saturate ( color ) ; // Adds the refelection to the material colour pbr . rgb += color ; return pbr ; }

In the snippet above we use values of up to 8. For better results, you can go higher, although this should already account for a significant part of the iridescent reflection.

We now have one last thing left to do. Calculating sin_thetaL and sin_thetaV. That requires to introduce yet another concept: the tangent vector. We will see how to calculate that in the next part of this tutorial.

Conclusion

You can find the complete series here:

Become a Patron!

You can download the Unity package for the CD-ROM Shader effect on Patreon.