[Из песочницы] Анимированные текстуры в OpenGL

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

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

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

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

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

a8ae09158f1b4f3b86b155f61120012a.jpg

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

  • 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 сек. Не лучшее решение, но зато самое простое.

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

29f5a2c400f94e5391bae07039c563f6.gif

Далее пример целиком:
#include 
#include 

#include 
#include 
#include 

#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;
}


Спасибо за внимание!

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

  • 29 сентября 2016 в 17:14

    0

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

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

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

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

© Habrahabr.ru