Search code examples
unity-game-engineshadermodulohlsl

Fragment shader fmod, why is this not repeating


I created the following fragment shader that creates a tile grid of size _Size using the fracfunction and draws a small seperator line in between each tile, I save the ID of the tile in its uv.z value so I can later adres the tile based on its id (uv.z).

_Size and _CurrentID can be adjusted through the inspector

Shader "Unlit/Fractals"
{
    Properties
    {
        [HideInInspector] _MainTex ("Texture", 2D) = "white" {}
        _Size ("Size", float) = 5
        _CurrentID ("ID", float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Size;
            float _CurrentID;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                _CurrentID = floor(_CurrentID);
                //Create a tile grid that is of _Size * _Size (5 in example), and create an ID for it in the .z value based on its grid position
                float3 uv = float3(frac(i.uv * _Size), (floor(i.uv.y * _Size) * _Size) + (floor(i.uv.x * _Size)));

                //Create lines to seperate the tiles
                float4 col = float4(1, 1, 1, 1);
                if ((uv.x > 0.98 && uv.x < 1) || (uv.y > .98 && uv.y < 1)) 
                {
                    col *= float4(uv.x, uv.y, 0, 1);           
                }
                else
                {
                    col = float4(0, 0, 0, 1);
                }

                //Loop through all the tiles based on the ID
                if (uv.z == fmod(_CurrentID, ((_Size) * (_Size)))) 
                {
                    col = float4(0, 1, 1, 1);
                }

                //This correctly goes through every grid tile once, confirming that uv grid ID 5 corresponds to grid position (0,1)
                /*if (uv.z == _CurrentID)
                {
                    col = float4(0, 1, 1, 1);
                }*/

                return col;
            }
            ENDCG
        }
    }
}

(note that the grid starts at (0,0) bottom left to (5,5) top right)

To ascertain that my ID's are set up correct I looped through each uv.z value with the floor of the _CurrentID set from the inspector, which lights up every tile once, when going from 0 to 24 (inclusive) as expected.

if (uv.z == _CurrentID)
{
    col = float4(0, 1, 1, 1);
}

_CurrentID 7 lights up the 8th tile as expected
example of _CurrentID = 7 lighting up the 8th tile as expected

Now just using the _CurrentID would mean I can only go through every tile once. To make this repeatable regardless of how big _CurrentID is I should be able to use fmod (modulo) (although the same happens using the % modulo operator) on the _CurrentID so it loops back to 0 when CurrnetID = 25. Which I (try to) do using the following piece of code:

if (uv.z == fmod(_CurrentID, ((_Size) * (_Size))))
{
    col = float4(0, 1, 1, 1);
}

This goes well for the first row (when _CurrentId >= 0 && < 5). However once I hit _CurrentID = 5 things start to break, as no tile will light up, despite previously being able to confirm that _CurrentID = 5 will light up the tile at grid (0, 1). When I set _CurrentID = 6 the proper tile starts lighting up again (grid pos (1,1)) which continues where grid (0, n) won't ever light up where n is greater than 0.

fmod = 5 won't light up the proper square
Example of _CurrentID = 5 using fmod.

Things start breaking even more once my CurrentID goes higher than 25, where it doesn't seem to modulo loop around at all. As seen in this gyazo gif. It just seems to light up random tiles.

Starting to doubt myself I double checked the modulo maths on WolframpAlpha, which seems correct.

I can "solve" the issue where it skips the first tile of every row by doing fmod(_CurrentID, ((_Size + 1) * (_Size + 1))), which will loop correctly through each tile on the first run (including the (0,n) tiles), but now my modulo starts looping at 36, after which it will still light up a random tile as shown in the gif.

What am I doing wrong here?

(Unity version 2020.1.1f1, same behavior confirmed in 2019.3.13)


Solution

  • It's probably a floating point precision issue since you are comparing floats for equality. Instead of doing that you could write something like:

    float id = _CurrentID % (_Size*_Size);
    float epsilon = .0001f;
    if (abs(uv.z - id) < epsilon)
    {
        col = float4(0, 1, 1, 1);
    }
    

    Or use ints for ids.