Card Shaders

by Justin Witte

For my R&D project, I wanted to create multiple shaders suitable for cards in a card game. These shaders should be easily applicable on any card. I wanted to create shaders with effects like the standard holographic effect, up to 3d effects and crystallized effects.

The idea behind creating these shaders is that I should be able to use them for any project of my own in which the player needs to use cards. These shaders could be cosmetics the player would be able to unlock, or perhaps special effects when something happens in the game.

Contents

  1. Goal
  2. Distortion shader
  3. Crystal Shader
  4. 3D shader
  5. Outline shader
  6. Holographic shader
  7. Result
  8. Conclusion
  9. Sources

Goal

My goal for the product is to create a minimum of four different shader effects applicable on cards, in 3D space. These shader effects should be easily applicable on multiple cards, by simply adding the shader effect to the material of the card.

Distortion shader

For the first shader, I wanted to create a crystal effect in which it would look like the card would have a lot of different edged parts, just like a crystal. So, my first thought was that I should manipulate the vertices of the card using a heightmap. My thought process was that this would create the different edges in the card, thus creating the crystallization. I could then add a lighting effect to create the shine effect a crystal has. So I applied this to the card and added some simple diffuse lighting to test it out. This was the result.

This did not really work the way I wanted it to. The edges got smoothed automatically, due to which it did not really give the crystal effect that I wanted to create. However, I thought that I would maybe be able to create a different shader out of this. So I starting trying something different with it.

When I applied a rainbow texture to the vertex manipulation and moved the shader overtime, the rainbow would move in a variety of different ways at the same time, due to the vertex manipulation. After toying around I got this.

I changed the variables a bit again and applied the effect on a self-made card. Next to that I used a mask to make sure the effect is only displayed on the art part of the card, this way it will not block the text. This gave me the following final result

I created this effect, by first manipulating the vertices in the vertex shader. Then, I rotate the uv of the rainbow asset and manipulate it more by the direction of the camera and the current timestamp, due to which it flows through the card instead of being static.

v2f vert(appdata v)
{
     v2f o;
     o.uv = TRANSFORM_TEX(v.uv, _HeightMap);
     o.uv2 = TRANSFORM_TEX(v.uv2, _Rainbow);
     o.uv3 = TRANSFORM_TEX(v.uv3, _Mask);
     o.position = UnityObjectToClipPos(v.vertex);

     // modify the vertices of the plane
     fixed4 heightMap = tex2Dlod(_HeightMap, float4(o.uv, 0, 0));
     v.vertex.y += heightMap.x * 100;

     // rotate the rainbow slightly
     o.uv2 = rotateRadians(o.uv2, float2(0, 0), -1);

     // calculate the camera direction towards this vertex
     o.camDirection = normalize(_WorldSpaceCameraPos.xyz - v.vertex.yxzw);

     // make sure the rainbow moves over time and according to the camera direction
     o.uv2 -= o.camDirection;
     o.uv2 -= _Time.y;
     return o;
}

Afterwards, in the fragment shader, I modify the color of the rainbow in the shader according to the camera direction and I apply the mask.

fixed4 frag(v2f i) : SV_Target
{
    // sample the textures of the rainbow and the mask
    const fixed4 rainbow = tex2D(_Rainbow, i.uv2);
    const fixed4 mask = tex2D(_Mask, i.uv3);

    // make sure the y axis of the camera direction is always positive
    i.camDirection.y = abs(i.camDirection.y);

    // calculate the rainbow color according to the camera direction
    fixed4 rainbowColor = float4(i.camDirection, 0) * rainbow * 8;

    return rainbowColor * 0.5 * mask.w;
}

Crystal Shader

