OpenGLで複数の画像を一つの頂点情報から表示する

# はじめに
OpenGLで複数の画像を一つの頂点情報から表示します。
行列を使って位置/大きさ/回転は自由に変更可能です。

![OneVertex.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/9e7d8150-f4ee-130c-6821-3abdd45f019a.png)


# 開発環境
````
OpenGL
    Vendor: Intel
    Version: 4.5.0 - Build 25.20.100.6373
    GLSL: 4.50 - Build 25.20.100.6373

Windows10
VisualStudio2017
````

# 使用ライブラリ
今回使うライブラリは全てnugetでインストールしています。
VisualStudioのパッケージマネージャコンソールを使う場合は、
以下ですべてインストールできます。

````
Install-Package glfw
Install-Package glew
Install-Package freeglut
Install-Package glm
Install-Package soil
````

追加のインクルードディレクトリ

````
$(SolutionDir)packages\soil.1.16.0\build\native\include;$(SolutionDir)packages\freeglut.2.8.1.15\build\native\include\GL;$(SolutionDir)packages\glm.0.9.9.600\build\native\include;$(SolutionDir)packages\glew.1.9.0.1\build\native\include\GL;$(SolutionDir)packages\glfw.3.3.0.1\build\native\include\GLFW;%(AdditionalIncludeDirectories)
````

追加のライブラリディレクトリ

````
$(SolutionDir)packages\soil.1.16.0\build\native\lib\Win32\Debug;$(SolutionDir)packages\freeglut.2.8.1.15\build\native\lib\v110\Win32\Debug\static;$(SolutionDir)packages\glfw.3.3.0.1\build\native\lib\static\v142\win32;$(SolutionDir)packages\glew.1.9.0.1\build\native\lib\v110\Win32\Debug\static;%(AdditionalLibraryDirectories)
````

追加の依存ファイル

````
glew.lib;glfw3.lib;soil.lib;%(AdditionalDependencies)
````

# ソースコード
できるだけシンプルなサンプルにしたかったので以下のふたつのファイルで実装しました。
実際にはクラスなどにまとめたほうが便利だと思います。

gli.h

````h
#pragma once
#ifndef GLI_HPP
#define GLI_HPP
// GLEW
#define GLEW_STATIC
#include <gl/glew.h>
// GLUT
#include <glut.h>
// GLFW
#include <GLFW/glfw3.h>
// GLM
#include <glm/glm.hpp>
#endif
````

main.cpp

````cpp
#include <iostream>
#include <soil.h>
#include <string>
#include <vector>
#include <cassert>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/ext.hpp>
#include "gli.h"

struct Transform {
	glm::vec3 position;
	glm::vec3 scale;
	glm::quat rotation;

	Transform() : position(), scale(1, 1, 1), rotation() {
	}

	void reset() {
		this->position = glm::vec3();
		this->scale = glm::vec3(1, 1, 1);
		this->rotation = glm::quat();
	}
};

static int gl_init(int argc, char* argv[], const char* title, int width, int height, bool fullScreen, GLFWwindow** outWindow) {
	(*outWindow) = NULL;
	if (!glfwInit()) return -1;
	// create window
	GLFWwindow* window = NULL;
	if (fullScreen) {
		GLFWmonitor* monitor = glfwGetPrimaryMonitor();
		const GLFWvidmode* mode = glfwGetVideoMode(monitor);
		glfwWindowHint(GLFW_RED_BITS, mode->redBits);
		glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
		glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
		glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
		window = glfwCreateWindow(mode->width, mode->height, title,
			monitor, NULL);
	}
	else {
		glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
		window = glfwCreateWindow(width, height, title, NULL, NULL);
	}
	if (!window) {
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) return -1;
	(*outWindow) = window;
	return 0;
}

void check_shader_state(GLuint shader, const std::string& baseStr) {
	GLsizei logSize;
	GLsizei bufSize = 2048;
	char buf[2048] = { 0 };
	glGetShaderInfoLog(shader, bufSize, &logSize, buf);
	std::string str = baseStr + '\n' + buf;
	std::cerr << str << std::endl;
	throw std::logic_error(str);
}

