Использование текстур в OpenGL довольно распространенная тема на различных обучающих сайтах и форумах. Но кода необходима сделать анимированную текстуру все становится не так просто. Мне известно 2 способа это сделать и 1 из них я опишу.

Проблема заключается в том, что преградой становится шина передачи данных между CPU и GPU. И на каждый фрейм загружать новую текстуру в видеопамять, еще и в большем разрешении становится проблематичным.

Первый способ это использовать 2 промежуточных пиксельных буфера: в один буфер рендерится, а из другого считывается, при чем для этих целей используется DMA что значительно ускоряет процесс.

Второй способ гораздо проще: использовать Spritesheet. Такой способ применялся еще в первых консольных приставках, его я и опишу.

Идея заключается в том что бы на одной текстуре разместить изображения нескольких фреймов, и для того что бы выбрать какой фрейм необходимо передать на GPU только текстурные координаты. Для генерации Sprite Sheet можно воспользоваться этим сайтом, или скачать в интернете. Я скачал вот такую фот картинку:



Приступим к реализации. Для этого необходимо подключить несколько сторонних библиотек

  • glew — собственно говоря сам OpenGL
  • glfw — библиотека для создания окна и контекста OpenGL
  • FreeImage — библиотека для открытия и декодирования изображений

Загружаем и рендерим квадрат с текстурой, есть много туториалов как это делать, например этот. Для удобства создадим структуру в которой будет храниться информация о количестве рядов и столбцов в Sprite Sheet, а так же текущий фрейм.

struct SpriteAnimator {
	unsigned int currentFreme;
	unsigned int rows;
	unsigned int columns;
};

Для использования анимации необходимо рассчитывать и менять текстурные координаты на каждый фрейм. Для это используются функции initSpriteAnimation — для инициализации, и spriteAnimationNextFrame — для пересчета текстурных координат на следующий фрейм:

SpriteAnimator initSpriteAnimation( int rows, int columns, float * texCoord) {
	SpriteAnimator animator;
	animator.currentFreme = 0;

	animator.rows = rows;
	animator.columns = columns;
	
	
	spriteAnimationUpdate(animator, texCoord);

	return animator;
}

void spriteAnimationNextFrame(SpriteAnimator & animator, float * texCoord) {
	const int maxFrame = animator.columns * animator.rows - 1;
	if (maxFrame == animator.currentFreme) {
		animator.currentFreme = 0;
	}
	else {
		animator.currentFreme++;
	}
	spriteAnimationUpdate(animator, texCoord);
}

void spriteAnimationUpdate(SpriteAnimator & animator, float * texCoord) {
	const int X = 0;
	const int Y = 1;

	const int V0 = 0;
	const int V1 = 2;
	const int V2 = 4;
	const int V3 = 6;


	const float frameWidth = 1.f / animator.columns;
	const float frameHeight = 1.f / animator.rows;

	const int row = animator.rows - animator.currentFreme / animator.columns;
	const int col = animator.currentFreme % animator.columns;


	texCoord[V0 + X] = frameWidth * col;
	texCoord[V0 + Y] = frameHeight * row;

	texCoord[V1 + X] = frameWidth * (col + 1);
	texCoord[V1 + Y] = frameHeight * row;

	texCoord[V2 + X] = frameWidth * (col + 1);
	texCoord[V2 + Y] = frameHeight * (row + 1);

	texCoord[V3 + X] = frameWidth * col;
	texCoord[V3 + Y] = frameHeight * (row + 1);
}

И собственно говоря сама цикл рендеринга:

while (!glfwWindowShouldClose(window))
{
	render(shader);
	Sleep(1000 / 25);
	spriteAnimationNextFrame(animator, texCoord);
		
	glfwSwapBuffers(window);
	glfwPollEvents();
}


Анимация имеет frame rate 25 кадров в секунду, поэтому используется задержка в 1/25 сек. Не лучшее решение, но зато самое простое.

В результате получилась вот такая вот анимация:


Далее пример целиком:
#include <chrono>
#include <thread>

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <FreeImage.h>

#define Sleep(ms) std::this_thread::sleep_for(std::chrono::milliseconds(ms));

float position[] = {-1.f, -1.f, 0,
					1.f, -1.f, 0,
					1.f, 1.f, 0,
					-1.f, 1.f, 0};
float texCoord[] = { 0.f, 0.f,
					1.f, 0.f,
					1.f, 1.f,
					0.f, 1.f };
GLuint indexes[] = { 0, 1,2,	
					2,3,0};