After trying to create a crystal shader and creating something completely different, I tried once again to create the crystal shader, this time trying a different method. I tried using this normal map. I can use this normal map to get a normal vector for every fragment, instead of for every vertex. This makes it way easier to get the detailed edges. As I can now use the normal of every fragment to calculate the lighting. This worked exactly like I wanted it to work. So I started adding a lighting effect. For this effect, I used diffuse lighting. In this lighting effect, I use the direction of the light to calculate the dotproduct between it and the normal of the fragment. It uses this along with a set diffuse color and the color of the light to calculate the lighting. After adding the lighting, I used a gradient to let the shader flow through the card. I also rotate the effect over time, to create a circling effect.

float4 frag (v2f i) : SV_Target
{
    // fetch the normal from the texture
    const float4 normal = tex2D(_NormalMap, i.uv);
    float4 visible = tex2D(_RangeTex, i.uv2);

    //fetch background texture
    float4 backgroundTexture = tex2D(_BackTex, i.uv3);

    // add lighting effect
    float4 lighting = calculateLighting(normal) * 0.7;

    // apply lighting effect to the fragment
    return lighting * visible.r + backgroundTexture;
}

This gave me the following end result.

3D Shader

As for the third shader, I wanted to create a 3d shader. To create this I wanted to use the same method the recent Spiderman games use to create their rooms in the skyscrapers. This method is called interior mapping. It uses parallax mapping on a cubemap to display the cubemap inverted onto a plane.

What interior mapping basically does is check where the player is looking on the plane and in the cubemap. It will then simulate the part of the cubemap the player is looking at onto the plane in the position the direction crosses the plane. This way it looks like the player is looking into the card, as if it were 3D. But they are actually just looking at a simple plain, a flat surface.

I could have also created this effect by creating a box behind the card, which would only be visible through the card. I decided not to use this technique for two main reasons. Firstly, this technique would be heavier as it would require more vertices and the amount of vertices would increase when adding multiple objects inside the card. Secondly, this effect would not work when the card would be laying down on a flat surface, as this surface would block the view to the box behind the card and the surface would be visible through the card instead.

So, I used interior mapping to create the 3d effect in the card. Which gave me the following result at first.

This was exactly the result I was looking for. However, I did want to add the frame of the card in front. As I only wanted the art of the card to be 3d, the frame on the card should still be in front. So I added the front of the card. Next to this I added a better looking cube map as well. Giving this result. To create this, I first calculated the view direction in the vertex shader.

v2f vert(appdata v)
{
    // Code partially inspired by: (Game Dev Guide, 2020)

    v2f o;
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _CubeMap);
    o.uv2 = TRANSFORM_TEX(v.uv2, _FrontTex);

    // get the direction of the camera towards the object
    float4 objCam = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
    const float3 viewDirection = v.vertex.xyz - objCam.xyz;
    
    const float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
    const float3 bitangent = cross(v.normal.xyz, v.tangent.xyz) * tangentSign;

    // calculating the view direction in tangent space
    o.realViewDir = float3(
        dot(viewDirection, v.tangent.xyz),
        dot(viewDirection, bitangent),
        dot(viewDirection, v.normal)
    );
    o.viewDirection = o.realViewDir * _CubeMap_ST.xyx;
    
    return o;
}

I use this view direction to calculate what part of the cube map needs to be visualized at what part of the plane. Afterwards, I put the card frame in front of the cubemap, by only using the cubemap colors at parts where the frame is transparent.

fixed4 frag(v2f i) : SV_Target
{
    // Code partially inspired by: (Game Dev Guide, 2020)

    // get the uv of the cubemap, making sure it is always below 1
    const float2 roomUV = frac(i.uv);

    // multiplying the uv to fit inside the width and height of the card
    float3 pos = float3(roomUV * 2 - 1, 1);

    // making sure the cubemap rotates correctly according to the view direction
    const float3 id = 1 / i.viewDirection;

    // getting the closest wall
    const float3 k = abs(id) - pos * id;
    const float kMin = getLowestFloat(k);
    pos += kMin * i.viewDirection;

    // flip the x axis to see the front of the cubemap as the part in the back 
    pos.x *= -1;

    // fetch color data from the assets
    const fixed4 room = texCUBE(_CubeMap, pos.xyz);
    fixed4 front = tex2D(_FrontTex, i.uv2);
    
    return room * ((front.w - 1) * -1) + front;
}

