diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f2c2b6..0dd6183 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,8 +115,8 @@ set(5.advanced_lighting set(6.pbr 1.1.lighting 1.2.lighting_textured - # 2.1.1.ibl_irradiance_conversion - # 2.1.2.ibl_irradiance + 2.1.1.ibl_irradiance_conversion + 2.1.2.ibl_irradiance # 2.2.ibl_specular ) diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.frag b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.frag new file mode 100644 index 0000000..de9adfb --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.frag @@ -0,0 +1,17 @@ +#version 330 core +out vec4 FragColor; +in vec3 WorldPos; + +uniform samplerCube environmentMap; + +void main() +{ + vec3 envColor = texture(environmentMap, WorldPos).rgb; + // envColor *= vec3(1.0, 0.0, 0.0); + + // HDR tonemap and gamma correct + envColor = envColor / (envColor + vec3(1.0)); + envColor = pow(envColor, vec3(1.0/2.2)); + + FragColor = vec4(envColor, 1.0); +} diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.vs b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.vs new file mode 100644 index 0000000..7de70de --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.background.vs @@ -0,0 +1,17 @@ +#version 330 core +layout (location = 0) in vec3 pos; + +uniform mat4 projection; +uniform mat4 view; + +out vec3 WorldPos; + +void main() +{ + WorldPos = pos; + + mat4 rotView = mat4(mat3(view)); + vec4 clipPos = projection * rotView * vec4(WorldPos, 1.0); + + gl_Position = clipPos.xyww; +} \ No newline at end of file diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.cubemap.vs b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.cubemap.vs new file mode 100644 index 0000000..a7ae633 --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.cubemap.vs @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec3 pos; + +out vec3 WorldPos; + +uniform mat4 projection; +uniform mat4 view; + +void main() +{ + WorldPos = pos; + + gl_Position = projection * view * vec4(WorldPos, 1.0); +} \ No newline at end of file diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.equirectangular_to_cubemap.frag b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.equirectangular_to_cubemap.frag new file mode 100644 index 0000000..cee67e6 --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.equirectangular_to_cubemap.frag @@ -0,0 +1,22 @@ +#version 330 core +out vec4 FragColor; +in vec3 WorldPos; + +uniform sampler2D equirectangularMap; + +const vec2 invAtan = vec2(0.1591, 0.3183); +vec2 SampleSphericalMap(vec3 v) +{ + vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); + uv *= invAtan; + uv += 0.5; + return uv; +} + +void main() +{ + vec2 uv = SampleSphericalMap(normalize(WorldPos)); + vec3 color = texture(equirectangularMap, uv).rgb; + + FragColor = vec4(color, 1.0); +} diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.frag b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.frag new file mode 100644 index 0000000..5c8175c --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.frag @@ -0,0 +1,126 @@ +#version 330 core +out vec4 FragColor; +in vec2 TexCoords; +in vec3 WorldPos; +in vec3 Normal; +in mat3 TBN; + +// material parameters +uniform vec3 albedo; +uniform float metallic; +uniform float roughness; +uniform float ao; + +// lights +uniform vec3 lightPositions[4]; +uniform vec3 lightColors[4]; + +uniform vec3 camPos; + +const float PI = 3.14159265359; +// ---------------------------------------------------------------------------- +float DistributionGGX(vec3 N, vec3 H, float roughness) +{ + float a = roughness*roughness; + float a2 = a*a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH*NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} +// ---------------------------------------------------------------------------- +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float r = (roughness + 1.0); + float k = (r*r) / 8.0; + + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return nom / denom; +} +// ---------------------------------------------------------------------------- +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +{ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = GeometrySchlickGGX(NdotV, roughness); + float ggx1 = GeometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} +// ---------------------------------------------------------------------------- +vec3 fresnelSchlick(float cosTheta, vec3 F0) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} +// ---------------------------------------------------------------------------- +vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) +{ + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} +// ---------------------------------------------------------------------------- +void main() +{ + vec3 N = Normal; + vec3 V = normalize(camPos - WorldPos); + vec3 R = reflect(-V, N); + + // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 + // of 0.04 and if it's a metal, use their albedo color as F0 (metallic workflow) + vec3 F0 = vec3(0.04); + F0 = mix(F0, albedo, metallic); + + // reflectance equation + vec3 Lo = vec3(0.0); + for(int i = 0; i < 4; ++i) + { + // calculate per-light radiance + vec3 L = normalize(lightPositions[i] - WorldPos); + vec3 H = normalize(V + L); + float distance = length(lightPositions[i] - WorldPos); + float attenuation = 1.0 / (distance * distance); + vec3 radiance = lightColors[i] * attenuation; + + // Cook-Torrance BRDF + float NDF = DistributionGGX(N, H, roughness); + float G = GeometrySmith(N, V, L, roughness); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + // kS is equal to Fresnel + vec3 kS = F; + // for energy conservation, the diffuse and specular light can't + // be above 1.0 (unless the surface emits light); to preserve this + // relationship the diffuse component (kD) should equal 1.0 - kS. + vec3 kD = vec3(1.0) - kS; + // multiply kD by the inverse metalness such that only non-metals + // have diffuse lighting, or a linear blend if partly metal (pure metals + // have no diffuse light). + kD *= 1.0 - metallic; + + vec3 nominator = NDF * G * F; + float denominator = 4 * max(dot(V, N), 0.0) * max(dot(L, N), 0.0) + 0.001; // 0.001 to prevent divide by zero. + vec3 brdf = nominator / denominator; + + // scale light by NdotL + float NdotL = max(dot(N, L), 0.0); + + // add to outgoing radiance Lo + Lo += (kD * albedo / PI + brdf) * radiance * NdotL; // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again + } + + vec3 ambient = vec3(0.002); + + vec3 color = ambient + Lo; + + // HDR tonemapping + color = color / (color + vec3(1.0)); + // gamma correct + color = pow(color, vec3(1.0/2.2)); + + FragColor = vec4(color, 1.0); +} diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.vs b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.vs new file mode 100644 index 0000000..1fe53b3 --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/2.1.1.pbr.vs @@ -0,0 +1,21 @@ +#version 330 core +layout (location = 0) in vec3 pos; +layout (location = 1) in vec2 texCoords; +layout (location = 2) in vec3 normal; + +out vec2 TexCoords; +out vec3 WorldPos; +out vec3 Normal; + +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; + +void main() +{ + TexCoords = texCoords; + WorldPos = vec3(model * vec4(pos, 1.0f)); + Normal = mat3(model) * normal; + + gl_Position = projection * view * vec4(WorldPos, 1.0); +} \ No newline at end of file diff --git a/src/6.pbr/2.1.1.ibl_irradiance_conversion/ibl_irradiance_conversion.cpp b/src/6.pbr/2.1.1.ibl_irradiance_conversion/ibl_irradiance_conversion.cpp new file mode 100644 index 0000000..44cf4aa --- /dev/null +++ b/src/6.pbr/2.1.1.ibl_irradiance_conversion/ibl_irradiance_conversion.cpp @@ -0,0 +1,519 @@ +// Std. Includes +#include + +// GLEW +#define GLEW_STATIC +#include + +// GLFW +#include + +// GL includes +#include +#include + +// GLM Mathemtics +#include +#include +#include + +// Other Libs +#include + +#include "stb_image.h" + +// Properties +const GLuint SCR_WIDTH = 1280, SCR_HEIGHT = 720; + +// Function prototypes +void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); +void mouse_callback(GLFWwindow* window, double xpos, double ypos); +void Do_Movement(); +GLuint loadTexture(GLchar const * path); +void renderSphere(); +void renderCube(); + +// camera +Camera camera(glm::vec3(0.0f, 0.0f, 3.0f)); + +// timing +GLfloat deltaTime = 0.0f; +GLfloat lastFrame = 0.0f; + +// The MAIN function, from here we start the application and run the Game loop +int main() +{ + // GLFW Init + // --------- + glfwInit(); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); + + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr); // Windowed + glfwMakeContextCurrent(window); + + // GLFW config + // ----------- + glfwSetKeyCallback(window, key_callback); + glfwSetCursorPosCallback(window, mouse_callback); + glfwSetScrollCallback(window, scroll_callback); + + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + + // Initialize GLEW to setup the OpenGL Function pointers + // ----------------------------------------------------- + glewExperimental = GL_TRUE; + glewInit(); + glGetError(); + + // Setup OpenGL state + // ------------------------- + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); // set depth function to less than AND equal for skybox depth trick. + + // load and initialize shaders + // ---------------------- + Shader pbrShader("2.1.1.pbr.vs", "2.1.1.pbr.frag"); + Shader equirectangularToCubemapShader("2.1.1.cubemap.vs", "2.1.1.equirectangular_to_cubemap.frag"); + Shader backgroundShader("2.1.1.background.vs", "2.1.1.background.frag"); + + pbrShader.Use(); + glUniform3f(glGetUniformLocation(pbrShader.Program, "albedo"), 0.5f, 0.0f, 0.0f); + glUniform1f(glGetUniformLocation(pbrShader.Program, "ao"), 1.0f); + + backgroundShader.Use(); + glUniform1i(glGetUniformLocation(backgroundShader.Program, "environmentMap"), 0); + + // lights + // ------ + glm::vec3 lightPositions[] = { + glm::vec3(-10.0f, 10.0f, 10.0f), + glm::vec3(10.0f, 10.0f, 10.0f), + glm::vec3(-10.0f, -10.0f, 10.0f), + glm::vec3(10.0f, -10.0f, 10.0f), + }; + glm::vec3 lightColors[] = { + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f) + }; + int nrRows = 7; + int nrColumns = 7; + float spacing = 2.5; + + // pbr: setup framebuffer + // ---------------------- + unsigned int captureFBO; + unsigned int captureRBO; + glGenFramebuffers(1, &captureFBO); + glGenRenderbuffers(1, &captureRBO); + + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO); + + // pbr: load the HDR environment map + // --------------------------------- + stbi_set_flip_vertically_on_load(true); + int width, height, nrComponents; + float *data = stbi_loadf(FileSystem::getPath("resources/textures/hdr/newport_loft.hdr").c_str(), &width, &height, &nrComponents, 0); + unsigned int hdrTexture; + if (data) + { + glGenTextures(1, &hdrTexture); + glBindTexture(GL_TEXTURE_2D, hdrTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); // note how we specify the texture's data value to be float + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + stbi_image_free(data); + } + else + { + std::cout << "Failed to load HDR image." << std::endl; + } + + // pbr: setup cubemap to render to and attach to framebuffer + // --------------------------------------------------------- + unsigned int envCubemap; + glGenTextures(1, &envCubemap); + glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); + for (unsigned int i = 0; i < 6; ++i) + { + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // pbr: set up projection and view matrices for capturing data onto the 6 cubemap face directions + // ---------------------------------------------------------------------------------------------- + glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); + glm::mat4 captureViews[] = + { + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) + }; + + // pbr: convert HDR equirectangular environment map to cubemap equivalent + // ---------------------------------------------------------------------- + equirectangularToCubemapShader.Use(); + glUniform1i(glGetUniformLocation(equirectangularToCubemapShader.Program, "equirectangularMap"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, hdrTexture); + glUniformMatrix4fv(glGetUniformLocation(equirectangularToCubemapShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(captureProjection)); + + glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions. + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + for (unsigned int i = 0; i < 6; ++i) + { + glUniformMatrix4fv(glGetUniformLocation(equirectangularToCubemapShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(captureViews[i])); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + renderCube(); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // initialize static shader uniforms before rendering + // -------------------------------------------------- + glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); + pbrShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + backgroundShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(backgroundShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + + // then before rendering, configure the viewport to the actual screen dimensions + glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); + + // Game loop + while (!glfwWindowShouldClose(window)) + { + // set frame time + GLfloat currentFrame = glfwGetTime(); + deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + // check and call events + glfwPollEvents(); + Do_Movement(); + + // clear the colorbuffer + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // render scene, supplying the convoluted irradiance map to the final shader. + // ------------------------------------------------------------------------------------------ + pbrShader.Use(); + glm::mat4 view = camera.GetViewMatrix(); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); + glUniform3fv(glGetUniformLocation(pbrShader.Program, "camPos"), 1, &camera.Position[0]); + + // render rows*column number of spheres with material properties defined by textures (they all have the same material properties) + glm::mat4 model; + for (int row = 0; row < nrRows; ++row) + { + glUniform1f(glGetUniformLocation(pbrShader.Program, "metallic"), (float)row / (float)nrRows); + for (int col = 0; col < nrColumns; ++col) + { + // we clamp the roughness to 0.025 - 1.0 as perfectly smooth surfaces (roughness of 0.0) tend to look a bit off + // on direct lighting. + glUniform1f(glGetUniformLocation(pbrShader.Program, "roughness"), glm::clamp((float)col / (float)nrColumns, 0.05f, 1.0f)); + + model = glm::mat4(); + model = glm::translate(model, glm::vec3( + (float)(col - (nrColumns / 2)) * spacing, + (float)(row - (nrRows / 2)) * spacing, + -2.0f + )); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); + renderSphere(); + } + } + + // render light source (simply re-render sphere at light positions) + // this looks a bit off as we use the same shader, but it'll make their positions obvious and + // keeps the codeprint small. + for (unsigned int i = 0; i < sizeof(lightPositions) / sizeof(lightPositions[0]); ++i) + { + glm::vec3 newPos = lightPositions[i] + glm::vec3(sin(glfwGetTime() * 5.0) * 5.0, 0.0, 0.0); + newPos = lightPositions[i]; + glUniform3fv(glGetUniformLocation(pbrShader.Program, ("lightPositions[" + std::to_string(i) + "]").c_str()), 1, &newPos[0]);\ + glUniform3fv(glGetUniformLocation(pbrShader.Program, ("lightColors[" + std::to_string(i) + "]").c_str()), 1, &lightColors[i][0]); + + model = glm::mat4(); + model = glm::translate(model, newPos); + model = glm::scale(model, glm::vec3(0.5f)); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); + renderSphere(); + } + + // render skybox (render as last to prevent overdraw) + backgroundShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(backgroundShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); + renderCube(); + + //equirectangularToCubemapShader.Use(); + //glUniformMatrix4fv(glGetUniformLocation(equirectangularToCubemapShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); + //glActiveTexture(GL_TEXTURE0); + //glBindTexture(GL_TEXTURE_2D, hdrTexture); + //renderCube(); + + + // Swap the buffers + glfwSwapBuffers(window); + } + + glfwTerminate(); + return 0; +} + + +// renders (and builds at first invocation) a sphere +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 positions; + std::vector uv; + std::vector normals; + std::vector 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 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 (uv.size() > 0) + { + data.push_back(uv[i].x); + data.push_back(uv[i].y); + } + if (normals.size() > 0) + { + data.push_back(normals[i].x); + data.push_back(normals[i].y); + data.push_back(normals[i].z); + } + } + 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, (GLvoid*)0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(3 * sizeof(float))); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(5 * sizeof(float))); + } + + glBindVertexArray(sphereVAO); + glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_INT, 0); +} +// RenderCube() Renders a 1x1 3D cube in NDC. +GLuint cubeVAO = 0; +GLuint cubeVBO = 0; +void renderCube() +{ + // Initialize (if necessary) + if (cubeVAO == 0) + { + GLfloat vertices[] = { + // Back face + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // Bottom-left + 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // bottom-left + -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,// top-left + // Front face + -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left + 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right + -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // top-left + -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left + // Left face + -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right + -1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-left + -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left + -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left + -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-right + -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right + // Right face + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left + 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-left + // Bottom face + -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, // top-left + 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,// bottom-left + 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, // bottom-left + -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, // bottom-right + -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right + // Top face + -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left + 1.0f, 1.0f , 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, // top-right + 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right + -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left + -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f // bottom-left + }; + glGenVertexArrays(1, &cubeVAO); + glGenBuffers(1, &cubeVBO); + // Fill buffer + glBindBuffer(GL_ARRAY_BUFFER, cubeVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + // Link vertex attributes + glBindVertexArray(cubeVAO); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat))); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + // Render Cube + glBindVertexArray(cubeVAO); + glDrawArrays(GL_TRIANGLES, 0, 36); + glBindVertexArray(0); +} + +#pragma region "User input" + +bool keys[1024]; +bool keysPressed[1024]; +// Moves/alters the camera positions based on user input +void Do_Movement() +{ + // Camera controls + if (keys[GLFW_KEY_W]) + camera.ProcessKeyboard(FORWARD, deltaTime); + if (keys[GLFW_KEY_S]) + camera.ProcessKeyboard(BACKWARD, deltaTime); + if (keys[GLFW_KEY_A]) + camera.ProcessKeyboard(LEFT, deltaTime); + if (keys[GLFW_KEY_D]) + camera.ProcessKeyboard(RIGHT, deltaTime); +} + +// Is called whenever a key is pressed/released via GLFW +void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) +{ + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) + glfwSetWindowShouldClose(window, GL_TRUE); + + if (key >= 0 && key <= 1024) + { + if (action == GLFW_PRESS) + keys[key] = true; + else if (action == GLFW_RELEASE) + { + keys[key] = false; + keysPressed[key] = false; + } + } +} + +GLfloat lastX = 400, lastY = 300; +bool firstMouse = true; +// Moves/alters the camera positions based on user input +void mouse_callback(GLFWwindow* window, double xpos, double ypos) +{ + if (firstMouse) + { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + GLfloat xoffset = xpos - lastX; + GLfloat yoffset = lastY - ypos; + + lastX = xpos; + lastY = ypos; + + camera.ProcessMouseMovement(xoffset, yoffset); +} + +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) +{ + camera.ProcessMouseScroll(yoffset); +} + +#pragma endregion \ No newline at end of file diff --git a/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.irradiance_convolution.frag b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.irradiance_convolution.frag new file mode 100644 index 0000000..25d5926 --- /dev/null +++ b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.irradiance_convolution.frag @@ -0,0 +1,43 @@ +#version 330 core +out vec4 FragColor; +in vec3 WorldPos; + +uniform samplerCube environmentMap; + +const float PI = 3.14159265359f; + +void main() +{ + // NOTE(Joey): the world vector acts as the normal of a tangent surface + // from the origin, aligned to WorldPos. Given this normal, calculate all + // incoming radiance of the environment. The result of this radiance + // is the radiance of light coming from -Normal direction, which is what + // we use in the PBR shader to sample irradiance. + vec3 N = normalize(WorldPos); + + vec3 irradiance = vec3(0.0); + + // NOTE(Joey): tangent space calculation from origin point + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = cross(up, N); + up = cross(N, right); + + float sampleDelta = 0.025f; + float nrSamples = 0.0f; + for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) + { + for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) + { + // spherical to cartesian (in tangent space) + vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); + // tangent space to world + vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; + + irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); + nrSamples++; + } + } + irradiance = PI * irradiance * (1.0 / float(nrSamples)); + + FragColor = vec4(irradiance, 1.0); +} diff --git a/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.frag b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.frag new file mode 100644 index 0000000..cfcd977 --- /dev/null +++ b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.frag @@ -0,0 +1,137 @@ +#version 330 core +out vec4 FragColor; +in vec2 TexCoords; +in vec3 WorldPos; +in vec3 Normal; +in mat3 TBN; + +// material parameters +uniform vec3 albedo; +uniform float metallic; +uniform float roughness; +uniform float ao; + +// IBL +uniform samplerCube irradianceMap; + +// lights +uniform vec3 lightPositions[4]; +uniform vec3 lightColors[4]; + +uniform vec3 camPos; + +const float PI = 3.14159265359; +// ---------------------------------------------------------------------------- +float DistributionGGX(vec3 N, vec3 H, float roughness) +{ + float a = roughness*roughness; + float a2 = a*a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH*NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} +// ---------------------------------------------------------------------------- +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float r = (roughness + 1.0); + float k = (r*r) / 8.0; + + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return nom / denom; +} +// ---------------------------------------------------------------------------- +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +{ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = GeometrySchlickGGX(NdotV, roughness); + float ggx1 = GeometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} +// ---------------------------------------------------------------------------- +vec3 fresnelSchlick(float cosTheta, vec3 F0) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} +// ---------------------------------------------------------------------------- +vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) +{ + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} +// ---------------------------------------------------------------------------- +void main() +{ + vec3 N = Normal; + vec3 V = normalize(camPos - WorldPos); + vec3 R = reflect(-V, N); + + // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 + // of 0.04 and if it's a metal, use their albedo color as F0 (metallic workflow) + vec3 F0 = vec3(0.04); + F0 = mix(F0, albedo, metallic); + + // reflectance equation + vec3 Lo = vec3(0.0); + for(int i = 0; i < 4; ++i) + { + // calculate per-light radiance + vec3 L = normalize(lightPositions[i] - WorldPos); + vec3 H = normalize(V + L); + float distance = length(lightPositions[i] - WorldPos); + float attenuation = 1.0 / (distance * distance); + vec3 radiance = lightColors[i] * attenuation; + + // Cook-Torrance BRDF + float NDF = DistributionGGX(N, H, roughness); + float G = GeometrySmith(N, V, L, roughness); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + // kS is equal to Fresnel + vec3 kS = F; + // for energy conservation, the diffuse and specular light can't + // be above 1.0 (unless the surface emits light); to preserve this + // relationship the diffuse component (kD) should equal 1.0 - kS. + vec3 kD = vec3(1.0) - kS; + // multiply kD by the inverse metalness such that only non-metals + // have diffuse lighting, or a linear blend if partly metal (pure metals + // have no diffuse light). + kD *= 1.0 - metallic; + + vec3 nominator = NDF * G * F; + float denominator = 4 * max(dot(V, N), 0.0) * max(dot(L, N), 0.0) + 0.001; // 0.001 to prevent divide by zero. + vec3 brdf = nominator / denominator; + + // scale light by NdotL + float NdotL = max(dot(N, L), 0.0); + + // add to outgoing radiance Lo + Lo += (kD * albedo / PI + brdf) * radiance * NdotL; // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again + } + + // ambient lighting (we now use IBL as the ambient term) + vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); + vec3 kD = 1.0 - kS; + kD *= 1.0 - metallic; + vec3 irradiance = texture(irradianceMap, N).rgb; + vec3 diffuse = irradiance * albedo; + vec3 ambient = (kD * diffuse) * ao; + // vec3 ambient = vec3(0.002); + + vec3 color = ambient + Lo; + + // HDR tonemapping + color = color / (color + vec3(1.0)); + // gamma correct + color = pow(color, vec3(1.0/2.2)); + + FragColor = vec4(color, 1.0); + // FragColor = vec4(N * 0.5 + 0.5, 1.0); +} diff --git a/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.vs b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.vs new file mode 100644 index 0000000..1fe53b3 --- /dev/null +++ b/src/6.pbr/2.1.2.ibl_irradiance/2.1.2.pbr.vs @@ -0,0 +1,21 @@ +#version 330 core +layout (location = 0) in vec3 pos; +layout (location = 1) in vec2 texCoords; +layout (location = 2) in vec3 normal; + +out vec2 TexCoords; +out vec3 WorldPos; +out vec3 Normal; + +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; + +void main() +{ + TexCoords = texCoords; + WorldPos = vec3(model * vec4(pos, 1.0f)); + Normal = mat3(model) * normal; + + gl_Position = projection * view * vec4(WorldPos, 1.0); +} \ No newline at end of file diff --git a/src/6.pbr/2.1.2.ibl_irradiance/ibl_irradiance.cpp b/src/6.pbr/2.1.2.ibl_irradiance/ibl_irradiance.cpp new file mode 100644 index 0000000..1506c52 --- /dev/null +++ b/src/6.pbr/2.1.2.ibl_irradiance/ibl_irradiance.cpp @@ -0,0 +1,599 @@ +// Std. Includes +#include + +// GLEW +#define GLEW_STATIC +#include + +// GLFW +#include + +// GL includes +#include +#include + +// GLM Mathemtics +#include +#include +#include + +// Other Libs +#include + +#include "stb_image.h" + +// Properties +const GLuint SCR_WIDTH = 1280, SCR_HEIGHT = 720; + +// Function prototypes +void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); +void mouse_callback(GLFWwindow* window, double xpos, double ypos); +void Do_Movement(); +GLuint loadTexture(GLchar const * path); +void renderSphere(); +void renderCube(); + +// camera +Camera camera(glm::vec3(0.0f, 0.0f, 3.0f)); + +// timing +GLfloat deltaTime = 0.0f; +GLfloat lastFrame = 0.0f; + +// The MAIN function, from here we start the application and run the Game loop +int main() +{ + // GLFW Init + // --------- + glfwInit(); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); + + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr); // Windowed + glfwMakeContextCurrent(window); + + // GLFW config + // ----------- + glfwSetKeyCallback(window, key_callback); + glfwSetCursorPosCallback(window, mouse_callback); + glfwSetScrollCallback(window, scroll_callback); + + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + + // Initialize GLEW to setup the OpenGL Function pointers + // ----------------------------------------------------- + glewExperimental = GL_TRUE; + glewInit(); + glGetError(); + + // Setup OpenGL state + // ------------------------- + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); // set depth function to less than AND equal for skybox depth trick. + + // load and initialize shaders + // ---------------------- + Shader pbrShader("2.1.2.pbr.vs", "2.1.2.pbr.frag"); + Shader equirectangularToCubemapShader("2.1.1.cubemap.vs", "2.1.1.equirectangular_to_cubemap.frag"); + Shader irradianceShader("2.1.1.cubemap.vs", "2.1.2.irradiance_convolution.frag"); + Shader backgroundShader("2.1.1.background.vs", "2.1.1.background.frag"); + + pbrShader.Use(); + glUniform1i(glGetUniformLocation(pbrShader.Program, "irradianceMap"), 0); + glUniform3f(glGetUniformLocation(pbrShader.Program, "albedo"), 0.5f, 0.0f, 0.0f); + glUniform1f(glGetUniformLocation(pbrShader.Program, "ao"), 1.0f); + + backgroundShader.Use(); + glUniform1i(glGetUniformLocation(backgroundShader.Program, "environmentMap"), 0); + + // lights + // ------ + glm::vec3 lightPositions[] = { + glm::vec3(-10.0f, 10.0f, 10.0f), + glm::vec3(10.0f, 10.0f, 10.0f), + glm::vec3(-10.0f, -10.0f, 10.0f), + glm::vec3(10.0f, -10.0f, 10.0f), + }; + glm::vec3 lightColors[] = { + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f), + glm::vec3(300.0f, 300.0f, 300.0f) + }; + int nrRows = 7; + int nrColumns = 7; + float spacing = 2.5; + + // pbr: setup framebuffer + // ---------------------- + unsigned int captureFBO; + unsigned int captureRBO; + glGenFramebuffers(1, &captureFBO); + glGenRenderbuffers(1, &captureRBO); + + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO); + + // pbr: load the HDR environment map + // --------------------------------- + stbi_set_flip_vertically_on_load(true); + int width, height, nrComponents; + float *data = stbi_loadf(FileSystem::getPath("resources/textures/hdr/newport_loft.hdr").c_str(), &width, &height, &nrComponents, 0); + unsigned int hdrTexture; + if (data) + { + glGenTextures(1, &hdrTexture); + glBindTexture(GL_TEXTURE_2D, hdrTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); // note how we specify the texture's data value to be float + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + stbi_image_free(data); + } + else + { + std::cout << "Failed to load HDR image." << std::endl; + } + + // pbr: setup cubemap to render to and attach to framebuffer + // --------------------------------------------------------- + unsigned int envCubemap; + glGenTextures(1, &envCubemap); + glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); + for (unsigned int i = 0; i < 6; ++i) + { + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // pbr: set up projection and view matrices for capturing data onto the 6 cubemap face directions + // ---------------------------------------------------------------------------------------------- + glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); + glm::mat4 captureViews[] = + { + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), + glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) + }; + + // pbr: convert HDR equirectangular environment map to cubemap equivalent + // ---------------------------------------------------------------------- + equirectangularToCubemapShader.Use(); + glUniform1i(glGetUniformLocation(equirectangularToCubemapShader.Program, "equirectangularMap"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, hdrTexture); + glUniformMatrix4fv(glGetUniformLocation(equirectangularToCubemapShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(captureProjection)); + + glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions. + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + for (unsigned int i = 0; i < 6; ++i) + { + glUniformMatrix4fv(glGetUniformLocation(equirectangularToCubemapShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(captureViews[i])); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + renderCube(); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // pbr: create an irradiance cubemap, and re-scale capture FBO to irradiance scale. + // -------------------------------------------------------------------------------- + unsigned int irradianceMap; + glGenTextures(1, &irradianceMap); + glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); + for (unsigned int i = 0; i < 6; ++i) + { + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB32F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32); + + // pbr: solve diffuse integral by convolution to create an irradiance (cube)map. + // ----------------------------------------------------------------------------- + irradianceShader.Use(); + glUniform1i(glGetUniformLocation(irradianceShader.Program, "environmentMap"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); + glUniformMatrix4fv(glGetUniformLocation(irradianceShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(captureProjection)); + + glViewport(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions. + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + for (unsigned int i = 0; i < 6; ++i) + { + glUniformMatrix4fv(glGetUniformLocation(irradianceShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(captureViews[i])); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + renderCube(); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + + // initialize static shader uniforms before rendering + // -------------------------------------------------- + glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); + pbrShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + backgroundShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(backgroundShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + + // then before rendering, configure the viewport to the actual screen dimensions + glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); + + // Game loop + while (!glfwWindowShouldClose(window)) + { + // set frame time + GLfloat currentFrame = glfwGetTime(); + deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + // check and call events + glfwPollEvents(); + Do_Movement(); + + // clear the colorbuffer + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // render scene, supplying the convoluted irradiance map to the final shader. + // ------------------------------------------------------------------------------------------ + pbrShader.Use(); + glm::mat4 view = camera.GetViewMatrix(); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); + glUniform3fv(glGetUniformLocation(pbrShader.Program, "camPos"), 1, &camera.Position[0]); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); + + // render rows*column number of spheres with material properties defined by textures (they all have the same material properties) + glm::mat4 model; + for (int row = 0; row < nrRows; ++row) + { + glUniform1f(glGetUniformLocation(pbrShader.Program, "metallic"), (float)row / (float)nrRows); + for (int col = 0; col < nrColumns; ++col) + { + // we clamp the roughness to 0.025 - 1.0 as perfectly smooth surfaces (roughness of 0.0) tend to look a bit off + // on direct lighting. + glUniform1f(glGetUniformLocation(pbrShader.Program, "roughness"), glm::clamp((float)col / (float)nrColumns, 0.05f, 1.0f)); + + model = glm::mat4(); + model = glm::translate(model, glm::vec3( + (float)(col - (nrColumns / 2)) * spacing, + (float)(row - (nrRows / 2)) * spacing, + -2.0f + )); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); + renderSphere(); + } + } + + // render light source (simply re-render sphere at light positions) + // this looks a bit off as we use the same shader, but it'll make their positions obvious and + // keeps the codeprint small. + for (unsigned int i = 0; i < sizeof(lightPositions) / sizeof(lightPositions[0]); ++i) + { + glm::vec3 newPos = lightPositions[i] + glm::vec3(sin(glfwGetTime() * 5.0) * 5.0, 0.0, 0.0); + newPos = lightPositions[i]; + glUniform3fv(glGetUniformLocation(pbrShader.Program, ("lightPositions[" + std::to_string(i) + "]").c_str()), 1, &newPos[0]); \ + glUniform3fv(glGetUniformLocation(pbrShader.Program, ("lightColors[" + std::to_string(i) + "]").c_str()), 1, &lightColors[i][0]); + + model = glm::mat4(); + model = glm::translate(model, newPos); + model = glm::scale(model, glm::vec3(0.5f)); + glUniformMatrix4fv(glGetUniformLocation(pbrShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); + renderSphere(); + } + + // render skybox (render as last to prevent overdraw) + backgroundShader.Use(); + glUniformMatrix4fv(glGetUniformLocation(backgroundShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); + //glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); // display irradiance map + renderCube(); + + // Swap the buffers + glfwSwapBuffers(window); + } + + glfwTerminate(); + return 0; +} + + +// renders (and builds at first invocation) a sphere +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 positions; + std::vector uv; + std::vector normals; + std::vector 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 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 (uv.size() > 0) + { + data.push_back(uv[i].x); + data.push_back(uv[i].y); + } + if (normals.size() > 0) + { + data.push_back(normals[i].x); + data.push_back(normals[i].y); + data.push_back(normals[i].z); + } + } + 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, (GLvoid*)0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(3 * sizeof(float))); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(5 * sizeof(float))); + } + + glBindVertexArray(sphereVAO); + glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_INT, 0); +} +// RenderCube() Renders a 1x1 3D cube in NDC. +GLuint cubeVAO = 0; +GLuint cubeVBO = 0; +void renderCube() +{ + // Initialize (if necessary) + if (cubeVAO == 0) + { + GLfloat vertices[] = { + // Back face + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // Bottom-left + 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // bottom-left + -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,// top-left + // Front face + -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left + 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right + -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // top-left + -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left + // Left face + -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right + -1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-left + -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left + -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left + -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-right + -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right + // Right face + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right + 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left + 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-left + // Bottom face + -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right + 1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, // top-left + 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,// bottom-left + 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, // bottom-left + -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, // bottom-right + -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right + // Top face + -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left + 1.0f, 1.0f , 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right + 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, // top-right + 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right + -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left + -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f // bottom-left + }; + glGenVertexArrays(1, &cubeVAO); + glGenBuffers(1, &cubeVBO); + // Fill buffer + glBindBuffer(GL_ARRAY_BUFFER, cubeVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + // Link vertex attributes + glBindVertexArray(cubeVAO); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat))); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + // Render Cube + glBindVertexArray(cubeVAO); + glDrawArrays(GL_TRIANGLES, 0, 36); + glBindVertexArray(0); +} + +// This function loads a texture from file. Note: texture loading functions like these are usually +// managed by a 'Resource Manager' that manages all resources (like textures, models, audio). +// For learning purposes we'll just define it as a utility function. +unsigned int loadTexture(char const * path) +{ + //Generate texture ID and load texture data + 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); + + // Parameters + 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; +} + +#pragma region "User input" + +bool keys[1024]; +bool keysPressed[1024]; +// Moves/alters the camera positions based on user input +void Do_Movement() +{ + // Camera controls + if (keys[GLFW_KEY_W]) + camera.ProcessKeyboard(FORWARD, deltaTime); + if (keys[GLFW_KEY_S]) + camera.ProcessKeyboard(BACKWARD, deltaTime); + if (keys[GLFW_KEY_A]) + camera.ProcessKeyboard(LEFT, deltaTime); + if (keys[GLFW_KEY_D]) + camera.ProcessKeyboard(RIGHT, deltaTime); +} + +// Is called whenever a key is pressed/released via GLFW +void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) +{ + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) + glfwSetWindowShouldClose(window, GL_TRUE); + + if (key >= 0 && key <= 1024) + { + if (action == GLFW_PRESS) + keys[key] = true; + else if (action == GLFW_RELEASE) + { + keys[key] = false; + keysPressed[key] = false; + } + } +} + +GLfloat lastX = 400, lastY = 300; +bool firstMouse = true; +// Moves/alters the camera positions based on user input +void mouse_callback(GLFWwindow* window, double xpos, double ypos) +{ + if (firstMouse) + { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + GLfloat xoffset = xpos - lastX; + GLfloat yoffset = lastY - ypos; + + lastX = xpos; + lastY = ypos; + + camera.ProcessMouseMovement(xoffset, yoffset); +} + +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) +{ + camera.ProcessMouseScroll(yoffset); +} + +#pragma endregion \ No newline at end of file