const char vertexShader[] = "attribute vec4 a_position;"
"attribute vec2 a_texCoord;"
"out vec2 v_texCoord;"
"void main(void) {"
"	v_texCoord = a_texCoord;"
"	gl_Position = a_position;"
"}";


const char fragmentShader[] = "uniform sampler2D text;"
"in vec2 v_texCoord;"
"void main (void)  { "
"	gl_FragColor = texture(text, v_texCoord);"
"}";

struct Shader {
	GLuint program;
	GLuint position;
	GLuint texCoord;
	GLuint tex;
};

struct SpriteAnimator {
	unsigned int currentFreme;
	unsigned int rows;
	unsigned int columns;
};

GLuint loadTexture(const char * path)
{
	int w, h;
	GLuint tex;


	FIBITMAP *dib(0);
	FREE_IMAGE_FORMAT fif = FreeImage_GetFileType(path, 0);

	if (fif == FIF_UNKNOWN)
		fif = FreeImage_GetFIFFromFilename(path);

	if (fif == FIF_UNKNOWN)
		return -1;

	if (FreeImage_FIFSupportsReading(fif)) {
		dib = FreeImage_Load(fif, path);
		if (!dib)	return -1;
	}

	w = FreeImage_GetWidth(dib);
	h = FreeImage_GetHeight(dib);

	const char * data = (const char *)FreeImage_GetBits(dib);

	
	glGenTextures(1, &tex);


	glBindTexture(GL_TEXTURE_2D, tex);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_BGR, GL_UNSIGNED_BYTE, data);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


	glBindTexture(GL_TEXTURE_2D, 0);

	return tex;
}

Shader createShader() {
	Shader shader;
	GLint statusF, statusV;
	GLuint vertexShaderId = glCreateShader(GL_VERTEX_SHADER);
	GLuint fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
	char * srcPrt;

	srcPrt = (char *)vertexShader;
	glShaderSource(vertexShaderId, 1, (const GLchar **)&srcPrt, NULL);

	srcPrt = (char *)fragmentShader;
	glShaderSource(fragmentShaderId , 1, (const GLchar **)&srcPrt, NULL);

	glCompileShader(vertexShaderId);
	glCompileShader(fragmentShaderId);

	glGetShaderiv(vertexShaderId, GL_COMPILE_STATUS, &statusV);
	glGetShaderiv(fragmentShaderId, GL_COMPILE_STATUS, &statusF);


	if (statusV == GL_FALSE){ /*сообщить об ошибке компиляции вершинного шейдера*/	}
	if (statusF == GL_FALSE) {	/*сообщить об ошибке компиляции фрагментного шейдера*/ }

	shader.program = glCreateProgram();

	glAttachShader(shader.program, vertexShaderId);
	glAttachShader(shader.program, fragmentShaderId);

	glLinkProgram(shader.program);
	glUseProgram(shader.program);


	shader.position = glGetAttribLocation(shader.program, "a_position");
	shader.texCoord = glGetAttribLocation(shader.program, "a_texCoord");

	glEnableVertexAttribArray(shader.position);
	glEnableVertexAttribArray(shader.texCoord);

	shader.tex = loadTexture("FireSpriteSheet.jpg");

	return shader;

}



void render(Shader & shader) {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glClearColor(0., 0., 0., 1.);

	glUseProgram(shader.program);
	glBindTexture(GL_TEXTURE_2D, shader.tex);

	glVertexAttribPointer(shader.position, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), position);
	glVertexAttribPointer(shader.texCoord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), texCoord);

	glDrawElements(GL_TRIANGLES, sizeof(indexes) / sizeof(indexes[0]), GL_UNSIGNED_INT, indexes);

	glBindTexture(GL_TEXTURE_2D, 0);
	glUseProgram(0);

	return;
}


void spriteAnimationUpdate(SpriteAnimator & animator, float * texCoord);

SpriteAnimator initSpriteAnimation( int rows, int columns, float * texCoord) {
	SpriteAnimator animator;
	animator.currentFreme = 0;

	animator.rows = rows;
	animator.columns = columns;
	
	
	spriteAnimationUpdate(animator, texCoord);

	return animator;
}

void spriteAnimationNextFrame(SpriteAnimator & animator, float * texCoord) {
	const int maxFrame = animator.columns * animator.rows - 1;
	if (maxFrame == animator.currentFreme) {
		animator.currentFreme = 0;
	}
	else {
		animator.currentFreme++;
	}
	spriteAnimationUpdate(animator, texCoord);
}