Now that I have interior mapping working with a frame in front of it, I wanted improve upon the interior mapping. I wanted to be able to add objects into the card, to create more depth in the card. So I took the same calculations I used for the cubemap, but used them on a 2D texture instead and modified them for it. With this function I can put an object into the card, on any depth I would want.

fixed4 placeObjectInDepth(sampler2D objectTexture, float2 uv, float3 viewDirection, float depth)
{
// getting the position of the uv correct, making sure the asset fits the whole object
float3 position = float3(uv * 2 - 1, 1);

// making sure the objects rotates in the right direction when angling the view
const float3 id = 1 / viewDirection;

// applying the depth to the position of the object
const float3 closestSide = abs(id) - position * id;
position += closestSide.z * viewDirection * depth;

// fixing the offset of the object and returning the texture
position.xy += 1;
return tex2D(objectTexture, position / 2);
}

Using this all together gives me the following result.

Outline Shader

Next, I wanted to create an outline shader. This shader specifically highlights the outlining of the card and any objects in it. I wanted to use gouraud lighting for the highlighting. But first I needed a way to detect the outlines of the card. I decided to do this by using a mask. I can use this mask to make sure only certain parts of the asset will be highlighted. The mask I created for this looks like this.

Instead of using a mask, I first tried detecting the edges through code. I tried this by checking the colors of the fragments and if it was dark enough, it would be highlighted as an outline. However this would create a problem when using dark colors in the card. More parts of the card than intended would then be highlighted as outline. An example of this problem is shown here. This result is way different from the actual outlines of the card. As some of the outlines are not actually a dark color and a lot of fragments of the card, which aren’t part of an outline, are dark enough to get detected. So I decided to use the mask instead.

Next, all that was left was create the gouraud lighting and applying it to the parts which are white in the mask. The calculations for the gouraud lighting go as follows.

float4 addGouraudLighting(float3 normal, float4 vertex)
{
// calculating the normal, light and view directions
const float3 normalDirection = normalize(mul(unity_ObjectToWorld, normal));
const float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
const float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, vertex));

// calculating the diffuse lighting
const float nDotL = max(0, abs(dot(normalDirection, lightDirection)));
const float3 diffuse = _DiffuseColor * _LightColor0.rgb * nDotL;

// calculating the reflection direction and the power of it
const float3 reflectionDirection = reflect(-lightDirection, normalDirection);
const float rv = max(0, dot(reflectionDirection, viewDirection));

// calculating the power of specular light and its direction
const float specularAmount = pow(rv, _Shininess);
const float3 specularLight = _SpecularColor.rgb * _LightColor0.rgb * specularAmount;

return float4(diffuse + specularLight, 1);
}

This together gives me the following final result.

Holographic Shader

For the last shader, I created a holographic shader. This shader emits rainbow colors, but only when looked at from an angle, this is inspired by holographic effect which can be seen on some cards from trading card games in real life, which only emit this effect when you look at the card from an angle as well. To do this, I used a rainbow texture to use to emit the rainbow. I calculate the direction from the camera to the object, I use this to check whether the card is looked at from an angle. Then I apply the effect only when the camera looks at it from an angle.

fixed4 frag (v2f i) : SV_Target
{
    // Code partially inspired by: (Binary Lunar, 2021)
    // make sure the rainbow effect is not that extreme
    float rainbowPar = 0.75;
    float3 viewRainbow = i.viewDirection * rainbowPar;

    //modify the uv of the rainbow by time so the rainbow moves slowly
    i.uv2 -= _Time.y * 0.1;

    // sample the rainbow texture
    fixed4 rainbow = tex2D(_Rainbow, i.uv2);

    // calculate the visibility of the rainbow according to the viewing angle along the x axis
    rainbow.rgb *= viewRainbow.x;
    
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv);
    return col + rainbow * 0.5;
}

