В этом девлоге я покажу вам любимую мной технику, которую я активно использую в своей игре Vagabond: замена палитр.
Замена палитр (Palette swapping) — это изменение палитры текстуры. В статье мы реализуем её при помощи шейдеров. В старые времена это была полезная техника, позволяющая без лишних трат памяти добавить ресурсам вариативности. Сегодня она используется в процедурной генерации для создания новых ресурсов.

Первым шагом будет подготовка изображений к замене палитр. В растровом изображении каждый пиксель содержит цвет, но нам нужно, чтобы вместо этого он содержат индекс своего цвета в палитре. Благодаря этому мы отделим структуру изображения (области одного цвета) от реальных цветов.
На самом деле, некоторые форматы изображений поддерживают такой способ хранения. Например, формат PNG имеет возможность сохранения индексированных цветов. К сожалению, многие библиотеки загрузки изображений создают массив цветов, даже если изображение было сохранено в индексированном режиме. Это относится и к используемой мной библиотеке SFML. Внутри в ней используется stb_image, который автоматически «удаляет палитру» изображений, т.е. заменяет индексы соответствующим цветом палитры.
Следовательно, чтобы избежать этой проблемы, нужно хранить изображение и палитру по отдельности. Изображение записано в оттенках серого, а уровень серого каждого пикселя соответствует индексу его цвета в палитре.
Вот пример того, что мы ожидаем получить:

Чтобы добиться этого, я использую небольшую функцию на Python, в которой применяется библиотека Pillow:
Сначала функция преобразует изображение в режим палитры. Затем она реинтерпретирует его как изображение в градациях серого. Затем извлекает палитру. Ничего сложного, основная работа выполняется библиотекой Pillow.
Подготовив изображения, мы готовы писать шейдер для замены палитр. Для передачи палитры шейдеру существует две стратегии: можно использовать текстуру или однородный массив. Я выяснил, что проще использовать однородный массив, поэтому использовал его.
Вот мой шейдер, я написал его на GLSL, но думаю, что его можно легко перенести на другой язык создания шейдеров:
Мы просто используем текстуру для считывания красного канала текущего пикселя. Красный канал — это значение с плавающей запятой в интервале от 0 до 1, поэтому мы умножаем его на 255 и преобразуем в
Анимация в начале статьи взята из внутриигровых скриншотов, на которых для изменения цвета тела персонажа я использую следующие палитры:
Замена палитр (Palette swapping) — это изменение палитры текстуры. В статье мы реализуем её при помощи шейдеров. В старые времена это была полезная техника, позволяющая без лишних трат памяти добавить ресурсам вариативности. Сегодня она используется в процедурной генерации для создания новых ресурсов.

Подготовка изображений
Первым шагом будет подготовка изображений к замене палитр. В растровом изображении каждый пиксель содержит цвет, но нам нужно, чтобы вместо этого он содержат индекс своего цвета в палитре. Благодаря этому мы отделим структуру изображения (области одного цвета) от реальных цветов.
На самом деле, некоторые форматы изображений поддерживают такой способ хранения. Например, формат PNG имеет возможность сохранения индексированных цветов. К сожалению, многие библиотеки загрузки изображений создают массив цветов, даже если изображение было сохранено в индексированном режиме. Это относится и к используемой мной библиотеке SFML. Внутри в ней используется stb_image, который автоматически «удаляет палитру» изображений, т.е. заменяет индексы соответствующим цветом палитры.
Следовательно, чтобы избежать этой проблемы, нужно хранить изображение и палитру по отдельности. Изображение записано в оттенках серого, а уровень серого каждого пикселя соответствует индексу его цвета в палитре.
Вот пример того, что мы ожидаем получить:

Чтобы добиться этого, я использую небольшую функцию на Python, в которой применяется библиотека Pillow:
import io
import numpy as np
from PIL import Image
def convert_to_indexed_image(image, palette_size):
# Convert to an indexed image
indexed_image = image.convert('RGBA').convert(mode='P', dither='NONE', colors=palette_size) # Be careful it can remove colors
# Save and load the image to update the info (transparency field in particular)
f = io.BytesIO()
indexed_image.save(f, 'png')
indexed_image = Image.open(f)
# Reinterpret the indexed image as a grayscale image
grayscale_image = Image.fromarray(np.asarray(indexed_image), 'L')
# Create the palette
palette = indexed_image.getpalette()
transparency = list(indexed_image.info['transparency'])
palette_colors = np.asarray([[palette[3*i:3*i+3] + [transparency[i]] for i in range(palette_size)]]).astype('uint8')
palette_image = Image.fromarray(palette_colors, mode='RGBA')
return grayscale_image, palette_image
Сначала функция преобразует изображение в режим палитры. Затем она реинтерпретирует его как изображение в градациях серого. Затем извлекает палитру. Ничего сложного, основная работа выполняется библиотекой Pillow.
Шейдер
Подготовив изображения, мы готовы писать шейдер для замены палитр. Для передачи палитры шейдеру существует две стратегии: можно использовать текстуру или однородный массив. Я выяснил, что проще использовать однородный массив, поэтому использовал его.
Вот мой шейдер, я написал его на GLSL, но думаю, что его можно легко перенести на другой язык создания шейдеров:
#version 330 core
in vec2 TexCoords;
uniform sampler2D Texture;
uniform vec4 Palette[32];
out vec4 Color;
void main()
{
Color = Palette[int(texture(Texture, TexCoords).r * 255)];
}
Мы просто используем текстуру для считывания красного канала текущего пикселя. Красный канал — это значение с плавающей запятой в интервале от 0 до 1, поэтому мы умножаем его на 255 и преобразуем в
int
, чтобы получить исходный уровень серого от 0 до 255, который сохранён в изображении. Далее мы используем его для получения цвета из палитры.Анимация в начале статьи взята из внутриигровых скриншотов, на которых для изменения цвета тела персонажа я использую следующие палитры:

Комментарии (5)
GCU
25.12.2019 23:37Как-то пропустили что текстура должна читаться без интерполяции выбирая ближайшего соседа, да и тема генерации mipmap для палитровых текстур не раскрыта.
atri1
26.12.2019 23:49mipmap для палитровых текстур не раскрыта.
texelFetch(,,0) https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/texelFetch.xhtml
П.С. пример в статье очень ситуативный, такие шайдеры пишуться под текущую идею стиля, их не сделать универсальными.
палитру можно генерировать в самом шейдере, можно передавть у текстуре, можно в униформах, можно другими условиями динамически генерировать...
GCU
27.12.2019 10:45texelFetch(,,0)
Ну это по сути что mipmap нет, и при уменьшении текстура может пестрить.
Я имел ввиду честный NEAREST_MIPMAP_NEAREST, поскольку в статье приводится код Python для генерации палитровой текстуры, то там же можно было генерировать ещё и все mipmap т.к. стандартный generateMipmap для этого не подходит.
Sirion
При всей любви к вашим переводам — совсем уж капитанская статья. Те же вещи у вас же были раскрыты намного глубже.
atri1
плюсую
статья бесполезна без базовых знаний логики шейдеров, если базовые знания есть то реализация такой логики будет быстрее прочтения этой статьи...
П.С. такойже пример в живую, комуто в пример делал давно https://www.shadertoy.com/view/ttjSD1