OpenGLでプレーンを描画する

# はじめに
OpenGLで3D空間に平面を描画します。
GLSLでライティング効果を計算します。

<img width="641" alt="ogl_plane_0.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/98466116-1120-b618-ebf3-c25070a1d293.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.hpp

````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.hpp"

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

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

		in vec3 aVertex;
		in vec3 aNormal;
		in vec2 aUV;
		
		uniform mat4 uMVPMatrix;
		uniform mat4 uNormalMatrix;
		uniform vec4 uLightPos;
		
		out vec4 position;
		out vec4 normal;
		out vec2 uv;
		out vec4 lightPos;
		//flat out int InstanceID; 
		
		void main(void) {
		  position = uMVPMatrix * vec4(aVertex, 1);
		  lightPos = uMVPMatrix * uLightPos;
		  normal = normalize(uNormalMatrix * vec4(aNormal, 0));
		  
		  uv = aUV;
		  gl_Position = position;
		}
)",
// フラグメントシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		 
		out vec4 outputC;
		
		uniform sampler2D uTexture;
		uniform float uShininess;
		uniform vec4 uDiffuse;
		uniform vec4 uSpecular;
		uniform vec4 uAmbient;
		
		in vec4 position;
		in vec4 normal;
		in vec2 uv;
		in vec4 lightPos;
		
		void main (void) {
		  vec4 color = texture(uTexture, uv);
		  vec3 light = normalize((lightPos * position.w - lightPos.w * position).xyz);
		  vec3 fnormal = normalize(normal.xyz);
		  float diffuse = max(dot(light, fnormal), 0.0);
		
		  vec3 view = -normalize(position.xyz);
		  vec3 halfway = normalize(light + view);
		  float specular = pow(max(dot(fnormal, halfway), 0.0), uShininess);
		  outputC = color * uDiffuse * diffuse
		                + uSpecular * specular
		                + color * uAmbient;
		}
)");

	// 画像を読み込む
	int texW, texH, texCh;
	unsigned char* data = SOIL_load_image("./logo.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 glm::vec3 size = glm::vec3(1, 1, 1);
	const float vertex[] = {
		size.x, -size.y, size.z,
		-size.x, -size.y, size.z,
		-size.x, size.y, size.z,
		size.x, size.y, size.z,
	};
	const float normal[] = {
		0, 0, 1,
		0, 0, 1,
		0, 0, 1,
		0, 0, 1
	};
	const float uv[] = {
		0, 1,
		1, 1,
		1, 0,
		0, 0
	};
	const float diffuse[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
	const float specular[4] = { 0.f, 0.f, 0.f, 1.0f };
	const float ambient[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
	const GLushort index[] = { 0, 1, 2, 2, 3, 0 };

	GLuint vao, vVBO, normVBO, 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, &normVBO);
	glBindBuffer(GL_ARRAY_BUFFER, normVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 12, normal, 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);

	// 行列計算
	glm::mat4 modelMat = glm::mat4(1.0f);
	glm::mat4 projMat = glm::perspective(30.f, 1280.0f / 720.0f, 1.f, 1000.f);
	glm::mat4 viewMat = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, -1, 0));
	glm::mat4 normMat = viewMat * modelMat;
	normMat = glm::inverse(normMat);
	normMat = glm::transpose(normMat);

	// シェーダに値を渡す
	glUseProgram(program);
	GLuint vertexAttrib = glGetAttribLocation(program, "aVertex");
	GLuint normalAttrib = glGetAttribLocation(program, "aNormal");
	GLuint uvAttrib = glGetAttribLocation(program, "aUV");
	GLuint textureUniform = glGetUniformLocation(program, "uTexture");
	GLuint mvpMatUniform = glGetUniformLocation(program, "uMVPMatrix");
	GLuint normalMatUniform = glGetUniformLocation(program, "uNormalMatrix");
	GLuint lightPosUniform = glGetUniformLocation(program, "uLightPos");
	GLuint shininessUniform = glGetUniformLocation(program, "uShininess");
	GLuint diffuseUniform = glGetUniformLocation(program, "uDiffuse");
	GLuint specularUniform = glGetUniformLocation(program, "uSpecular");
	GLuint ambientUniform = glGetUniformLocation(program, "uAmbient");

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

	glBindBuffer(GL_ARRAY_BUFFER, normVBO);
	glVertexAttribPointer(normalAttrib, 3, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(normalAttrib);

	glBindBuffer(GL_ARRAY_BUFFER, uvVBO);
	glVertexAttribPointer(uvAttrib, 2, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(uvAttrib);
	glUniform1i(textureUniform, 0);
	glUniform1f(shininessUniform, 50);
	glUniform4fv(diffuseUniform, 1, diffuse);
	glUniform4fv(specularUniform, 1, specular);
	glUniform4fv(ambientUniform, 1, ambient);
	glUniform4f(lightPosUniform, 3, 0, 0, 1);
	glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
	glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));

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

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glEnable(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);
		glBindTexture(GL_TEXTURE_2D, texture);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

		// 0キーで状態をリセット
		if (glfwGetKey(window, GLFW_KEY_0)) {
			modelMat = glm::mat4(1.0f);
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 1キーでX方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_1)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(1, 0, 0));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 2キーでY方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_2)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(0, 1, 0));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 3キーでZ方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_3)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(0, 0, 1));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		}

		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		glBindTexture(GL_TEXTURE_2D, 0);
		glBindVertexArray(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)

