[Improve file explorer & implement Assimp with lighting/material support]

- Reworked file explorer UI for clearer hierarchy and usability
- Actually wired up Assimp properly for model loading
- Added basic lighting support
- Added independent material support per mesh
(oh my gosh this took 4 days to actually get working, let alone not crashing 😭)
This commit is contained in:
Anemunt
2025-12-09 03:09:37 -05:00
parent 9f7007f496
commit 9adb1ff2f5
14 changed files with 1725 additions and 200 deletions

View File

@@ -6,41 +6,101 @@ in vec3 Normal;
in vec2 TexCoord; in vec2 TexCoord;
uniform sampler2D texture1; uniform sampler2D texture1;
uniform sampler2D texture2; uniform sampler2D overlayTex;
uniform sampler2D normalMap;
uniform float mixAmount = 0.2; uniform float mixAmount = 0.2;
uniform bool hasOverlay = false;
uniform bool hasNormalMap = false;
uniform vec3 lightPos;
uniform vec3 viewPos; uniform vec3 viewPos;
uniform vec3 lightColor = vec3(1.0); uniform vec3 materialColor = vec3(1.0);
uniform float ambientStrength = 0.2; uniform float ambientStrength = 0.2;
uniform float specularStrength = 0.5; uniform float specularStrength = 0.5;
uniform float shininess = 32.0; uniform float shininess = 32.0;
const int MAX_LIGHTS = 10;
uniform int lightCount = 0; // up to MAX_LIGHTS
// type: 0 dir, 1 point, 2 spot
uniform int lightTypeArr[MAX_LIGHTS];
uniform vec3 lightDirArr[MAX_LIGHTS];
uniform vec3 lightPosArr[MAX_LIGHTS];
uniform vec3 lightColorArr[MAX_LIGHTS];
uniform float lightIntensityArr[MAX_LIGHTS];
uniform float lightRangeArr[MAX_LIGHTS];
uniform float lightInnerCosArr[MAX_LIGHTS];
uniform float lightOuterCosArr[MAX_LIGHTS];
// Single directional light controlled by hierarchy (fallback if none set)
uniform vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
uniform vec3 lightColor = vec3(1.0);
uniform float lightIntensity = 1.0;
void main() void main()
{ {
vec3 norm = normalize(Normal); vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 viewDir = normalize(viewPos - FragPos); vec3 viewDir = normalize(viewPos - FragPos);
// Ambient
vec3 ambient = ambientStrength * lightColor;
// Diffuse
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// Specular (Blinn-Phong)
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(norm, halfwayDir), 0.0), shininess);
vec3 specular = specularStrength * spec * lightColor;
// Texture mixing (corrected) // Texture mixing (corrected)
vec4 tex1 = texture(texture1, TexCoord); vec4 tex1 = texture(texture1, TexCoord);
vec4 tex2 = texture(texture2, TexCoord); vec3 texColor = tex1.rgb;
vec4 mixedTex = mix(tex1, tex2, mixAmount); if (hasOverlay) {
vec3 texColor = mixedTex.rgb; vec3 overlay = texture(overlayTex, TexCoord).rgb;
texColor = mix(texColor, overlay, mixAmount);
}
vec3 baseColor = texColor * materialColor;
vec3 result = (ambient + diffuse + specular) * texColor; // Normal map (tangent-space)
FragColor = vec4(result, mixedTex.a); // Preserve alpha if needed if (hasNormalMap) {
vec3 mapN = texture(normalMap, TexCoord).xyz * 2.0 - 1.0;
vec3 dp1 = dFdx(FragPos);
vec3 dp2 = dFdy(FragPos);
vec2 duv1 = dFdx(TexCoord);
vec2 duv2 = dFdy(TexCoord);
vec3 tangent = normalize(dp1 * duv2.y - dp2 * duv1.y);
vec3 bitangent = normalize(-dp1 * duv2.x + dp2 * duv1.x);
mat3 TBN = mat3(tangent, bitangent, normalize(Normal));
norm = normalize(TBN * mapN);
}
vec3 ambient = ambientStrength * baseColor;
vec3 lighting = ambient;
int count = min(lightCount, MAX_LIGHTS);
for (int i = 0; i < count; ++i) {
int ltype = lightTypeArr[i];
vec3 ldir = (ltype == 0) ? -normalize(lightDirArr[i]) : lightPosArr[i] - FragPos;
float dist = length(ldir);
vec3 lDirN = normalize(ldir);
float attenuation = 1.0;
if (ltype != 0) {
float range = lightRangeArr[i];
if (range > 0.0 && dist > range) continue;
if (range > 0.0) {
float falloff = clamp(1.0 - (dist / range), 0.0, 1.0);
attenuation = falloff * falloff;
}
}
float intensity = lightIntensityArr[i];
if (intensity <= 0.0) continue;
float diff = max(dot(norm, lDirN), 0.0);
vec3 diffuse = diff * lightColorArr[i] * intensity;
vec3 halfwayDir = normalize(lDirN + viewDir);
float spec = pow(max(dot(norm, halfwayDir), 0.0), shininess);
vec3 specular = specularStrength * spec * lightColorArr[i] * intensity;
if (ltype == 2) {
float cosTheta = dot(-lDirN, normalize(lightDirArr[i]));
float spotAtten = smoothstep(lightOuterCosArr[i], lightInnerCosArr[i], cosTheta);
attenuation *= spotAtten;
}
lighting += attenuation * (diffuse + specular) * baseColor;
}
float alpha = tex1.a;
FragColor = vec4(lighting, alpha); // Preserve alpha if needed
} }

View File

@@ -16,5 +16,6 @@ void main()
FragPos = vec3(model * vec4(aPos, 1.0)); FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal; Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
gl_Position = projection * view * vec4(FragPos, 1.0); gl_Position = projection * view * vec4(FragPos, 1.0);
} }

View File

@@ -9,9 +9,11 @@ finish() {
local duration=$((end_time - start_time)) local duration=$((end_time - start_time))
if [ $exit_code -eq 0 ]; then if [ $exit_code -eq 0 ]; then
echo -e "================================\n Modularity - Native Linux Build Complete\n================================"
echo -e "[Complete]: Your Modularity Build Completed in ${duration}s!\nThe build should be located under Modularity within another folder called 'Build'" echo -e "[Complete]: Your Modularity Build Completed in ${duration}s!\nThe build should be located under Modularity within another folder called 'Build'"
else else
echo "[!]: Your Modularity Build Failed after ${duration}s (exit code ${exit_code})." echo -e "================================\n Modularity - Native Linux Build Failed\n================================"
echo "[Failed]: Your Modularity Build Failed after ${duration}s (exit code ${exit_code})."
fi fi
exit $exit_code exit $exit_code

View File

@@ -16,6 +16,7 @@ public:
void setBool(const std::string &name, bool value) const; void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const; void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const; void setFloat(const std::string &name, float value) const;
void setVec2(const std::string &name, const glm::vec2 &value) const;
void setVec3(const std::string &name, const glm::vec3 &value) const; void setVec3(const std::string &name, const glm::vec3 &value) const;
void setMat4(const std::string &name, const glm::mat4 &mat) const; void setMat4(const std::string &name, const glm::mat4 &mat) const;

View File

@@ -101,6 +101,9 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
return FileCategory::Model; return FileCategory::Model;
} }
// Material files
if (ext == ".mat") return FileCategory::Material;
// Texture files // Texture files
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" ||
ext == ".tga" || ext == ".dds" || ext == ".hdr") { ext == ".tga" || ext == ".dds" || ext == ".hdr") {
@@ -139,6 +142,7 @@ const char* FileBrowser::getFileIcon(const fs::directory_entry& entry) const {
case FileCategory::Folder: return "folder"; case FileCategory::Folder: return "folder";
case FileCategory::Scene: return "scene"; case FileCategory::Scene: return "scene";
case FileCategory::Model: return "model"; case FileCategory::Model: return "model";
case FileCategory::Material: return "material";
case FileCategory::Texture: return "image"; case FileCategory::Texture: return "image";
case FileCategory::Shader: return "shader"; case FileCategory::Shader: return "shader";
case FileCategory::Script: return "code"; case FileCategory::Script: return "code";

View File

@@ -11,6 +11,7 @@ enum class FileCategory {
Folder, Folder,
Scene, Scene,
Model, Model,
Material,
Texture, Texture,
Shader, Shader,
Script, Script,

View File

@@ -1,5 +1,7 @@
#include "Engine.h" #include "Engine.h"
#include "ModelLoader.h"
#include <iostream> #include <iostream>
#include <fstream>
void window_size_callback(GLFWwindow* window, int width, int height) { void window_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height); glViewport(0, 0, width, height);
@@ -109,13 +111,8 @@ void Engine::run() {
if (aspect <= 0.0f) aspect = 1.0f; if (aspect <= 0.0f) aspect = 1.0f;
glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE); glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE);
renderer.beginRender(view, proj); renderer.beginRender(view, proj, camera.position);
renderer.renderScene(camera, sceneObjects);
for (const auto& obj : sceneObjects) {
renderer.renderObject(obj);
}
renderer.renderSkybox(view, proj);
renderer.endRender(); renderer.endRender();
} }
@@ -229,6 +226,108 @@ void Engine::importOBJToScene(const std::string& filepath, const std::string& ob
} }
} }
void Engine::importModelToScene(const std::string& filepath, const std::string& objectName) {
auto& modelLoader = getModelLoader();
ModelLoadResult result = modelLoader.loadModel(filepath);
if (!result.success) {
addConsoleMessage("Failed to load model: " + result.errorMessage, ConsoleMessageType::Error);
return;
}
int id = nextObjectId++;
std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName;
SceneObject obj(name, ObjectType::Model, id);
obj.meshPath = filepath;
obj.meshId = result.meshIndex;
sceneObjects.push_back(obj);
selectedObjectId = id;
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
addConsoleMessage(
"Imported model: " + name + " (" +
std::to_string(result.vertexCount) + " verts, " +
std::to_string(result.faceCount) + " faces, " +
std::to_string(result.meshCount) + " meshes)",
ConsoleMessageType::Success
);
}
void Engine::loadMaterialFromFile(SceneObject& obj) {
if (obj.materialPath.empty()) return;
try {
std::ifstream f(obj.materialPath);
if (!f.is_open()) {
addConsoleMessage("Failed to open material: " + obj.materialPath, ConsoleMessageType::Error);
return;
}
std::string line;
while (std::getline(f, line)) {
line.erase(0, line.find_first_not_of(" \t\r\n"));
if (line.empty() || line[0] == '#') continue;
auto pos = line.find('=');
if (pos == std::string::npos) continue;
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 1);
if (key == "color") {
sscanf(val.c_str(), "%f,%f,%f", &obj.material.color.r, &obj.material.color.g, &obj.material.color.b);
} else if (key == "ambient") {
obj.material.ambientStrength = std::stof(val);
} else if (key == "specular") {
obj.material.specularStrength = std::stof(val);
} else if (key == "shininess") {
obj.material.shininess = std::stof(val);
} else if (key == "textureMix") {
obj.material.textureMix = std::stof(val);
} else if (key == "albedo") {
obj.albedoTexturePath = val;
} else if (key == "overlay") {
obj.overlayTexturePath = val;
} else if (key == "normal") {
obj.normalMapPath = val;
} else if (key == "useOverlay") {
obj.useOverlay = std::stoi(val) != 0;
}
}
addConsoleMessage("Applied material: " + obj.materialPath, ConsoleMessageType::Success);
projectManager.currentProject.hasUnsavedChanges = true;
} catch (...) {
addConsoleMessage("Failed to read material: " + obj.materialPath, ConsoleMessageType::Error);
}
}
void Engine::saveMaterialToFile(const SceneObject& obj) {
if (obj.materialPath.empty()) {
addConsoleMessage("Material path is empty", ConsoleMessageType::Warning);
return;
}
try {
std::ofstream f(obj.materialPath);
if (!f.is_open()) {
addConsoleMessage("Failed to open material for writing: " + obj.materialPath, ConsoleMessageType::Error);
return;
}
f << "# Material\n";
f << "color=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n";
f << "ambient=" << obj.material.ambientStrength << "\n";
f << "specular=" << obj.material.specularStrength << "\n";
f << "shininess=" << obj.material.shininess << "\n";
f << "textureMix=" << obj.material.textureMix << "\n";
f << "useOverlay=" << (obj.useOverlay ? 1 : 0) << "\n";
f << "albedo=" << obj.albedoTexturePath << "\n";
f << "overlay=" << obj.overlayTexturePath << "\n";
f << "normal=" << obj.normalMapPath << "\n";
addConsoleMessage("Saved material: " + obj.materialPath, ConsoleMessageType::Success);
} catch (...) {
addConsoleMessage("Failed to save material: " + obj.materialPath, ConsoleMessageType::Error);
}
}
void Engine::handleKeyboardShortcuts() { void Engine::handleKeyboardShortcuts() {
static bool f11Pressed = false; static bool f11Pressed = false;
if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_PRESS && !f11Pressed) { if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_PRESS && !f11Pressed) {
@@ -375,6 +474,12 @@ void Engine::loadScene(const std::string& sceneName) {
projectManager.currentProject.hasUnsavedChanges = false; projectManager.currentProject.hasUnsavedChanges = false;
projectManager.currentProject.saveProjectFile(); projectManager.currentProject.saveProjectFile();
selectedObjectId = -1; selectedObjectId = -1;
bool hasDirLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) {
return o.type == ObjectType::DirectionalLight;
});
if (!hasDirLight) {
addObject(ObjectType::DirectionalLight, "Directional Light");
}
addConsoleMessage("Loaded scene: " + sceneName, ConsoleMessageType::Success); addConsoleMessage("Loaded scene: " + sceneName, ConsoleMessageType::Success);
} else { } else {
addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error); addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error);
@@ -396,6 +501,7 @@ void Engine::createNewScene(const std::string& sceneName) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
addObject(ObjectType::Cube, "Cube"); addObject(ObjectType::Cube, "Cube");
addObject(ObjectType::DirectionalLight, "Directional Light");
saveCurrentScene(); saveCurrentScene();
addConsoleMessage("Created new scene: " + sceneName, ConsoleMessageType::Success); addConsoleMessage("Created new scene: " + sceneName, ConsoleMessageType::Success);
@@ -405,6 +511,21 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
int id = nextObjectId++; int id = nextObjectId++;
std::string name = baseName + " " + std::to_string(id); std::string name = baseName + " " + std::to_string(id);
sceneObjects.push_back(SceneObject(name, type, id)); sceneObjects.push_back(SceneObject(name, type, id));
// Light defaults
if (type == ObjectType::PointLight) {
sceneObjects.back().light.type = LightType::Point;
sceneObjects.back().light.range = 12.0f;
sceneObjects.back().light.intensity = 2.0f;
} else if (type == ObjectType::SpotLight) {
sceneObjects.back().light.type = LightType::Spot;
sceneObjects.back().light.range = 15.0f;
sceneObjects.back().light.intensity = 2.5f;
} else if (type == ObjectType::AreaLight) {
sceneObjects.back().light.type = LightType::Area;
sceneObjects.back().light.range = 10.0f;
sceneObjects.back().light.intensity = 3.0f;
sceneObjects.back().light.size = glm::vec2(2.0f, 2.0f);
}
selectedObjectId = id; selectedObjectId = id;
if (projectManager.currentProject.isLoaded) { if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
@@ -424,6 +545,13 @@ void Engine::duplicateSelected() {
newObj.scale = it->scale; newObj.scale = it->scale;
newObj.meshPath = it->meshPath; newObj.meshPath = it->meshPath;
newObj.meshId = it->meshId; newObj.meshId = it->meshId;
newObj.material = it->material;
newObj.materialPath = it->materialPath;
newObj.albedoTexturePath = it->albedoTexturePath;
newObj.overlayTexturePath = it->overlayTexturePath;
newObj.normalMapPath = it->normalMapPath;
newObj.useOverlay = it->useOverlay;
newObj.light = it->light;
sceneObjects.push_back(newObj); sceneObjects.push_back(newObj);
selectedObjectId = id; selectedObjectId = id;

View File

@@ -61,6 +61,7 @@ private:
char importModelName[128] = ""; // For Assimp models char importModelName[128] = ""; // For Assimp models
char fileBrowserSearch[256] = ""; char fileBrowserSearch[256] = "";
float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range
// Private methods // Private methods
SceneObject* getSelectedObject(); SceneObject* getSelectedObject();
@@ -106,6 +107,8 @@ private:
void duplicateSelected(); void duplicateSelected();
void deleteSelected(); void deleteSelected();
void setParent(int childId, int parentId); void setParent(int childId, int parentId);
void loadMaterialFromFile(SceneObject& obj);
void saveMaterialToFile(const SceneObject& obj);
// Console/logging // Console/logging
void addConsoleMessage(const std::string& message, ConsoleMessageType type); void addConsoleMessage(const std::string& message, ConsoleMessageType type);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
#include "ProjectManager.h" #include "ProjectManager.h"
#include "Rendering.h" #include "Rendering.h"
#include "ModelLoader.h"
// Project implementation // Project implementation
Project::Project(const std::string& projectName, const fs::path& basePath) Project::Project(const std::string& projectName, const fs::path& basePath)
@@ -249,8 +250,25 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n"; file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n"; file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n"; file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n";
file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n";
file << "materialAmbient=" << obj.material.ambientStrength << "\n";
file << "materialSpecular=" << obj.material.specularStrength << "\n";
file << "materialShininess=" << obj.material.shininess << "\n";
file << "materialTextureMix=" << obj.material.textureMix << "\n";
file << "materialPath=" << obj.materialPath << "\n";
file << "albedoTex=" << obj.albedoTexturePath << "\n";
file << "overlayTex=" << obj.overlayTexturePath << "\n";
file << "normalMap=" << obj.normalMapPath << "\n";
file << "useOverlay=" << (obj.useOverlay ? 1 : 0) << "\n";
file << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n";
file << "lightIntensity=" << obj.light.intensity << "\n";
file << "lightRange=" << obj.light.range << "\n";
file << "lightInner=" << obj.light.innerAngle << "\n";
file << "lightOuter=" << obj.light.outerAngle << "\n";
file << "lightSize=" << obj.light.size.x << "," << obj.light.size.y << "\n";
file << "lightEnabled=" << (obj.light.enabled ? 1 : 0) << "\n";
if (obj.type == ObjectType::OBJMesh && !obj.meshPath.empty()) { if ((obj.type == ObjectType::OBJMesh || obj.type == ObjectType::Model) && !obj.meshPath.empty()) {
file << "meshPath=" << obj.meshPath << "\n"; file << "meshPath=" << obj.meshPath << "\n";
} }
@@ -308,6 +326,10 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
currentObj->name = value; currentObj->name = value;
} else if (key == "type") { } else if (key == "type") {
currentObj->type = static_cast<ObjectType>(std::stoi(value)); currentObj->type = static_cast<ObjectType>(std::stoi(value));
if (currentObj->type == ObjectType::DirectionalLight) currentObj->light.type = LightType::Directional;
else if (currentObj->type == ObjectType::PointLight) currentObj->light.type = LightType::Point;
else if (currentObj->type == ObjectType::SpotLight) currentObj->light.type = LightType::Spot;
else if (currentObj->type == ObjectType::AreaLight) currentObj->light.type = LightType::Area;
} else if (key == "parentId") { } else if (key == "parentId") {
currentObj->parentId = std::stoi(value); currentObj->parentId = std::stoi(value);
} else if (key == "position") { } else if (key == "position") {
@@ -325,11 +347,61 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
&currentObj->scale.x, &currentObj->scale.x,
&currentObj->scale.y, &currentObj->scale.y,
&currentObj->scale.z); &currentObj->scale.z);
} else if (key == "materialColor") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->material.color.r,
&currentObj->material.color.g,
&currentObj->material.color.b);
} else if (key == "materialAmbient") {
currentObj->material.ambientStrength = std::stof(value);
} else if (key == "materialSpecular") {
currentObj->material.specularStrength = std::stof(value);
} else if (key == "materialShininess") {
currentObj->material.shininess = std::stof(value);
} else if (key == "materialTextureMix") {
currentObj->material.textureMix = std::stof(value);
} else if (key == "materialPath") {
currentObj->materialPath = value;
} else if (key == "albedoTex") {
currentObj->albedoTexturePath = value;
} else if (key == "overlayTex") {
currentObj->overlayTexturePath = value;
} else if (key == "normalMap") {
currentObj->normalMapPath = value;
} else if (key == "useOverlay") {
currentObj->useOverlay = (std::stoi(value) != 0);
} else if (key == "lightColor") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->light.color.r,
&currentObj->light.color.g,
&currentObj->light.color.b);
} else if (key == "lightIntensity") {
currentObj->light.intensity = std::stof(value);
} else if (key == "lightRange") {
currentObj->light.range = std::stof(value);
} else if (key == "lightInner") {
currentObj->light.innerAngle = std::stof(value);
} else if (key == "lightOuter") {
currentObj->light.outerAngle = std::stof(value);
} else if (key == "lightSize") {
sscanf(value.c_str(), "%f,%f",
&currentObj->light.size.x,
&currentObj->light.size.y);
} else if (key == "lightEnabled") {
currentObj->light.enabled = (std::stoi(value) != 0);
} else if (key == "meshPath") { } else if (key == "meshPath") {
currentObj->meshPath = value; currentObj->meshPath = value;
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) { if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
std::string err; std::string err;
currentObj->meshId = g_objLoader.loadOBJ(value, err); currentObj->meshId = g_objLoader.loadOBJ(value, err);
} else if (!value.empty() && currentObj->type == ObjectType::Model) {
ModelLoadResult result = getModelLoader().loadModel(value);
if (result.success) {
currentObj->meshId = result.meshIndex;
} else {
std::cerr << "Failed to load model from scene: " << result.errorMessage << std::endl;
currentObj->meshId = -1;
}
} }
} else if (key == "children" && !value.empty()) { } else if (key == "children" && !value.empty()) {
std::stringstream ss(value); std::stringstream ss(value);

View File

@@ -1,5 +1,7 @@
#include "Rendering.h" #include "Rendering.h"
#include "Camera.h" #include "Camera.h"
#include "ModelLoader.h"
#include <unordered_map>
#define TINYOBJLOADER_IMPLEMENTATION #define TINYOBJLOADER_IMPLEMENTATION
#include "../include/ThirdParty/tiny_obj_loader.h" #include "../include/ThirdParty/tiny_obj_loader.h"
@@ -409,6 +411,20 @@ Renderer::~Renderer() {
if (rbo) glDeleteRenderbuffers(1, &rbo); if (rbo) glDeleteRenderbuffers(1, &rbo);
} }
Texture* Renderer::getTexture(const std::string& path) {
if (path.empty()) return nullptr;
auto it = textureCache.find(path);
if (it != textureCache.end()) return it->second.get();
auto tex = std::make_unique<Texture>(path);
if (!tex->GetID()) {
return nullptr;
}
Texture* raw = tex.get();
textureCache[path] = std::move(tex);
return raw;
}
void Renderer::initialize() { void Renderer::initialize() {
shader = new Shader("Resources/Shaders/vert.glsl", "Resources/Shaders/frag.glsl"); shader = new Shader("Resources/Shaders/vert.glsl", "Resources/Shaders/frag.glsl");
if (shader->ID == 0) { if (shader->ID == 0) {
@@ -475,7 +491,7 @@ void Renderer::resize(int w, int h) {
} }
} }
void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj) { void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos) {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight); glViewport(0, 0, currentWidth, currentHeight);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
@@ -484,10 +500,12 @@ void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj) {
shader->use(); shader->use();
shader->setMat4("view", view); shader->setMat4("view", view);
shader->setMat4("projection", proj); shader->setMat4("projection", proj);
shader->setVec3("viewPos", cameraPos);
texture1->Bind(GL_TEXTURE0); texture1->Bind(GL_TEXTURE0);
texture2->Bind(GL_TEXTURE1); texture2->Bind(GL_TEXTURE1);
shader->setInt("texture1", 0); shader->setInt("texture1", 0);
shader->setInt("texture2", 1); shader->setInt("overlayTex", 1);
shader->setInt("normalMap", 2);
} }
void Renderer::renderSkybox(const glm::mat4& view, const glm::mat4& proj) { void Renderer::renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
@@ -511,6 +529,38 @@ void Renderer::renderObject(const SceneObject& obj) {
model = glm::scale(model, obj.scale); model = glm::scale(model, obj.scale);
shader->setMat4("model", model); shader->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength);
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
}
if (!overlayUsed && texture2) {
texture2->Bind(GL_TEXTURE1);
}
shader->setBool("hasOverlay", overlayUsed);
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}
}
shader->setBool("hasNormalMap", normalUsed);
switch (obj.type) { switch (obj.type) {
case ObjectType::Cube: case ObjectType::Cube:
@@ -530,24 +580,136 @@ void Renderer::renderObject(const SceneObject& obj) {
} }
} }
break; break;
case ObjectType::Model:
if (obj.meshId >= 0) {
Mesh* modelMesh = getModelLoader().getMesh(obj.meshId);
if (modelMesh) {
modelMesh->draw();
}
}
break;
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight:
// Lights are not rendered as geometry
break;
case ObjectType::DirectionalLight:
// Not rendered as geometry
break;
} }
} }
void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects) { void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects) {
if (!shader) return;
shader->use(); shader->use();
shader->setMat4("view", camera.getViewMatrix()); shader->setMat4("view", camera.getViewMatrix());
shader->setMat4("projection", glm::perspective(glm::radians(FOV), (float)currentWidth / (float)currentHeight, NEAR_PLANE, FAR_PLANE)); shader->setMat4("projection", glm::perspective(glm::radians(FOV), (float)currentWidth / (float)currentHeight, NEAR_PLANE, FAR_PLANE));
shader->setVec3("lightPos", glm::vec3(4.0f, 6.0f, 4.0f)); shader->setVec3("viewPos", camera.position);
shader->setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));
shader->setFloat("ambientStrength", 0.25f); shader->setFloat("ambientStrength", 0.25f);
shader->setFloat("specularStrength", 0.8f); shader->setFloat("specularStrength", 0.8f);
shader->setFloat("shininess", 64.0f); shader->setFloat("shininess", 64.0f);
shader->setFloat("mixAmount", 0.3f); shader->setFloat("mixAmount", 0.3f);
texture1->Bind(0); // Collect up to 10 lights
texture2->Bind(1); struct LightUniform {
int type = 0; // 0 dir,1 point,2 spot
glm::vec3 dir = glm::vec3(0.0f, -1.0f, 0.0f);
glm::vec3 pos = glm::vec3(0.0f);
glm::vec3 color = glm::vec3(1.0f);
float intensity = 1.0f;
float range = 10.0f;
float inner = glm::cos(glm::radians(15.0f));
float outer = glm::cos(glm::radians(25.0f));
};
auto forwardFromRotation = [](const SceneObject& obj) {
glm::vec3 f = glm::normalize(glm::vec3(
glm::sin(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x)),
glm::sin(glm::radians(obj.rotation.x)),
glm::cos(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x))
));
if (glm::length(f) < 1e-3f ||
!std::isfinite(f.x) || !std::isfinite(f.y) || !std::isfinite(f.z)) {
f = glm::vec3(0.0f, -1.0f, 0.0f);
}
return f;
};
std::vector<LightUniform> lights;
lights.reserve(10);
// Add directionals first, then spots, then points
for (const auto& obj : sceneObjects) {
if (obj.light.enabled && obj.type == ObjectType::DirectionalLight) {
LightUniform l;
l.type = 0;
l.dir = forwardFromRotation(obj);
l.color = obj.light.color;
l.intensity = obj.light.intensity;
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
if (lights.size() < 10) {
for (const auto& obj : sceneObjects) {
if (obj.light.enabled && obj.type == ObjectType::SpotLight) {
LightUniform l;
l.type = 2;
l.pos = obj.position;
l.dir = forwardFromRotation(obj);
l.color = obj.light.color;
l.intensity = obj.light.intensity;
l.range = obj.light.range;
l.inner = glm::cos(glm::radians(obj.light.innerAngle));
l.outer = glm::cos(glm::radians(obj.light.outerAngle));
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
}
if (lights.size() < 10) {
for (const auto& obj : sceneObjects) {
if (obj.light.enabled && obj.type == ObjectType::PointLight) {
LightUniform l;
l.type = 1;
l.pos = obj.position;
l.color = obj.light.color;
l.intensity = obj.light.intensity;
l.range = obj.light.range;
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
}
int count = static_cast<int>(lights.size());
shader->setInt("lightCount", count);
for (int i = 0; i < count; ++i) {
const auto& l = lights[i];
std::string idx = "[" + std::to_string(i) + "]";
shader->setInt("lightTypeArr" + idx, l.type);
shader->setVec3("lightDirArr" + idx, l.dir);
shader->setVec3("lightPosArr" + idx, l.pos);
shader->setVec3("lightColorArr" + idx, l.color);
shader->setFloat("lightIntensityArr" + idx, l.intensity);
shader->setFloat("lightRangeArr" + idx, l.range);
shader->setFloat("lightInnerCosArr" + idx, l.inner);
shader->setFloat("lightOuterCosArr" + idx, l.outer);
}
// Bind base textures once per frame (used by objects)
if (texture1) texture1->Bind(0);
else glBindTexture(GL_TEXTURE_2D, 0);
if (texture2) texture2->Bind(1);
else glBindTexture(GL_TEXTURE_2D, 0);
if (texture1) texture1->Bind(0);
if (texture2) texture2->Bind(1);
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
// Skip light types in the main render pass (they are handled as gizmos)
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight) {
continue;
}
glm::mat4 model = glm::mat4(1.0f); glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position); model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
@@ -556,6 +718,38 @@ void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>&
model = glm::scale(model, obj.scale); model = glm::scale(model, obj.scale);
shader->setMat4("model", model); shader->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength);
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
}
if (!overlayUsed && texture2) {
texture2->Bind(GL_TEXTURE1);
}
shader->setBool("hasOverlay", overlayUsed);
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}
}
shader->setBool("hasNormalMap", normalUsed);
Mesh* meshToDraw = nullptr; Mesh* meshToDraw = nullptr;
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh; if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
@@ -563,6 +757,8 @@ void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>&
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh; else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) { else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId); meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId != -1) {
meshToDraw = getModelLoader().getMesh(obj.meshId);
} }
if (meshToDraw) { if (meshToDraw) {

View File

@@ -5,6 +5,7 @@
#include "../include/Shaders/Shader.h" #include "../include/Shaders/Shader.h"
#include "../include/Textures/Texture.h" #include "../include/Textures/Texture.h"
#include "../include/Skybox/Skybox.h" #include "../include/Skybox/Skybox.h"
#include <unordered_map>
// Cube vertex data (position + normal + texcoord) // Cube vertex data (position + normal + texcoord)
extern float vertices[]; extern float vertices[];
@@ -59,6 +60,7 @@ private:
Shader* shader = nullptr; Shader* shader = nullptr;
Texture* texture1 = nullptr; Texture* texture1 = nullptr;
Texture* texture2 = nullptr; Texture* texture2 = nullptr;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCache;
Mesh* cubeMesh = nullptr; Mesh* cubeMesh = nullptr;
Mesh* sphereMesh = nullptr; Mesh* sphereMesh = nullptr;
Mesh* capsuleMesh = nullptr; Mesh* capsuleMesh = nullptr;
@@ -71,11 +73,12 @@ public:
~Renderer(); ~Renderer();
void initialize(); void initialize();
Texture* getTexture(const std::string& path);
void resize(int w, int h); void resize(int w, int h);
int getWidth() const { return currentWidth; } int getWidth() const { return currentWidth; }
int getHeight() const { return currentHeight; } int getHeight() const { return currentHeight; }
void beginRender(const glm::mat4& view, const glm::mat4& proj); void beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos);
void renderSkybox(const glm::mat4& view, const glm::mat4& proj); void renderSkybox(const glm::mat4& view, const glm::mat4& proj);
void renderObject(const SceneObject& obj); void renderObject(const SceneObject& obj);
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects); void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects);

