in Shaders, Tutorial, Unity

CD-ROM Shader: Diffraction Grating – Part 1

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 d 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
}

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;

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

to:

#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;
}

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);		
}

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 w which satisfy the grating equation:

    \[\left | \sin{\theta_L} - \codt \sin{ \theta_V } \right |= \frac{n \cdot w}{d}\]

with n being an integer number greater than 0.

Given a certain pixel, the values for \theta_L (given by the light direction), \theta_V (given by the view direction) and d (the gap distance) are known. The unknown variables are w and n. The easiest thing to do is to loop over certain values of n, 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;
}

In the snippet above we use values of n 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.

💖 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
Twitter_logo

YouTube_logo
📧 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

Comment

12 Comments

  1. Hello Alan Zucconi:
    Thanks for your great tutorial.
    I have a question about the CG code.
    In the “For loop” of the code, you set n not bigger than 8,
    Can you explain the reason of that?
    Thank you very much.

    • Sure!
      I am looping through multiple of the wavelength to take into account all colours that are subjected to constructive interference.
      With values higher than 8 you won’t see any difference since the wavelengths are outside the visible spectrum!

  2. Nice explanation, but “A link to download the Unity project used in this series is also provided at the end of the page” should probably specify that it is a link to purchase the project. I was a little dumbfounded when I realized it would cost $10 to see the code in action. I can understand selling the shader, but would have preferred not being deceived.

    • I believe this problem stems from the expectation that all content that is online should be free by default. If someone tells you that the biscuits are in the aisle number 3 of a supermarket, you do not act surprised because “he didn’t tell me I had to pay for it”. Likewise, don’t forget the website is called alanzucconi dot com(mercial).

      Also, just a little clarification. I am not selling the shader. With that you have access to all my content. So it’s more like a subscription (Netflix style) rather than a more traditional buy-what-you-want model.

      Lastly, there was no deception. At the end of the tutorial, there is indeed a link to download the Unity package. A deception implies a lie. For instance, if you had paid and the package did not include what I have promised, then and only then you could say you have been deceived. I understand you came here with the expectation of accessing the Unity package for free; but please, keep in mind that was *your* expectation. If you’re on my website I believe is because you are a content creator yourself. Trust me when I say that when you’ll start selling your games, you’ll experience a lot of comments from people who are saying exactly what you’re saying to me now. And is not nice.

  3. hey Alan,
    super amazing tutorial! As a beginner and student of CG art, I’m really grateful for your help!!
    I followed each step showed in your article, but there is a error that “Shader error in ‘Custom/Iridescent_1’: Unexpected identifier “worldTangent”. Expected one of: typedef const void inline uniform nointerpolation extern shared static volatile row_major column_major struct or a user-defined type at line 27″
    ///////////////////////////and here is my shader code////////////////////////////////////////
    Shader “Custom/Iridescent_1” {
    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
    }
    SubShader{
    Tags { “RenderType” = “Opaque” }
    LOD 200

    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Diffraction fullforwardshadows
    #pragma target 3.0
    #include “UnityPBSLighting.cginc”

    float _Distance;
    float3 worldTangent;
    //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);
    worldTangent = normalize(mul(unity_ObejectToWorld, float4(uv_tangent, 0)));

    sampler2D _MainTex;

    struct Input {
    float2 uv_MainTex;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    // Add instancing support for this shader. You need to check ‘Enable Instancing’ on materials that use the shader.
    // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
    // #pragma instancing_options assumeuniformscaling
    UNITY_INSTANCING_CBUFFER_START(Props)
    // put more per-instance properties here
    UNITY_INSTANCING_CBUFFER_END

    void surf (Input IN, inout SurfaceOutputStandard o) {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    // Metallic and smoothness come from slider variables
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
    }

    //actually this Global Illumination is a proxy for Unity Standard PBR function
    void LightingDiffraction_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
    {
    LightingStandard_GI(s, data, gi);
    }

    // Based on GPU Gems
    // Optimised by Alan Zucconi
    inline fixed3 bump3y(fixed3 x, fixed3 yoffset)
    {
    float3 y = 1 – x * x;
    y = saturate(y – yoffset);
    return y;
    }
    fixed3 spectral_zucconi6(float w)
    {
    // w: [400, 700]
    // x: [0, 1]
    //Mapping waves to RGB modularly
    fixed x = saturate((w – 400.0) / 300.0);

    const float3 c1 = float3(3.54585104, 2.93225262, 2.41593945);
    const float3 x1 = float3(0.69549072, 0.49228336, 0.27699880);
    const float3 y1 = float3(0.02312639, 0.15225084, 0.52607955);

    const float3 c2 = float3(3.90307140, 3.21182957, 3.96587128);
    const float3 x2 = float3(0.11748627, 0.86755042, 0.66077860);
    const float3 y2 = float3(0.84897130, 0.88445281, 0.73949448);

    return
    bump3y(c1 * (x – x1), y1) +
    bump3y(c2 * (x – x2), y2);
    }

    inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
    {
    //Original color
    fixed4 pbr = LightingStandard(s, viewDir, gi);
    //
    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 color
    fixed3 color = 0;
    for (int n = 1; n <= 8; n++)
    {
    float wavelength = u * d / n;
    color += spectral_zucconi6(wavelength);
    }
    color = saturate(color);

    //Adds the reflection to the material color
    pbr.rgb += color;
    return pbr;
    }
    ENDCG
    }
    FallBack "Diffuse"
    }

    Thank you very much!!

    • Hi, thank you very much!
      I had a quick look at the shader code and it seems there are a few issues!

      One is a typo in “unity_ObejectToWorld”! 🙂

      I suspect the actual issue is that you have to calculate the world tangent in the surf function. At the moment that code is floating outside a method, which is unlikely to work!

      If you’re having more issues, the full source code is available here: https://www.patreon.com/posts/13032957

Webmentions

  • Game Dev Digest Issue #159 - Level Up Your Skills - GameDev.net - GraphicUV March 8, 2022

    […] Quest for Very Wide Outlines – An Exploration of GPU Silhouette Rendering.bgolus.medium.comCD-ROM Shader: Diffraction Grating – Part 1 – This post will guide you through the creation of a shader that reproduces the rainbow […]

  • The Nature of Light - Alan Zucconi March 8, 2022

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