Кстати на том же проекте я обнаружил, что у матриц для перехода YUV->RGB->YUV (ну или наоборот), которые лежат в стандарте и интрнетах, хромает точность, то есть матрица для прямого перехода - не совсем точно обратная матрица для обратного (и наоборот).
Если вы делаете это преобразование один раз, то ок и никто не заметит (разве что чел который отличит 0xA34B8F от 0xA34С8F). Но если вы будете это гонять туда-сюда каждую итерацию, то ошибка накопится очень быстро (даже если сидеть во флотах все время) и цвета распердолит во все стороны. Так что будьте осторожны.
Если вы делаете это преобразование один раз, то ок и никто не заметит (разве что чел который отличит 0xA34B8F от 0xA34С8F). Но если вы будете это гонять туда-сюда каждую итерацию, то ошибка накопится очень быстро (даже если сидеть во флотах все время) и цвета распердолит во все стороны. Так что будьте осторожны.
👍7
Прогаем с кенгурой
На тему видео вы, возможно, видели что-то типа 4:2:2 или 4:2:0. Это цветовая субдискретизация и показывает сколько сэмплов (пикселей простым языком) цветоразностных каналов на яркостный. Первая 4 - частота яркостного канала, Вторая 2 - сколько на эти 4 будет…
Вот еще момент. Для человеческого зрения в информативном плане важнее яркостная компонента, собственно поэтому цветовую и можно пожать.
Вообще в этом плане yuv замечателен:
- яркостный канал стандартизирован и выверен физически
- срез uv имеет одинаковую яркость для всех цветов
- нативно и по дефолту идет из видео
- позволяет достаточно красиво смешивать цвета
- можно легко сделать простой автобаланс белого (среднее значение uv по всей картинке сдвигаем в 0)
- максимально легкая конверсия в rgb и прочие (перемножение с матрицей)
- можно делать коррекцию яркости, не боясь попердолить цвет
Ну и тд
Единственный минус - он нативно не идет в шейдерах, картинках и дисплеях🤷🏼♂️
Вообще в этом плане yuv замечателен:
- яркостный канал стандартизирован и выверен физически
- срез uv имеет одинаковую яркость для всех цветов
- нативно и по дефолту идет из видео
- позволяет достаточно красиво смешивать цвета
- можно легко сделать простой автобаланс белого (среднее значение uv по всей картинке сдвигаем в 0)
- максимально легкая конверсия в rgb и прочие (перемножение с матрицей)
- можно делать коррекцию яркости, не боясь попердолить цвет
Ну и тд
Единственный минус - он нативно не идет в шейдерах, картинках и дисплеях🤷🏼♂️
🔥5
Так, переходим к практике.
Как сделать супер простой контрол для сатурейшена? Супер просто)))
Готово.
Идея такая: сатурейшн это расстояние от серого до нашего цвета. Поэтому берем из yuv яркость (там на самом деле достаточно одной компоненты) и двигаем наш цвет от этой серости. Все
Как сделать супер простой контрол для сатурейшена? Супер просто)))
float4 src = texture0.sample(texture0Smplr, In.v_texcoord);
float3 yuv = rgb2yuv(src.rgb);
src.rgb = mix(src.rgb, yuv.x, -uniform.amount);
Готово.
Идея такая: сатурейшн это расстояние от серого до нашего цвета. Поэтому берем из yuv яркость (там на самом деле достаточно одной компоненты) и двигаем наш цвет от этой серости. Все
👍3
И перевод для самых ленивых (не забывайте транспонировать матрицы по необходимости, ибо в метале и гл-е они лежат по столбцам)
static float3 rgb2yuv(float3 rgb) {
float3x3 kRGBtoYUVMatrix = float3x3(
0.29900, 0.58700, 0.11400,
-0.14713, -0.28886, 0.43600,
0.61500, -0.51499, -0.10001
);
return transpose(kRGBtoYUVMatrix) * rgb;
}
static float3 yuv2rgb(float3 yuv) {
float3x3 kYUVtoRGBMatrix = float3x3(
1.00000, 0.00000, 1.13983,
1.00000, -0.39465, -0.58060,
1.00000, 2.03211, 0.00000
);
return transpose(kYUVtoRGBMatrix) * yuv;
}
👍3
Проблема (или наоборот фича) такого подхода в том, что у вас не получится выжечь область изначально с малым сатурейшеном, так как мы по факту просто масштабируем вектор цвета относительно его яркости (серого состояния) максимум на его изначальную величину (то есть в два раза):
res = src - t * (lightness - src)
t = -1: res = src + lightness - src = lightness
t = 0: res = src
t = 1: res = src + (src - lightness)
(src - lightness) - это по сути исходный сатурейшн, на который мы и сдвигаем цвет.👍2
Раз уж мы целиком засунули все в YUV, то можно и сатурейшн там же сделать. Нужно только помнить, что на плоскости UV (0; 0) - это серый. Тогда получаем:
И тут тоже сатурейшн будет от серого до в два раза больше (если линейно менять
float4 src = texture0.sample(texture0Smplr, In.v_texcoord);
float3 yuv = rgb2yuv(src.rgb);
yuv.yz *= 1 + uniform.amount;
src.rgb = yuv2rgb(yuv);
И тут тоже сатурейшн будет от серого до в два раза больше (если линейно менять
amount в пределах [-1; 1], а то ведь можно сунуть что-то типа exp(10 * amount) или того хуже - но это уже на ваше усмотрение.👍3
Кстати, самый простой автобаланс белого также строится на базе факта, что 0 на UV плоскости это серый:
1 - предполагаем, что средний цвет картинки должен быть серым
2 - берем среднее значение uv по всей картинке
3 - сдвигаем эту точку в 0 для всех пикселей
4 - ...
5 - PROFIT
Если хочется чего-то поинтереснее, но при этом достаточно легковесного, то я делал такой подход на базе гистограмм, который потом можно показать на кривых.
1 - предполагаем, что средний цвет картинки должен быть серым
2 - берем среднее значение uv по всей картинке
3 - сдвигаем эту точку в 0 для всех пикселей
4 - ...
5 - PROFIT
Если хочется чего-то поинтереснее, но при этом достаточно легковесного, то я делал такой подход на базе гистограмм, который потом можно показать на кривых.
👍4❤1🤔1
Дальше буду рассказывать про те же цветовые модели, но в контексте градиентов, потому как про смешение цветов надо рассказать
🔥6👍1
А как вы храните 100500 девайсов для разработки?
(отжал коробку у ребенка)
(отжал коробку у ребенка)
😁8🙈1
Вот вам красивый градиент без смешения цветов для начала - наткнулся на него в куче шейдеров пока диск разбирал
float area(float center, float width, float x) {
return smoothstep(center - width, center, x) - smoothstep(center, center + width, x);
}
float3 gradient(float t) {
return float3(
area(0.25, 0.5, t),
area(0.5, 0.5, t),
area(0.75, 0.5, t)
);
}
fragment
float4 fs_main(
FS_INPUT In [[stage_in]],
constant FS_UNIFORM& uniform [[buffer(16)]])
{
return float4(gradient(In.v_texcoord.x), 1);
}
🔥4
Итак, градиенты и смешение цветов. Чтобы было удобно с этим всем разбираться, сделал такую функцию:
r, g, b - условны (в первых примерах буду использовать чистые цвета, но по сути там может быть что угодно)
градиент будет r-g-b-r
На картинке - результат для sRGB (без каких-либо преобразований дополнительных). Как видно, цвета грязные и темнят, хотя так быть не должно.
НИКОГДА НЕ МЕШАЙТЕ ЦВЕТА В sRGB (а это почти всегда дефолтное цветовое пространство) - мешайте как минимум в linear RGB
float3 gradient4(float3 r, float3 g, float3 b, float x) {
return select(select(mix(r, g, x * 3),
mix(g, b, x * 3 - 1.0),
x > 1.0 / 3.0),
mix(b, r, x * 3 - 2.0),
x > 2.0 / 3.0);
}
r, g, b - условны (в первых примерах буду использовать чистые цвета, но по сути там может быть что угодно)
градиент будет r-g-b-r
На картинке - результат для sRGB (без каких-либо преобразований дополнительных). Как видно, цвета грязные и темнят, хотя так быть не должно.
НИКОГДА НЕ МЕШАЙТЕ ЦВЕТА В sRGB (а это почти всегда дефолтное цветовое пространство) - мешайте как минимум в linear RGB
👍3
Почему нельзя в sRGB? Цвет в нем попердолен кривой, которую можно аппроксимировать гаммой 2.2 или тупо 2 (грубо, но приемелемо для графических редакторов). То есть значения в канале идут нелинейно, а значит при их смешении будут искажения.
Вот тупой вариант:
На картинке снизу sRGB, сверху - псевдо-linear RGB.
Как видим, переходы стади чище, но видны экстремумы и некоторое выцветание.
Вот тупой вариант:
float3 lrgb = sqrt(gradient4(r * r, g * g, b * b, uv.x));
На картинке снизу sRGB, сверху - псевдо-linear RGB.
Как видим, переходы стади чище, но видны экстремумы и некоторое выцветание.
❤4
Сверху катмулл-ром, снизу - линейная интерполяция
float3 catmullRom(float3 r, float3 g, float3 b, float3 a, float x, float t0) {
float t1 = t0 + 1.0 / 3.0;
float t2 = t1 + 1.0 / 3.0;
float t3 = t2 + 1.0 / 3.0;
float t = x;
float3 D0 = r;
float3 D1 = g;
float3 D2 = b;
float3 D3 = a;
float3 C1 = (t1-t)/(t1-t0)*D0 + (t-t0)/(t1-t0)*D1;
float3 C2 = (t2-t)/(t2-t1)*D1 + (t-t1)/(t2-t1)*D2;
float3 C3 = (t3-t)/(t3-t2)*D2 + (t-t2)/(t3-t2)*D3;
float3 B1 = (t2-t)/(t2-t0)*C1 + (t-t0)/(t2-t0)*C2;
float3 B2 = (t3-t)/(t3-t1)*C2 + (t-t1)/(t3-t1)*C3;
float3 A = (t2-t)/(t2-t1)*B1 + (t-t1)/(t2-t1)*B2;
return A;
}
float3 gradientCatmullRom(float3 r, float3 g, float3 b, float x) {
return select(select(catmullRom(b, r, g, b, x, -1.0 / 3.0),
catmullRom(r, g, b, r, x, 0),
x > 1.0 / 3.0),
catmullRom(g, b, r, g, x, 1.0 / 3.0),
x > 2.0 / 3.0);
}
❤1
Если шаги градиента стоят относительно далеко друг от друга, то он дает довольно красивую интерполяцию, но есть нюансы: синий на примере попердолен, хотя зеленый стал мягче (скорее всего артефакт сплайна).
Можно использовать что попроще, например smoothstep:
Как видим, тут (сверху) результат даже лучше, чем у катмул-рома (снизу)
Можно использовать что попроще, например smoothstep:
inline float3 smoothLerp(float3 a, float3 b, float t)
{
t = clamp(t, 0.0, 1.0);
t = t * t * (3.0 - 2.0 * t);
return a + (b - a) * t;
}
float3 gradientSmooth4(float3 r, float3 g, float3 b, float x)
{
float3 seg0 = smoothLerp(r, g, x * 3.0);
float3 seg1 = smoothLerp(g, b, x * 3.0 - 1.0);
float3 seg2 = smoothLerp(b, r, x * 3.0 - 2.0);
return select(select(seg0, seg1, x > 1.0 / 3.0), seg2, x > 2.0 / 3.0);
}
Как видим, тут (сверху) результат даже лучше, чем у катмул-рома (снизу)
❤1
Итого. Чтобы сделать достаточно простой/быстрый и при этом качественный градиент, нужно его делать в linearRGB и мешать не линейно, а через smoothstep. В итоге получится максимально быстро и красиво.
👍4