# 解説

## シェーダ

シェーダに関しては以下のサイトを参考にしています。
[2005年10月07日 [OpenGL][GLSL] 第２回 Gouraud シェーディングと Phong シェーディング](http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20051007)

上記サイトは過去バージョンのものなので、ここに載せているシェーダは
4.5向けに修正されたものです。
(例えば varying ではなく in へ置き換え、gl_FragColorは削除されたのでoutを使用、など...)

main.cpp(一部抜粋)

````cpp
	// シェーダを作成する
	GLuint program = load_shader(
		// バーテックスシェーダ
		R"(
        #version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require

		in vec3 aVertex;
		in vec3 aNormal;
		in vec2 aUV;
		
		uniform mat4 uMVPMatrix;
		uniform mat4 uNormalMatrix;
		uniform vec4 uLightPos;
		
		out vec4 position;
		out vec4 normal;
		out vec2 uv;
		out vec4 lightPos;
		//flat out int InstanceID; 
		
		void main(void) {
		  position = uMVPMatrix * vec4(aVertex, 1);
		  lightPos = uMVPMatrix * uLightPos;
		  normal = normalize(uNormalMatrix * vec4(aNormal, 0));
		  
		  uv = aUV;
		  gl_Position = position;
		}
)",
// フラグメントシェーダ
R"(
		#version 410
		#extension GL_ARB_explicit_attrib_location : require
		#extension GL_ARB_explicit_uniform_location : require
		 
		out vec4 outputC;
		
		uniform sampler2D uTexture;
		uniform float uShininess;
		uniform vec4 uDiffuse;
		uniform vec4 uSpecular;
		uniform vec4 uAmbient;
		
		in vec4 position;
		in vec4 normal;
		in vec2 uv;
		in vec4 lightPos;
		
		void main (void) {
		  vec4 color = texture(uTexture, uv);
		  vec3 light = normalize((lightPos * position.w - lightPos.w * position).xyz);
		  vec3 fnormal = normalize(normal.xyz);
		  float diffuse = max(dot(light, fnormal), 0.0);
		
		  vec3 view = -normalize(position.xyz);
		  vec3 halfway = normalize(light + view);
		  float specular = pow(max(dot(fnormal, halfway), 0.0), uShininess);
		  outputC = color * uDiffuse * diffuse
		                + uSpecular * specular
		                + color * uAmbient;
		}
)");
````

## ライトの設定

ライトの位置を設定
少し右においているので、最初の画像でも影がついている

main.cpp(一部抜粋)

````cpp
glUniform4f(lightPosUniform, 3, 0, 0, 1);
````

## マテリアルの設定

オブジェクトの材質を設定しています。

main.cpp(一部抜粋)

````cpp

	const float diffuse[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
	const float specular[4] = { 0.f, 0.f, 0.f, 1.0f };
	const float ambient[4] = { 0.5f, 0.5f, 0.5f, 1.0f };

	... 中略 ...

	glUniform1f(shininessUniform, 50);
	glUniform4fv(diffuseUniform, 1, diffuse);
	glUniform4fv(specularUniform, 1, specular);
	glUniform4fv(ambientUniform, 1, ambient);
````

## 回転

2DのスプライトはZ方向にしか回転できませんが、
プレーンはXYZ方向それぞれに回転できます。
この回転したプレーンを４つ組み合わせれば箱のようなものも描画できます。

main.cpp(一部抜粋)

````
		// 0キーで状態をリセット
		if (glfwGetKey(window, GLFW_KEY_0)) {
			modelMat = glm::mat4(1.0f);
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 1キーでX方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_1)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(1, 0, 0));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 2キーでY方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_2)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(0, 1, 0));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		// 3キーでZ方向に回転
		} else if (glfwGetKey(window, GLFW_KEY_3)) {
			modelMat = glm::mat4(1.0f);
			modelMat = glm::rotate(modelMat, 30 * (3.14f / 180.0f), glm::vec3(0, 0, 1));
			glm::mat4 normMat = viewMat * modelMat;
			normMat = glm::inverse(normMat);
			normMat = glm::transpose(normMat);
			glUniformMatrix4fv(mvpMatUniform, 1, GL_FALSE, glm::value_ptr(projMat * viewMat * modelMat));
			glUniformMatrix4fv(normalMatUniform, 1, GL_FALSE, glm::value_ptr(normMat));
		}

````

## 実行結果

1キーを押した状態(X方向へ回転)
<img width="641" alt="ogl_plane_1.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/b7a08f31-0b00-7386-2584-3faa9a405629.png">


2キーを押した状態(Y方向へ回転)
<img width="641" alt="ogl_plane_2.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/fcb022e4-eb15-edab-fc39-a290b34a2cde.png">


3キーを押した状態(Z方向へ回転)
<img width="641" alt="ogl_plane_3.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/f9b2331a-3ddc-e892-5d36-2604a9de5ff7.png">