Перейти к содержимому

Fingercomp's Playground

  • записей
    87
  • комментария
    452
  • просмотров
    461 897
Fingercomp

Обновлено 2024-01-06. @logic обратил внимание, что T1-палитра работает не так, как я описывал. Переписал этот раздел и заодно добавил немного больше объяснений по коду.

Сообщение добавил Fingercomp

Палитры OpenComputers

Fingercomp

3 780 просмотров

Немногие знают, как работают палитры в OpenComputers. Расскажу здесь, как избавиться от необходимости прописывать гектары цветов в палитре, покажу, как упаковываются цвета в OpenComputers и дам пару алгоритмов для работы с индексами.

 

Сразу условимся, что индексы палитр у нас будут начинаться с нуля.

 

На каждой из трёх уровней видеокарт и мониторов своя поддерживаемая палитра цветов. Будем двигаться снизу вверх.


Первый уровень
Палитра состоит из двух цветов: чёрного и заданного в конфиге (по умолчанию белого). Конвертация цвета в индекс палитры тривиальна:

  • цвет нулевой — и индекс нулевой (чёрный цвет);
  • цвет ненулевой — индекс единичный.

Цвет в индекс (deflate) и обратно (inflate) превращать — одно удовольствие:

local palette = {
  0x000000, CONFIG.monochromeColor
}

local function t1deflate(index)
  if index == 0 then
    return 0
  else
    return 1
  end
end

local function t1inflate(index)
  return palette[index + 1]
end

Как и говорил.


Второй уровень
В палитре второго уровня имеется 16 закреплённых цветов:

local palette = {0xFFFFFF, 0xFFCC33, 0xCC66CC, 0x6699FF,
                 0xFFFF33, 0x33CC33, 0xFF6699, 0x333333,
                 0xCCCCCC, 0x336699, 0x9933CC, 0x333399,
                 0x663300, 0x336600, 0xFF3333, 0x000000}


При конвертации цвета в индекс палитры вернётся ближайший к данному цвет из палитры. Насколько цвета друг к другу близки, рассчитывается по специальной формуле, которая учитывает, что человеческий глаз лучше воспринимает зелёный, нежели красный и синий. В коде этим занимается функция delta. Вот как она выглядит (вместе с функций extract, выделяющей из числа вида 0xABCDEF числа 0xAB, 0xCD, 0xEF):

local function extract(color)
  color = color % 0x1000000
  local r = math.floor(color / 0x10000)
  local g = math.floor((color - r * 0x10000) / 0x100)
  local b = color - r * 0x10000 - g * 0x100
  return r, g, b
end

local function delta(color1, color2)
  local r1, g1, b1 = extract(color1)
  local r2, g2, b2 = extract(color2)
  local dr = r1 - r2
  local dg = g1 - g2
  local db = b1 - b2
  return (0.2126 * dr^2 +
          0.7152 * dg^2 +
          0.0722 * db^2)
end
 

Теперь можно конвертировать цвет в индекс палитры. Суть такова: выбираем из двух цветов ближайший и возвращаем его.

local function t2deflate(color)
  -- Сначала проверяем, совпадает ли данный цвет
  -- с каким-либо из палитры
  for idx, v in pairs(palette) do
    if v == color then
      return idx
    end
  end
  
  -- Составляем таблицу разниц между цветами
  local deltas = {}
  
  for idx, v in pairs(palette) do
    table.append(deltas, {idx, delta(v, color)})
  end
  
  -- Сортируем по увеличению разницы
  table.sort(deltas, function(a, b)
    return a[2] < b[2]
  end)
  
  -- Первый элемент будет с наименьшей разницей,
  -- то есть искомый. Возвращаем индекс.
  
  return deltas[1][1] - 1
end

 

Обратная же процедура — превращение индекса палитры в цвет — неизменна.

local t2inflate = t1inflate


Третий уровень
Палитра третьего уровня содержит уже 256 цветов: первые 16 цветов изменяются, а остальные соответствуют цветам палитры RGB-6-8-5. Это означает, что можно смешивать 6 оттенков красного, 8 оттенков зелёного и 5 оттенков синего. В общем-то, довольно очевидна причина такого выбора: человеческий глаз лучше всего различает оттенки зелёного и хуже всего — оттенки синего.


В любом случае, здесь алгоритмец будет посложнее. Сначала нужно сгенерировать палитру.
Начнём с первых 16 цветов. Они не включаются в палитру RGB-6-8-5, поэтому их заполнять нужно отдельно. В OpenComputers по умолчанию они содержат оттенки серого. Так как чёрный и белый уже включены в основную, зафиксированную палитру, то заново их дублировать смысла нет.

local palette = {}

-- grayscale
for i = 1, 16, 1 do
  palette[i] = 0xFF * i / (16 + 1) * 0x10101
end


Таким образом в таблице получаются следующие оттенки серого:

0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0

