При работе с полигональными ассетами можно отрисовывать только по одному объекту за раз (если не учитывать такие приёмы, как batching и instancing), но если использовать поля расстояний со знаком (signed distance fields, SDF), то мы не этим не ограничены. Если две позиции имеют одинаковую координату, то функции расстояний со знаком возвратят одинаковое значение, и за одно вычисление мы можем получить несколько фигур. Чтобы понять, как преобразовывать пространство, используемое для генерации полей расстояний со знаком, я рекомендую разобраться, как создавать фигуры с помощью функций расстояний со знаком и комбинировать sdf-фигуры.
Для этого туториала я модифицирую сопряжение между квадратом и кругом, но вы можете использовать его для любой другой фигуры. Это похоже на конфигурацию для предыдущего туториала.
Здесь важно то, что модифицируемая часть находится до использования позиций для генерации фигур.
А находящаяся в одной папке с шейдером функция 2D_SDF.cginc, которую мы будем расширять, поначалу выглядит так:
Одна из самых простых операций — отзеркаливание мира относительно оси. Чтобы отзеркалить его относительно оси y, мы берём абсолютное значение компонента x нашей позиции. Таким образом координаты справа и слева от оси будут одинаковыми.
Чаще всего использующий эту функцию код будет выглядеть примерно как
Получилось уже довольно красиво, но так мы получаем только одну ось для отзеркаливания. Мы можем расширить функцию, поворачивая пространство так, как мы это делали при повороте фигур. Сначала нужно повернуть пространство, затем отзеркалить его, а затем выполнить поворот обратно. Таким образом мы сможем выполнять отзеркаливание относительно любого угла. То же самое возможно при переносе пространства и выполнении обратного переноса после отзеркаливания. (Если вы выполняете обе операции, то перед отзеркаливанием не забывайте сначала выполнять перенос, а затем поворот, после чего первым идёт поворот.)
Если вы знаете, как работает генерация шума, то понимаете, что для процедурной генерации мы часто повторяем позицию и получаем мелкие ячейки, которые по сути являются одинаковыми, отличаясь только незначительными параметрами. Мы можем сделать то же самое и для полей расстояний.
Так как функция
Проблема ячеек в том, что мы теряем непрерывность, за которую любим поля расстояний. Это неплохо, если фигуры находятся только в середине ячеек, но в показанном выше примере это может привести к значительным артефактам, которых желательно избегать, когда поля расстояний используются для множества задач, в которых обычно могут применяться поля расстояний.
Есть одно решение, которое работает не в каждом случае, но когда оно срабатывает, это замечательно: отзеркаливать каждую вторую ячейку. Для этого нам необходим индекс ячейки пикселя, но у нас всё равно нет в функции возвращаемого значения, поэтому мы можем просто использовать его для возврата индекса ячейки.
Для вычисления индекса ячейки мы делим позицию на период. Таким образом, 0-1 — это первая ячейка, 1-2 — вторая, и т.д… и мы можем с лёгкостью это дискретизировать. Чтобы получить индекс ячейки, мы затем просто округляем значение в меньшую сторону и возвращаем результат. Важно то, что мы вычисляем индекс ячейки до деления с остатком для повторения ячеек; в противном случае мы бы получали везде индекс 0, потому что позиция не может превышать период.
Имея эту информацию, мы можем переворачивать ячейки. Чтобы понять, нужно или не нужно переворачивать, мы делим индекс ячейки по модулю 2. Результат этой операции попеременно равен 0 и 1 или -1 каждую вторую ячейку. Чтобы изменение было более постоянным, мы берём абсолютное значение и получаем значение, переключающееся между 0 и 1.
Чтобы использовать это значение для переворачивания между обычной и перевёрнутой позицией, нам нужна функция, не делающая ничего для значения 0, и вычитающая позицию из периода, в котором переворачивание равно 1. То есть мы выполняем линейную интерполяцию от обычной к перевёрнутой позиции с помощью переменной переворота. Так как переменная переворота является 2d-вектором, его компоненты переворачиваются по отдельности.
Ещё одна отличная функция — это повторение пространства в радиальном паттерне.
Чтобы получить этот эффект, мы сначала вычисляем радиальную позицию. Для этого мы кодируем угол относительно центра оси x и расстояние от центра по оси y.
Затем мы повторяем угол. Так как передавать количество повторений гораздо проще, чем угол каждого куска, мы сначала вычислим размер каждого куска. Вся окружность равна 2 * pi, поэтому для получения нужной части мы делим 2 * pi на величину ячейки.
Имея эту информацию, мы можем повторять компонент x радиальной позиции через каждые cellSize единиц. Мы выполняем повторение делением с остатком, поэтому как и раньше получаем проблемы с отрицательными числами, которую можно устранить с помощью двух функций деления с остатком.
Затем нужно перенести новую позицию обратно к обычным координатам xy. Здесь мы используем функцию sincos с компонентом x радиальной позиции в качестве угла, чтобы записать синус в координату x позиции и косинус в координату y. С помощью этого этапа мы получаем нормализованную позицию. Чтобы получить правильное направление от центра, нужно умножить его на компонент y радиальной позиции, что обозначает длину.
Затем мы также можем добавить индекс ячейки и отзеркаливание, как это делали с обычными ячейками.
Нужно вычислять индекс ячейки после вычисления радиальной позиции, но до получения её остатка от деления. Мы получаем его, разделив компонент x радиальной позиции и округлив результат вниз. В этом случае индекс может быть также отрицательным, и это проблема, если количество ячеек нечётно. Например, при 3 ячейках мы получаем 1 ячейку с индексом 0, 1 ячейку с индексом -1 и 2 полуячейки с индексами 1 и -2. Чтобы обойти эту проблему, мы прибавляем к округлённой в меньшую сторону переменной количество ячеек, а затем делим с остатком на размер ячейки.
Чтобы отзеркалить это, нам нужно, чтобы координаты были указаны в радианах, поэтому чтобы избежать повторного вычисления радиальных координат за пределами функции мы добавим ей опцию с помощью аргумента bool. Обычно в шейдерах ветвление (конструкции if) не приветствуются, но в данном случае все пиксели на экране пройдут по одному пути, так что это нормально.
Отзеркаливание должно происходить после зацикливания радиальной координаты, но до того, как она будет обратно преобразована в обычную позицию. Мы узнаём, нужно ли переворачивать текущую ячейку, поделив с остатком индекс ячейки на 2. Обычно это должно давать нам нули и единицы, но в моём случае появляются несколько двоек, что странно, и всё-таки мы с этим справимся. Чтобы устранить двойки, мы просто вычитаем 1 из переменной переворота, а затем берём абсолютное значение. Таким образом нули и двойки становятся единицами, а единицы — нулями, как нам и нужно, только в обратном порядке.
Так как нули и единицы находятся в неверном порядке, мы выполняем линейную интерполяцию от перевёрнутой версии к неперевёрнутой, а не наоборот, как раньше. Чтобы перевернуть координату, мы просто вычитаем позицию из размера ячейки.
Но для изменения пространства не обязательно его повторять. Например, в туториале об основах мы его поворачивали, переносили и масштабировали. Можно также сделать следующее: перемещать каждую ось на основании другой с помощью синусоиды. Это сделает расстояния функции расстояний со знаком менее точными, но пока они не колышутся слишком сильно, всё будет в порядке.
Сначала мы вычисляем величину изменения позиции, перевернув компоненты x и y, а затем умножив их на частоту колыханий. Затем мы берём синус от этого значения и умножаем его на величину колыхания, которую мы хотим добавить. После этого мы просто прибавляем этот коэффициент колыхания к позиции и снова применяем результат к позиции.
Также мы можем анимировать это колыхание, изменяя его позицию, применяя колыхание в позиции смещения и возвращая пространство назад. Чтобы числа с плавающей запятой не становились слишком большими, я выполняю деление с остатком pi * 2 на частоту колыханий, это коррелирует с колыханием (синусоида повторяется каждые pi * 2 единиц), поэтому мы избегаем скачков и слишком больших смещений.
Теперь вы знаете все основы функций расстояний со знаком, которые я смог вспомнить. В следующем туториале я попытаюсь сделать с ними что-нибудь интересное.
Конфигурация
Для этого туториала я модифицирую сопряжение между квадратом и кругом, но вы можете использовать его для любой другой фигуры. Это похоже на конфигурацию для предыдущего туториала.
Здесь важно то, что модифицируемая часть находится до использования позиций для генерации фигур.
Shader "Tutorial/036_SDF_Space_Manpulation/Type"{
Properties{
_InsideColor("Inside Color", Color) = (.5, 0, 0, 1)
_OutsideColor("Outside Color", Color) = (0, .5, 0, 1)
_LineDistance("Mayor Line Distance", Range(0, 2)) = 1
_LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05
[IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4
_SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01
}
SubShader{
//the material is completely non-transparent and is rendered at the same time as the other opaque geometry
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#include "2D_SDF.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata{
float4 vertex : POSITION;
};
struct v2f{
float4 position : SV_POSITION;
float4 worldPos : TEXCOORD0;
};
v2f vert(appdata v){
v2f o;
//calculate the position in clip space to render the object
o.position = UnityObjectToClipPos(v.vertex);
//calculate world position of vertex
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
float scene(float2 position) {
// manipulate position with cool methods here!
float2 squarePosition = position;
squarePosition = translate(squarePosition, float2(2, 2));
squarePosition = rotate(squarePosition, .125);
float squareShape = rectangle(squarePosition, float2(1, 1));
float2 circlePosition = position;
circlePosition = translate(circlePosition, float2(1, 1.5));
float circleShape = circle(circlePosition, 1);
float combination = merge(circleShape, squareShape);
return combination;
}
float4 _InsideColor;
float4 _OutsideColor;
float _LineDistance;
float _LineThickness;
float _SubLines;
float _SubLineThickness;
fixed4 frag(v2f i) : SV_TARGET{
float dist = scene(i.worldPos.xz);
fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist));
float distanceChange = fwidth(dist) * 0.5;
float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance;
float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance);
float distanceBetweenSubLines = _LineDistance / _SubLines;
float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines;
float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance);
return col * majorLines * subLines;
}
ENDCG
}
}
FallBack "Standard"
}
А находящаяся в одной папке с шейдером функция 2D_SDF.cginc, которую мы будем расширять, поначалу выглядит так:
#ifndef SDF_2D
#define SDF_2D
//transforms
float2 rotate(float2 samplePosition, float rotation){
const float PI = 3.14159;
float angle = rotation * PI * 2 * -1;
float sine, cosine;
sincos(angle, sine, cosine);
return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x);
}
float2 translate(float2 samplePosition, float2 offset){
//move samplepoint in the opposite direction that we want to move shapes in
return samplePosition - offset;
}
float2 scale(float2 samplePosition, float scale){
return samplePosition / scale;
}
//combinations
///basic
float merge(float shape1, float shape2){
return min(shape1, shape2);
}
float intersect(float shape1, float shape2){
return max(shape1, shape2);
}
float subtract(float base, float subtraction){
return intersect(base, -subtraction);
}
float interpolate(float shape1, float shape2, float amount){
return lerp(shape1, shape2, amount);
}
/// round
float round_merge(float shape1, float shape2, float radius){
float2 intersectionSpace = float2(shape1 - radius, shape2 - radius);
intersectionSpace = min(intersectionSpace, 0);
float insideDistance = -length(intersectionSpace);
float simpleUnion = merge(shape1, shape2);
float outsideDistance = max(simpleUnion, radius);
return insideDistance + outsideDistance;
}
float round_intersect(float shape1, float shape2, float radius){
float2 intersectionSpace = float2(shape1 + radius, shape2 + radius);
intersectionSpace = max(intersectionSpace, 0);
float outsideDistance = length(intersectionSpace);
float simpleIntersection = intersect(shape1, shape2);
float insideDistance = min(simpleIntersection, -radius);
return outsideDistance + insideDistance;
}
float round_subtract(float base, float subtraction, float radius){
return round_intersect(base, -subtraction, radius);
}
///champfer
float champfer_merge(float shape1, float shape2, float champferSize){
const float SQRT_05 = 0.70710678118;
float simpleMerge = merge(shape1, shape2);
float champfer = (shape1 + shape2) * SQRT_05;
champfer = champfer - champferSize;
return merge(simpleMerge, champfer);
}
float champfer_intersect(float shape1, float shape2, float champferSize){
const float SQRT_05 = 0.70710678118;
float simpleIntersect = intersect(shape1, shape2);
float champfer = (shape1 + shape2) * SQRT_05;
champfer = champfer + champferSize;
return intersect(simpleIntersect, champfer);
}
float champfer_subtract(float base, float subtraction, float champferSize){
return champfer_intersect(base, -subtraction, champferSize);
}
/// round border intersection
float round_border(float shape1, float shape2, float radius){
float2 position = float2(shape1, shape2);
float distanceFromBorderIntersection = length(position);
return distanceFromBorderIntersection - radius;
}
float groove_border(float base, float groove, float width, float depth){
float circleBorder = abs(groove) - width;
float grooveShape = subtract(circleBorder, base + depth);
return subtract(base, grooveShape);
}
//shapes
float circle(float2 samplePosition, float radius){
//get distance from center and grow it according to radius
return length(samplePosition) - radius;
}
float rectangle(float2 samplePosition, float2 halfSize){
float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize;
float outsideDistance = length(max(componentWiseEdgeDistance, 0));
float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0);
return outsideDistance + insideDistance;
}
#endif
Повторение пространства
Зеркальное отражение
Одна из самых простых операций — отзеркаливание мира относительно оси. Чтобы отзеркалить его относительно оси y, мы берём абсолютное значение компонента x нашей позиции. Таким образом координаты справа и слева от оси будут одинаковыми.
(-1, 1)
превращается в (1, 1)
, и оказывается внутри круга, использующего в качестве начала координат (1, 1)
и с радиусом больше 0.Чаще всего использующий эту функцию код будет выглядеть примерно как
position = mirror(position);
, поэтому мы можем его немного упростить. Мы просто объявим аргумент позиции как inout. Таким образом, при записи в аргумент он также будет изменять переменную, которую мы передаём в функцию. Возвращаемое значение тогда может иметь тип void, потому что мы всё равно не используем возвращаемое значение.//in 2D_SDF.cginc
void mirror(inout float2 position){
position.x = abs(position.x);
}
//in shader function
mirror(position);
Получилось уже довольно красиво, но так мы получаем только одну ось для отзеркаливания. Мы можем расширить функцию, поворачивая пространство так, как мы это делали при повороте фигур. Сначала нужно повернуть пространство, затем отзеркалить его, а затем выполнить поворот обратно. Таким образом мы сможем выполнять отзеркаливание относительно любого угла. То же самое возможно при переносе пространства и выполнении обратного переноса после отзеркаливания. (Если вы выполняете обе операции, то перед отзеркаливанием не забывайте сначала выполнять перенос, а затем поворот, после чего первым идёт поворот.)
//in shader function
float rotation = _Time.y * 0.25;
position = rotate(position, rotation);
mirror(position);
position = rotate(position, -rotation);
Ячейки
Если вы знаете, как работает генерация шума, то понимаете, что для процедурной генерации мы часто повторяем позицию и получаем мелкие ячейки, которые по сути являются одинаковыми, отличаясь только незначительными параметрами. Мы можем сделать то же самое и для полей расстояний.
Так как функция
fmod
(а также использование % для деления с остатком) даёт нам остаток, а не определение остатка, то нам придётся воспользоваться хитростью. Сначала мы возьмём остаток от целочисленного деления функцией fmod. Для положительных чисел именно это нам и нужно, а для отрицательных это нужный нам результат минус период. Исправить это можно, прибавив период и снова взяв остаток от деления. Прибавление периода даст нужный результат для отрицательных значений ввода, а для положительных значений ввода — значение на один период выше. Второй остаток от деления ничего не сделают со значениями для отрицательных значений ввода, потому что они уже находятся в интервале от 0 до периода, а для положительных значений ввода мы по сути вычтем один период.//in 2D_SDF.cginc
void cells(inout float2 position, float2 period){
position = fmod(position, period);
//negative positions lead to negative modulo
position += period;
//negative positions now have correct cell coordinates, positive input positions too high
position = fmod(position, period);
//second mod doesn't change values between 0 and period, but brings down values that are above period.
}
//in shader function
cells(position, float2(3, 3));
Проблема ячеек в том, что мы теряем непрерывность, за которую любим поля расстояний. Это неплохо, если фигуры находятся только в середине ячеек, но в показанном выше примере это может привести к значительным артефактам, которых желательно избегать, когда поля расстояний используются для множества задач, в которых обычно могут применяться поля расстояний.
Есть одно решение, которое работает не в каждом случае, но когда оно срабатывает, это замечательно: отзеркаливать каждую вторую ячейку. Для этого нам необходим индекс ячейки пикселя, но у нас всё равно нет в функции возвращаемого значения, поэтому мы можем просто использовать его для возврата индекса ячейки.
Для вычисления индекса ячейки мы делим позицию на период. Таким образом, 0-1 — это первая ячейка, 1-2 — вторая, и т.д… и мы можем с лёгкостью это дискретизировать. Чтобы получить индекс ячейки, мы затем просто округляем значение в меньшую сторону и возвращаем результат. Важно то, что мы вычисляем индекс ячейки до деления с остатком для повторения ячеек; в противном случае мы бы получали везде индекс 0, потому что позиция не может превышать период.
//in 2D_SDF.cginc
float2 cells(inout float2 position, float2 period){
position = fmod(position, period);
//negative positions lead to negative modulo
position += period;
//negative positions now have correct cell coordinates, positive input positions too high
position = fmod(position, period);
//second mod doesn't change values between 0 and period, but brings down values that are above period.
float2 cellIndex = position / period;
cellIndex = floor(cellIndex);
return cellIndex;
}
Имея эту информацию, мы можем переворачивать ячейки. Чтобы понять, нужно или не нужно переворачивать, мы делим индекс ячейки по модулю 2. Результат этой операции попеременно равен 0 и 1 или -1 каждую вторую ячейку. Чтобы изменение было более постоянным, мы берём абсолютное значение и получаем значение, переключающееся между 0 и 1.
Чтобы использовать это значение для переворачивания между обычной и перевёрнутой позицией, нам нужна функция, не делающая ничего для значения 0, и вычитающая позицию из периода, в котором переворачивание равно 1. То есть мы выполняем линейную интерполяцию от обычной к перевёрнутой позиции с помощью переменной переворота. Так как переменная переворота является 2d-вектором, его компоненты переворачиваются по отдельности.
//in shader function
float2 period = 3;
float2 cell = cells(position, period);
float2 flip = abs(fmod(cell, 2));
position = lerp(position, period - position, flip);
Радиальные ячейки
Ещё одна отличная функция — это повторение пространства в радиальном паттерне.
Чтобы получить этот эффект, мы сначала вычисляем радиальную позицию. Для этого мы кодируем угол относительно центра оси x и расстояние от центра по оси y.
float2 radialPosition = float2(atan2(position.x, position.y), length(position));
Затем мы повторяем угол. Так как передавать количество повторений гораздо проще, чем угол каждого куска, мы сначала вычислим размер каждого куска. Вся окружность равна 2 * pi, поэтому для получения нужной части мы делим 2 * pi на величину ячейки.
const float PI = 3.14159;
float cellSize = PI * 2 / cells;
Имея эту информацию, мы можем повторять компонент x радиальной позиции через каждые cellSize единиц. Мы выполняем повторение делением с остатком, поэтому как и раньше получаем проблемы с отрицательными числами, которую можно устранить с помощью двух функций деления с остатком.
radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize);
Затем нужно перенести новую позицию обратно к обычным координатам xy. Здесь мы используем функцию sincos с компонентом x радиальной позиции в качестве угла, чтобы записать синус в координату x позиции и косинус в координату y. С помощью этого этапа мы получаем нормализованную позицию. Чтобы получить правильное направление от центра, нужно умножить его на компонент y радиальной позиции, что обозначает длину.
//in 2D_SDF.cginc
void radial_cells(inout float2 position, float cells){
const float PI = 3.14159;
float cellSize = PI * 2 / cells;
float2 radialPosition = float2(atan2(position.x, position.y), length(position));
radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize);
sincos(radialPosition.x, position.x, position.y);
position = position * radialPosition.y;
}
//in shader function
float2 period = 6;
radial_cells(position, period, false);
Затем мы также можем добавить индекс ячейки и отзеркаливание, как это делали с обычными ячейками.
Нужно вычислять индекс ячейки после вычисления радиальной позиции, но до получения её остатка от деления. Мы получаем его, разделив компонент x радиальной позиции и округлив результат вниз. В этом случае индекс может быть также отрицательным, и это проблема, если количество ячеек нечётно. Например, при 3 ячейках мы получаем 1 ячейку с индексом 0, 1 ячейку с индексом -1 и 2 полуячейки с индексами 1 и -2. Чтобы обойти эту проблему, мы прибавляем к округлённой в меньшую сторону переменной количество ячеек, а затем делим с остатком на размер ячейки.
//in 2D_SDF.cginc
float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells);
//at the end of the function:
return cellIndex;
Чтобы отзеркалить это, нам нужно, чтобы координаты были указаны в радианах, поэтому чтобы избежать повторного вычисления радиальных координат за пределами функции мы добавим ей опцию с помощью аргумента bool. Обычно в шейдерах ветвление (конструкции if) не приветствуются, но в данном случае все пиксели на экране пройдут по одному пути, так что это нормально.
Отзеркаливание должно происходить после зацикливания радиальной координаты, но до того, как она будет обратно преобразована в обычную позицию. Мы узнаём, нужно ли переворачивать текущую ячейку, поделив с остатком индекс ячейки на 2. Обычно это должно давать нам нули и единицы, но в моём случае появляются несколько двоек, что странно, и всё-таки мы с этим справимся. Чтобы устранить двойки, мы просто вычитаем 1 из переменной переворота, а затем берём абсолютное значение. Таким образом нули и двойки становятся единицами, а единицы — нулями, как нам и нужно, только в обратном порядке.
Так как нули и единицы находятся в неверном порядке, мы выполняем линейную интерполяцию от перевёрнутой версии к неперевёрнутой, а не наоборот, как раньше. Чтобы перевернуть координату, мы просто вычитаем позицию из размера ячейки.
//in 2D_SDF.cginc
float radial_cells(inout float2 position, float cells, bool mirrorEverySecondCell = false){
const float PI = 3.14159;
float cellSize = PI * 2 / cells;
float2 radialPosition = float2(atan2(position.x, position.y), length(position));
float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells);
radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize);
if(mirrorEverySecondCell){
float flip = fmod(cellIndex, 2);
flip = abs(flip-1);
radialPosition.x = lerp(cellSize - radialPosition.x, radialPosition.x, flip);
}
sincos(radialPosition.x, position.x, position.y);
position = position * radialPosition.y;
return cellIndex;
}
//in shader function
float2 period = 6;
radial_cells(position, period, true);
Колышущееся пространство
Но для изменения пространства не обязательно его повторять. Например, в туториале об основах мы его поворачивали, переносили и масштабировали. Можно также сделать следующее: перемещать каждую ось на основании другой с помощью синусоиды. Это сделает расстояния функции расстояний со знаком менее точными, но пока они не колышутся слишком сильно, всё будет в порядке.
Сначала мы вычисляем величину изменения позиции, перевернув компоненты x и y, а затем умножив их на частоту колыханий. Затем мы берём синус от этого значения и умножаем его на величину колыхания, которую мы хотим добавить. После этого мы просто прибавляем этот коэффициент колыхания к позиции и снова применяем результат к позиции.
//in 2D_SDF.cginc
void wobble(inout float2 position, float2 frequency, float2 amount){
float2 wobble = sin(position.yx * frequency) * amount;
position = position + wobble;
}
//in shader function
wobble(position, 5, .05);
Также мы можем анимировать это колыхание, изменяя его позицию, применяя колыхание в позиции смещения и возвращая пространство назад. Чтобы числа с плавающей запятой не становились слишком большими, я выполняю деление с остатком pi * 2 на частоту колыханий, это коррелирует с колыханием (синусоида повторяется каждые pi * 2 единиц), поэтому мы избегаем скачков и слишком больших смещений.
//in shader function
const float PI = 3.14159;
float frequency = 5;
float offset = _Time.y;
offset = fmod(offset, PI * 2 / frequency);
position = translate(position, offset);
wobble(position, 5, .05);
position = translate(position, -offset);
Исходники
Библиотека двухмерных SDF
#ifndef SDF_2D
#define SDF_2D
//transforms
float2 rotate(float2 samplePosition, float rotation){
const float PI = 3.14159;
float angle = rotation * PI * 2 * -1;
float sine, cosine;
sincos(angle, sine, cosine);
return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x);
}
float2 translate(float2 samplePosition, float2 offset){
//move samplepoint in the opposite direction that we want to move shapes in
return samplePosition - offset;
}
float2 scale(float2 samplePosition, float scale){
return samplePosition / scale;
}
//combinations
///basic
float merge(float shape1, float shape2){
return min(shape1, shape2);
}
float intersect(float shape1, float shape2){
return max(shape1, shape2);
}
float subtract(float base, float subtraction){
return intersect(base, -subtraction);
}
float interpolate(float shape1, float shape2, float amount){
return lerp(shape1, shape2, amount);
}
/// round
float round_merge(float shape1, float shape2, float radius){
float2 intersectionSpace = float2(shape1 - radius, shape2 - radius);
intersectionSpace = min(intersectionSpace, 0);
float insideDistance = -length(intersectionSpace);
float simpleUnion = merge(shape1, shape2);
float outsideDistance = max(simpleUnion, radius);
return insideDistance + outsideDistance;
}
float round_intersect(float shape1, float shape2, float radius){
float2 intersectionSpace = float2(shape1 + radius, shape2 + radius);
intersectionSpace = max(intersectionSpace, 0);
float outsideDistance = length(intersectionSpace);
float simpleIntersection = intersect(shape1, shape2);
float insideDistance = min(simpleIntersection, -radius);
return outsideDistance + insideDistance;
}
float round_subtract(float base, float subtraction, float radius){
return round_intersect(base, -subtraction, radius);
}
///champfer
float champfer_merge(float shape1, float shape2, float champferSize){
const float SQRT_05 = 0.70710678118;
float simpleMerge = merge(shape1, shape2);
float champfer = (shape1 + shape2) * SQRT_05;
champfer = champfer - champferSize;
return merge(simpleMerge, champfer);
}
float champfer_intersect(float shape1, float shape2, float champferSize){
const float SQRT_05 = 0.70710678118;
float simpleIntersect = intersect(shape1, shape2);
float champfer = (shape1 + shape2) * SQRT_05;
champfer = champfer + champferSize;
return intersect(simpleIntersect, champfer);
}
float champfer_subtract(float base, float subtraction, float champferSize){
return champfer_intersect(base, -subtraction, champferSize);
}
/// round border intersection
float round_border(float shape1, float shape2, float radius){
float2 position = float2(shape1, shape2);
float distanceFromBorderIntersection = length(position);
return distanceFromBorderIntersection - radius;
}
float groove_border(float base, float groove, float width, float depth){
float circleBorder = abs(groove) - width;
float grooveShape = subtract(circleBorder, base + depth);
return subtract(base, grooveShape);
}
// space repetition
void mirror(inout float2 position){
position.x = abs(position.x);
}
float2 cells(inout float2 position, float2 period){
//find cell index
float2 cellIndex = position / period;
cellIndex = floor(cellIndex);
//negative positions lead to negative modulo
position = fmod(position, period);
//negative positions now have correct cell coordinates, positive input positions too high
position += period;
//second mod doesn't change values between 0 and period, but brings down values that are above period.
position = fmod(position, period);
return cellIndex;
}
float radial_cells(inout float2 position, float cells, bool mirrorEverySecondCell = false){
const float PI = 3.14159;
float cellSize = PI * 2 / cells;
float2 radialPosition = float2(atan2(position.x, position.y), length(position));
float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells);
radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize);
if(mirrorEverySecondCell){
float flip = fmod(cellIndex, 2);
flip = abs(flip-1);
radialPosition.x = lerp(cellSize - radialPosition.x, radialPosition.x, flip);
}
sincos(radialPosition.x, position.x, position.y);
position = position * radialPosition.y;
return cellIndex;
}
void wobble(inout float2 position, float2 frequency, float2 amount){
float2 wobble = sin(position.yx * frequency) * amount;
position = position + wobble;
}
//shapes
float circle(float2 samplePosition, float radius){
//get distance from center and grow it according to radius
return length(samplePosition) - radius;
}
float rectangle(float2 samplePosition, float2 halfSize){
float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize;
float outsideDistance = length(max(componentWiseEdgeDistance, 0));
float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0);
return outsideDistance + insideDistance;
}
#endif
Базовый демонстрационный шейдер
- https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/036_SDF_space_manipulation/sdf_mirror.shader
- https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/036_SDF_space_manipulation/sdf_cells.shader
- https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/036_SDF_space_manipulation/sdf_wobble.shader
Shader "Tutorial/036_SDF_Space_Manpulation/Mirror"{
Properties{
_InsideColor("Inside Color", Color) = (.5, 0, 0, 1)
_OutsideColor("Outside Color", Color) = (0, .5, 0, 1)
_LineDistance("Mayor Line Distance", Range(0, 2)) = 1
_LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05
[IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4
_SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01
}
SubShader{
//the material is completely non-transparent and is rendered at the same time as the other opaque geometry
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#include "2D_SDF.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata{
float4 vertex : POSITION;
};
struct v2f{
float4 position : SV_POSITION;
float4 worldPos : TEXCOORD0;
};
v2f vert(appdata v){
v2f o;
//calculate the position in clip space to render the object
o.position = UnityObjectToClipPos(v.vertex);
//calculate world position of vertex
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
float scene(float2 position) {
// modify position here!
float2 squarePosition = position;
squarePosition = translate(squarePosition, float2(2, 2));
squarePosition = rotate(squarePosition, .125);
float squareShape = rectangle(squarePosition, float2(1, 1));
float2 circlePosition = position;
circlePosition = translate(circlePosition, float2(1, 1.5));
float circleShape = circle(circlePosition, 1);
float combination = merge(circleShape, squareShape);
return combination;
}
float4 _InsideColor;
float4 _OutsideColor;
float _LineDistance;
float _LineThickness;
float _SubLines;
float _SubLineThickness;
fixed4 frag(v2f i) : SV_TARGET{
float dist = scene(i.worldPos.xz);
fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist));
float distanceChange = fwidth(dist) * 0.5;
float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance;
float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance);
float distanceBetweenSubLines = _LineDistance / _SubLines;
float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines;
float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance);
return col * majorLines * subLines;
}
ENDCG
}
}
FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects
}
Теперь вы знаете все основы функций расстояний со знаком, которые я смог вспомнить. В следующем туториале я попытаюсь сделать с ними что-нибудь интересное.