// main.cpp // A minimal physically based renderer with ImGui integration, // normal mapping (with per-vertex tangents), YAML-based material loading/saving, // and support for loading 3D models (OBJ files) via tinyobjloader. // Uses OpenGL, GLFW, GLEW, GLM, ImGui (and IM_PI), stb_image, yaml-cpp, and tinyobjloader. // Compile (e.g. on Linux) with: // g++ main.cpp -lglfw -lGLEW -lGL -ldl -limgui -lyaml-cpp -o pbr_renderer // (Adjust include paths and linker flags 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 (ImGui's internal constant) #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 // tinyobjloader for OBJ file loading. #define TINYOBJLOADER_IMPLEMENTATION #include "tiny_obj_loader.h" // -------------------------------------------------------------------------------------- // Shader sources // -------------------------------------------------------------------------------------- // --- Vertex Shader --- // Now accepts a tangent attribute (location = 3) so we can compute a TBN matrix. const char* vertexShaderSource = R"( #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; layout (location = 3) in vec3 aTangent; 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; // For tangent, we simply transform with the model matrix. Tangent = mat3(model) * aTangent; TexCoords = aTexCoords; gl_Position = projection * view * vec4(WorldPos, 1.0); } )"; // --- Fragment Shader --- // Supports texture maps, normal mapping (via TBN), and uses ImGui's IM_PI constant. 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 parameters. uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; // Texture map usage flags and samplers. uniform bool useAlbedoMap; uniform bool useMetallicMap; uniform bool useRoughnessMap; uniform bool useAOMap; uniform bool useNormalMap; uniform sampler2D albedoMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; uniform sampler2D normalMap; // Use ImGui's internal PI constant. uniform float PI; // Light parameters. uniform vec3 lightPos; uniform vec3 lightColor; 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(){ vec3 N = normalize(Normal); if (useNormalMap) { vec3 tangentNormal = texture(normalMap, TexCoords).rgb; tangentNormal = tangentNormal * 2.0 - 1.0; vec3 T = normalize(Tangent); vec3 B = normalize(cross(N, T)); mat3 TBN = mat3(T, B, N); N = normalize(TBN * tangentNormal); } vec3 V = normalize(camPos - WorldPos); vec3 albedoValue = albedo; if (useAlbedoMap) albedoValue = texture(albedoMap, TexCoords).rgb; float metallicValue = metallic; if (useMetallicMap) metallicValue = texture(metallicMap, TexCoords).r; float roughnessValue = roughness; if (useRoughnessMap) roughnessValue = texture(roughnessMap, TexCoords).r; float aoValue = ao; if (useAOMap) aoValue = texture(aoMap, TexCoords).r; vec3 F0 = vec3(0.04); F0 = mix(F0, albedoValue, metallicValue); vec3 L = normalize(lightPos - WorldPos); vec3 H = normalize(V + L); float NDF = DistributionGGX(N, H, roughnessValue); float G = GeometrySmith(N, V, L, roughnessValue); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator; vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallicValue; float NdotL = max(dot(N, L), 0.0); vec3 irradiance = lightColor * NdotL; vec3 diffuse = (albedoValue / PI); vec3 color = (kD * diffuse + specular) * irradiance; vec3 ambient = vec3(0.03) * albedoValue * aoValue; color += ambient; color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0); } )"; // -------------------------------------------------------------------------------------- // Helper functions: Shader compilation and linking. // -------------------------------------------------------------------------------------- GLuint compileShader(GLenum shaderType, const char* source) { GLuint shader = glCreateShader(shaderType); 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 compilation failed:\n" << infoLog << std::endl; } return shader; } GLuint createProgram(GLuint vertexShader, GLuint fragmentShader) { GLuint program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); glLinkProgram(program); GLint success; glGetProgramiv(program, GL_LINK_STATUS, &success); if (!success){ char infoLog[512]; glGetProgramInfoLog(program, 512, nullptr, infoLog); std::cerr << "Program linking failed:\n" << infoLog << std::endl; } return program; } // -------------------------------------------------------------------------------------- // Texture loading helper using stb_image. // -------------------------------------------------------------------------------------- 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::cout << "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 textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); // 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 textureID; } // -------------------------------------------------------------------------------------- // Data Structures for Meshes and Models // -------------------------------------------------------------------------------------- struct Material { glm::vec3 albedo; float metallic; float roughness; float ao; // Texture IDs. GLuint albedoTex; GLuint metallicTex; GLuint roughnessTex; GLuint aoTex; GLuint normalTex; // Flags indicating texture usage. bool useAlbedoMap; bool useMetallicMap; bool useRoughnessMap; bool useAOMap; bool useNormalMap; // File path buffers for manual texture loading via ImGui. char albedoPath[256]; char metallicPath[256]; char roughnessPath[256]; char aoPath[256]; char normalPath[256]; 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), useAlbedoMap(false), useMetallicMap(false), useRoughnessMap(false), useAOMap(false), useNormalMap(false) { strcpy(albedoPath, ""); strcpy(metallicPath, ""); strcpy(roughnessPath, ""); strcpy(aoPath, ""); strcpy(normalPath, ""); } }; struct SphereInstance { glm::vec3 position; float rotation; // In radians. Material material; }; // For OBJ models, we define a Vertex structure. struct Vertex { glm::vec3 position; glm::vec3 normal; glm::vec2 texcoord; glm::vec3 tangent; }; struct Model { GLuint VAO; GLuint VBO; GLuint EBO; unsigned int indexCount; // Transformation. glm::vec3 position; float rotation; // Around Y axis. Material material; // Filename of the loaded model (for reference). std::string filename; }; // -------------------------------------------------------------------------------------- // YAML Material Loading & 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; } } } catch(const std::exception& e) { std::cerr << "Error loading YAML material from file " << 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); out << YAML::EndMap; std::ofstream fout(filename); if (!fout.is_open()){ std::cerr << "Failed to open file for saving material: " << filename << std::endl; return false; } fout << out.c_str(); fout.close(); return true; } // -------------------------------------------------------------------------------------- // OBJ Model Loading using tinyobjloader. // -------------------------------------------------------------------------------------- // We define a Vertex structure (see above). This function loads an OBJ file // and outputs vertices and indices in the expected format (11 floats per vertex: // position[3], normal[3], texcoord[2], tangent[3]). Tangents are computed after loading. bool loadOBJModel(const std::string& filename, std::vector& outVertices, std::vector& outIndices) { tinyobj::attrib_t attrib; std::vector shapes; std::vector objMaterials; std::string warn, err; std::cout << "tinyobjloader Loading: " << filename << std::endl; bool ret = tinyobj::LoadObj(&attrib, &shapes, &objMaterials, &warn, &err, filename.c_str()); if (!warn.empty()) std::cout << "tinyobjloader warning: " << warn << std::endl; if (!err.empty()) { std::cerr << "tinyobjloader error: " << err << std::endl; return false; } if (!ret) { std::cerr << "Failed to load/parse OBJ file: " << filename << std::endl; return false; } // For simplicity, we push every vertex (no deduplication). for (size_t s = 0; s < shapes.size(); s++) { for (size_t f = 0; f < shapes[s].mesh.indices.size(); f++) { tinyobj::index_t idx = shapes[s].mesh.indices[f]; Vertex vertex; vertex.position = glm::vec3( attrib.vertices[3 * idx.vertex_index + 0], attrib.vertices[3 * idx.vertex_index + 1], attrib.vertices[3 * idx.vertex_index + 2] ); if (idx.normal_index >= 0) { vertex.normal = glm::vec3( attrib.normals[3 * idx.normal_index + 0], attrib.normals[3 * idx.normal_index + 1], attrib.normals[3 * idx.normal_index + 2] ); } else { vertex.normal = glm::vec3(0.0f, 1.0f, 0.0f); } if (idx.texcoord_index >= 0) { vertex.texcoord = glm::vec2( attrib.texcoords[2 * idx.texcoord_index + 0], attrib.texcoords[2 * idx.texcoord_index + 1] ); } else { vertex.texcoord = glm::vec2(0.0f, 0.0f); } // Initialize tangent to zero; we will compute later. vertex.tangent = glm::vec3(0.0f); outVertices.push_back(vertex); outIndices.push_back(static_cast(outVertices.size() - 1)); } } // Compute tangents per triangle. std::vector tanAccum(outVertices.size(), glm::vec3(0.0f)); for (size_t i = 0; i < outIndices.size(); i += 3) { unsigned int i0 = outIndices[i + 0]; unsigned int i1 = outIndices[i + 1]; unsigned int i2 = outIndices[i + 2]; const glm::vec3& p0 = outVertices[i0].position; const glm::vec3& p1 = outVertices[i1].position; const glm::vec3& p2 = outVertices[i2].position; const glm::vec2& uv0 = outVertices[i0].texcoord; const glm::vec2& uv1 = outVertices[i1].texcoord; const glm::vec2& uv2 = outVertices[i2].texcoord; glm::vec3 deltaPos1 = p1 - p0; glm::vec3 deltaPos2 = p2 - p0; glm::vec2 deltaUV1 = uv1 - uv0; glm::vec2 deltaUV2 = uv2 - uv0; float r = 1.0f; float denom = (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x); if (fabs(denom) > 1e-6f) r = 1.0f / denom; glm::vec3 tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r; tanAccum[i0] += tangent; tanAccum[i1] += tangent; tanAccum[i2] += tangent; } // Normalize and store tangents. for (size_t i = 0; i < outVertices.size(); i++) { outVertices[i].tangent = glm::normalize(tanAccum[i]); } return true; } // Create a Model from an OBJ file by loading its data and generating OpenGL buffers. Model loadModelFromOBJ(const std::string& filename) { Model model; model.filename = filename; std::vector vertices; std::vector indices; if (!loadOBJModel(filename, vertices, indices)) { std::cerr << "Failed to load model: " << filename << std::endl; model.indexCount = 0; return model; } model.indexCount = indices.size(); // Generate buffers. glGenVertexArrays(1, &model.VAO); glGenBuffers(1, &model.VBO); glGenBuffers(1, &model.EBO); glBindVertexArray(model.VAO); glBindBuffer(GL_ARRAY_BUFFER, model.VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, model.EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW); // Vertex attributes: position (location 0), normal (1), texcoord (2), tangent (3). glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position)); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal)); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texcoord)); glEnableVertexAttribArray(2); glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, tangent)); glEnableVertexAttribArray(3); glBindVertexArray(0); // Set default transformation. model.position = glm::vec3(0.0f); model.rotation = 0.0f; // Default material. model.material = Material(); return model; } // -------------------------------------------------------------------------------------- // Main // -------------------------------------------------------------------------------------- int main(){ // Initialize GLFW. 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 OBJ Models", nullptr, nullptr); if (!window){ std::cerr << "Failed to create GLFW window!" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); // Initialize GLEW. glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK){ std::cerr << "Failed to initialize GLEW!" << std::endl; return -1; } glEnable(GL_DEPTH_TEST); // ---------------------------------------------------------------------------------- // Setup ImGui context. // ---------------------------------------------------------------------------------- IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); // ---------------------------------------------------------------------------------- // Compile shaders and create shader program. // ---------------------------------------------------------------------------------- GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = createProgram(vertexShader, fragmentShader); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // ---------------------------------------------------------------------------------- // Generate the sphere mesh. // ---------------------------------------------------------------------------------- std::vector sphereVertices; std::vector sphereIndices; // Reuse our earlier sphere generator that outputs: pos (3), normal (3), texcoord (2), tangent (3). auto generateSphere = [&](std::vector& 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 = static_cast(x) / X_SEGMENTS; float ySegment = static_cast(y) / 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. vertices.push_back(xPos); vertices.push_back(yPos); vertices.push_back(zPos); // Texcoords. vertices.push_back(xSegment); vertices.push_back(ySegment); // Tangent (simple approximation). 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); } } }; generateSphere(sphereVertices, sphereIndices, 64, 64); unsigned int sphereIndexCount = sphereIndices.size(); 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); // Layout: 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); // ---------------------------------------------------------------------------------- // Prepare scene data. // ---------------------------------------------------------------------------------- std::vector spheres; spheres.push_back({ glm::vec3(0.0f), 0.0f, Material() }); std::vector models; // For OBJ models. // Global scene/light parameters. glm::vec3 camPos(0.0f, 0.0f, 5.0f); glm::vec3 lightPos(0.0f, 0.0f, 10.0f); glm::vec3 lightColor(300.0f, 300.0f, 300.0f); // For YAML loading and OBJ model loading via ImGui. static char yamlPathBuffer[256] = ""; static char objPathBuffer[256] = ""; // ---------------------------------------------------------------------------------- // Main render loop. // ---------------------------------------------------------------------------------- while (!glfwWindowShouldClose(window)) { glfwPollEvents(); // Start new ImGui frame. ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // ------------------------ Global Scene Controls ------------------------ { ImGui::Begin("Scene Controls"); // Global light controls. ImGui::Text("Light Controls"); ImGui::DragFloat3("Light Position", glm::value_ptr(lightPos), 0.1f); ImGui::ColorEdit3("Light Color", glm::value_ptr(lightColor)); ImGui::Separator(); // Sphere controls. if (ImGui::CollapsingHeader("Spheres")) { if (ImGui::Button("Add Sphere")) { spheres.push_back({ glm::vec3(0.0f), 0.0f, Material() }); } ImGui::Separator(); for (size_t i = 0; i < spheres.size(); i++) { std::string header = "Sphere " + std::to_string(i); if (ImGui::CollapsingHeader(header.c_str())) { ImGui::DragFloat3(("Position##S" + std::to_string(i)).c_str(), glm::value_ptr(spheres[i].position), 0.1f); ImGui::DragFloat(("Rotation (radians)##S" + std::to_string(i)).c_str(), &spheres[i].rotation, 0.01f); ImGui::ColorEdit3(("Albedo##S" + std::to_string(i)).c_str(), glm::value_ptr(spheres[i].material.albedo)); ImGui::SliderFloat(("Metallic##S" + std::to_string(i)).c_str(), &spheres[i].material.metallic, 0.0f, 1.0f); ImGui::SliderFloat(("Roughness##S" + std::to_string(i)).c_str(), &spheres[i].material.roughness, 0.05f, 1.0f); ImGui::SliderFloat(("AO##S" + std::to_string(i)).c_str(), &spheres[i].material.ao, 0.0f, 1.0f); // Texture controls... ImGui::InputText(("Albedo Texture Path##S" + std::to_string(i)).c_str(), spheres[i].material.albedoPath, sizeof(spheres[i].material.albedoPath)); if (ImGui::Button(("Load Albedo Texture##S" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(spheres[i].material.albedoPath); if (tex != 0) { spheres[i].material.albedoTex = tex; spheres[i].material.useAlbedoMap = true; } } // Similar input/buttons for Metallic, Roughness, AO, and Normal... ImGui::InputText(("Metallic Texture Path##S" + std::to_string(i)).c_str(), spheres[i].material.metallicPath, sizeof(spheres[i].material.metallicPath)); if (ImGui::Button(("Load Metallic Texture##S" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(spheres[i].material.metallicPath); if (tex != 0) { spheres[i].material.metallicTex = tex; spheres[i].material.useMetallicMap = true; } } ImGui::InputText(("Roughness Texture Path##S" + std::to_string(i)).c_str(), spheres[i].material.roughnessPath, sizeof(spheres[i].material.roughnessPath)); if (ImGui::Button(("Load Roughness Texture##S" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(spheres[i].material.roughnessPath); if (tex != 0) { spheres[i].material.roughnessTex = tex; spheres[i].material.useRoughnessMap = true; } } ImGui::InputText(("AO Texture Path##S" + std::to_string(i)).c_str(), spheres[i].material.aoPath, sizeof(spheres[i].material.aoPath)); if (ImGui::Button(("Load AO Texture##S" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(spheres[i].material.aoPath); if (tex != 0) { spheres[i].material.aoTex = tex; spheres[i].material.useAOMap = true; } } ImGui::InputText(("Normal Texture Path##S" + std::to_string(i)).c_str(), spheres[i].material.normalPath, sizeof(spheres[i].material.normalPath)); if (ImGui::Button(("Load Normal Texture##S" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(spheres[i].material.normalPath); if (tex != 0) { spheres[i].material.normalTex = tex; spheres[i].material.useNormalMap = true; } } static char sphereYAMLPath[256] = ""; ImGui::InputText(("YAML Material Path##S" + std::to_string(i)).c_str(), sphereYAMLPath, sizeof(sphereYAMLPath)); if (ImGui::Button(("Load Material from YAML##S" + std::to_string(i)).c_str())) { Material newMat = loadMaterialFromYAML(sphereYAMLPath); spheres[i].material = newMat; } static char saveYAMLPath[256] = ""; ImGui::InputText(("Save YAML Path##S" + std::to_string(i)).c_str(), saveYAMLPath, sizeof(saveYAMLPath)); if (ImGui::Button(("Save Material to YAML##S" + std::to_string(i)).c_str())) { if (saveMaterialToYAML(std::string(saveYAMLPath), spheres[i].material)) { std::cout << "Saved material for sphere " << i << " to " << saveYAMLPath << std::endl; } } if (ImGui::Button(("Remove Sphere##S" + std::to_string(i)).c_str())) { spheres.erase(spheres.begin() + i); break; } } } } // Model (OBJ) controls. if (ImGui::CollapsingHeader("3D Models (OBJ)")) { ImGui::InputText("OBJ Model Path", objPathBuffer, sizeof(objPathBuffer)); if (ImGui::Button("Load OBJ Model")) { Model m = loadModelFromOBJ(std::string(objPathBuffer)); if(m.indexCount > 0) { models.push_back(m); } } ImGui::Separator(); for (size_t i = 0; i < models.size(); i++) { std::string header = "Model " + std::to_string(i) + " [" + models[i].filename + "]"; if (ImGui::CollapsingHeader(header.c_str())) { ImGui::DragFloat3(("Position##M" + std::to_string(i)).c_str(), glm::value_ptr(models[i].position), 0.1f); ImGui::DragFloat(("Rotation (radians)##M" + std::to_string(i)).c_str(), &models[i].rotation, 0.01f); ImGui::ColorEdit3(("Albedo##M" + std::to_string(i)).c_str(), glm::value_ptr(models[i].material.albedo)); ImGui::SliderFloat(("Metallic##M" + std::to_string(i)).c_str(), &models[i].material.metallic, 0.0f, 1.0f); ImGui::SliderFloat(("Roughness##M" + std::to_string(i)).c_str(), &models[i].material.roughness, 0.05f, 1.0f); ImGui::SliderFloat(("AO##M" + std::to_string(i)).c_str(), &models[i].material.ao, 0.0f, 1.0f); // Similar texture controls as for spheres. ImGui::InputText(("Albedo Texture Path##M" + std::to_string(i)).c_str(), models[i].material.albedoPath, sizeof(models[i].material.albedoPath)); if (ImGui::Button(("Load Albedo Texture##M" + std::to_string(i)).c_str())) { GLuint tex = LoadTexture(models[i].material.albedoPath); if (tex != 0) { models[i].material.albedoTex = tex; models[i].material.useAlbedoMap = true; } } // ... (Repeat for other maps: Metallic, Roughness, AO, Normal) if (ImGui::Button(("Remove Model##M" + std::to_string(i)).c_str())) { // Delete GL buffers. glDeleteVertexArrays(1, &models[i].VAO); glDeleteBuffers(1, &models[i].VBO); glDeleteBuffers(1, &models[i].EBO); models.erase(models.begin() + i); break; } } } } ImGui::End(); } // ---------------------- Rendering ---------------------- int width, height; glfwGetFramebufferSize(window, &width, &height); float aspect = width / static_cast(height); glViewport(0, 0, width, height); 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(camPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0)); 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(camPos)); glUniform3fv(glGetUniformLocation(shaderProgram, "lightPos"), 1, glm::value_ptr(lightPos)); glUniform3fv(glGetUniformLocation(shaderProgram, "lightColor"), 1, glm::value_ptr(lightColor)); glUniform1f(glGetUniformLocation(shaderProgram, "PI"), IM_PI); // Render spheres. glBindVertexArray(sphereVAO); for (const auto& sphere : spheres) { glm::mat4 model = glm::translate(glm::mat4(1.0f), sphere.position); model = glm::rotate(model, sphere.rotation, glm::vec3(0.0f, 1.0f, 0.0f)); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(sphere.material.albedo)); glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), sphere.material.metallic); glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), sphere.material.roughness); glUniform1f(glGetUniformLocation(shaderProgram, "ao"), sphere.material.ao); glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), sphere.material.useAlbedoMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), sphere.material.useMetallicMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), sphere.material.useRoughnessMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), sphere.material.useAOMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), sphere.material.useNormalMap ? 1 : 0); if (sphere.material.useAlbedoMap) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, sphere.material.albedoTex); glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0); } if (sphere.material.useMetallicMap) { glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, sphere.material.metallicTex); glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1); } if (sphere.material.useRoughnessMap) { glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, sphere.material.roughnessTex); glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2); } if (sphere.material.useAOMap) { glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, sphere.material.aoTex); glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3); } if (sphere.material.useNormalMap) { glActiveTexture(GL_TEXTURE4); glBindTexture(GL_TEXTURE_2D, sphere.material.normalTex); glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4); } glDrawElements(GL_TRIANGLES, sphereIndexCount, GL_UNSIGNED_INT, 0); } glBindVertexArray(0); // Render OBJ models. for (const auto& m : models) { glm::mat4 model = glm::translate(glm::mat4(1.0f), m.position); model = glm::rotate(model, m.rotation, glm::vec3(0.0f, 1.0f, 0.0f)); glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(m.material.albedo)); glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), m.material.metallic); glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), m.material.roughness); glUniform1f(glGetUniformLocation(shaderProgram, "ao"), m.material.ao); glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), m.material.useAlbedoMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), m.material.useMetallicMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), m.material.useRoughnessMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), m.material.useAOMap ? 1 : 0); glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), m.material.useNormalMap ? 1 : 0); if (m.material.useAlbedoMap) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m.material.albedoTex); glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0); } if (m.material.useMetallicMap) { glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m.material.metallicTex); glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1); } if (m.material.useRoughnessMap) { glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, m.material.roughnessTex); glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2); } if (m.material.useAOMap) { glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, m.material.aoTex); glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3); } if (m.material.useNormalMap) { glActiveTexture(GL_TEXTURE4); glBindTexture(GL_TEXTURE_2D, m.material.normalTex); glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4); } glBindVertexArray(m.VAO); glDrawElements(GL_TRIANGLES, m.indexCount, GL_UNSIGNED_INT, 0); glBindVertexArray(0); } // Render ImGui. ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); } // ---------------------------------------------------------------------------------- // Cleanup. // ---------------------------------------------------------------------------------- glDeleteVertexArrays(1, &sphereVAO); glDeleteBuffers(1, &sphereVBO); glDeleteBuffers(1, &sphereEBO); for (auto& m : models) { glDeleteVertexArrays(1, &m.VAO); glDeleteBuffers(1, &m.VBO); glDeleteBuffers(1, &m.EBO); } glDeleteProgram(shaderProgram); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwTerminate(); return 0; }