Pull to refresh

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

Reading time3 min
Views3.2K
Original author: Pierre Vigier
В этом девлоге я покажу вам любимую мной технику, которую я активно использую в своей игре 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, который сохранён в изображении. Далее мы используем его для получения цвета из палитры.

Анимация в начале статьи взята из внутриигровых скриншотов, на которых для изменения цвета тела персонажа я использую следующие палитры:

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+4
Comments5

Articles