Next to this, I modified and tweaked the tiling and offset of the rainbow in the inspector, to make the effect for each direction look bigger and to move the colors of the rainbow a bit. When I put this together it gives the following final result.

Result

After applying some finishing touches, The final product now contains a scene with previews of 5 different shaders applicable for cards, A distorted shader, a crystallization shader, a 3d shader, an outline shader and a holographic shader. These shaders are all showcased on different cards in this scene. These cards can be moved around by the player. They can also zoom in on the cards if they want to take a closer look. All of these shader effects could be applied on other cards easily. However, for the outline shader you would need to create a mask which applies to that card. You would also need to create a cube map for the 3D shader. Next to that, you could also create some transparent textures of objects to put in the 3d card, to create more depth and make the card more interesting.

Conclusion

I planned to create four different shader effects, I ended up creating five. If I had more experience with shaders from the get go I might have been able to create more. However, this is something said easily and only shows me I have improved in creating shaders from when I started this project.

I wanted to make sure these shaders could easily be reused by simply dragging the shader script onto the material. However, to keep a high quality on some of the shaders, I was forced to use card-specific textures for the shaders. Specifically the outline and 3d shaders need some extra textures to make sure they work properly. I don’t think I could have worked around this and at the same time not lose any quality, but this could be researched more in depth in the future.

Next to this, there would be a lot of effects that could be made for the cards in addition to these to improve upon this product in the future. However, improving on these exact shaders would be hard. The variables could be tweaked around. However the functionality works as intended and thus not really something to improve upon.

Sources

Advanced shaders in Unity – Parallax mapping | Habrador. (z.d.). Geraadpleegd op 17 maart 2023, van https://www.habrador.com/tutorials/shaders/3-parallax-mapping/

Binary Lunar. (2021, 31 december). Holographic Card Shader Graph : Blender + Unity 2021 Tutorial [Video]. YouTube. Geraadpleegd op 31 maart 2023, van https://www.youtube.com/watch?v=6hiMUtcMdZM

Cardsmith, M. (z.d.). MTG Cardsmith: A Magic: The Gathering Custom Card Maker. Geraadpleegd op 28 maart 2023, van https://mtgcardsmith.com/

Clemons, K. (z.d.). Creating Height Maps and Normal Maps. Geraadpleegd op 17 maart 2023, van https://kcclemo.neocities.org/creating-height-and-normal-maps/

Code Monkey. (2021, 4 september). How to make Unity GLOW! (Unity Tutorial) [Video]. YouTube. Geraadpleegd op 15 maart 2023, van https://www.youtube.com/watch?v=bkPe1hxOmbI

Esclavo del juego. (2022, 23 januari). Unity Shader Graph – Carta Holográfica | Tutorial [Video]. YouTube. Geraadpleegd op 13 maart 2023, van https://www.youtube.com/watch?v=wRmzrkZNv0k

French, J. (2022, 10 juni). How to convert the mouse position to world space in Unity (2D + 3D). Game Dev Beginner. Geraadpleegd op 30 maart 2023, van https://gamedevbeginner.com/how-to-convert-the-mouse-position-to-world-space-in-unity-2d-3d/

Game Dev Guide. (2020, 5 mei). Creating an Interior Mapping Shader using Unity’s Shader Graph – Game Dev Sandbox [Video]. YouTube. Geraadpleegd op 21 maart 2023, van https://www.youtube.com/watch?v=dUjNoIxQXAA

Holofoil Card Shader Breakdown. (2021, 21 april). Cyanilux. Geraadpleegd op 13 maart 2023, van https://www.cyanilux.com/tutorials/holofoil-card-shader-breakdown/

Ignore Solutions. (2020, 28 maart). Holographic Card Effect w/ Unity Shader Graph! [Video]. YouTube. Geraadpleegd op 15 maart 2023, van https://www.youtube.com/watch?v=7hZaE_Um0Rk

