Режим работы фрагментного шейдера, почему это не повторяется

#unity3d #shader #modulo #hlsl

#unity3d #шейдер #modulo #hlsl

Вопрос:

Я создал следующий фрагментный шейдер, который _Size с frac помощью функции создает сетку плиток определенного размера и рисует небольшую разделительную линию между каждым тайлом, я сохраняю идентификатор тайла в его uv.z значении, чтобы позже я мог адресовать тайл на основе его идентификатора ( uv.z ).

_Size и _CurrentID можно настроить с помощью инспектора

 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 amp;amp; uv.x < 1) || (uv.y > .98 amp;amp; 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
        }
    }
}
  

(обратите внимание, что сетка начинается с (0,0) нижнего левого по (5,5) верхний правый угол)

Чтобы убедиться, что мои идентификаторы настроены правильно, я перебрал каждое uv.z значение с помощью floor из _CurrentID набора из инспектора, который подсвечивает каждую плитку один раз при переходе от 0 к 24 (включительно), как и ожидалось.

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

_CurrentID 7 подсвечивает 8-й фрагмент, как и ожидалось
пример _CurrentID = 7 подсветки 8-го тайла, как ожидалось

Теперь простое использование _CurrentID означало бы, что я могу просмотреть каждую плитку только один раз. Чтобы сделать это повторяемым независимо от того, насколько большой _CurrentID я должен иметь возможность использовать fmod (по модулю) (хотя то же самое происходит с использованием оператора % modulo) на _CurrentID , чтобы он возвращался к 0, когда CurrnetID = 25 . Что я (пытаюсь) сделать, используя следующий фрагмент кода:

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

Это хорошо подходит для первой строки (когда _CurrentId >= 0 amp;amp; < 5). Однако, как только я нажимаю, _CurrentID = 5 все начинает ломаться, поскольку ни одна плитка не загорится, несмотря на то, что ранее была возможность подтвердить, что _CurrentID = 5 загорится плитка в grid (0, 1). Когда я устанавливаю _CurrentID = 6 , снова начинает загораться соответствующая плитка (grid pos (1,1)), которая продолжается там, где grid (0, n) никогда не будет загораться, где n больше 0.

fmod = 5 не подсвечивает нужный квадрат
Пример _CurrentID = 5 использования fmod.

Вещи начинают ломаться еще больше, как только мой CurrentID поднимается выше 25, где, похоже, он вообще не зацикливается по модулю. Как видно из этого gif-файла gyazo. Кажется, что он просто загорается случайными фрагментами.

Начиная сомневаться в себе, я дважды проверил математику по модулю на WolframpAlpha, которая кажется правильной.

Я могу «решить» проблему, из-за которой он пропускает первую плитку каждой строки, выполнив fmod(_CurrentID, ((_Size 1) * (_Size 1))) , который будет корректно перебирать каждую плитку при первом запуске (включая плитки (0, n)), но теперь мой modulo начинает цикл с 36, после чего он все равно будет загораться случайной плиткой, как показано в gif.

Что я здесь делаю не так?

(Версия Unity 2020.1.1f1, такое же поведение подтверждено в 2019.3.13)

Ответ №1:

Вероятно, это проблема точности с плавающей запятой, поскольку вы сравниваете значения с плавающей запятой для равенства. Вместо этого вы могли бы написать что-то вроде:

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

Или используйте целые числа для идентификаторов.

Комментарии:

1. Потрясающе! Я не думал о том, что точность с плавающей запятой является проблемой, поскольку я использую оба _CurrentID и uv.z , но я думаю, что даже тогда для этого все еще существует риск.