in shader, tutorial, Unity3D

CD-ROM Shader: Diffraction Grating – Part 1

Share Button

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:

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:

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:

to:

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:

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:

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

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:

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:

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

📧 Stay updated

A new tutorial is released every week.

💖 Support this blog

This websites 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.

Paypal
Ko-fi
Twitter_logo

Write a Comment

Comment

  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

  • The Nature of Light - Alan Zucconi March 19, 2018

    I tried, but it still didn’t work tho. I guess I should buy the source then
    Thanks a lot anyway