Jettelly. (2020, 19 november). Superficie holográfica | Unity Tutorial | Capítulo 01 (Optimized) [Video]. YouTube. Geraadpleegd op 13 maart 2023, van https://www.youtube.com/watch?v=ta5vUc1ciNY

Koimoi. (2022, 20 augustus). Breaking Bad’s Aaron Paul Confirms He Isn’t Reprising Jesse Pinkman Again & Our Hearts Are Shattered: Geraadpleegd op 29 maart 2023, van https://www.koimoi.com/hollywood-news/breaking-bads-aaron-paul-confirms-he-isnt-reprising-jesse-pinkman-again-smile-because-it-happened/

LearnOpenGL – Parallax Mapping. (z.d.). Geraadpleegd op 17 maart 2023, van https://learnopengl.com/Advanced-Lighting/Parallax-Mapping

OMGRiley. (2018, 15 januari). Add Mask To Shader. Unity forums. Geraadpleegd op 22 maart 2023, van https://forum.unity.com/threads/add-mask-to-shader.789917/

Reid, V. A. P. B. L. (2018, 27 maart). Texture Mask Shader in Unity Tutorial. Linden Reid. Geraadpleegd op 22 maart 2023, van https://lindenreidblog.com/2018/02/25/texture-mask-shader-unity-tutorial/

Shader Slider – Unity Answers. (z.d.). Geraadpleegd op 31 maart 2023, van https://answers.unity.com/questions/219816/shader-slider.html

Shader turns either magenta or invisible when using any “LightMode” tag in Unity. (z.d.). Stack Overflow. Geraadpleegd op 23 maart 2023, van https://stackoverflow.com/questions/59062925/shader-turns-either-magenta-or-invisible-when-using-any-lightmode-tag-in-unity

Skybox Cube mapping Reflection mapping, cube, 3D Computer Graphics, cloud, landscape png | PNGWing. (z.d.). Geraadpleegd op 28 maart 2023, van https://www.pngwing.com/en/free-png-kwgux

Soldiers by AndreeWallin on DeviantArt. (2008, 14 juli). DeviantArt. Geraadpleegd op 31 maart 2023, van https://www.deviantart.com/andreewallin/art/Soldiers-91647332

Stylized Station. (2022, 17 oktober). how this game made millions of rooms using 1 polygon [Video]. YouTube. Geraadpleegd op 16 maart 2023, van https://www.youtube.com/watch?v=eX7x1mJrQJs

Technologies, U. (z.d.). Unity – Manual: Cubemap. Geraadpleegd op 16 maart 2023, van https://docs.unity3d.com/550/Documentation/Manual/class-Cubemap.html#:~:text=Select%20Assets%20%3E%20Create%20%3E%20Legacy%20%3E,empty%20slots%20in%20the%20inspector.&text=Textures%20for%20the%20corresponding%20cubemap%20face.&text=Width%20and%20Height%20of%20each,automatically%20to%20fit%20this%20size.

The Book Of Shaders. (z.d.). The Book of Shaders. Geraadpleegd op 16 maart 2023, van https://thebookofshaders.com/

Times, Y. (z.d.). The Howling Wolves In January. Yorkshire Times. Geraadpleegd op 28 maart 2023, van https://yorkshiretimes.co.uk/article/The-Howling-Wolves-In-January

warka00 & Martin-Kraus. (2012, 25 september). Cubemap on a cube as texture. Unity forum. Geraadpleegd op 16 maart 2023, van https://forum.unity.com/threads/cubemap-on-a-cube-as-texture.152470/

Wikipedia contributors. (2020, 26 juli). Parallax mapping. Wikipedia. Geraadpleegd op 16 maart 2023, van https://en.wikipedia.org/wiki/Parallax_mapping

Wikipedia contributors. (2023, 23 januari). Cube mapping. Wikipedia. Geraadpleegd op 16 maart 2023, van https://en.wikipedia.org/wiki/Cube_mapping

Leave a Reply

Your email address will not be published. Required fields are marked *