裏でこっそりフレームバッファ2013/11/06

今までのサンプルではテクスチャなどを画面に直接描画していましたが、これを画面ではなくフレームバッファと呼ばれる自分で作成したバッファに描画することができます(画面に描画するのも、デフォルトのフレームバッファに描画するということらしいですが)。
そんなことをして何が嬉しいのかと言いますと…

昨日のサンプルでは描画しているテクスチャは一枚だけでした。これがテクスチャなどの枚数が増えてくると、描画にいくらかの時間がかかるようになります。そうすると描画途中の絵が画面に出ることになり、絵がちらついたりします。
フレームバッファを使用する場合、絵は一旦フレームバッファに描画して、全て描画した後に画面に表示します。描画途中の絵が画面に出ることはないのでちらつきはありません。オフスクリーンレンダリングと呼ばれる方法です。


フレームバッファクラス sys::FrameBufferを見てみます。
FrameBuffer.h
#ifndef	___FRAME_BUFFER_H___
#define	___FRAME_BUFFER_H___

#include "common.h"
#include "Texture.h"


namespace sys
{

/********************************
    フレームバッファテクスチャ
 ********************************/
class FrameBuffer : public Texture
{
	GLuint		frame_buffer;			// フレームバッファオブジェクト
	GLfloat		mat_projection[4*4];	// 透視変換行列

public :

		FrameBuffer(void)				// コンストラクタ
		{
			frame_buffer = 0;
		}
		FrameBuffer(int _w, int _h)
		{
			make(_w, _h);
		}
		~FrameBuffer()					// デストラクタ
		{
			clear();
		}

	void	make(int, int);				// 作成
	void	clear(void);				// 削除
	void	bind(void);					// 使用
};

}

#endif
/********************* End of File ********************************/

FrameBuffer.cpp
/*********************************************

		フレームバッファテクスチャ

 *********************************************/

#include "FrameBuffer.h"
#include "Renderer.h"


namespace sys
{

/********************************
    作成
		引数	_width  = 幅
				_height = 高さ
 ********************************/
void	FrameBuffer::make(int _width, int _height)
{
	glGenFramebuffers(1, &frame_buffer);					// フレームバッファオブジェクト作成
	glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);

	format	= FORMAT_RGBA;			// テクスチャフォーマット
	width	= _width;				// 幅
	height	= _height;				// 高さ
	Texture::make(NULL);			// テクスチャ作成
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);	// フレームバッファにアタッチ

	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	for (int i = 0; i < 4*4; i++) {							// 透視変換行列初期化
		mat_projection[i] = 0.0f;
	}
	mat_projection[0]  =  2.0f/width;
	mat_projection[5]  = -2.0f/height;
	mat_projection[10] =  1.0f;
	mat_projection[15] =  1.0f;
}

/**********
    削除
 **********/
void	FrameBuffer::clear(void)
{
	if ( frame_buffer ) {
		glDeleteFramebuffers(1, &frame_buffer);
		frame_buffer = 0;
		Texture::clear();
	}
}

/**********
    使用
 **********/
void	FrameBuffer::bind(void)
{
	glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
	glViewport(0, 0, width, height);						// ビューポート設定
	Renderer::mat_projection = mat_projection;				// 透視変換行列設定
	if ( Renderer::current_shader ) {
		glUniformMatrix4fv(Renderer::current_shader->projection, 1, GL_FALSE, mat_projection);
	}
}

}

/**************** End of File ******************************************/

テクスチャとしてカラーバッファを持っていますので、sys::Textureの派生クラスになっています。普通はデプスバッファやステンシルバッファといったレンダーバッファを持っているものなんですが、ここではカラーバッファしか使いません。

作成(make)時には幅と高さを指定しています。今回のように画面全体のフレームバッファとして使う場合はサイズは仮想画面と同じになりますが、描画(されること)のできるテクスチャとして他にも使いようがありますのでサイズは指定できるようになっています。
描画時の座標をサイズに合わせるために、座標変換の行列はフレームバッファで持っておきます。

描画クラス sys::Rendererの変更部分です。
Renderer.cpp
/************************************************
    初期化
		引数	width, height = 端末画面サイズ
 ************************************************/
void	Renderer::init(int width, int height)
{
	initShader();								// シェーダ初期化
	frame_buffer = new FrameBuffer(SCREEN_WIDTH, SCREEN_HEIGHT);		// フレームバッファ作成

	if ( width*SCREEN_HEIGHT < height*SCREEN_WIDTH ) {		// 横長(上下カット)
		screen_rect.w = width;
		screen_rect.h = width*SCREEN_HEIGHT/SCREEN_WIDTH;
	}
	else {													// 縦長(左右カット)
		screen_rect.w = height*SCREEN_WIDTH/SCREEN_HEIGHT;
		screen_rect.h = height;
	}
	screen_rect.x = (width - screen_rect.w)/2;
	screen_rect.y = (height - screen_rect.h)/2;
}

まず初期化部分では、仮想画面サイズでフレームバッファを作成しています。
ビューポートの設定はフレームバッファの切り替えごとに行うので、ここでは値の計算だけしておきます。

/********************
    稼働(前処理)
 ********************/
void	Renderer::update(void)
{
	glEnable(GL_BLEND);					// αブレンド初期化
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	frame_buffer->bind();				// フレームバッファ使用
}

/********************
    描画(後処理)
 ********************/
void	Renderer::draw(void)
{
	static const
	GLfloat	_projection[4*4] =
			{
				1.0f, 0.0f, 0.0f, 0.0f,
				0.0f, 1.0f, 0.0f, 0.0f,
				0.0f, 0.0f, 1.0f, 0.0f,
				0.0f, 0.0f, 0.0f, 1.0f,
			};

	static const
	GLfloat	_texcoords[] =
			{
				0.0f, 0.0f,
				1.0f, 0.0f,
				0.0f, 1.0f,
				1.0f, 1.0f
			};

	static const
	GLfloat	_vertices[] =
			{
				-1.0f, -1.0f,
				 1.0f, -1.0f,
				-1.0f,  1.0f,
				 1.0f,  1.0f,
			};

	static const
	GLubyte	_colors[] =
			{
				255, 255, 255, 255,
				255, 255, 255, 255,
				255, 255, 255, 255,
				255, 255, 255, 255,
			};

	ShaderProgram*	_shader = use_shader(SHADER_TEXTURE);

	// フレームバッファテクスチャ描画
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glViewport(screen_rect.x, screen_rect.y, screen_rect.w, screen_rect.h);
	glUniformMatrix4fv(_shader->projection, 1, GL_FALSE, _projection);
	glBindTexture(GL_TEXTURE_2D, frame_buffer->texture);
	glVertexAttribPointer(_shader->texcoord, 2, GL_FLOAT, GL_FALSE, 0, _texcoords);
	glVertexAttribPointer(_shader->position, 2, GL_FLOAT, GL_FALSE, 0, _vertices);
	glVertexAttribPointer(_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, _colors);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

今までは Rendererの処理はメインの処理前にだけ行っていましたが、最後にフレームバッファを画面に描画しなければならないので後にも処理を入れています。

プロジェクト一式は、こちらです。