Замена палитр в игре при помощи шейдеров

от автора

В этом девлоге я покажу вам любимую мной технику, которую я активно использую в своей игре Vagabond: замена палитр.

Замена палитр (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/


Комментарии

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

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