// main.cpp // // A physically based renderer that demonstrates height mapping via parallax, // improved lighting (point & directional lights), and renders a skybox for background. // It uses procedural spheres and a plane. Materials (including height maps) can be loaded // and saved via YAML (using yaml-cpp) and are editable via ImGui. // // Compile (on Linux): // g++ main.cpp -lglfw -lGLEW -lGL -ldl -limgui -lyaml-cpp -o pbr_renderer // // Adjust library paths and names as needed. #include #include #include #include #include #include #include #include #include #include #include #include // ImGui includes. #include "imgui.h" #include "imgui_internal.h" // For IM_PI #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" // stb_image for texture loading. #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" // yaml-cpp for YAML material loading/saving. #include // ------------------------------------------ // Shader sources // ------------------------------------------ // Vertex Shader (common to objects) const char* vertexShaderSource = R"( #version 330 core layout (location = 0) in vec3 aPos; // position layout (location = 1) in vec3 aNormal; // normal layout (location = 2) in vec2 aTexCoords; // texture coordinates layout (location = 3) in vec3 aTangent; // tangent out vec3 WorldPos; out vec3 Normal; out vec2 TexCoords; out vec3 Tangent; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main(){ WorldPos = vec3(model * vec4(aPos, 1.0)); Normal = mat3(transpose(inverse(model))) * aNormal; Tangent = mat3(model) * aTangent; // simple transform (ignore scale issues) TexCoords = aTexCoords; gl_Position = projection * view * vec4(WorldPos, 1.0); } )"; // Fragment Shader with parallax (height) mapping and two light types. const char* fragmentShaderSource = R"(#version 330 core out vec4 FragColor; in vec3 WorldPos; in vec3 Normal; in vec2 TexCoords; in vec3 Tangent; uniform vec3 camPos; // Base material uniforms. uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; // Texture samplers & usage flags. uniform bool useAlbedoMap; uniform bool useMetallicMap; uniform bool useRoughnessMap; uniform bool useAOMap; uniform bool useNormalMap; uniform bool useHeightMap; // Height map flag uniform sampler2D albedoMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; uniform sampler2D normalMap; uniform sampler2D heightMap; // Height map sampler // Parallax mapping scale factor. uniform float parallaxScale; // Use ImGui's internal PI constant. uniform float PI; // Point light uniforms. uniform vec3 pointLightPos; uniform vec3 pointLightColor; // Directional light uniforms. uniform bool useDirLight; uniform vec3 dirLightDir; uniform vec3 dirLightColor; // NEW: Environment reflection uniforms. uniform samplerCube envMap; uniform float envReflectionIntensity; // // Helper functions for PBR // vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } 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 denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return a2 / max(denom, 0.001); } float GeometrySchlickGGX(float NdotV, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } 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 ggx1 = GeometrySchlickGGX(NdotV, roughness); float ggx2 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } void main(){ // Start with the input geometry normal. vec3 Nn = normalize(Normal); // Compute the TBN matrix if using normal or height maps. mat3 TBN = mat3(1.0); if (useNormalMap || useHeightMap) { vec3 T = normalize(Tangent); vec3 B = normalize(cross(Normal, T)); TBN = mat3(T, B, Normal); } // Compute modified texture coordinates (parallax mapping) if height mapping is enabled. vec2 texCoordsModified = TexCoords; if (useHeightMap) { // Transform view direction into tangent space. vec3 viewDirT = normalize(TBN * (camPos - WorldPos)); // Sample the height value. float heightValue = texture(heightMap, TexCoords).r; // Offset the texture coordinates. texCoordsModified = TexCoords - viewDirT.xy * (heightValue * parallaxScale); } // Apply normal mapping if enabled. if (useNormalMap) { vec3 tangentNormal = texture(normalMap, texCoordsModified).rgb; tangentNormal = tangentNormal * 2.0 - 1.0; Nn = normalize(TBN * tangentNormal); } vec3 V = normalize(camPos - WorldPos); // Sample material textures using modified texture coordinates. vec3 albedoValue = albedo; if (useAlbedoMap) albedoValue = texture(albedoMap, texCoordsModified).rgb; float metallicValue = metallic; if (useMetallicMap) metallicValue = texture(metallicMap, texCoordsModified).r; float roughnessValue = roughness; if (useRoughnessMap) roughnessValue = texture(roughnessMap, texCoordsModified).r; float aoValue = ao; if (useAOMap) aoValue = texture(aoMap, texCoordsModified).r; // Calculate reflectance at normal incidence. vec3 F0 = vec3(0.04); F0 = mix(F0, albedoValue, metallicValue); // --- Compute point light contribution --- vec3 Lp = normalize(pointLightPos - WorldPos); vec3 Hp = normalize(V + Lp); float NDFp = DistributionGGX(Nn, Hp, roughnessValue); float Gp = GeometrySmith(Nn, V, Lp, roughnessValue); vec3 Fp = fresnelSchlick(max(dot(Hp, V), 0.0), F0); vec3 numeratorP = NDFp * Gp * Fp; float denominatorP = 4.0 * max(dot(Nn, V), 0.0) * max(dot(Nn, Lp), 0.0) + 0.001; vec3 specularP = numeratorP / denominatorP; vec3 kS = Fp; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallicValue; float NdotLp = max(dot(Nn, Lp), 0.0); vec3 irradianceP = pointLightColor * NdotLp; vec3 diffuseP = (albedoValue / PI); vec3 lightingPoint = (kD * diffuseP + specularP) * irradianceP; // --- Compute directional light contribution --- vec3 lightingDir = vec3(0.0); if (useDirLight) { vec3 Ld = normalize(-dirLightDir); vec3 Hd = normalize(V + Ld); float NDFd = DistributionGGX(Nn, Hd, roughnessValue); float Gd = GeometrySmith(Nn, V, Ld, roughnessValue); vec3 Fd = fresnelSchlick(max(dot(Hd, V), 0.0), F0); vec3 numeratorD = NDFd * Gd * Fd; float denominatorD = 4.0 * max(dot(Nn, V), 0.0) * max(dot(Nn, Ld), 0.0) + 0.001; vec3 specularD = numeratorD / denominatorD; vec3 kS_d = Fd; vec3 kD_d = vec3(1.0) - kS_d; kD_d *= 1.0 - metallicValue; float NdotLd = max(dot(Nn, Ld), 0.0); vec3 irradianceD = dirLightColor * NdotLd; vec3 diffuseD = (albedoValue / PI); lightingDir = (kD_d * diffuseD + specularD) * irradianceD; } // Sum direct lighting contributions. vec3 lighting = lightingPoint + lightingDir; // --- Compute environment reflection contribution --- // Calculate reflection vector and sample cubemap. vec3 R = reflect(-V, Nn); vec3 envSpec = texture(envMap, R).rgb; lighting += envSpec * envReflectionIntensity; // Add a simple ambient term. vec3 ambient = vec3(0.03) * albedoValue * aoValue; lighting += ambient; // Tone mapping and gamma correction. lighting = lighting / (lighting + vec3(1.0)); lighting = pow(lighting, vec3(1.0/2.2)); FragColor = vec4(lighting, 1.0); } )"; // Skybox shaders const char* skyboxVertexShaderSource = R"( #version 330 core layout (location = 0) in vec3 aPos; out vec3 TexCoords; uniform mat4 view; uniform mat4 projection; void main(){ TexCoords = aPos; vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; // set w component to 1.0 to always pass depth test } )"; const char* skyboxFragmentShaderSource = R"( #version 330 core out vec4 FragColor; in vec3 TexCoords; uniform samplerCube skybox; void main(){ FragColor = texture(skybox, TexCoords); } )"; // ------------------------------------------ // Helper functions // ------------------------------------------ GLuint compileShader(GLenum type, const char* source) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if(!success) { char infoLog[512]; glGetShaderInfoLog(shader,512,nullptr,infoLog); std::cerr << "Shader compile error:\n" << infoLog << std::endl; } return shader; } GLuint createProgram(GLuint vs, GLuint fs) { GLuint program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); GLint success; glGetProgramiv(program, GL_LINK_STATUS, &success); if(!success) { char infoLog[512]; glGetProgramInfoLog(program,512,nullptr,infoLog); std::cerr << "Program link error:\n" << infoLog << std::endl; } return program; } GLuint LoadTexture(const char* path) { int width, height, nrChannels; stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0); if(!data) { std::cerr << "Failed to load texture: " << path << std::endl; return 0; } GLenum format; if(nrChannels == 1) format = GL_RED; else if(nrChannels == 3) format = GL_RGB; else if(nrChannels == 4) format = GL_RGBA; GLuint texID; glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_2D, texID); glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); // Set texture 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); return texID; } // Load a cubemap (skybox) from 6 images. Expect faces in order: right, left, top, bottom, front, back. GLuint loadCubemap(std::vector faces) { GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); int width, height, nrChannels; for(unsigned int i=0; i& vertices, std::vector& indices, unsigned int X_SEGMENTS = 64, unsigned int Y_SEGMENTS = 64) { vertices.clear(); indices.clear(); 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 * IM_PI) * std::sin(ySegment * IM_PI); float yPos = std::cos(ySegment * IM_PI); float zPos = std::sin(xSegment * 2.0f * IM_PI) * std::sin(ySegment * IM_PI); // Position. vertices.push_back(xPos); vertices.push_back(yPos); vertices.push_back(zPos); // Normal (for a unit sphere, same as position). vertices.push_back(xPos); vertices.push_back(yPos); vertices.push_back(zPos); // Texcoords. vertices.push_back(xSegment); vertices.push_back(ySegment); // Tangent: approximate (derivative with respect to u). float tangentX = -std::sin(xSegment * 2.0f * IM_PI); float tangentY = 0.0f; float tangentZ = std::cos(xSegment * 2.0f * IM_PI); vertices.push_back(tangentX); vertices.push_back(tangentY); vertices.push_back(tangentZ); } } for (unsigned int y = 0; y < Y_SEGMENTS; ++y) { for (unsigned int x = 0; x < X_SEGMENTS; ++x) { unsigned int i0 = y * (X_SEGMENTS + 1) + x; unsigned int i1 = i0 + 1; unsigned int i2 = i0 + (X_SEGMENTS + 1); unsigned int i3 = i2 + 1; indices.push_back(i0); indices.push_back(i2); indices.push_back(i1); indices.push_back(i1); indices.push_back(i2); indices.push_back(i3); } } } // Generate a plane mesh (quad) in the XZ plane centered at origin. // The plane spans [-1,1] in X and Z, y=0. void generatePlaneMesh(std::vector& vertices, std::vector& indices) { // 4 vertices: position (3), normal (3), texcoords (2), tangent (3) = 11 floats. float planeVerts[] = { // positions // normals // texcoords // tangent -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }; unsigned int planeIndices[] = { 0, 1, 2, 0, 2, 3 }; vertices.assign(planeVerts, planeVerts + sizeof(planeVerts) / sizeof(float)); indices.assign(planeIndices, planeIndices + sizeof(planeIndices)/sizeof(unsigned int)); } // ------------------------------------------ // Material and Object Structures // ------------------------------------------ // Extend Material to include height map. struct Material { glm::vec3 albedo; float metallic; float roughness; float ao; // Texture IDs. GLuint albedoTex; GLuint metallicTex; GLuint roughnessTex; GLuint aoTex; GLuint normalTex; GLuint heightTex; // new height map texture. // Flags indicating texture usage. bool useAlbedoMap; bool useMetallicMap; bool useRoughnessMap; bool useAOMap; bool useNormalMap; bool useHeightMap; // flag for height map // File path buffers. char albedoPath[256]; char metallicPath[256]; char roughnessPath[256]; char aoPath[256]; char normalPath[256]; char heightPath[256]; // file path for height map. Material() : albedo(0.5f, 0.0f, 0.0f), metallic(0.0f), roughness(0.5f), ao(1.0f), albedoTex(0), metallicTex(0), roughnessTex(0), aoTex(0), normalTex(0), heightTex(0), useAlbedoMap(false), useMetallicMap(false), useRoughnessMap(false), useAOMap(false), useNormalMap(false), useHeightMap(false) { strcpy(albedoPath, ""); strcpy(metallicPath, ""); strcpy(roughnessPath, ""); strcpy(aoPath, ""); strcpy(normalPath, ""); strcpy(heightPath, ""); } }; struct SphereInstance { glm::vec3 position; float rotation; // in radians. Material material; }; struct PlaneInstance { glm::vec3 position; float rotation; // around Y axis. Material material; }; // ------------------------------------------ // YAML Material Loading and Saving // ------------------------------------------ Material loadMaterialFromYAML(const std::string& filename) { Material mat; try { YAML::Node config = YAML::LoadFile(filename); if (config["albedo"]) { mat.albedo = glm::vec3(config["albedo"][0].as(), config["albedo"][1].as(), config["albedo"][2].as()); } if (config["metallic"]) mat.metallic = config["metallic"].as(); if (config["roughness"]) mat.roughness = config["roughness"].as(); if (config["ao"]) mat.ao = config["ao"].as(); if (config["albedo_texture"]) { std::string path = config["albedo_texture"].as(); strncpy(mat.albedoPath, path.c_str(), sizeof(mat.albedoPath)); GLuint tex = LoadTexture(mat.albedoPath); if (tex != 0) { mat.albedoTex = tex; mat.useAlbedoMap = true; } } if (config["metallic_texture"]) { std::string path = config["metallic_texture"].as(); strncpy(mat.metallicPath, path.c_str(), sizeof(mat.metallicPath)); GLuint tex = LoadTexture(mat.metallicPath); if (tex != 0) { mat.metallicTex = tex; mat.useMetallicMap = true; } } if (config["roughness_texture"]) { std::string path = config["roughness_texture"].as(); strncpy(mat.roughnessPath, path.c_str(), sizeof(mat.roughnessPath)); GLuint tex = LoadTexture(mat.roughnessPath); if (tex != 0) { mat.roughnessTex = tex; mat.useRoughnessMap = true; } } if (config["ao_texture"]) { std::string path = config["ao_texture"].as(); strncpy(mat.aoPath, path.c_str(), sizeof(mat.aoPath)); GLuint tex = LoadTexture(mat.aoPath); if (tex != 0) { mat.aoTex = tex; mat.useAOMap = true; } } if (config["normal_texture"]) { std::string path = config["normal_texture"].as(); strncpy(mat.normalPath, path.c_str(), sizeof(mat.normalPath)); GLuint tex = LoadTexture(mat.normalPath); if (tex != 0) { mat.normalTex = tex; mat.useNormalMap = true; } } if (config["height_texture"]) { std::string path = config["height_texture"].as(); strncpy(mat.heightPath, path.c_str(), sizeof(mat.heightPath)); GLuint tex = LoadTexture(mat.heightPath); if (tex != 0) { mat.heightTex = tex; mat.useHeightMap = true; } } } catch(const std::exception& e) { std::cerr << "Error loading YAML material from " << filename << ": " << e.what() << std::endl; } return mat; } bool saveMaterialToYAML(const std::string& filename, const Material& mat) { YAML::Emitter out; out << YAML::BeginMap; out << YAML::Key << "albedo" << YAML::Value << YAML::Flow << std::vector{mat.albedo.r, mat.albedo.g, mat.albedo.b}; out << YAML::Key << "metallic" << YAML::Value << mat.metallic; out << YAML::Key << "roughness" << YAML::Value << mat.roughness; out << YAML::Key << "ao" << YAML::Value << mat.ao; if (mat.useAlbedoMap) out << YAML::Key << "albedo_texture" << YAML::Value << std::string(mat.albedoPath); if (mat.useMetallicMap) out << YAML::Key << "metallic_texture" << YAML::Value << std::string(mat.metallicPath); if (mat.useRoughnessMap) out << YAML::Key << "roughness_texture" << YAML::Value << std::string(mat.roughnessPath); if (mat.useAOMap) out << YAML::Key << "ao_texture" << YAML::Value << std::string(mat.aoPath); if (mat.useNormalMap) out << YAML::Key << "normal_texture" << YAML::Value << std::string(mat.normalPath); if (mat.useHeightMap) out << YAML::Key << "height_texture" << YAML::Value << std::string(mat.heightPath); out << YAML::EndMap; std::ofstream fout(filename); if(!fout.is_open()){ std::cerr << "Failed to open " << filename << " for saving material." << std::endl; return false; } fout << out.c_str(); fout.close(); return true; } // ------------------------------------------ // Skybox geometry // ------------------------------------------ // Cube vertices for the skybox. float skyboxVertices[] = { // positions -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f }; int main(){ if(!glfwInit()){ std::cerr << "Failed to initialize GLFW!" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window = glfwCreateWindow(800, 600, "PBR Renderer with Height Maps, Improved Lighting & Skybox", nullptr, nullptr); if(!window){ std::cerr << "Failed to create GLFW window!" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glewExperimental = GL_TRUE; if(glewInit() != GLEW_OK){ std::cerr << "Failed to initialize GLEW!" << std::endl; return -1; } glEnable(GL_DEPTH_TEST); // Setup ImGui. IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); // Compile main shaders. GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = createProgram(vs, fs); glDeleteShader(vs); glDeleteShader(fs); // Compile skybox shaders. GLuint skybox_vs = compileShader(GL_VERTEX_SHADER, skyboxVertexShaderSource); GLuint skybox_fs = compileShader(GL_FRAGMENT_SHADER, skyboxFragmentShaderSource); GLuint skyboxShader = createProgram(skybox_vs, skybox_fs); glDeleteShader(skybox_vs); glDeleteShader(skybox_fs); // Generate sphere mesh. std::vector sphereVertices; std::vector sphereIndices; generateSphereMesh(sphereVertices, sphereIndices); GLuint sphereVAO, sphereVBO, sphereEBO; glGenVertexArrays(1, &sphereVAO); glGenBuffers(1, &sphereVBO); glGenBuffers(1, &sphereEBO); glBindVertexArray(sphereVAO); glBindBuffer(GL_ARRAY_BUFFER, sphereVBO); glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), sphereVertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereEBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sphereIndices.size() * sizeof(unsigned int), sphereIndices.data(), GL_STATIC_DRAW); // Attributes: pos (3), normal (3), texcoords (2), tangent (3) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(8 * sizeof(float))); glEnableVertexAttribArray(3); glBindVertexArray(0); // Generate plane mesh. std::vector planeVertices; std::vector planeIndices; generatePlaneMesh(planeVertices, planeIndices); GLuint planeVAO, planeVBO, planeEBO; glGenVertexArrays(1, &planeVAO); glGenBuffers(1, &planeVBO); glGenBuffers(1, &planeEBO); glBindVertexArray(planeVAO); glBindBuffer(GL_ARRAY_BUFFER, planeVBO); glBufferData(GL_ARRAY_BUFFER, planeVertices.size() * sizeof(float), planeVertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, planeEBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, planeIndices.size() * sizeof(unsigned int), planeIndices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(8 * sizeof(float))); glEnableVertexAttribArray(3); glBindVertexArray(0); // Set up skybox VAO & VBO. GLuint skyboxVAO, skyboxVBO; glGenVertexArrays(1, &skyboxVAO); glGenBuffers(1, &skyboxVBO); glBindVertexArray(skyboxVAO); glBindBuffer(GL_ARRAY_BUFFER, skyboxVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); // Load a default cubemap for the skybox. std::vector faces{ "skybox/right.jpg", "skybox/left.jpg", "skybox/top.jpg", "skybox/bottom.jpg", "skybox/front.jpg", "skybox/back.jpg" }; GLuint cubemapTexture = loadCubemap(faces); // ------------------------------------------ // Scene Data // ------------------------------------------ std::vector spheres; spheres.push_back({ glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, Material() }); std::vector planes; planes.push_back({ glm::vec3(0.0f, -1.0f, 0.0f), 0.0f, Material() }); // Global lighting parameters. glm::vec3 camPosVec(0.0f, 1.0f, 5.0f); glm::vec3 pointLightPos(0.0f, 3.0f, 3.0f); glm::vec3 pointLightColor(300.0f, 300.0f, 300.0f); // Directional light. bool useDirLight = true; glm::vec3 dirLightDir(-0.2f, -1.0f, -0.3f); glm::vec3 dirLightColor(0.8f, 0.8f, 0.8f); // Toggle for which object to render: 0 = sphere, 1 = plane. int activeObject = 0; // For material YAML loading/saving. static char yamlPathBuffer[256] = ""; // Declare a global parallax scale variable so it is available later. static float globalParallaxScale = 0.05f; // Main Loop. while(!glfwWindowShouldClose(window)){ glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // ---------------------- ImGui UI ----------------------- { ImGui::Begin("Scene Controls"); ImGui::Text("Global Lighting"); ImGui::DragFloat3("Camera Pos", glm::value_ptr(camPosVec), 0.1f); ImGui::DragFloat3("Point Light Pos", glm::value_ptr(pointLightPos), 0.1f); ImGui::ColorEdit3("Point Light Color", glm::value_ptr(pointLightColor)); ImGui::Checkbox("Use Directional Light", &useDirLight); ImGui::DragFloat3("Dir Light Dir", glm::value_ptr(dirLightDir), 0.1f); ImGui::ColorEdit3("Dir Light Color", glm::value_ptr(dirLightColor)); ImGui::Separator(); ImGui::RadioButton("Render Sphere", &activeObject, 0); ImGui::RadioButton("Render Plane", &activeObject, 1); ImGui::Separator(); // Material controls for the active object. Material* mat = nullptr; if(activeObject == 0 && !spheres.empty()) mat = &spheres[0].material; else if(activeObject == 1 && !planes.empty()) mat = &planes[0].material; if(mat){ ImGui::Text("Material Properties:"); ImGui::ColorEdit3("Albedo", glm::value_ptr(mat->albedo)); ImGui::SliderFloat("Metallic", &mat->metallic, 0.0f, 1.0f); ImGui::SliderFloat("Roughness", &mat->roughness, 0.05f, 1.0f); ImGui::SliderFloat("AO", &mat->ao, 0.0f, 1.0f); ImGui::SliderFloat("Parallax Scale", &globalParallaxScale, 0.0f, 0.2f); ImGui::InputText("Albedo Texture Path", mat->albedoPath, sizeof(mat->albedoPath)); if(ImGui::Button("Load Albedo Texture")){ GLuint tex = LoadTexture(mat->albedoPath); if(tex != 0){ mat->albedoTex = tex; mat->useAlbedoMap = true; } } ImGui::InputText("Metallic Texture Path", mat->metallicPath, sizeof(mat->metallicPath)); if(ImGui::Button("Load Metallic Texture")){ GLuint tex = LoadTexture(mat->metallicPath); if(tex != 0){ mat->metallicTex = tex; mat->useMetallicMap = true; } } ImGui::InputText("Roughness Texture Path", mat->roughnessPath, sizeof(mat->roughnessPath)); if(ImGui::Button("Load Roughness Texture")){ GLuint tex = LoadTexture(mat->roughnessPath); if(tex != 0){ mat->roughnessTex = tex; mat->useRoughnessMap = true; } } ImGui::InputText("AO Texture Path", mat->aoPath, sizeof(mat->aoPath)); if(ImGui::Button("Load AO Texture")){ GLuint tex = LoadTexture(mat->aoPath); if(tex != 0){ mat->aoTex = tex; mat->useAOMap = true; } } ImGui::InputText("Normal Texture Path", mat->normalPath, sizeof(mat->normalPath)); if(ImGui::Button("Load Normal Texture")){ GLuint tex = LoadTexture(mat->normalPath); if(tex != 0){ mat->normalTex = tex; mat->useNormalMap = true; } } ImGui::InputText("Height Texture Path", mat->heightPath, sizeof(mat->heightPath)); if(ImGui::Button("Load Height Texture")){ GLuint tex = LoadTexture(mat->heightPath); if(tex != 0){ mat->heightTex = tex; mat->useHeightMap = true; } } ImGui::InputText("YAML Material Path", yamlPathBuffer, sizeof(yamlPathBuffer)); if(ImGui::Button("Load Material from YAML")){ Material m = loadMaterialFromYAML(yamlPathBuffer); *mat = m; } if(ImGui::Button("Save Material to YAML")){ if(saveMaterialToYAML(std::string(yamlPathBuffer), *mat)) std::cout << "Material saved to " << yamlPathBuffer << std::endl; } } ImGui::Separator(); ImGui::Text("Skybox: Using default paths in code."); ImGui::End(); } // ---------------------- Rendering ---------------------- int scrWidth, scrHeight; glfwGetFramebufferSize(window, &scrWidth, &scrHeight); float aspect = scrWidth / static_cast(scrHeight); glViewport(0, 0, scrWidth, scrHeight); glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f); glm::mat4 view = glm::lookAt(camPosVec, glm::vec3(0.0f,0.0f,0.0f), glm::vec3(0,1,0)); // Render scene objects using the main shader. glUseProgram(shaderProgram); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniform3fv(glGetUniformLocation(shaderProgram, "camPos"), 1, glm::value_ptr(camPosVec)); glUniform3fv(glGetUniformLocation(shaderProgram, "pointLightPos"), 1, glm::value_ptr(pointLightPos)); glUniform3fv(glGetUniformLocation(shaderProgram, "pointLightColor"), 1, glm::value_ptr(pointLightColor)); glUniform1i(glGetUniformLocation(shaderProgram, "useDirLight"), useDirLight); glUniform3fv(glGetUniformLocation(shaderProgram, "dirLightDir"), 1, glm::value_ptr(dirLightDir)); glUniform3fv(glGetUniformLocation(shaderProgram, "dirLightColor"), 1, glm::value_ptr(dirLightColor)); glUniform1f(glGetUniformLocation(shaderProgram, "parallaxScale"), globalParallaxScale); glUniform1f(glGetUniformLocation(shaderProgram, "PI"), IM_PI); // Set environment reflection uniforms. glActiveTexture(GL_TEXTURE6); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glUniform1i(glGetUniformLocation(shaderProgram, "envMap"), 6); glUniform1f(glGetUniformLocation(shaderProgram, "envReflectionIntensity"), 0.3f); // adjust intensity as needed if(activeObject == 0 && !spheres.empty()){ // Render sphere. glm::mat4 model = glm::translate(glm::mat4(1.0f), spheres[0].position); model = glm::rotate(model, spheres[0].rotation, glm::vec3(0,1,0)); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model)); Material& m = spheres[0].material; glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(m.albedo)); glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), m.metallic); glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), m.roughness); glUniform1f(glGetUniformLocation(shaderProgram, "ao"), m.ao); glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), m.useAlbedoMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), m.useMetallicMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), m.useRoughnessMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), m.useAOMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), m.useNormalMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useHeightMap"), m.useHeightMap ? 1 : 0); if(m.useAlbedoMap){ glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m.albedoTex); glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0); } if(m.useMetallicMap){ glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m.metallicTex); glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1); } if(m.useRoughnessMap){ glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, m.roughnessTex); glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2); } if(m.useAOMap){ glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, m.aoTex); glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3); } if(m.useNormalMap){ glActiveTexture(GL_TEXTURE4); glBindTexture(GL_TEXTURE_2D, m.normalTex); glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4); } if(m.useHeightMap){ glActiveTexture(GL_TEXTURE5); glBindTexture(GL_TEXTURE_2D, m.heightTex); glUniform1i(glGetUniformLocation(shaderProgram, "heightMap"), 5); } glBindVertexArray(sphereVAO); glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); } else if(activeObject == 1 && !planes.empty()){ // Render plane. glm::mat4 model = glm::translate(glm::mat4(1.0f), planes[0].position); model = glm::rotate(model, planes[0].rotation, glm::vec3(0,1,0)); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model)); Material& m = planes[0].material; glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(m.albedo)); glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), m.metallic); glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), m.roughness); glUniform1f(glGetUniformLocation(shaderProgram, "ao"), m.ao); glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), m.useAlbedoMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), m.useMetallicMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), m.useRoughnessMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), m.useAOMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), m.useNormalMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useHeightMap"), m.useHeightMap ? 1 : 0); if(m.useAlbedoMap){ glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m.albedoTex); glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0); } if(m.useMetallicMap){ glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m.metallicTex); glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1); } if(m.useRoughnessMap){ glActiveTexture(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, m.roughnessTex); glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2); } if(m.useAOMap){ glActiveTexture(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, m.aoTex); glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3); } if(m.useNormalMap){ glActiveTexture(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, m.normalTex); glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4); } if(m.useHeightMap){ glActiveTexture(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, m.heightTex); glUniform1i(glGetUniformLocation(shaderProgram, "heightMap"), 5); } glBindVertexArray(planeVAO); glDrawElements(GL_TRIANGLES, planeIndices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); } // ---------------------- Render Skybox ---------------------- glDepthFunc(GL_LEQUAL); glUseProgram(skyboxShader); // Remove translation from the view matrix. glm::mat4 viewSky = glm::mat4(glm::mat3(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader, "view"), 1, GL_FALSE, glm::value_ptr(viewSky)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glUniform1i(glGetUniformLocation(skyboxShader, "skybox"), 0); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); } // Cleanup glDeleteVertexArrays(1, &sphereVAO); glDeleteBuffers(1, &sphereVBO); glDeleteBuffers(1, &sphereEBO); glDeleteVertexArrays(1, &planeVAO); glDeleteBuffers(1, &planeVBO); glDeleteBuffers(1, &planeEBO); glDeleteVertexArrays(1, &skyboxVAO); glDeleteBuffers(1, &skyboxVBO); glDeleteProgram(shaderProgram); glDeleteProgram(skyboxShader); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwTerminate(); return 0; }