Введение.
Для меня одна из самых интересных вещей, связанных с шейдерами, - это процедурные текстуры. Чтобы начать работать с ними, мы собираемся создать простой шаблон шахматной доски.
Этот урок опирается на простой шейдер с одними свойствами, но, как всегда, вы также можете использовать технику для генерации цветов в более сложных шейдерах.
Полосы.
Я возьму мировое положение поверхности, чтобы создать текстуру шахматной доски, таким образом мы сможем позже перемещать и поворачивать модель, и сгенерированные шаблоны будут соответствовать друг другу. Если вы хотите, чтобы шаблон перемещался и вращался вместе с моделью, вы также можете использовать координаты пространства объектов (те что приходят в шейдер из 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
Комментариев нет:
Отправить комментарий