void spriteAnimationUpdate(SpriteAnimator & animator, float * texCoord) {
	const int X = 0;
	const int Y = 1;

	const int V0 = 0;
	const int V1 = 2;
	const int V2 = 4;
	const int V3 = 6;


	const float frameWidth = 1.f / animator.columns;
	const float frameHeight = 1.f / animator.rows;

	const int row = animator.rows - animator.currentFreme / animator.columns;
	const int col = animator.currentFreme % animator.columns;


	texCoord[V0 + X] = frameWidth * col;
	texCoord[V0 + Y] = frameHeight * row;

	texCoord[V1 + X] = frameWidth * (col + 1);
	texCoord[V1 + Y] = frameHeight * row;

	texCoord[V2 + X] = frameWidth * (col + 1);
	texCoord[V2 + Y] = frameHeight * (row + 1);

	texCoord[V3 + X] = frameWidth * col;
	texCoord[V3 + Y] = frameHeight * (row + 1);
}

int main() {

	GLFWwindow* window;
	if (!glfwInit())
		return -1;
	window = glfwCreateWindow(640, 480, "Simple animated texture with OpenGL", NULL, NULL);
	if (!window)
	{
		glfwTerminate();
		return -1;
	}

	glfwMakeContextCurrent(window);
	glewInit();


	Shader shader = createShader();
	SpriteAnimator animator = initSpriteAnimation(6, 6, texCoord);

	while (!glfwWindowShouldClose(window))
	{

		render(shader);
		Sleep(1000 / 25);
		//Sleep(1000);
		spriteAnimationNextFrame(animator, texCoord);
		
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glfwDestroyWindow(window);
	glfwTerminate();

	return 0;
}


Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (27)


  1. bigfatbrowncat
    29.09.2016 17:14
    +1

    >Такой способ применялся еще в первых консольных приставках

    Несколько раньше. Спрайтовая анимация существует столько же, сколько вся компьютерная графика. Она намного старше, чем OpenGL.

    Слово «спрайт» было придумано в 1970-е годы одним сотрудником компании Texas Instruments: их новая микросхема TMS9918 могла аппаратно отображать небольшие картинки поверх неподвижного фона


    За статью спасибо. Отличное справочное пособие по быстрому началу «плоского» OpenGL-приложения.


    1. AllexIn
      29.09.2016 23:00
      +4

      Речь не о спрайтах же. А о текстурных атласах.


      1. bigfatbrowncat
        01.10.2016 18:49

        Я, видимо, в двух терминах запутался. Я всегда понимал онятие «спрайт» двояко. Во-первых, прямоугольная картинка, выводимая на экран, а во-вторых — картинка, текстура которой вырезается из карты. Видимо, надо лучше матчасть учить…


        1. Darthman
          01.10.2016 21:49
          +1

          Спрайт это совокупность положения, текстуры, анимации и т.п.


      1. Sergey99999
        02.10.2016 18:14

        Все таки это не текстурный атлас.
        тут хорошо объясняют разницу между этими вещами


        1. AllexIn
          02.10.2016 18:25
          +2

          Железо — это металл. А металл — это железо?
          sptire-sheet — это частный случай текстурного атласа.
          Так что назвать sprite-sheet текстурным атласом — вполне корректно. А вот обратное — не всегда корректно.


  1. afiskon
    29.09.2016 17:23

    Большое спасибо. Очень интересно почитать про OpenGL. Некоторое время назад занимался его изучением (остановился на таком — камера, текстуры, вывод текста, освещение без теней), но в настоящее время подзабил. Пожалуйста, пишите еще!


    1. Sergey99999
      02.10.2016 18:16

      Спасибо.
      Буду писать еще.


  1. konshyn
    29.09.2016 17:32

    Такой способ применялся еще в первых консольных приставках


    А как решать такие проблемы:
    1. Просадка кадров. Когда текстура — это 150 кадров, рассчитана на анимацию 2 секунды по 75 кадров, а получается только например 50 кадров? Пропускать кадры просто?

    2. Когда кадров в текстуре 30 и рассчитана на секунду, а фпс получается 75?


    1. engune
      29.09.2016 18:06
      +4

      Я у себя в движке решаю так: знаю что анимация занимает 2,5 сек. При каждом рендере кадра обновляю анимацию — вычисляю кадр так:
      текущий кадр = (прошло времени после старта / общая время анимации) * количество кадров
      потеряются кадры только, если будет просадка по производительности

      Основная идея — не привязываемся к FPS, смотрим только на время


      1. ertaquo
        29.09.2016 22:20

        Плюс для остальных вещей, типа обработки перемещений объектов и т. п., можно использовать deltaTime.


      1. konshyn
        01.10.2016 19:27

        Спасибо. В целом, я так и думал, но так как я только начал вникать в эту тему, то я мало в чем уверен.
        Вообще, хотелось бы почитать про the best practice.


    1. Darthman
      30.09.2016 18:37
      +2

      >>2. Когда кадров в текстуре 30 и рассчитана на секунду, а фпс получается 75?

      Я у себя, например, интеполирую кадры анимации, получая более плавную картинку.


      1. AllexIn
        01.10.2016 16:20

        Интерполируете как? fade? Дает ощущение мыла… ИМХО — лучше просто четкое переключение. Тем более если FPS у нас хотя бы 30, мгновенная смена кадра даст вполне нормальный результат.


        1. Darthman
          01.10.2016 21:49
          +2

          Нет, двунаправленно по motion flow векторам.


      1. konshyn
        01.10.2016 19:25

        А что значит интерполировать? В данной ситуации разве это не то же самое, что и

        текущий кадр = (прошло времени после старта / общая время анимации) * количество кадров
        ?

        Если нет, то можете посоветовать где почитать что-нибудь?


        1. Darthman
          01.10.2016 21:50
          +2

          Нет, не тоже самое. попробуйте замедлить скорость смены кадров до 5-10 в секунду и все будет дерганым. Да и даже на 25 все не сказать чтобы жутко плавно было. Речь о том, чтобы именно не было этой дерганности.


        1. Overlordff
          02.10.2016 18:05

          Если я не ошибаюсь, имеется в виду, что цвет пикселя, выводимого на экран, линейно (а может и билинейно) интерполируется между предыдущим и следующим кадром.
          Интерполяция
          Линейная интерполяция


        1. Sergey99999
          03.10.2016 12:26

          Это не простая штука — на сколько я понял, нужно определить вектор движения каждого пикселя. Исходя из этого, когда грубо говоря знаем от куда и куда перемещается пиксель — определяем где он должен быть между кадрами.
          В этой статье написано как это делается.


          1. Darthman
            03.10.2016 12:31
            +1

            Именно


    1. Darthman
      30.09.2016 18:38
      +1

      >>Пропускать кадры просто?
      А что Вас удивляет? Разумеется пропускать, нельзя показать все 150.

      Вы еще учтите, что такое количество кадров жрет нехило памяти, и это очень жирно, даже сейчас с гигабайтами.


  1. HOMPAIN
    02.10.2016 17:36

    Здесь нет смысла объединять текстурки с кадрами в одну, лучше тупо сделать 36 маленьких текстур и при рендере ставить нужную.
    Вы от этого способа только проигрываете:
    1)Немного меньше скорость чтения из такой текструры, чем из кадра отдельно
    2)Проблемы с фитрацией на краях кадров
    3)Из количества кадров должен извлекаться корень, что бы не было пустых мест на текстуре
    4)Количества кадров ограничено максимальным размером текстуры
    5)Такую текстуру тяжелее стримить без пролагов чем отдельные небольшие кадры(т.е. плавно прогружать уже по ходу игры)

    А если очень хочется всё в кучу свалить, то специально для таких вещей есть Array Texture
    https://www.opengl.org/wiki/Array_Texture


    1. Sergey99999
      02.10.2016 18:31

      Если нужно нужно использовать текстуры размером например 100х200 (не буду ручаться за все видеокарты), некоторые видеокарты выделят память 256х256. и так для каждого кадра.
      Так же прирост в производительности если плавно переключатся между кадрами(интерполировать два кадра), использовать несколько анимаций в одной текстуре, или если рендерятся несколько объектов где одна и та же анимация не синхронная — в таких случаях не надо каждый раз переключатся между текстурами.


    1. Darthman
      03.10.2016 12:36
      +2

      >>лучше тупо сделать 36 маленьких текстур
      И чем же лучше? У вас на экране, скажем 100 таких анимаций одновременно, но все в разных стадиях, на разных кадрах. В вашем случаем мы получаем 100 смен стейтов, 100 Draw Call. Ужас, кошмар и тормоза.
      Если текстура не кратная степени двойки, то в вашем случае будет довольно немалый оверхед по памяти лишней съеденной.

      >>1)Немного меньше скорость чтения из такой текструры, чем из кадра отдельно
      Наложение текстуры по UV всей текстуры или кусочка не отличается по скорости никак.

      >> 2)Проблемы с фитрацией на краях кадров
      Какие у вас проблемы, покажите пример?

      >> 3)Из количества кадров должен извлекаться корень, что бы не было пустых мест на текстуре
      Это вообще я не понял что за минус такой.

      >> 4)Количества кадров ограничено максимальным размером текстуры
      Вам кто-то запрещает бить анимации на несколько подобных атласов, или не хватает 4096х4096 текстур?

      >> 5)Такую текстуру тяжелее стримить без пролагов чем отдельные небольшие кадры(т.е. плавно прогружать уже по ходу игры)
      Такую текстуру можно грузить по мипмапам, и анимация будет уже целиком в памяти в худшем качестве, пока ваши 36 картинок будут еще грузиться.


      1. HOMPAIN
        03.10.2016 19:49

        1.
        >Наложение текстуры по UV всей текстуры или кусочка не отличается по скорости никак.

        Немного но отличается. При чтении данных из текстуры берётся блок из нескольких пикселей и помещается в кэш. Потом чтение соседних пикселей происходит из этого кэша. При рисовании небольшого спрайта из большой текстуры, в кэш попадают блоки содержащие пиксели за границей спрайта. Следовательно нужно больше блоков копировать в кэш, чем если просто рисовать маленькую текстуру. Я давно тестировал потери от этого на видюхе ati radion HD 4850. В эксперименте я составил каллаж из маленьких спрайтов рандомного размера в диапазоне 20-400 пикселей, общий размер текстуры 4096х4096. Затем я рандомно рисовал спрайты отдельными текстурами и аналогично, вырезая из большой. Потери в скорости при этом были около 20%

        2.
        >Какие у вас проблемы, покажите пример?

        Пример:
        В анимации есть яркие(белые спрайты) и тёмные(чёрные). Они находятся рядом. Тогда при включённой линейной интерполяции у белых будет образовываться чёрная рамка, у чёрных наоборот белая. При мипмапинге, сжатии текстур и анизотропной фильтрации заползания будут только увеличиваться.

        3.
        >> 3)Из количества кадров должен извлекаться корень, что бы не было пустых мест на текстуре
        Это вообще я не понял что за минус такой.

        Если увас будет 16 кадров, то они хорошо лягут в спрайт щит. Они будут в виде таблицы 4x4. Если спрайтов 11, то в последних 2 строчках будут пустые места.
        16 кадров__11 кадров
        + + + +_____+ + + +
        + + + +_____+ + + +
        + + + +_____+ + + 0
        + + + +_____0 0 0 0

        4.
        >> 4)Количества кадров ограничено максимальным размером текстуры
        Вам кто-то запрещает бить анимации на несколько подобных атласов, или не хватает 4096х4096 текстур?

        Мне никто не запрещает, 4096х4096 мне не хватает. А вам?

        5.
        >> 5)Такую текстуру тяжелее стримить без пролагов чем отдельные небольшие кадры(т.е. плавно прогружать уже по ходу игры)
        Такую текстуру можно грузить по мипмапам, и анимация будет уже целиком в памяти в худшем качестве, пока ваши 36 картинок будут еще грузиться.

        Мипмапы с такими штуками не очень хорошо работают, поскольку соседние кадры залезают в один мипмап(см пункт 2). И это всё равно будет хуже. У вас просто будет мыло. А отдельные кадры можно подгружать уже по ходу воспроизведения анимации и тут также можно использовать мипмапы.


        1. Darthman
          03.10.2016 21:12

          >>Потери в скорости при этом были около 20%
          Довольно голословно. Демку бы.

          >> будет образовываться чёрная рамка, у чёрных наоборот белая
          Анимации на атласе имеют обыкновение по краям быть прозрачными, либо не менять свой цвет от кадра к кадру так сильно. Надумано довольно.

          >>Если спрайтов 11, то в последних 2 строчках будут пустые места
          Бывает такое, но это не очень критично, если честно.

          >> 4096х4096 мне не хватает. А вам?
          А мне хватает 2048х2048

          >>А отдельные кадры можно подгружать уже по ходу воспроизведения анимации
          А если не успеете? Всё будет крешиться? Или не рисоваться?
          Мне такой подход кажется неприемлемым. Пользователь не должен видеть подгрузку ни в каком виде.


          1. DmitryMry
            04.10.2016 20:11

            Анимации на атласе имеют обыкновение по краям быть прозрачными, либо не менять свой цвет от кадра к кадру так сильно. Надумано довольно.

            Так и любой нормальный упаковщик текстур умеет добавлять заданный зазор между изображениями, специально для того, чтобы такая проблема не проявлялась.