Хабрахабр

[Перевод] Пространственные манипуляции в 2D с помощью Signed Distance Fields

При работе с полигональными ассетами можно отрисовывать только по одному объекту за раз (если не учитывать такие приёмы, как batching и instancing), но если использовать поля расстояний со знаком (signed distance fields, SDF), то мы не этим не ограничены. Если две позиции имеют одинаковую координату, то функции расстояний со знаком возвратят одинаковое значение, и за одно вычисление мы можем получить несколько фигур. Чтобы понять, как преобразовывать пространство, используемое для генерации полей расстояний со знаком, я рекомендую разобраться, как создавать фигуры с помощью функций расстояний со знаком и комбинировать sdf-фигуры.

Конфигурация

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

Здесь важно то, что модифицируемая часть находится до использования позиций для генерации фигур.

Shader "Tutorial/036_SDF_Space_Manpulation/Type" 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.

Мы просто объявим аргумент позиции как inout. Чаще всего использующий эту функцию код будет выглядеть примерно как position = mirror(position);, поэтому мы можем его немного упростить. Возвращаемое значение тогда может иметь тип 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

Базовый демонстрационный шейдер

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
}

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть