small-projects/physicaly-based-renderer/main.cpp

1010 lines
43 KiB
C++
Raw Normal View History

2025-04-09 00:58:12 +00:00
// main.cpp
// A minimal physically based renderer with ImGui integration,
2025-04-09 01:13:17 +00:00
// 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
2025-04-09 00:58:12 +00:00
// (Adjust include paths and linker flags as needed.)
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <fstream>
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
#include <string>
2025-04-09 01:13:17 +00:00
#include <map>
2025-04-09 00:58:12 +00:00
// ImGui includes.
#include "imgui.h"
2025-04-09 01:13:17 +00:00
#include "imgui_internal.h" // For IM_PI (ImGui's internal constant)
2025-04-09 00:58:12 +00:00
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
// stb_image for texture loading.
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
2025-04-09 01:13:17 +00:00
// yaml-cpp for YAML material loading/saving.
2025-04-09 00:58:12 +00:00
#include <yaml-cpp/yaml.h>
2025-04-09 01:13:17 +00:00
// tinyobjloader for OBJ file loading.
#define TINYOBJLOADER_IMPLEMENTATION
#include "tiny_obj_loader.h"
2025-04-09 00:58:12 +00:00
// --------------------------------------------------------------------------------------
// 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;
2025-04-09 01:13:17 +00:00
// For tangent, we simply transform with the model matrix.
2025-04-09 00:58:12 +00:00
Tangent = mat3(model) * aTangent;
TexCoords = aTexCoords;
gl_Position = projection * view * vec4(WorldPos, 1.0);
}
)";
// --- Fragment Shader ---
2025-04-09 01:13:17 +00:00
// Supports texture maps, normal mapping (via TBN), and uses ImGui's IM_PI constant.
2025-04-09 00:58:12 +00:00
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) {
2025-04-09 01:13:17 +00:00
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
2025-04-09 00:58:12 +00:00
float NdotH2 = NdotH * NdotH;
2025-04-09 01:13:17 +00:00
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
2025-04-09 00:58:12 +00:00
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);
2025-04-09 01:13:17 +00:00
float ggx1 = GeometrySchlickGGX(NdotV, roughness);
float ggx2 = GeometrySchlickGGX(NdotL, roughness);
2025-04-09 00:58:12 +00:00
return ggx1 * ggx2;
}
void main(){
vec3 N = normalize(Normal);
if (useNormalMap) {
vec3 tangentNormal = texture(normalMap, TexCoords).rgb;
2025-04-09 01:13:17 +00:00
tangentNormal = tangentNormal * 2.0 - 1.0;
2025-04-09 00:58:12 +00:00
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);
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
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;
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedoValue, metallicValue);
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
vec3 L = normalize(lightPos - WorldPos);
vec3 H = normalize(V + L);
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
float NDF = DistributionGGX(N, H, roughnessValue);
float G = GeometrySmith(N, V, L, roughnessValue);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
2025-04-09 01:13:17 +00:00
vec3 numerator = NDF * G * F;
2025-04-09 00:58:12 +00:00
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001;
2025-04-09 01:13:17 +00:00
vec3 specular = numerator / denominator;
2025-04-09 00:58:12 +00:00
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallicValue;
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
float NdotL = max(dot(N, L), 0.0);
vec3 irradiance = lightColor * NdotL;
vec3 diffuse = (albedoValue / PI);
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
vec3 color = (kD * diffuse + specular) * irradiance;
vec3 ambient = vec3(0.03) * albedoValue * aoValue;
color += ambient;
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2));
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
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;
}
// --------------------------------------------------------------------------------------
2025-04-09 01:13:17 +00:00
// Data Structures for Meshes and Models
2025-04-09 00:58:12 +00:00
// --------------------------------------------------------------------------------------
struct Material {
glm::vec3 albedo;
float metallic;
float roughness;
float ao;
// Texture IDs.
GLuint albedoTex;
GLuint metallicTex;
GLuint roughnessTex;
GLuint aoTex;
GLuint normalTex;
2025-04-09 01:13:17 +00:00
// Flags indicating texture usage.
2025-04-09 00:58:12 +00:00
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;
2025-04-09 01:13:17 +00:00
float rotation; // In radians.
2025-04-09 00:58:12 +00:00
Material material;
};
2025-04-09 01:13:17 +00:00
// 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;
};
2025-04-09 00:58:12 +00:00
// --------------------------------------------------------------------------------------
2025-04-09 01:13:17 +00:00
// YAML Material Loading & Saving
2025-04-09 00:58:12 +00:00
// --------------------------------------------------------------------------------------
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<float>(),
config["albedo"][1].as<float>(),
config["albedo"][2].as<float>());
}
if (config["metallic"])
mat.metallic = config["metallic"].as<float>();
if (config["roughness"])
mat.roughness = config["roughness"].as<float>();
if (config["ao"])
mat.ao = config["ao"].as<float>();
if (config["albedo_texture"]) {
std::string path = config["albedo_texture"].as<std::string>();
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<std::string>();
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<std::string>();
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<std::string>();
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<std::string>();
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<float>{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;
}
2025-04-09 01:13:17 +00:00
// --------------------------------------------------------------------------------------
// 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<Vertex>& outVertices, std::vector<unsigned int>& outIndices) {
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> 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<unsigned int>(outVertices.size() - 1));
}
}
// Compute tangents per triangle.
std::vector<glm::vec3> 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<Vertex> vertices;
std::vector<unsigned int> 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;
}
2025-04-09 00:58:12 +00:00
// --------------------------------------------------------------------------------------
// 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);
2025-04-09 01:13:17 +00:00
GLFWwindow* window = glfwCreateWindow(800, 600, "PBR Renderer with OBJ Models", nullptr, nullptr);
2025-04-09 00:58:12 +00:00
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);
// ----------------------------------------------------------------------------------
2025-04-09 01:13:17 +00:00
// Generate the sphere mesh.
2025-04-09 00:58:12 +00:00
// ----------------------------------------------------------------------------------
std::vector<float> sphereVertices;
std::vector<unsigned int> sphereIndices;
2025-04-09 01:13:17 +00:00
// Reuse our earlier sphere generator that outputs: pos (3), normal (3), texcoord (2), tangent (3).
auto generateSphere = [&](std::vector<float>& vertices, std::vector<unsigned int>& 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<float>(x) / X_SEGMENTS;
float ySegment = static_cast<float>(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);
}
}
};
2025-04-09 00:58:12 +00:00
generateSphere(sphereVertices, sphereIndices, 64, 64);
2025-04-09 01:13:17 +00:00
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)
2025-04-09 00:58:12 +00:00
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);
// ----------------------------------------------------------------------------------
2025-04-09 01:13:17 +00:00
// Prepare scene data.
2025-04-09 00:58:12 +00:00
// ----------------------------------------------------------------------------------
std::vector<SphereInstance> spheres;
spheres.push_back({ glm::vec3(0.0f), 0.0f, Material() });
2025-04-09 01:13:17 +00:00
std::vector<Model> models; // For OBJ models.
2025-04-09 00:58:12 +00:00
// 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);
2025-04-09 01:13:17 +00:00
// For YAML loading and OBJ model loading via ImGui.
2025-04-09 00:58:12 +00:00
static char yamlPathBuffer[256] = "";
2025-04-09 01:13:17 +00:00
static char objPathBuffer[256] = "";
2025-04-09 00:58:12 +00:00
// ----------------------------------------------------------------------------------
// Main render loop.
// ----------------------------------------------------------------------------------
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// Start new ImGui frame.
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
2025-04-09 01:13:17 +00:00
// ------------------------ Global Scene Controls ------------------------
2025-04-09 00:58:12 +00:00
{
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();
2025-04-09 01:13:17 +00:00
// 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;
}
2025-04-09 00:58:12 +00:00
}
2025-04-09 01:13:17 +00:00
// 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;
}
2025-04-09 00:58:12 +00:00
}
2025-04-09 01:13:17 +00:00
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;
}
2025-04-09 00:58:12 +00:00
}
2025-04-09 01:13:17 +00:00
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;
}
2025-04-09 00:58:12 +00:00
}
2025-04-09 01:13:17 +00:00
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;
2025-04-09 00:58:12 +00:00
}
}
2025-04-09 01:13:17 +00:00
}
}
// 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);
2025-04-09 00:58:12 +00:00
}
2025-04-09 01:13:17 +00:00
}
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;
2025-04-09 00:58:12 +00:00
}
}
}
}
2025-04-09 01:13:17 +00:00
2025-04-09 00:58:12 +00:00
ImGui::End();
}
2025-04-09 01:13:17 +00:00
// ---------------------- Rendering ----------------------
2025-04-09 00:58:12 +00:00
int width, height;
glfwGetFramebufferSize(window, &width, &height);
float aspect = width / static_cast<float>(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);
2025-04-09 01:13:17 +00:00
// Render spheres.
glBindVertexArray(sphereVAO);
2025-04-09 00:58:12 +00:00
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);
}
2025-04-09 01:13:17 +00:00
glDrawElements(GL_TRIANGLES, sphereIndexCount, GL_UNSIGNED_INT, 0);
2025-04-09 00:58:12 +00:00
}
glBindVertexArray(0);
2025-04-09 01:13:17 +00:00
// 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.
2025-04-09 00:58:12 +00:00
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// ----------------------------------------------------------------------------------
// Cleanup.
// ----------------------------------------------------------------------------------
2025-04-09 01:13:17 +00:00
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);
}
2025-04-09 00:58:12 +00:00
glDeleteProgram(shaderProgram);
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwTerminate();
return 0;
}