Шаблон «шахматки».

Введение.

Для меня одна из самых интересных вещей, связанных с шейдерами, - это процедурные текстуры. Чтобы начать работать с ними, мы собираемся создать простой шаблон шахматной доски.
Этот урок опирается на простой шейдер с одними свойствами, но, как всегда, вы также можете использовать технику для генерации цветов в более сложных шейдерах.

Полосы.

Я возьму мировое положение поверхности, чтобы создать текстуру шахматной доски, таким образом мы сможем позже перемещать и поворачивать модель, и сгенерированные шаблоны будут соответствовать друг другу. Если вы хотите, чтобы шаблон перемещался и вращался вместе с моделью, вы также можете использовать координаты пространства объектов (те что приходят в шейдер из Unity, не умножая их ни на что).
Чтобы можно было использовать мировое положение в шейдере фрагментов, мы добавляем его в вершину, которая в структуре передаваемой из вершинного во фрагментный шейдер, а затем генерируем мировое положение в вершинном шейдере и записываем его в структуру.
struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
}

v2f vert(appdata v){
    v2f o;
    // Преобразуем положение вершины из координат объекта в координаты экрана
    o.position = UnityObjectToClipPos(v.vertex);
    // Рассчитываем мировую позицию вершины
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    return o;
}
Затем в шейдере фрагментов мы используя координату вершины в пространстве будем вычислять какой у нее должен быть цвет. Начнем с одномерного шахматного поля, так что просто чередуем черные и белые линии. Для этого мы берем одну из осей позиции. Мы начинаем с округления вниз. Это означает, что нам нужно следующее меньшее целое число. Мы делаем это, чтобы убедиться, что у нас есть только один цвет на единицу. Т.е. в координатах 0-1 цвет будет 0, 1-2 цвет будет 1… Но это еще на самом деле это пока еще не цвет.
Затем мы узнаем, что наш результат четный или нечетный. Для этого разделим значение на два и возьмем дробную часть (часть числа после точки). Так что теперь все четные числа дают 0 (потому что после деления на 2 четных числа все еще целые числа, поэтому их дробная часть равна 0), а все нечетные поля приводят к 0,5 (потому что после деления на 2 нечетные числа заканчиваются дробными , 1 становится 0,5, 3 равно 1,5 ...). Чтобы сделать нечетные числа белыми, а не серыми, мы можем умножить наше значение на 2.
Окончательно получаем в координатах 0-1 цвет 0, в 1-2 цвет 1, в 2-3 снова 0…
fixed4 frag(v2f i) : SV_TARGET{
    //округляем до целого вниз 
    float chessboard = floor(i.worldPos.x);
    //определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
    chessboard = frac(chessboard * 0.5);
    //умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
    chessboard *= 2;
    return chessboard;
}

Шахматка в 2d и 3d.

Теперь мы сделаем шаблон двумерным. Для этого нам нужно добавить дополнительную ось в значение, которое мы оцениваем. Это потому что, когда мы добавляем один в наши строки, все четные значения становятся нечетными, а нечетные значения становятся четными. Это также является основной причиной, по которой мы округляли вниз. Мы легко могли бы сделать рисунок в одном измерении без этого, но это облегчает добавление второй оси.
fixed4 frag(v2f i) : SV_TARGET{
    //добавляем вторую размерность 
    float chessboard = floor(i.worldPos.x) + floor(i.worldPos.y);
    // определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
    chessboard = frac(chessboard * 0.5);
    // умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
    chessboard *= 2;
    return chessboard;
}


После этого мы можем пойти еще дальше и добавить третье измерение так же, как мы добавили второе.
fixed4 frag(v2f i) : SV_TARGET{
    // добавляем третью размерность
    float chessboard = floor(i.worldPos.x) + floor(i.worldPos.y) + floor(i.worldPos.z);
    // определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
    chessboard = frac(chessboard * 0.5);
    // умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
    chessboard *= 2;
    return chessboard;
}

Масштабирование.

Затем я хотел бы добавить возможность сделать рисунок больше или меньше. Для этого мы добавляем новое свойство для масштаба шаблона. Мы делим позицию на масштаб, прежде чем будем делать с ней что-либо еще, таким образом, если масштаб меньше единицы, шаблон генерируется так, как если бы объект был больше, чем он есть, и как таковой он имеет большую плотность рисунка на площадь поверхности.
Еще одно небольшое изменение, которое я сделал, заключается в том, что теперь мы используем округление вниз на всем векторе вместо компонентов отдельно. Это ничего не меняет, я просто думаю, что так читать лучше.
//...

