text: #include <basic/camera.h>
  #include <basic/shader.h>
  #include <basic/program.h>
  #include <basic/model.h>
  #include <stb_image.h>
  
  #include <iostream>
  #include <memory>
  #include <random>
  
  using namespace std;
  
  void OnFramebufferSize(GLFWwindow* window, int width, int height);
  void OnMouse(GLFWwindow* window, double xpos, double ypos);
  void OnScroll(GLFWwindow* window, double xoffset, double yoffset);
  void OnKey(GLFWwindow *window);
  void OnRender();
  void OnGeoRender();
  void OnSSAORender();
  void OnBlurRender();
  void OnInit();
  void OnDisable();
  unsigned int LoadTexture(const string path);
  void RenderSphere();
  void RenderQuad();
  
  #define SCR_WIDTH 1000
  #define SCR_HEIGHT 800
  
  Camera camera(glm::vec3(0.0f, 0.0f, 0.8f));
  std::shared_ptr<Shader> shader;
  std::shared_ptr<Shader> geometry;
  std::shared_ptr<Shader> lighting;
  std::shared_ptr<Shader> ssao;
  std::shared_ptr<Shader> blur;
  std::shared_ptr<Program> pro;
  Model cyborg;
  unsigned int albedo, normal, metallic, roughness, ao;
  unsigned int geoFBO;
  unsigned int ssaoFBO;
  unsigned int blurFBO;
  unsigned int gPosition, gNormal, gAlbedo, noiseTexture, depthRBO, ssaoBuffer, blurBuffer;
  std::vector<glm::vec3> ssaoNoise;
  std::vector<glm::vec3> ssaoKernel;
  
  float lerp(float a, float b, float f)
  {
  	return a + f * (b - a);
  }
  
  int main()
  {
  	pro = make_shared<Program>(SCR_WIDTH, SCR_HEIGHT, 100, 100, "ssao");
  
  	pro->RegisterFramebuffer(OnFramebufferSize);
  	pro->RegisterMouse(OnMouse);
  	pro->RegisterScroll(OnScroll);
  	pro->RegisterKey(OnKey);
  	pro->RegisterInit(OnInit);
  	pro->RegisterRender(OnGeoRender);
  	pro->RegisterRender(OnSSAORender);
  	pro->RegisterRender(OnBlurRender);
  	pro->RegisterRender(OnRender);
  	pro->RegisterDisable(OnDisable);
  	
  	pro->Run();
  	return 0;
  }
  
  void OnInit()
  {
  	glfwWindowHint(GLFW_SAMPLES, 4);
  	glEnable(GL_DEPTH_TEST);
  
  	shader = make_shared<Shader>("./shader/ssao.vs", "./shader/pbr.fs");
  	geometry = make_shared<Shader>("./shader/geometry.vs", "./shader/geometry.fs");
  	ssao = make_shared<Shader>("./shader/ssao.vs", "./shader/ssao.fs");
  	blur = make_shared<Shader>("./shader/ssao.vs", "./shader/blur.fs");
  	cyborg = Model("../resources/objects/bunny.obj");
  
  	glGenFramebuffers(1, &geoFBO);
  	glBindFramebuffer(GL_FRAMEBUFFER, geoFBO);
  
  	glGenTextures(1, &gPosition);
  	glBindTexture(GL_TEXTURE_2D, gPosition);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
  
  	glGenTextures(1, &gNormal);
  	glBindTexture(GL_TEXTURE_2D, gNormal);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
  
  	glGenTextures(1, &gAlbedo);
  	glBindTexture(GL_TEXTURE_2D, gAlbedo);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedo, 0);
  
  	unsigned int attachments[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2};
  	glDrawBuffers(3, attachments);
  	
  	glGenRenderbuffers(1, &depthRBO);
  	glBindRenderbuffer(GL_RENDERBUFFER, depthRBO);
  	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
  	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRBO);
  
  	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
  		std::cout << "Framebuffer not complete!" << std::endl;
  	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  	
  	glGenFramebuffers(1, &ssaoFBO);
  	glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
  	glGenTextures(1, &ssaoBuffer);
  	glBindTexture(GL_TEXTURE_2D, ssaoBuffer);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoBuffer, 0);
  	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
  		std::cout << "SSAO Framebuffer not complete!" << std::endl;
  	
  	std::uniform_real_distribution<GLfloat> randomFloat(0.0, 1.0);
  	std::default_random_engine genertor;
  	
  	for (unsigned int i = 0; i < 64; i++)
  	{
  		glm::vec3 sample(randomFloat(genertor) * 2.0 - 1.0, randomFloat(genertor) * 2.0 - 1.0, randomFloat(genertor));
  		sample = glm::normalize(sample);
  		sample *= randomFloat(genertor);
  		float scale = float(i) / 64.0;
  
  		scale = lerp(0.1f, 1.0f, scale * scale);
  		sample *= scale;
  		ssaoKernel.push_back(sample);
  	}
  
  	for (unsigned int i = 0; i < 16; i++)
  	{
  		glm::vec3 noise = glm::vec3(randomFloat(genertor) * 2.0 - 1.0, randomFloat(genertor) * 2.0 - 1.0, 0.0);
  		ssaoNoise.push_back(noise);
  	}
  	glGenTextures(1, &noiseTexture);
  	glBindTexture(GL_TEXTURE_2D, noiseTexture);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  	
  	glGenFramebuffers(1, &blurFBO);
  	glBindFramebuffer(GL_FRAMEBUFFER, blurFBO);
  	glGenTextures(1, &blurBuffer);
  	glBindTexture(GL_TEXTURE_2D, blurBuffer);
  	glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, blurBuffer, 0);
  	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
  		std::cout << "SSAO Blur Framebuffer not complete!" << std::endl;
  	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  
  	glm::mat4 model = glm::mat4();
  	model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
  	glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
  	
  	shader->use();
  	shader->setInt("gPosition", 0);
  	shader->setInt("gNormal", 1);
  	shader->setInt("gAlbedo", 2);
  	shader->setInt("ssao", 3);
  	ssao->use();
  	ssao->setInt("gPosition", 0);
  	ssao->setInt("gNormal", 1);
  	ssao->setInt("texNoise", 2);
  	blur->use();
  	blur->setInt("ssao", 0);
  }
  
  glm::mat4 projection;
  void OnGeoRender()
  {
  	glBindFramebuffer(GL_FRAMEBUFFER, geoFBO);
  	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  	projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 50.0f);
  	glm::mat4 view = camera.GetViewMatrix();
  	glm::mat4 model;
  	geometry->use();
  	geometry->setMat4("projection", projection);
  	geometry->setMat4("view", view);
  	model = glm::mat4();
  	model = glm::translate(model, glm::vec3(0.0, 0.0f, 0.0f));
  	geometry->setMat4("model", model);
  	cyborg.Draw(geometry);
  	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }
  
  void OnSSAORender()
  {
  	glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
  	glClear(GL_COLOR_BUFFER_BIT);
  	ssao->use();
  	for (unsigned int i = 0; i < 64; i++)
  	{
  		ssao->setVec3("samples[" + std::to_string(i) + "]", ssaoKernel[i]);
  	}
  	ssao->setMat4("projection", projection);
  	glActiveTexture(GL_TEXTURE0);
  	glBindTexture(GL_TEXTURE_2D, gPosition);
  	glActiveTexture(GL_TEXTURE1);
  	glBindTexture(GL_TEXTURE_2D, gNormal);
  	glActiveTexture(GL_TEXTURE2);
  	glBindTexture(GL_TEXTURE_2D, noiseTexture);
  
  	RenderQuad();
  	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }
  
  void OnBlurRender()
  {
  	glBindFramebuffer(GL_FRAMEBUFFER, blurFBO);
  	glClear(GL_COLOR_BUFFER_BIT);
  	blur->use();
  	glActiveTexture(GL_TEXTURE0);
  	glBindTexture(GL_TEXTURE_2D, ssaoBuffer);
  	RenderQuad();
  	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }
  
  glm::vec3 lightPos = glm::vec3(2.0, 4.0, -2.0);
  glm::vec3 lightColor = glm::vec3(1.0, 1.0, 1.0);
  void OnRender()
  {
  	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  	shader->use();
  	glm::vec3 lightPosView = glm::vec3(camera.GetViewMatrix() * glm::vec4(lightPos, 1.0));
  	shader->setVec3("light.Position", lightPosView);
  	shader->setVec3("light.Color", lightColor);
  
  	const float constant = 1.0; // note that we don't send this to the shader, we assume it is always 1.0 (in our case)
  	const float linear = 0.09;
  	const float quadratic = 0.032;
  	shader->setFloat("light.Linear", linear);
  	shader->setFloat("light.Quadratic", quadratic);
  
  	glActiveTexture(GL_TEXTURE0);
  	glBindTexture(GL_TEXTURE_2D, gPosition);
  	glActiveTexture(GL_TEXTURE1);
  	glBindTexture(GL_TEXTURE_2D, gNormal);
  	glActiveTexture(GL_TEXTURE2);
  	glBindTexture(GL_TEXTURE_2D, gAlbedo);
  	glActiveTexture(GL_TEXTURE3); // add extra SSAO texture to lighting pass
  	glBindTexture(GL_TEXTURE_2D, blurBuffer);
  	RenderQuad();
  }
  
  void OnDisable()
  {
  
  }
  
  float lastX = 800.0f / 2.0;
  float lastY = 600.0 / 2.0;
  bool firstMouse = true;
  
  void OnMouse(GLFWwindow* window, double xpos, double ypos)
  {
  	if (firstMouse)
  	{
  		lastX = xpos;
  		lastY = ypos;
  		firstMouse = false;
  	}
  
  	float xoffset = xpos - lastX;
  	float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
  
  	lastX = xpos;
  	lastY = ypos;
  
  	camera.ProcessMouseMovement(xoffset, yoffset);
  }
  
  void OnScroll(GLFWwindow* window, double xoffset, double yoffset)
  {
  	camera.ProcessMouseScroll(yoffset);
  }
  
  void OnKey(GLFWwindow *window)
  {
  	float cameraSpeed = 2.5 * 0.01f;
  	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
  		camera.ProcessKeyboard(FORWARD, 0.01f);
  	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
  		camera.ProcessKeyboard(BACKWARD, 0.01f);
  	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
  		camera.ProcessKeyboard(LEFT, 0.01f);
  	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
  		camera.ProcessKeyboard(RIGHT, 0.01f);
  }
  
  void OnFramebufferSize(GLFWwindow* window, int width, int height)
  {
  	// make sure the viewport matches the new window dimensions; note that width and 
  	// height will be significantly larger than specified on retina displays.
  	glViewport(0, 0, width, height);
  }
  
  unsigned int sphereVAO = 0;
  unsigned int indexCount;
  void RenderSphere()
  {
  	if (sphereVAO == 0)
  	{
  		glGenVertexArrays(1, &sphereVAO);
  
  		unsigned int vbo, ebo;
  		glGenBuffers(1, &vbo);
  		glGenBuffers(1, &ebo);
  
  		std::vector<glm::vec3> positions;
  		std::vector<glm::vec2> uv;
  		std::vector<glm::vec3> normals;
  		std::vector<unsigned int> indices;
  
  		const unsigned int X_SEGMENTS = 64;
  		const unsigned int Y_SEGMENTS = 64;
  		const float PI = 3.14159265359;
  		for (unsigned int y = 0; y <= Y_SEGMENTS; ++y)
  		{
  			for (unsigned int x = 0; x <= X_SEGMENTS; ++x)
  			{
  				float xSegment = (float)x / (float)X_SEGMENTS;
  				float ySegment = (float)y / (float)Y_SEGMENTS;
  				float xPos = std::cos(xSegment * 2.0f * PI) * std::sin(ySegment * PI);
  				float yPos = std::cos(ySegment * PI);
  				float zPos = std::sin(xSegment * 2.0f * PI) * std::sin(ySegment * PI);
  
  				positions.push_back(glm::vec3(xPos, yPos, zPos));
  				uv.push_back(glm::vec2(xSegment, ySegment));
  				normals.push_back(glm::vec3(xPos, yPos, zPos));
  			}
  		}
  
  		bool oddRow = false;
  		for (int y = 0; y < Y_SEGMENTS; ++y)
  		{
  			if (!oddRow) // even rows: y == 0, y == 2; and so on
  			{
  				for (int x = 0; x <= X_SEGMENTS; ++x)
  				{
  					indices.push_back(y       * (X_SEGMENTS + 1) + x);
  					indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
  				}
  			}
  			else
  			{
  				for (int x = X_SEGMENTS; x >= 0; --x)
  				{
  					indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
  					indices.push_back(y       * (X_SEGMENTS + 1) + x);
  				}
  			}
  			oddRow = !oddRow;
  		}
  		indexCount = indices.size();
  
  		std::vector<float> data;
  		for (int i = 0; i < positions.size(); ++i)
  		{
  			data.push_back(positions[i].x);
  			data.push_back(positions[i].y);
  			data.push_back(positions[i].z);
  			if (normals.size() > 0)
  			{
  				data.push_back(normals[i].x);
  				data.push_back(normals[i].y);
  				data.push_back(normals[i].z);
  			}
  			if (uv.size() > 0)
  			{
  				data.push_back(uv[i].x);
  				data.push_back(uv[i].y);
  			}
  		}
  		glBindVertexArray(sphereVAO);
  		glBindBuffer(GL_ARRAY_BUFFER, vbo);
  		glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(float), &data[0], GL_STATIC_DRAW);
  		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
  		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
  		float stride = (3 + 2 + 3) * sizeof(float);
  		glEnableVertexAttribArray(0);
  		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
  		glEnableVertexAttribArray(1);
  		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
  		glEnableVertexAttribArray(2);
  		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
  	}
  
  	glBindVertexArray(sphereVAO);
  	glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_INT, 0);
  }
  
  unsigned int quadVAO = 0;
  unsigned int quadVBO;
  void RenderQuad()
  {
  	if (quadVAO == 0)
  	{
  		float quadVertices[] = {
  			// positions        // texture Coords
  			-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
  			-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
  			1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
  			1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
  		};
  		// setup plane VAO
  		glGenVertexArrays(1, &quadVAO);
  		glGenBuffers(1, &quadVBO);
  		glBindVertexArray(quadVAO);
  		glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
  		glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
  		glEnableVertexAttribArray(0);
  		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
  		glEnableVertexAttribArray(1);
  		glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
  	}
  	glBindVertexArray(quadVAO);
  	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  	glBindVertexArray(0);
  }
  
  unsigned int LoadTexture(const string _path)
  {
  	const char *path = _path.c_str();
  	unsigned int textureID;
  	glGenTextures(1, &textureID);
  
  	int width, height, nrComponents;
  	unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
  	if (data)
  	{
  		GLenum format;
  		if (nrComponents == 1)
  			format = GL_RED;
  		else if (nrComponents == 3)
  			format = GL_RGB;
  		else if (nrComponents == 4)
  			format = GL_RGBA;
  
  		glBindTexture(GL_TEXTURE_2D, textureID);
  		glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
  		glGenerateMipmap(GL_TEXTURE_2D);
  
  		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  
  		stbi_image_free(data);
  	}
  	else
  	{
  		std::cout << "Texture failed to load at path: " << path << std::endl;
  		stbi_image_free(data);
  	}
  
  	return textureID;
  }