GLuint load_shader(const std::string& vert, const std::string& frag) {
	// 頂点シェーダ作成
	GLuint vertShId = glCreateShader(GL_VERTEX_SHADER);
	if (vertShId == 0) {
		check_shader_state(vertShId, "failed a create vertex shader");
	}
	const char* vertShCStr = vert.c_str();
	glShaderSource(vertShId, 1, &vertShCStr, NULL);
	glCompileShader(vertShId);
	// エラーを確認
	GLint compileErr;
	glGetShaderiv(vertShId, GL_COMPILE_STATUS, &compileErr);
	if (GL_FALSE == compileErr) {
		check_shader_state(vertShId, "failed a compile vertex shader");
	}
	// フラグメントシェーダ作成
	GLuint fragShId = glCreateShader(GL_FRAGMENT_SHADER);
	if (fragShId == 0) {
		check_shader_state(fragShId, "failed a create fragment shader");
	}
	const char* fragShCStr = frag.c_str();
	glShaderSource(fragShId, 1, &fragShCStr, NULL);
	glCompileShader(fragShId);
	// エラーを確認
	glGetShaderiv(fragShId, GL_COMPILE_STATUS, &compileErr);
	if (GL_FALSE == compileErr) {
		check_shader_state(fragShId, "failed a compile fragment shader");
	}
	// プログラムの作成, リンク
	GLuint program = glCreateProgram();
	if (program == 0) {
		throw std::logic_error("failed a create program");
	}
	glAttachShader(program, vertShId);
	glAttachShader(program, fragShId);
	glLinkProgram(program);
	GLint status;
	glGetProgramiv(program, GL_LINK_STATUS, &status);
	if (GL_FALSE == status) {
		throw std::logic_error("failed a link program");
	}
	glDeleteShader(vertShId);
	glDeleteShader(fragShId);
	return program;
}

glm::mat4 compute_transform(glm::vec3 position, glm::vec3 scale, glm::quat rotation) {
	glm::mat4 t = glm::translate(glm::mat4(1.0f), position);
	glm::mat4 r = glm::toMat4(rotation);
	glm::mat4 s = glm::scale(glm::mat4(1.0f), scale);
	return t * r * s;
}