//show values to edit in inspector
Properties{
    _Scale ("Pattern Size", Range(0,10)) = 1
}

//...

float _Scale;

//...

fixed4 frag(v2f i) : SV_TARGET{
    //масштабируем позицию и сразу округляем вниз
    float3 adjustedWorldPos = floor(i.worldPos / _Scale);
    //добавляем все размерности 
    float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
    // определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
    chessboard = frac(chessboard * 0.5);
    // умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
    chessboard *= 2;
    return chessboard;
}

//...

Настраиваемые цвета.

Наконец, я хотел бы добавить возможность добавления цветов в шаблон, один для четных областей, один для нечетного. Мы добавляем два новых свойства и соответствующие значения для этих цветов в шейдер.
Затем в конце нашего фрагментного шейдера мы выполняем линейную интерполяцию между двумя цветами. Поскольку мы имеем только два разных значения (ноль и один), мы можем ожидать, что интерполяция вернет либо цвет, из которого он интерполируется (для ввода 0), либо цвет, который он интерполирует (для ввода 1). Если вы смущены интерполяцией, прочитайте урок «Интерполяция цвета».
//...

//показываем значения для редактирования в инспекторе
Properties{
    _Scale ("Pattern Size", Range(0,10)) = 1
    _EvenColor("Color 1", Color) = (0,0,0,1)
    _OddColor("Color 2", Color) = (1,1,1,1)
}

//...

float4 _EvenColor;
float4 _OddColor;

//...

fixed4 frag(v2f i) : SV_TARGET{
    // масштабируем позицию и сразу округляем вниз
    float3 adjustedWorldPos = floor(i.worldPos / _Scale);
    // добавляем все размерности
    float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
    // определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
    chessboard = frac(chessboard * 0.5);
    // умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
    chessboard *= 2;

    //интерполируем между цветом для четных полей (0) и цветом для нечетных полей (1)
    float4 color = lerp(_EvenColor, _OddColor, chessboard);
    return color;
}

//...

Полный шейдер для интерполяции, генерирующий шаблон шахматной доски на поверхности, должен теперь выглядеть так:
Shader "Tutorial/011_Chessboard"
{
    // показываем значения для редактирования в инспекторе
    Properties{
        _Scale ("Pattern Size", Range(0,10)) = 1
        _EvenColor("Color 1", Color) = (0,0,0,1)
        _OddColor("Color 2", Color) = (1,1,1,1)
    }

    SubShader{
        // материал полностью не прозрачный и визуализируется с остальными непрозрачными
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}
        

        Pass{
            CGPROGRAM
            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            float _Scale;

            float4 _EvenColor;
            float4 _OddColor;

            struct appdata{
                float4 vertex : POSITION;
            };

            struct v2f{
                float4 position : SV_POSITION;
                float3 worldPos : TEXCOORD0;
            };

            v2f vert(appdata v){
                v2f o;
                // Преобразуем положение вершины из координат объекта в координаты экрана
                o.position = UnityObjectToClipPos(v.vertex);
                // Рассчитываем мировую позицию вершины
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET{
                // масштабируем позицию и сразу округляем вниз
                float3 adjustedWorldPos = floor(i.worldPos / _Scale);
                // добавляем все размерности
                float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
                // определяем остаток от деления на 2, получаем 0 для четных и 0.5 для нечетных
                chessboard = frac(chessboard * 0.5);
                // умножаем на 2, чтобы получить 1 а не 0.5, т.е. белый цвет, а не серый
                chessboard *= 2;

                // интерполируем между цветом для четных полей (0) и цветом для нечетных полей (1)
                float4 color = lerp(_EvenColor, _OddColor, chessboard);
                return color;
            }

            ENDCG
        }
    }
    FallBack "Standard" //fallback добавляет проход для расчета теней
}
Надеюсь, вам понравилось делать этот простой шахматный шейдер, и это помогло вам понять, как создавать «текстуры» в шейдерах с помощью простых математических операций.


Здесь вы также можете найти исходный код для этого шейдера: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/011_ChessBoard/Chessboard.shader

Перевод Беляев В.А. ака seaman

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

Отправить комментарий