Эти цвета мы записываем в индексы от 0 до 15. Теперь нужно сгенерировать остальные цвета — они не изменяются. Здесь будет посложнее.
Посмотрим на картинку с палитрой:
RGB_6-8-5levels_palette.png
В OpenComputers левая верхняя ячейка палитры (0x000000) имеет индекс 16, а правая нижняя (0xFFFFFF) имеет индекс 255. Индексы распределяются слева направо, сверху вниз. То есть правая верхняя ячейка (0x00FFFF) имеет индекс 55, а вторая сверху и левая (0x330000) — это номер 56. Отсюда вытекает следующий алгоритм нахождения цвета: сначала найти индексы отдельно по R, G, B, затем для каждого из этих трёх индексов найти соответствующий ему оттенок цвета, а затем всё сложить.

for idx = 16, 255, 1 do
  local i = idx - 16
  local iB = i % 5
  local iG = (i / 5) % 8
  local iR = (i / 5 / 8) % 6
  local r = math.floor(iR * 0xFF / (6 - 1) + 0.5)
  local g = math.floor(iG * 0xFF / (8 - 1) + 0.5)
  local b = math.floor(iB * 0xFF / (5 - 1) + 0.5)
  palette[idx + 1] = r * 0x10000 + g * 0x100 + b
end

Идея следующая. Каждый из трёх каналов принимает значение от 0 до 255 (0xFF). Разбиваем их на определённое число ступеней (по-умному — квантуем): 6 для красного, 8 для зелёного и 5 для синего. Например, синий канал мы разобьём так:

  • 0/4 · 255 = 0
  • 1/4 · 255 = 63.75
  • 2/4 · 255 = 127.5
  • 3/4 · 255 = 191.25
  • 4/4 · 255 = 255

В знаменателе тут 4, а не 5, потому что считаем с нуля. Затем округляем до ближайшего целого конструкцией math.floor(x + 0.5). Перебрав все комбинации, мы получим все 6 × 5 × 8 = 240 цветов неизменяемой части палитры.

 

Всё. Палитра есть, теперь можно, наконец-то, конвертировать индексы между цветами.
Из индексов получить цвет довольно просто. Достаточно использовать ту же функцию, что и для предыдущих уровней:

t3inflate = t2inflate


С обратной же конвертацией всё несколько сложнее. Функция, используемая в OC, подбирает ближайший цвет хитрым алгоритмом, который я привожу ниже.

local function t3deflate(color)
  local paletteIndex = t2deflate(color)

  -- Если цвет из палитры, то используем значение выше
  for k, v in pairs(palette) do
    if v == color then
      return paletteIndex
    end
  end
  
  -- Иначе используем хитромудрый код
  local r, g, b = extract(color)
  local idxR = math.floor(r * (6 - 1) / 0xFF + 0.5)
  local idxG = math.floor(g * (8 - 1) / 0xFF + 0.5)
  local idxB = math.floor(b * (5 - 1) / 0xFF + 0.5)
  local deflated = 16 + idxR * 8 * 5 + idxG * 5 + idxB
  
  if (delta(t3inflate(deflated % 0x100), color) < 
      delta(t3inflate(paletteIndex & 0x100), color)) then
    return deflated
  else
    return paletteIndex
  end
end
Хитромудрость здесь не сильно сложная, на самом деле. Мы сначала находим индекс самого близкого цвета из изменяемой части палитры (paletteIndex). Дальше высчитываем индекс цвета из неизменяемой части (deflated), для чего в каждом канале отдельно ищем номер ближайшей ступени, на которые ранее квантовали. Последний if сравнивает 2 варианта и возвращает самый похожий (с точки зрения человека) на исходный.
 

В общем-то, это всё. Показал портированный со Scala на Lua код, который используется в OpenComputers. С помощью этого можно оптимизировать операции с экраном, выбирая поддерживаемые монитором цвета. И заодно избавиться от таблиц цветов, которые некоторые буквально берут и копипастят в файл, даже не задумываясь об изменяемых цветах палитры.
Особенно это важно, когда берётся значение цвета через gpu.get, потому что следующий код всегда вернёт false:

local gpu = require("component").gpu

gpu.setForeground(0x20AFFF)
gpu.setBackground(0x20AFFF)
gpu.set(1, 1, "HI")
return select(2, gpu.get(1, 1)) == 0x20AFFF

И всё потому, что gpu.get возвращает уже приведённый к индексу из палитры цвет. А 0x20AFFF в палитре, если не менять первые 16 цветов, не имеется.


Enjoy :P

  • Нравится 12


3 комментария


Рекомендуемые комментарии

Отлично! Давно хотел написать статью по работе с цветом. А теперь можно просто оставить ссылку, т. к. все основы хорошо разобраны.

Поделиться комментарием


Ссылка на комментарий
Гость
Добавить комментарий...

×   Вы вставили отформатированное содержимое.   Удалить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отобразить как ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...