Разработка 2D-игр на Go долгое время сталкивалась с компромиссом при реализации визуальных эффектов. Создание взрывов, повреждений техники, водной глади или интерфейсных подсветок традиционно требует сотен покадровых текстур (sprite sheets), что раздувает размер игры и перегружает видеопамять. Альтернатива — попиксельная обработка кадров силами процессора — быстро снижает FPS.
Популярный открытый 2D-движок Ebitengine предлагает другое решение: встроенный язык Kage. Он позволяет писать пиксельные (фрагментные) шейдеры на строгом подмножестве Go, которые движок на лету компилирует в нативные инструкции GPU для OpenGL, Metal и DirectX.
Архитектура транслятора Kage: подмножество Go на GPU
Kage — это предметно-ориентированный язык (DSL), созданный для того, чтобы Go-разработчикам не приходилось изучать диалекты GLSL, HLSL или MSL. Исходный код форматируется утилитой gofmt, проверяется линтерами и выглядит как обычный Go-файл.
Однако код Kage исполняется не в рантайме Go, а параллельно на тысячах вычислительных ядер GPU для каждого пикселя. Из-за этого язык накладывает строгие ограничения:
- Поддерживаемые типы: скаляры
floatиint, векторы (vec2,vec3,vec4,ivec*), матрицы (mat2,mat3,mat4), фиксированные массивы, векторная арифметика и swizzling (color.rgba,pos.xy). - Исключенные абстракции: в Kage нет строк, срезов (slices), указателей, структур, интерфейсов, импортов, горутин и методов.
- Единицы координат: документация рекомендует начинать файл директивой
//kage:unit pixels, переводящей вычисления в абсолютные экранные пиксели вместо устаревших нормализованных texels.
Важная деталь — точность чисел с плавающей точкой: мобильные GPU могут снижать точность float, поэтому сравнения на строгое равенство в шейдерах исключаются.
Анатомия функции Fragment и передача параметров
Точкой входа любого шейдера служит функция Fragment. В движке нет вертексной стадии: Kage отвечает только за цвет фрагментов.
Сигнатура функции принимает до четырех аргументов:
func Fragment(dstPos vec4, src0Pos vec2, color vec4, custom vec4) vec4
dstPos: экранные координаты целевого пикселя (vec4(x, y, 0, 1)).src0Pos: координаты точки в исходной текстуре в пикселях.color: цвет вершины для модуляции оттенка.custom: пользовательские параметры для отрисовки треугольников.- Результат: вектор цвета
vec4(r, g, b, a)с каналами от 0.0 до 1.0.
Динамические параметры из игрового цикла передаются через глобальные экспортируемые переменные с заглавной буквы (uniforms). Внутри шейдера перезаписывать их нельзя: они неизменны для всего кадра отрисовки.
Каталог процедурных визуальных эффектов
Практическая сила Kage раскрывается в пяти базовых алгоритмах, проверенных в открытых коммерческих играх Roboden и Decipherism:
- Маска повреждений (Damage Mask)
В шейдер передаются две текстуры: спрайт объекта и маска сколов. Функция считывает пиксель через
imageSrc0At(src0Pos)и проверяет альфа-канал: только еслиc.a != 0.0 && mask.a != 0.0, накладывается потемнение в зависимости от уровня здоровьяHP, нормализованного черезclamp(HP + (1.0 - mask.a), 0.0, 1.0). Прозрачный фон остается нетронутым. - Контур выбора объекта (Pick Outline)
Для подсветки выбранного юнита шейдер проверяет прозрачность соседних точек со смещением. Обнаружив границу спрайта, Kage плавно подмешивает пульсирующий цвет через синус от uniform-переменной времени
Time, избавляя от рисования рамок вручную. - Имитация ЭЛТ-монитора и глитчи (CRT Display)
Для стилизации под ретро-экран координаты
src0Posискажаются выпуклой линзой к центру. Поверх накладывается синусоидальная модуляция по оси Y, создающая полосы развертки (scanlines), а сдвиг координат каналов формирует цветовую аберрацию. - Зацикленная процедурная анимация (UV Shift)
Вода или конвейерные ленты анимируются смещением текстурных координат: координата
src0Pos.xскладывается сTime * Speed, после чего берется остаток от деления на ширину текстуры. Эффект работает без тяжелых спрайт-листов. - Эффект растворения и постройки (Dissolve)
Вертикальная координата фрагмента сравнивается с переменной прогресса
Progress. Пиксели выше порога отсекаются вызовомdiscard(), а на линии среза рисуется яркая полоса свечения.
Сравнение графических подходов в 2D-играх
| Метод реализации | Память под ассеты | Нагрузка на CPU | Сложность кода | Портируемость |
|---|---|---|---|---|
| Шейдеры Kage | Минимальная (1 спрайт) | Нулевая (GPU) | Низкая (Go-синтаксис) | Полная (OpenGL, Metal, DX) |
| Растровые спрайт-листы | Высокая (сотни кадров) | Низкая | Высокая (ручной арт) | Полная |
| Попиксельный рендер CPU | Средняя | Экстремальная | Средняя | Низкая масштабируемость |
| Нативный GLSL / Metal | Минимальная | Нулевая | Высокая (разные языки) | Требует отдельных файлов |
Пошаговое руководство по внедрению шейдера
Подключение шейдера в Ebitengine состоит из четырех шагов:
1. Написание исходного кода шейдера:
//kage:unit pixels
package main
var Time float
func Fragment(dstPos vec4, src0Pos vec2, color vec4) vec4 {
pos := src0Pos
pos.x += sin(src0Pos.y/10.0 + Time*2.0) * 3.0
return imageSrc0At(pos) * color
}
2. Однократная компиляция при загрузке ассетов:
shader, err := ebiten.NewShader(shaderSourceBytes)
if err != nil {
log.Fatalf("Ошибка компиляции Kage: %v", err)
}
3. Передача параметров и отрисовка в кадре:
opts := &ebiten.DrawRectShaderOptions{}
opts.Images[0] = spriteImage
opts.Uniforms = map[string]any{"Time": float32(gameTime)}
dst.DrawRectShader(spriteWidth, spriteHeight, shader, opts)
4. Проверка и диагностика: Официальный демонстрационный пример запускается командой:
go run github.com/hajimehoshi/ebiten/v2/examples/shader@latest
При сбоях в рендере:
- Проверьте ошибку от
ebiten.NewShader. - Замените тело функции на тестовый цвет
return vec4(1.0, 0.0, 1.0, 1.0). - Убедитесь, что размеры исходных изображений в
opts.Imagesстрого совпадают с шириной и высотой прямоугольника отрисовкиDrawRectShader.
Ограничения и инженерные выводы
Главное архитектурное правило Ebitengine: все входные текстуры в массиве Images обязаны совпадать по размеру с областью отрисовки, иначе чтение соседних слоев приведет к искажению координат.
Кроме того, в релизе Ebitengine 2.10 появилось экспериментальное API предкомпиляции шейдеров. Следует помнить, что внутренний идентификатор SourceID может меняться между версиями движка, требуя инвалидации кэша при обновлении go.mod.
Использование Kage позволяет создавать выразительные визуальные эффекты без раздувания ассетов, сохраняя кодовую базу в рамках единой экосистемы Go.
