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.

Introduction

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 is in blue and the tangent direction in red. The angles the light source and viewer make with the normal direction are called and , respectively. The analogous angles with respect to are and . As stated before, using and for our calculations will result in a “flat” reflection, since all slits share the same . We need to find a to use and instead since they correctly capture the local orientation.

So far, we know that:

Since and are orthogonal, the following property holds:

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

❓ Where does the cosine come from?

unit vectors. A basic operation that can be applied to unit vectors is the dot product. All the vectors discussed share a common property: they have a length of 1. For this reason, they are also called. A basic operation that can be applied to unit vectors is the 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 . 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); 1 2 3 4 5 // 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)) ); 1 worldTangent = normalize ( mul ( unity_ObjectToWorld , float4 ( uv_tangent , 0 ) ) ) ;

❓ How to pass the tangent direction to 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. The iridescent reflection is calculated in the lighting function. However, it needs the tangent vector worldTangent which is calculated in the surface function. 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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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?

wavelength declared in the for loop contains the wavelengths of light that contribute to the iridescent reflection of the current pixel. The variabledeclared 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.

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.