View File

@@ -7,7 +7,39 @@ enum class ObjectType {
Sphere, Sphere,
Capsule, Capsule,
OBJMesh, OBJMesh,
Model // New type for Assimp-loaded models (FBX, GLTF, etc.) Model, // New type for Assimp-loaded models (FBX, GLTF, etc.)
DirectionalLight,
PointLight,
SpotLight,
AreaLight
};
struct MaterialProperties {
glm::vec3 color = glm::vec3(1.0f);
float ambientStrength = 0.2f;
float specularStrength = 0.5f;
float shininess = 32.0f;
float textureMix = 0.3f; // Blend factor between albedo and overlay
};
enum class LightType {
Directional = 0,
Point = 1,
Spot = 2,
Area = 3
};
struct LightComponent {
LightType type = LightType::Point;
glm::vec3 color = glm::vec3(1.0f);
float intensity = 1.0f;
float range = 10.0f;
// Spot
float innerAngle = 15.0f;
float outerAngle = 25.0f;
// Area (rect) size in world units
glm::vec2 size = glm::vec2(1.0f, 1.0f);
bool enabled = true;
}; };
enum class ConsoleMessageType { enum class ConsoleMessageType {
@@ -28,8 +60,15 @@ public:
int parentId = -1; int parentId = -1;
std::vector<int> childIds; std::vector<int> childIds;
bool isExpanded = true; bool isExpanded = true;
std::string meshPath; // Path to OBJ file (for OBJMesh type) std::string meshPath; // Path to imported model file
int meshId = -1; // Index into loaded meshes cache int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader)
MaterialProperties material;
std::string materialPath; // Optional external material asset
std::string albedoTexturePath;
std::string overlayTexturePath;
std::string normalMapPath;
bool useOverlay = false;
LightComponent light; // Only used when type is a light
SceneObject(const std::string& name, ObjectType type, int id) SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {} : name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}

View File

@@ -102,6 +102,11 @@ void Shader::setFloat(const std::string &name, float value) const
glUniform1f(glGetUniformLocation(ID, name.c_str()), value); glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
} }
void Shader::setVec2(const std::string &name, const glm::vec2 &value) const
{
glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void Shader::setVec3(const std::string &name, const glm::vec3 &value) const void Shader::setVec3(const std::string &name, const glm::vec3 &value) const
{ {
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);