int main(int argc, char* argv[]) {
	const int WIDTH = 1280;
	const int HEIGHT = 720;
	GLFWwindow* window;
	gl_init(argc, argv, "Sample", WIDTH, HEIGHT, false, &window);

	// シェーダを作成する
	GLuint program = load_shader(
		// バーテックスシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		
		uniform mat4 uWorldTransform;
		uniform mat4 uViewProj;
		
		in vec3 aVertex;
		in vec2 aUV;
		
		out vec2 uv;
		
		void main() {
			vec4 pos = vec4(aVertex, 1.0);
			gl_Position =  uViewProj * uWorldTransform * pos;
			uv = aUV;
		}
)",
// フラグメントシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		
		in vec2 uv;
		out vec4 outColor;
		uniform sampler2D uTexture;
		
		void main() {
		    outColor = texture(uTexture, uv);
		}
)");

	// 画像を読み込む
	int texW, texH, texCh;
	unsigned char* data = SOIL_load_image("TestImage.png", &texW, &texH, &texCh, SOIL_LOAD_RGBA);
	assert(data != nullptr);
	assert(texCh == 4);
	GLuint texture;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);
	glEnable(GL_TEXTURE_2D);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texW, texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
	glGenerateMipmap(GL_TEXTURE_2D);
	glDisable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, 0);
	// 画像のための頂点を用意する
	const float vertex[] = {
		-0.5f, 0.5f,  0,
		0.5f, 0.5f,   0,
		0.5f, -0.5f,  0,
		-0.5f, -0.5f, 0,
	};
	const float uv[] = {
		0.f, 0.f,
		1.f, 0.f,
		1.f, 1.f,
		0.f, 1.f
	};
	const GLushort index[] = { 0, 1, 2, 2, 3, 0 };

	GLuint vao, vVBO, uvVBO, ibo;
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	glGenBuffers(1, &vVBO);
	glBindBuffer(GL_ARRAY_BUFFER, vVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 12, vertex, GL_STATIC_DRAW);

	glGenBuffers(1, &uvVBO);
	glBindBuffer(GL_ARRAY_BUFFER, uvVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, uv, GL_STATIC_DRAW);

	glGenBuffers(1, &ibo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 6, index, GL_STATIC_DRAW);

	// シェーダに値を渡す
	glUseProgram(program);
	GLuint vertexAttrib = glGetAttribLocation(program, "aVertex");
	GLuint uvAttrib = glGetAttribLocation(program, "aUV");
	GLuint textureUniform = glGetUniformLocation(program, "uTexture");
	GLuint viewProjUniform = glGetUniformLocation(program, "uViewProj");
	GLuint worldTransformUniform = glGetUniformLocation(program, "uWorldTransform");

	glBindBuffer(GL_ARRAY_BUFFER, vVBO);
	glVertexAttribPointer(vertexAttrib, 3, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(vertexAttrib);

	glBindBuffer(GL_ARRAY_BUFFER, uvVBO);
	glVertexAttribPointer(uvAttrib, 2, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(uvAttrib);
	glUniform1i(textureUniform, 0);

	// SimpleViewProjection行列を計算する
	// 参考:「ゲームプログラミングC++」第五章
	glm::mat4 svpScale = glm::scale(
		glm::mat4(1.0f),
		glm::vec3(2.0f / static_cast<float>(WIDTH), 2.0f / static_cast<float>(HEIGHT), 1)
	);
	glm::mat4 svpTranslate = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 1));
	glm::mat4 simpleViewProjection = svpTranslate * svpScale;
	std::cout << glm::to_string(simpleViewProjection) << std::endl;
	glUniformMatrix4fv(viewProjUniform, 1, GL_FALSE, glm::value_ptr(simpleViewProjection));

	// 全てのバインドを解除
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glUseProgram(0);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture);
	glDisable(GL_CULL_FACE);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	// 描画するオブジェクトの形状
	std::vector<Transform> transformVec;
	const float LAYOUT_MARGIN = 300;
	// 中央:
	Transform temp;
	transformVec.emplace_back(temp);
	temp.reset();

	// 右:
	temp.position.x += LAYOUT_MARGIN;
	temp.scale.y = 2;
	transformVec.emplace_back(temp);
	temp.reset();

	// 左:
	temp.position.x -= LAYOUT_MARGIN;
	temp.rotation = glm::angleAxis(45.0f * (3.14f / 180.0f), glm::vec3(0, 0, 1));
	transformVec.emplace_back(temp);
	temp.reset();

	// 下:
	temp.position.y -= LAYOUT_MARGIN;
	temp.rotation = glm::angleAxis(120.0f * (3.14f / 180.0f), glm::vec3(0, 0, 1));
	transformVec.emplace_back(temp);
	temp.reset();

	// 上:
	temp.position.y += LAYOUT_MARGIN;
	transformVec.emplace_back(temp);
	temp.reset();

	while (!glfwWindowShouldClose(window)) {
		glClearColor(0, 0, 0, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glUseProgram(program);
		glBindVertexArray(vao);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

		for (Transform transform : transformVec) {
			glm::mat4 world = compute_transform(
				transform.position,
				transform.scale,
				transform.rotation
			);
			world = glm::scale(world, glm::vec3(texW, texH, 1));

			glUniformMatrix4fv(worldTransformUniform, 1, GL_FALSE, glm::value_ptr(world));
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
		}
		glBindVertexArray(0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		glUseProgram(0);

		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glfwDestroyWindow(window);
	glfwTerminate();
	return 0;
}
````

# 画像
画像はなんでもいいです。
最初の画像ではこの画像を使っています。(200x200)
![TestImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/09f6641f-f68f-dc5f-2317-8030454b1ecc.png)

# 解説

## main

頂点情報を一つしか使っていない点が重要です。
Transformの中身を書き換えるだけでオブジェクトを動かしたり変形したりできます。
オブジェクトの数だけ頂点情報を作ってしまうことも出来ますが、既に存在するVBOの変更はちょっと手間がかかります。
複雑な座標変換を行うなら尚更です。

main.cpp

````cpp
while (!glfwWindowShouldClose(window)) {
		glClearColor(0, 0, 0, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glUseProgram(program);
		glBindVertexArray(vao);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

		for (Transform transform : transformVec) {
			glm::mat4 world = compute_transform(
				transform.position,
				transform.scale,
				transform.rotation
			);
			world = glm::scale(world, glm::vec3(texW, texH, 1));

			glUniformMatrix4fv(worldTransformUniform, 1, GL_FALSE, glm::value_ptr(world));
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
		}
		glBindVertexArray(0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		glUseProgram(0);

		glfwSwapBuffers(window);
		glfwPollEvents();
}
````

## 比較
比較のためにオブジェクトの移動のたびにVBOを変更する例も載せておきます。
ちょっと面倒だったので上記の例と完全に同じ実行結果になるように実装してません。
もしこの方法で上記と同じ実行結果になるよう実装したらもっと冗長になってしまうと思います。

main.cpp

````cpp
...

int main(int argc, char* argv[]) {
	const int WIDTH = 1280;
	const int HEIGHT = 720;
	GLFWwindow* window;
	gl_init(argc, argv, "Sample", WIDTH, HEIGHT, false, &window);

	// シェーダを作成する
	GLuint program = load_shader(
		// バーテックスシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		
		uniform mat4 uMVPMatrix;
		
		in vec3 aVertex;
		in vec2 aUV;
		
		out vec2 uv;
		
		void main() {
			vec4 pos = vec4(aVertex, 1.0);
			gl_Position =  uMVPMatrix * pos;
			uv = aUV;
		}
)",
// フラグメントシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		
		in vec2 uv;
		out vec4 outColor;
		uniform sampler2D uTexture;
		
		void main() {
		    outColor = texture(uTexture, uv);
		}
)");

	// 画像を読み込む
	int texW, texH, texCh;
	unsigned char* data = SOIL_load_image("TestImage.png", &texW, &texH, &texCh, SOIL_LOAD_RGBA);
	assert(data != nullptr);
	assert(texCh == 4);
	GLuint texture;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);
	glEnable(GL_TEXTURE_2D);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texW, texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
	glGenerateMipmap(GL_TEXTURE_2D);
	glDisable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, 0);
	// 画像のための頂点を用意する
	const float vertex[] = {
		0, 0,  0,
		200, 0,   0,
		200, 200,  0,
		0, 200, 0,
	};
	const float uv[] = {
		0.f, 0.f,
		1.f, 0.f,
		1.f, 1.f,
		0.f, 1.f
	};
	const GLushort index[] = { 0, 1, 2, 2, 3, 0 };

	GLuint vao, vVBO, uvVBO, ibo;
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	glGenBuffers(1, &vVBO);
	glBindBuffer(GL_ARRAY_BUFFER, vVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 12, vertex, GL_DYNAMIC_DRAW);

	glGenBuffers(1, &uvVBO);
	glBindBuffer(GL_ARRAY_BUFFER, uvVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, uv, GL_STATIC_DRAW);

	glGenBuffers(1, &ibo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 6, index, GL_STATIC_DRAW);

	// シェーダに値を渡す
	glUseProgram(program);
	GLuint vertexAttrib = glGetAttribLocation(program, "aVertex");
	GLuint uvAttrib = glGetAttribLocation(program, "aUV");
	GLuint textureUniform = glGetUniformLocation(program, "uTexture");
	GLuint mvpUniform = glGetUniformLocation(program, "uMVPMatrix");
	// GLuint viewProjUniform = glGetUniformLocation(program, "uViewProj");
	// GLuint worldTransformUniform = glGetUniformLocation(program, "uWorldTransform");

	glBindBuffer(GL_ARRAY_BUFFER, vVBO);
	glVertexAttribPointer(vertexAttrib, 3, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(vertexAttrib);

	glBindBuffer(GL_ARRAY_BUFFER, uvVBO);
	glVertexAttribPointer(uvAttrib, 2, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(uvAttrib);
	glUniform1i(textureUniform, 0);

	// SimpleViewProjection行列を計算する
	// 参考:「ゲームプログラミングC++」第五章
	/*
	glm::mat4 svpScale = glm::scale(
		glm::mat4(1.0f),
		glm::vec3(2.0f / static_cast<float>(WIDTH), 2.0f / static_cast<float>(HEIGHT), 1)
	);
	glm::mat4 svpTranslate = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 1));
	glm::mat4 simpleViewProjection = svpTranslate * svpScale;
	std::cout << glm::to_string(simpleViewProjection) << std::endl;
	glUniformMatrix4fv(viewProjUniform, 1, GL_FALSE, glm::value_ptr(simpleViewProjection));
	*/
	glm::mat4 ortho = glm::ortho(0.0f, static_cast<float>(WIDTH), static_cast<float>(HEIGHT), 0.0f, -1.0f, 1.0f);
	glUniformMatrix4fv(mvpUniform, 1, GL_FALSE, glm::value_ptr(ortho));

	// 全てのバインドを解除
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glUseProgram(0);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture);
	glDisable(GL_CULL_FACE);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	while (!glfwWindowShouldClose(window)) {
		glClearColor(0, 0, 0, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glUseProgram(program);
		glBindVertexArray(vao);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

		glBindBuffer(GL_ARRAY_BUFFER, vVBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 12, vertex, GL_DYNAMIC_DRAW);

		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);

		// 移動後の頂点配列を用意する
		const float MOVE_X = 1080;
		const float vertex2[] = {
			0 + MOVE_X, 0,  0,
			200 + MOVE_X, 0,   0,
			200 + MOVE_X, 200,  0,
			0 + MOVE_X, 200, 0,
		};
		glBindBuffer(GL_ARRAY_BUFFER, vVBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 12, vertex2, GL_DYNAMIC_DRAW);

		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);

		glBindVertexArray(0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		glUseProgram(0);

		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glfwDestroyWindow(window);
	glfwTerminate();
	return 0;
}
````

### 実行結果

![MultipleVertex.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/ce0aeae1-c260-3722-1c1e-2de791982a40.png)