in Maths, Shaders, Tutorial, Unity

CD-ROM Shader: Diffraction Grating – Part 2

This post completes the series on how to create a shader for CD-ROMs.

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.


In the first part of this tutorial, we have created a first approximation for the iridescent reflections that CD-ROMs exhibit. It’s important to remember that this shader is physically based. To correctly simulate the reflection we want, we need to make sure that the tracks on our CD-ROM are arranged in a circular way. This will ensure a radial reflection.

Slit Orientation

The grating equation that we have derived in The Mathematics of Diffraction Grating has a big limitation: it assumes that the slits are all aligned in the same direction. While this is often the case with insects’ exoskeletons, the bands on the surface of a CD-ROM are arranged in a circular pattern. If we naively implement the solution presented in the section above, we will obtain a rather disappointing reflection (below, right).

To correct this issue, we need to take into account the local orientation of the slits on a CD-ROM. Using the normal vector will not help here since all the slits share the same normal direction, which points away from the surface of the disk. What correctly captures the local orientation of a slit is its tangent vector (above, left).


In the diagram above, the normal direction N is in blue and the tangent direction T in red. The angles the light source and viewer make with the normal direction N are called \theta_L and \theta_V, respectively. The analogous angles with respect to T are \Theta_L and \Theta_V. As stated before, using \theta_L and \theta_V for our calculations will result in a “flat” reflection, since all slits share the same N. We need to find a to use \Theta_L and \Theta_V instead since they correctly capture the local orientation.

So far, we know that:

    \[N \cdot L = \cos{\theta_L}\; \; \; N \cdot V = \cos{\theta_V}\]

    \[T \cdot L = \cos{\Theta_L}\; \; \;  T \cdot V = \cos{\Theta_V}\]

Since T and N are orthogonal, the following property holds:

    \[T \cdot L = \cos{\Theta_L} = \sin{\theta_L}\]

    \[T \cdot V = \cos{\Theta_V} = \sin{\theta_V}\]

That is very convenient also because Cg offers a native implementation of the dot product. What’s left now is to calculate T.

❓ Where does the cosine come from?
All the vectors discussed share a common property: they have a length of 1. For this reason, they are also called unit vectors. A basic operation that can be applied to unit vectors is the dot product.

Loosely speaking the dot product of two unit vectors provides a measure for their alignment. In fact, it turns out that the dot product of two unit vectors is actually the cosine of the angle between them. This is where the cosine in the equations above comes from

This is where the cosine in the equations above comes from.

Calculating the Tangent Vector

To complete our shader, we need to calculate the tangent vector T. Normally, this could be provided directly in the mesh vertices. However, given how simple the surface of a CD-ROM is, we can calculate it directly. Please, keep in mind that the approach presented in this tutorial is rather simple and will only work under the assumption that the surface of your CD-ROM mesh is correctly UV-mapped.

The diagram above shows how the tangent directions are calculated. The underlying assumption is that the surface of the disk is UV mapped like a quad, with coordinates ranging from (0,0) to (1,1). Once that is known, each point on the CD-ROM surface is remapped onto (-1,-1) to (+1,+1). With this change of the frame of reference, we have that the new coordinate of a point also corresponds with its direction away from the centre (green arrow). We can rotate that direction 90 degrees to find a vector that is tangent to concentric tracks of the CD-ROM (in red).

These operations need to be done in the surf function of the shader since the UV coordinates are not available in the lighting function LightingDiffraction.

// IN.uv_MainTex: [ 0, +1]
// uv:            [-1, +1]
fixed2 uv = IN.uv_MainTex * 2 -1;
fixed2 uv_orthogonal = normalize(uv);
fixed3 uv_tangent = fixed3(-uv_orthogonal.y, 0, uv_orthogonal.x);

What’s left now is to convert the calculated tangent from object space to world space. The conversion will take into account the object translation, rotation and scale.

worldTangent = normalize( mul(unity_ObjectToWorld, float4(uv_tangent, 0)) );
❓ How to pass the tangent direction to the lighting function?
The iridescent reflection is calculated in the lighting function LightingDiffraction. However, it needs the tangent vector worldTangent which is calculated in the surface function surf. The signature of the lighting function cannot be changed, meaning that it cannot be made to accept more parameters than the ones it has already.

If you are unfamiliar with shaders, there is a very simple way to pass additional parameters. Simply add them as variables in the body of the shader. In this case, we can use a shared variable float3 worldTangent; which is initialised by surf and used by LightingDiffraction.


❓ How to switch between coordinate spaces?
Coordinates are not absolute. They are always dependent on a reference point. Depending on what we are doing, it might be more convenient to store vectors in a particular coordinate space instead of another.

In the context of shaders, it is possible to change the coordinate spaces of a point simply by multiplying it with a special matrix. The one that allows converting coordinates expressed in object space in world space is unity_ObjectToWorld. If you are using an old version of Unity, that constant will be called _Object2World instead.

⭐ 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.

Putting All Together…

We now have all we need to calculate the colour contribution of the iridescence reflection:

inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
	// Original colour
	fixed4 pbr = LightingStandard(s, viewDir, gi);
	// --- Diffraction grating effect ---
	float3 L = gi.light.dir;
	float3 V = viewDir;
	float3 T = worldTangent;

	float d = _Distance;
	float cos_ThetaL = dot(L, T);
	float cos_ThetaV = dot(V, T);
	float u = abs(cos_ThetaL - cos_ThetaV);

	if (u == 0)
		return pbr;

	// Reflection colour
	fixed3 color = 0;
	for (int n = 1; n <= 8; n++)
		float wavelength = u * d / n;
		color += spectral_zucconi6(wavelength);
	color = saturate(color);

	// Adds the refelection to the material colour
	pbr.rgb += color;
	return pbr;
❓ How this relates to the rainbow?
The variable wavelength declared in the for loop contains the wavelengths of light that contribute to the iridescent reflection of the current pixel.

Each wavelength in the visible range (400 to 700 nanometers) is perceived by the human brain as a different colour. In particular, the wavelength in the visible range maps to the colours of the rainbow.

The post Improving the Rainbow shows how each wavelength can be converted to its respective colours. The function used for this project is spectral_zucconi6, which is an optimised version of the solution presented in the GPU Gems shader book.


You can find the complete series here:

Become a Patron!
You can download the Unity package for the CD-ROM Shader effect on Patreon.

💖 Support this blog

This website exists thanks to the contribution of patrons on Patreon. If you think these posts have either helped or inspired you, please consider supporting this blog.

Patreon Patreon_button

📧 Stay updated

You will be notified when a new tutorial is released!

📝 Licensing

You are free to use, adapt and build upon this tutorial for your own projects (even commercially) as long as you credit me.

You are not allowed to redistribute the content of this tutorial on other platforms, especially the parts that are only available on Patreon.

If the knowledge you have gained had a significant impact on your project, a mention in the credit would be very appreciated. ❤️🧔🏻

Write a Comment



  • Car Paint Shader: Thin-Film Interference - Alan Zucconi December 5, 2017

    […] CD-ROM Shader: Diffraction Grating – Part 2 – Alan Zucconi August 28, 2022 […]

  • The Nature of Light - Alan Zucconi December 5, 2017

    […] 7. CD-ROM Shader: Diffraction Grating (Part […]