Замена палитр (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, который сохранён в изображении. Далее мы используем его для получения цвета из палитры.
Анимация в начале статьи взята из внутриигровых скриншотов, на которых для изменения цвета тела персонажа я использую следующие палитры:
ссылка на оригинал статьи https://habr.com/ru/post/480836/
Добавить комментарий