From 7831bea4e2ae4da7f4ffff0e0d1cfcef756ffff3 Mon Sep 17 00:00:00 2001
From: Anemunt <69436164+darkresident55@users.noreply.github.com>
Date: Wed, 10 Dec 2025 15:13:05 -0500
Subject: [PATCH] Added Post Processing, Improved UI a lot, Made File Explorer
look nicer, Fixed up Raycast Selection, Added Placeholder Playmode Button,
Added cameras, Organized Create menu in Inspector, Added outlines to selected
objects, added view output for viewing cameras while in Playmode area.
---
Resources/Shaders/postfx_blur_frag.glsl | 31 +
Resources/Shaders/postfx_bright_frag.glsl | 15 +
Resources/Shaders/postfx_frag.glsl | 49 +
Resources/Shaders/postfx_vert.glsl | 10 +
.../ThirdParty/BloomFilter/BloomFilter.pde | 144 +++
.../ThirdParty/BloomFilter/bloomFrag.glsl | 23 +
.../ThirdParty/BloomFilter/bloomVert.glsl | 18 +
.../ThirdParty/BloomFilter/blurFrag.glsl | 59 +
.../ThirdParty/BloomFilter/blurVert.glsl | 18 +
.../ThirdParty/MotionBlur/motionBlur_f.glsl | 48 +
.../ThirdParty/MotionBlur/motionBlur_v.glsl | 12 +
.../ProcessingPostFX/shader/blurFrag.glsl | 59 +
.../shader/brightPassFrag.glsl | 23 +
.../ProcessingPostFX/shader/sobelFrag.glsl | 38 +
Resources/imgui.ini | 34 +-
src/EditorUI.cpp | 114 +-
src/Engine.cpp | 116 +-
src/Engine.h | 5 +
src/EnginePanels.cpp | 1030 +++++++++++++++--
src/ModelLoader.cpp | 62 +-
src/ModelLoader.h | 4 +-
src/ProjectManager.cpp | 58 +-
src/Rendering.cpp | 314 ++++-
src/Rendering.h | 37 +-
src/SceneObject.h | 33 +-
25 files changed, 2131 insertions(+), 223 deletions(-)
create mode 100644 Resources/Shaders/postfx_blur_frag.glsl
create mode 100644 Resources/Shaders/postfx_bright_frag.glsl
create mode 100644 Resources/Shaders/postfx_frag.glsl
create mode 100644 Resources/Shaders/postfx_vert.glsl
create mode 100644 Resources/ThirdParty/BloomFilter/BloomFilter.pde
create mode 100644 Resources/ThirdParty/BloomFilter/bloomFrag.glsl
create mode 100644 Resources/ThirdParty/BloomFilter/bloomVert.glsl
create mode 100644 Resources/ThirdParty/BloomFilter/blurFrag.glsl
create mode 100644 Resources/ThirdParty/BloomFilter/blurVert.glsl
create mode 100644 Resources/ThirdParty/MotionBlur/motionBlur_f.glsl
create mode 100644 Resources/ThirdParty/MotionBlur/motionBlur_v.glsl
create mode 100644 Resources/ThirdParty/ProcessingPostFX/shader/blurFrag.glsl
create mode 100644 Resources/ThirdParty/ProcessingPostFX/shader/brightPassFrag.glsl
create mode 100755 Resources/ThirdParty/ProcessingPostFX/shader/sobelFrag.glsl
diff --git a/Resources/Shaders/postfx_blur_frag.glsl b/Resources/Shaders/postfx_blur_frag.glsl
new file mode 100644
index 0000000..f8e392a
--- /dev/null
+++ b/Resources/Shaders/postfx_blur_frag.glsl
@@ -0,0 +1,31 @@
+#version 330 core
+out vec4 FragColor;
+
+in vec2 TexCoord;
+
+uniform sampler2D image;
+uniform vec2 texelSize;
+uniform bool horizontal = true;
+uniform float sigma = 3.0;
+uniform int radius = 5;
+
+const float PI = 3.14159265359;
+
+void main() {
+ float twoSigma2 = 2.0 * sigma * sigma;
+ vec2 dir = horizontal ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+
+ vec3 result = texture(image, TexCoord).rgb;
+ float weightSum = 1.0;
+
+ for (int i = 1; i <= radius; ++i) {
+ float w = exp(-(float(i * i)) / twoSigma2);
+ vec2 offset = dir * texelSize * float(i);
+ result += texture(image, TexCoord + offset).rgb * w;
+ result += texture(image, TexCoord - offset).rgb * w;
+ weightSum += 2.0 * w;
+ }
+
+ result /= weightSum;
+ FragColor = vec4(result, 1.0);
+}
diff --git a/Resources/Shaders/postfx_bright_frag.glsl b/Resources/Shaders/postfx_bright_frag.glsl
new file mode 100644
index 0000000..0e2369c
--- /dev/null
+++ b/Resources/Shaders/postfx_bright_frag.glsl
@@ -0,0 +1,15 @@
+#version 330 core
+out vec4 FragColor;
+
+in vec2 TexCoord;
+
+uniform sampler2D sceneTex;
+uniform float threshold = 1.0;
+
+void main() {
+ vec3 c = texture(sceneTex, TexCoord).rgb;
+ float luma = dot(c, vec3(0.2125, 0.7154, 0.0721));
+ float bright = max(luma - threshold, 0.0);
+ vec3 masked = c * step(0.0, bright);
+ FragColor = vec4(masked, 1.0);
+}
diff --git a/Resources/Shaders/postfx_frag.glsl b/Resources/Shaders/postfx_frag.glsl
new file mode 100644
index 0000000..650826d
--- /dev/null
+++ b/Resources/Shaders/postfx_frag.glsl
@@ -0,0 +1,49 @@
+#version 330 core
+out vec4 FragColor;
+
+in vec2 TexCoord;
+
+uniform sampler2D sceneTex;
+uniform sampler2D bloomTex;
+uniform sampler2D historyTex;
+
+uniform bool enableBloom = false;
+uniform float bloomIntensity = 0.8;
+
+uniform bool enableColorAdjust = false;
+uniform float exposure = 0.0; // EV stops
+uniform float contrast = 1.0;
+uniform float saturation = 1.0;
+uniform vec3 colorFilter = vec3(1.0);
+
+uniform bool enableMotionBlur = false;
+uniform bool hasHistory = false;
+uniform float motionBlurStrength = 0.15;
+
+vec3 applyColorAdjust(vec3 color) {
+ if (enableColorAdjust) {
+ color *= exp2(exposure);
+ color = (color - 0.5) * contrast + 0.5;
+ float luma = dot(color, vec3(0.299, 0.587, 0.114));
+ color = mix(vec3(luma), color, saturation);
+ color *= colorFilter;
+ }
+ return color;
+}
+
+void main() {
+ vec3 color = texture(sceneTex, TexCoord).rgb;
+ if (enableBloom) {
+ vec3 glow = texture(bloomTex, TexCoord).rgb;
+ color += glow * bloomIntensity;
+ }
+
+ color = applyColorAdjust(color);
+
+ if (enableMotionBlur && hasHistory) {
+ vec3 history = texture(historyTex, TexCoord).rgb;
+ color = mix(color, history, clamp(motionBlurStrength, 0.0, 0.98));
+ }
+
+ FragColor = vec4(color, 1.0);
+}
diff --git a/Resources/Shaders/postfx_vert.glsl b/Resources/Shaders/postfx_vert.glsl
new file mode 100644
index 0000000..c0c8eb3
--- /dev/null
+++ b/Resources/Shaders/postfx_vert.glsl
@@ -0,0 +1,10 @@
+#version 330 core
+layout (location = 0) in vec2 aPos;
+layout (location = 1) in vec2 aTexCoord;
+
+out vec2 TexCoord;
+
+void main() {
+ TexCoord = aTexCoord;
+ gl_Position = vec4(aPos, 0.0, 1.0);
+}
diff --git a/Resources/ThirdParty/BloomFilter/BloomFilter.pde b/Resources/ThirdParty/BloomFilter/BloomFilter.pde
new file mode 100644
index 0000000..f0a5dd3
--- /dev/null
+++ b/Resources/ThirdParty/BloomFilter/BloomFilter.pde
@@ -0,0 +1,144 @@
+import controlP5.*;
+
+ControlP5 cp5;
+
+PGraphics canvas;
+
+PGraphics brightPass;
+PGraphics horizontalBlurPass;
+PGraphics verticalBlurPass;
+
+PShader bloomFilter;
+PShader blurFilter;
+
+int angle = 0;
+
+final int surfaceWidth = 250;
+final int surfaceHeight = 250;
+
+float luminanceFilter = 0.8;
+float blurSize = 20;
+float sigma = 12;
+
+void setup()
+{
+ size(1000, 250, P3D);
+
+ addUI();
+
+ canvas = createGraphics(surfaceWidth, surfaceHeight, P3D);
+
+ brightPass = createGraphics(surfaceWidth, surfaceHeight, P2D);
+ brightPass.noSmooth();
+
+ horizontalBlurPass = createGraphics(surfaceWidth, surfaceHeight, P2D);
+ horizontalBlurPass.noSmooth();
+
+ verticalBlurPass = createGraphics(surfaceWidth, surfaceHeight, P2D);
+ verticalBlurPass.noSmooth();
+
+ bloomFilter = loadShader("bloomFrag.glsl");
+ blurFilter = loadShader("blurFrag.glsl");
+}
+
+void draw()
+{
+ background(0);
+
+ bloomFilter.set("brightPassThreshold", luminanceFilter);
+
+ blurFilter.set("blurSize", (int)blurSize);
+ blurFilter.set("sigma", sigma);
+
+ canvas.beginDraw();
+ render(canvas);
+ canvas.endDraw();
+
+ // bright pass
+ brightPass.beginDraw();
+ brightPass.shader(bloomFilter);
+ brightPass.image(canvas, 0, 0);
+ brightPass.endDraw();
+
+ // blur horizontal pass
+ horizontalBlurPass.beginDraw();
+ blurFilter.set("horizontalPass", 1);
+ horizontalBlurPass.shader(blurFilter);
+ horizontalBlurPass.image(brightPass, 0, 0);
+ horizontalBlurPass.endDraw();
+
+ // blur vertical pass
+ verticalBlurPass.beginDraw();
+ blurFilter.set("horizontalPass", 0);
+ verticalBlurPass.shader(blurFilter);
+ verticalBlurPass.image(horizontalBlurPass, 0, 0);
+ verticalBlurPass.endDraw();
+
+ // draw original
+ image(canvas.copy(), 0, 0);
+ text("Original", 20, height - 20);
+
+ // draw bright pass
+ image(brightPass, surfaceWidth, 0);
+ text("Bright Pass", surfaceWidth + 20, height - 20);
+
+ image(verticalBlurPass, (surfaceWidth * 2), 0);
+ text("Blur", (surfaceWidth * 2) + 20, height - 20);
+
+ // draw
+ image(canvas, (surfaceWidth * 3), 0);
+ blendMode(SCREEN);
+ image(verticalBlurPass, (surfaceWidth * 3), 0);
+ blendMode(BLEND);
+ text("Combined", (surfaceWidth * 3) + 20, height - 20);
+
+ // fps
+ fill(0, 255, 0);
+ text("FPS: " + frameRate, 20, 20);
+}
+
+void render(PGraphics pg)
+{
+ pg.background(0, 0);
+ pg.stroke(255, 0, 0);
+
+ for (int i = -1; i < 2; i++)
+ {
+ if (i == -1)
+ pg.fill(0, 255, 0);
+ else if (i == 0)
+ pg.fill(255);
+ else
+ pg.fill(0, 200, 200);
+
+ pg.pushMatrix();
+ // left-right, up-down, near-far
+ pg.translate(surfaceWidth / 2 + (i * 50), surfaceHeight / 2, 0);
+ pg.rotateX(radians(angle));
+ pg.rotateZ(radians(angle));
+ pg.box(30);
+ pg.popMatrix();
+ }
+
+ angle = ++angle % 360;
+}
+
+void addUI()
+{
+ cp5 = new ControlP5(this);
+
+ cp5.addSlider("luminanceFilter")
+ .setPosition(200, 5)
+ .setRange(0, 1)
+ ;
+
+ cp5.addSlider("blurSize")
+ .setPosition(400, 5)
+ .setRange(0, 100)
+ ;
+
+ cp5.addSlider("sigma")
+ .setPosition(600, 5)
+ .setRange(1, 100)
+ ;
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/BloomFilter/bloomFrag.glsl b/Resources/ThirdParty/BloomFilter/bloomFrag.glsl
new file mode 100644
index 0000000..39505fc
--- /dev/null
+++ b/Resources/ThirdParty/BloomFilter/bloomFrag.glsl
@@ -0,0 +1,23 @@
+#ifdef GL_ES
+precision mediump float;
+precision mediump int;
+#endif
+
+uniform sampler2D texture;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+uniform float brightPassThreshold;
+
+void main() {
+ vec3 luminanceVector = vec3(0.2125, 0.7154, 0.0721);
+ vec4 c = texture2D(texture, vertTexCoord.st) * vertColor;
+
+ float luminance = dot(luminanceVector, c.xyz);
+ luminance = max(0.0, luminance - brightPassThreshold);
+ c.xyz *= sign(luminance);
+ c.a = 1.0;
+
+ gl_FragColor = c;
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/BloomFilter/bloomVert.glsl b/Resources/ThirdParty/BloomFilter/bloomVert.glsl
new file mode 100644
index 0000000..aee3242
--- /dev/null
+++ b/Resources/ThirdParty/BloomFilter/bloomVert.glsl
@@ -0,0 +1,18 @@
+#define PROCESSING_TEXTURE_SHADER
+
+uniform mat4 transform;
+uniform mat4 texMatrix;
+
+attribute vec4 vertex;
+attribute vec4 color;
+attribute vec2 texCoord;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+void main() {
+ gl_Position = transform * vertex;
+
+ vertColor = color;
+ vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/BloomFilter/blurFrag.glsl b/Resources/ThirdParty/BloomFilter/blurFrag.glsl
new file mode 100644
index 0000000..e6a145f
--- /dev/null
+++ b/Resources/ThirdParty/BloomFilter/blurFrag.glsl
@@ -0,0 +1,59 @@
+// Adapted from:
+// http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html
+
+#ifdef GL_ES
+precision mediump float;
+precision mediump int;
+#endif
+
+#define PROCESSING_TEXTURE_SHADER
+
+uniform sampler2D texture;
+
+// The inverse of the texture dimensions along X and Y
+uniform vec2 texOffset;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+uniform int blurSize;
+uniform int horizontalPass; // 0 or 1 to indicate vertical or horizontal pass
+uniform float sigma; // The sigma value for the gaussian function: higher value means more blur
+ // A good value for 9x9 is around 3 to 5
+ // A good value for 7x7 is around 2.5 to 4
+ // A good value for 5x5 is around 2 to 3.5
+ // ... play around with this based on what you need :)
+
+const float pi = 3.14159265;
+
+void main() {
+ float numBlurPixelsPerSide = float(blurSize / 2);
+
+ vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+
+ // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
+ vec3 incrementalGaussian;
+ incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma);
+ incrementalGaussian.y = exp(-0.5 / (sigma * sigma));
+ incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;
+
+ vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0);
+ float coefficientSum = 0.0;
+
+ // Take the central sample first...
+ avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x;
+ coefficientSum += incrementalGaussian.x;
+ incrementalGaussian.xy *= incrementalGaussian.yz;
+
+ // Go through the remaining 8 vertical samples (4 on each side of the center)
+ for (float i = 1.0; i <= numBlurPixelsPerSide; i++) {
+ avgValue += texture2D(texture, vertTexCoord.st - i * texOffset *
+ blurMultiplyVec) * incrementalGaussian.x;
+ avgValue += texture2D(texture, vertTexCoord.st + i * texOffset *
+ blurMultiplyVec) * incrementalGaussian.x;
+ coefficientSum += 2.0 * incrementalGaussian.x;
+ incrementalGaussian.xy *= incrementalGaussian.yz;
+ }
+
+ gl_FragColor = avgValue / coefficientSum;
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/BloomFilter/blurVert.glsl b/Resources/ThirdParty/BloomFilter/blurVert.glsl
new file mode 100644
index 0000000..aee3242
--- /dev/null
+++ b/Resources/ThirdParty/BloomFilter/blurVert.glsl
@@ -0,0 +1,18 @@
+#define PROCESSING_TEXTURE_SHADER
+
+uniform mat4 transform;
+uniform mat4 texMatrix;
+
+attribute vec4 vertex;
+attribute vec4 color;
+attribute vec2 texCoord;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+void main() {
+ gl_Position = transform * vertex;
+
+ vertColor = color;
+ vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/MotionBlur/motionBlur_f.glsl b/Resources/ThirdParty/MotionBlur/motionBlur_f.glsl
new file mode 100644
index 0000000..0a5264b
--- /dev/null
+++ b/Resources/ThirdParty/MotionBlur/motionBlur_f.glsl
@@ -0,0 +1,48 @@
+#version 330 core
+//Per-Object Motion Blur
+
+uniform sampler2D uTexInput; // texture we're blurring
+uniform sampler2D uTexVelocity; // velocity buffer
+
+in vec2 texture_coordinate;
+
+uniform float uVelocityScale; //currentFps / targetFps
+ /*
+ What's uVelocityScale? It's used to address the following problem:
+ if the framerate is very high, velocity will be very small as the
+ amount of motion in between frames will be low. Correspondingly, if
+ the framerate is very low the motion between frames will be high
+ and velocity will be much larger. This ties the blur size to the
+ framerate, which is technically correct if you equate framrate with
+ shutter speed, however is undesirable for realtime rendering where
+ the framerate can vary. To fix it we need to cancel out the framerate
+ */
+
+int MAX_SAMPLES = 16; //32
+
+layout (location=0) out vec4 result;
+
+void main(void) {
+
+ vec2 velocity = texture(uTexVelocity, texture_coordinate).rg * 2.0 - 1.0;
+ velocity *= uVelocityScale;
+
+ //get the size of on pixel (texel)
+ vec2 texelSize = 1.0 / vec2(textureSize(uTexInput, 0));
+ //mprove performance by adapting the number of samples according to the velocity
+ float speed = length(velocity / texelSize);
+ int nSamples = clamp(int(speed), 1, MAX_SAMPLES);
+ result = vec4(0.0);
+
+ velocity = normalize(velocity) * texelSize;
+ float hlim = float(-nSamples) * 0.5 + 0.5;
+ //the actual blurring of the current pixel
+ vec2 offset;
+ for (int i = 0; i < nSamples; ++i)
+ {
+ offset = velocity * (hlim + float(i));
+ result += texture(uTexInput, texture_coordinate + offset);
+ }
+ //average the result
+ result /= float(nSamples);
+ }
\ No newline at end of file
diff --git a/Resources/ThirdParty/MotionBlur/motionBlur_v.glsl b/Resources/ThirdParty/MotionBlur/motionBlur_v.glsl
new file mode 100644
index 0000000..3daae51
--- /dev/null
+++ b/Resources/ThirdParty/MotionBlur/motionBlur_v.glsl
@@ -0,0 +1,12 @@
+
+#version 330 compatibility
+
+out vec2 texture_coordinate;
+
+void main()
+{
+ //set the position of the current vertex
+ gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
+
+ texture_coordinate = vec2(gl_MultiTexCoord0);
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/ProcessingPostFX/shader/blurFrag.glsl b/Resources/ThirdParty/ProcessingPostFX/shader/blurFrag.glsl
new file mode 100644
index 0000000..e6a145f
--- /dev/null
+++ b/Resources/ThirdParty/ProcessingPostFX/shader/blurFrag.glsl
@@ -0,0 +1,59 @@
+// Adapted from:
+// http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html
+
+#ifdef GL_ES
+precision mediump float;
+precision mediump int;
+#endif
+
+#define PROCESSING_TEXTURE_SHADER
+
+uniform sampler2D texture;
+
+// The inverse of the texture dimensions along X and Y
+uniform vec2 texOffset;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+uniform int blurSize;
+uniform int horizontalPass; // 0 or 1 to indicate vertical or horizontal pass
+uniform float sigma; // The sigma value for the gaussian function: higher value means more blur
+ // A good value for 9x9 is around 3 to 5
+ // A good value for 7x7 is around 2.5 to 4
+ // A good value for 5x5 is around 2 to 3.5
+ // ... play around with this based on what you need :)
+
+const float pi = 3.14159265;
+
+void main() {
+ float numBlurPixelsPerSide = float(blurSize / 2);
+
+ vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+
+ // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
+ vec3 incrementalGaussian;
+ incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma);
+ incrementalGaussian.y = exp(-0.5 / (sigma * sigma));
+ incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;
+
+ vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0);
+ float coefficientSum = 0.0;
+
+ // Take the central sample first...
+ avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x;
+ coefficientSum += incrementalGaussian.x;
+ incrementalGaussian.xy *= incrementalGaussian.yz;
+
+ // Go through the remaining 8 vertical samples (4 on each side of the center)
+ for (float i = 1.0; i <= numBlurPixelsPerSide; i++) {
+ avgValue += texture2D(texture, vertTexCoord.st - i * texOffset *
+ blurMultiplyVec) * incrementalGaussian.x;
+ avgValue += texture2D(texture, vertTexCoord.st + i * texOffset *
+ blurMultiplyVec) * incrementalGaussian.x;
+ coefficientSum += 2.0 * incrementalGaussian.x;
+ incrementalGaussian.xy *= incrementalGaussian.yz;
+ }
+
+ gl_FragColor = avgValue / coefficientSum;
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/ProcessingPostFX/shader/brightPassFrag.glsl b/Resources/ThirdParty/ProcessingPostFX/shader/brightPassFrag.glsl
new file mode 100644
index 0000000..39505fc
--- /dev/null
+++ b/Resources/ThirdParty/ProcessingPostFX/shader/brightPassFrag.glsl
@@ -0,0 +1,23 @@
+#ifdef GL_ES
+precision mediump float;
+precision mediump int;
+#endif
+
+uniform sampler2D texture;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+uniform float brightPassThreshold;
+
+void main() {
+ vec3 luminanceVector = vec3(0.2125, 0.7154, 0.0721);
+ vec4 c = texture2D(texture, vertTexCoord.st) * vertColor;
+
+ float luminance = dot(luminanceVector, c.xyz);
+ luminance = max(0.0, luminance - brightPassThreshold);
+ c.xyz *= sign(luminance);
+ c.a = 1.0;
+
+ gl_FragColor = c;
+}
\ No newline at end of file
diff --git a/Resources/ThirdParty/ProcessingPostFX/shader/sobelFrag.glsl b/Resources/ThirdParty/ProcessingPostFX/shader/sobelFrag.glsl
new file mode 100755
index 0000000..cafb5c1
--- /dev/null
+++ b/Resources/ThirdParty/ProcessingPostFX/shader/sobelFrag.glsl
@@ -0,0 +1,38 @@
+// Adapted from:
+// http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html
+
+#ifdef GL_ES
+precision mediump float;
+precision mediump int;
+#endif
+
+#define PROCESSING_TEXTURE_SHADER
+
+uniform sampler2D texture;
+
+varying vec4 vertColor;
+varying vec4 vertTexCoord;
+
+uniform vec2 resolution;
+
+void main(void) {
+ float x = 1.0 / resolution.x;
+ float y = 1.0 / resolution.y;
+ vec4 horizEdge = vec4( 0.0 );
+ horizEdge -= texture2D( texture, vec2( vertTexCoord.x - x, vertTexCoord.y - y ) ) * 1.0;
+ horizEdge -= texture2D( texture, vec2( vertTexCoord.x - x, vertTexCoord.y ) ) * 2.0;
+ horizEdge -= texture2D( texture, vec2( vertTexCoord.x - x, vertTexCoord.y + y ) ) * 1.0;
+ horizEdge += texture2D( texture, vec2( vertTexCoord.x + x, vertTexCoord.y - y ) ) * 1.0;
+ horizEdge += texture2D( texture, vec2( vertTexCoord.x + x, vertTexCoord.y ) ) * 2.0;
+ horizEdge += texture2D( texture, vec2( vertTexCoord.x + x, vertTexCoord.y + y ) ) * 1.0;
+ vec4 vertEdge = vec4( 0.0 );
+ vertEdge -= texture2D( texture, vec2( vertTexCoord.x - x, vertTexCoord.y - y ) ) * 1.0;
+ vertEdge -= texture2D( texture, vec2( vertTexCoord.x , vertTexCoord.y - y ) ) * 2.0;
+ vertEdge -= texture2D( texture, vec2( vertTexCoord.x + x, vertTexCoord.y - y ) ) * 1.0;
+ vertEdge += texture2D( texture, vec2( vertTexCoord.x - x, vertTexCoord.y + y ) ) * 1.0;
+ vertEdge += texture2D( texture, vec2( vertTexCoord.x , vertTexCoord.y + y ) ) * 2.0;
+ vertEdge += texture2D( texture, vec2( vertTexCoord.x + x, vertTexCoord.y + y ) ) * 1.0;
+ vec3 edge = sqrt((horizEdge.rgb * horizEdge.rgb) + (vertEdge.rgb * vertEdge.rgb));
+
+ gl_FragColor = vec4(edge, texture2D(texture, vertTexCoord.xy).a);
+}
\ No newline at end of file
diff --git a/Resources/imgui.ini b/Resources/imgui.ini
index 7a00c41..27d96c2 100644
--- a/Resources/imgui.ini
+++ b/Resources/imgui.ini
@@ -14,25 +14,25 @@ Size=500,250
Collapsed=0
[Window][DockSpace]
-Pos=0,21
-Size=1920,985
+Pos=0,23
+Size=1920,983
Collapsed=0
[Window][Viewport]
-Pos=306,42
-Size=1265,741
+Pos=306,46
+Size=1265,737
Collapsed=0
DockId=0x00000002,0
[Window][Hierarchy]
-Pos=0,42
-Size=304,741
+Pos=0,46
+Size=304,737
Collapsed=0
DockId=0x00000001,0
[Window][Inspector]
-Pos=1573,42
-Size=347,964
+Pos=1573,46
+Size=347,960
Collapsed=0
DockId=0x00000008,0
@@ -60,25 +60,31 @@ Size=1000,800
Collapsed=0
[Window][Camera]
-Pos=0,42
-Size=304,741
+Pos=0,46
+Size=304,737
Collapsed=0
DockId=0x00000001,1
[Window][Environment]
-Pos=1573,42
-Size=347,964
+Pos=1573,46
+Size=347,960
Collapsed=0
DockId=0x00000008,1
+[Window][Project Manager]
+Pos=787,785
+Size=784,221
+Collapsed=0
+DockId=0x00000006,1
+
[Docking][Data]
-DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,42 Size=1920,964 Split=X
+DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X
DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y
DockNode ID=0x00000003 Parent=0x00000007 SizeRef=1858,792 Split=X
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=304,758 Selected=0xBABDAE5E
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=694,758 CentralNode=1 Selected=0xC450F867
DockNode ID=0x00000004 Parent=0x00000007 SizeRef=1858,221 Split=X Selected=0xEA83D666
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=929,221 Selected=0xEA83D666
- DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0x9C21DE82
+ DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0xDA0DCE3C
DockNode ID=0x00000008 Parent=0xD71539A0 SizeRef=347,1015 Selected=0x36DC96AB
diff --git a/src/EditorUI.cpp b/src/EditorUI.cpp
index 351da32..aafcd76 100644
--- a/src/EditorUI.cpp
+++ b/src/EditorUI.cpp
@@ -188,71 +188,87 @@ void applyModernTheme() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
- colors[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
- colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
- colors[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.14f, 0.98f);
+ ImVec4 slate = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
+ ImVec4 panel = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
+ ImVec4 overlay = ImVec4(0.07f, 0.08f, 0.10f, 0.98f);
+ ImVec4 accent = ImVec4(0.33f, 0.63f, 0.98f, 1.00f);
+ ImVec4 accentMuted = ImVec4(0.25f, 0.46f, 0.78f, 1.00f);
+ ImVec4 highlight = ImVec4(0.18f, 0.23f, 0.30f, 1.00f);
- colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
- colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.32f, 1.00f);
- colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
+ colors[ImGuiCol_Text] = ImVec4(0.87f, 0.89f, 0.92f, 1.00f);
+ colors[ImGuiCol_TextDisabled] = ImVec4(0.52f, 0.56f, 0.62f, 1.00f);
- colors[ImGuiCol_Button] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
- colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f);
- colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.26f, 0.30f, 1.00f);
+ colors[ImGuiCol_WindowBg] = panel;
+ colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f);
+ colors[ImGuiCol_PopupBg] = overlay;
+ colors[ImGuiCol_Border] = ImVec4(0.21f, 0.24f, 0.28f, 0.60f);
+ colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
+ colors[ImGuiCol_MenuBarBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f);
- colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
- colors[ImGuiCol_FrameBgHovered] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f);
- colors[ImGuiCol_FrameBgActive] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
+ colors[ImGuiCol_Header] = highlight;
+ colors[ImGuiCol_HeaderHovered] = ImVec4(0.22f, 0.28f, 0.36f, 1.00f);
+ colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.44f, 0.72f, 1.00f);
- colors[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
- colors[ImGuiCol_TitleBgActive] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
- colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.06f, 0.06f, 0.08f, 1.00f);
+ colors[ImGuiCol_Button] = ImVec4(0.17f, 0.19f, 0.22f, 1.00f);
+ colors[ImGuiCol_ButtonHovered] = ImVec4(0.23f, 0.27f, 0.33f, 1.00f);
+ colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.36f, 0.46f, 1.00f);
- colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
- colors[ImGuiCol_TabHovered] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
- colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
- colors[ImGuiCol_TabUnfocused] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
- colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
+ colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f);
+ colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f);
+ colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.32f, 0.41f, 1.00f);
- colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
- colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f);
- colors[ImGuiCol_SeparatorActive] = ImVec4(0.40f, 0.40f, 0.48f, 1.00f);
+ colors[ImGuiCol_TitleBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f);
+ colors[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.12f, 0.14f, 1.00f);
+ colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.04f, 0.05f, 0.06f, 1.00f);
- colors[ImGuiCol_ScrollbarBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
- colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
- colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f);
- colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.42f, 1.00f);
+ colors[ImGuiCol_Tab] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f);
+ colors[ImGuiCol_TabHovered] = ImVec4(0.20f, 0.28f, 0.38f, 1.00f);
+ colors[ImGuiCol_TabActive] = ImVec4(0.16f, 0.19f, 0.23f, 1.00f);
+ colors[ImGuiCol_TabUnfocused] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
+ colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.13f, 0.15f, 0.18f, 1.00f);
- colors[ImGuiCol_CheckMark] = ImVec4(0.45f, 0.72f, 0.95f, 1.00f);
- colors[ImGuiCol_SliderGrab] = ImVec4(0.45f, 0.72f, 0.95f, 1.00f);
- colors[ImGuiCol_SliderGrabActive] = ImVec4(0.55f, 0.78f, 1.00f, 1.00f);
+ colors[ImGuiCol_Separator] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f);
+ colors[ImGuiCol_SeparatorHovered] = ImVec4(0.24f, 0.32f, 0.42f, 1.00f);
+ colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.44f, 0.60f, 1.00f);
- colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.26f, 0.30f, 1.00f);
- colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.45f, 0.72f, 0.95f, 0.67f);
- colors[ImGuiCol_ResizeGripActive] = ImVec4(0.45f, 0.72f, 0.95f, 0.95f);
+ colors[ImGuiCol_ScrollbarBg] = ImVec4(0.07f, 0.08f, 0.10f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.21f, 0.23f, 0.27f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.26f, 0.30f, 0.36f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.30f, 0.36f, 0.44f, 1.00f);
- colors[ImGuiCol_DockingPreview] = ImVec4(0.45f, 0.72f, 0.95f, 0.70f);
- colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
+ colors[ImGuiCol_CheckMark] = accent;
+ colors[ImGuiCol_SliderGrab] = accent;
+ colors[ImGuiCol_SliderGrabActive] = accentMuted;
- colors[ImGuiCol_TextSelectedBg] = ImVec4(0.45f, 0.72f, 0.95f, 0.35f);
- colors[ImGuiCol_NavHighlight] = ImVec4(0.45f, 0.72f, 0.95f, 1.00f);
+ colors[ImGuiCol_ResizeGrip] = ImVec4(0.22f, 0.26f, 0.33f, 1.00f);
+ colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.32f, 0.46f, 0.62f, 0.80f);
+ colors[ImGuiCol_ResizeGripActive] = accent;
- style.WindowRounding = 4.0f;
- style.ChildRounding = 4.0f;
- style.FrameRounding = 4.0f;
- style.PopupRounding = 4.0f;
- style.ScrollbarRounding = 4.0f;
- style.GrabRounding = 4.0f;
- style.TabRounding = 4.0f;
+ colors[ImGuiCol_DockingPreview] = ImVec4(0.33f, 0.63f, 0.98f, 0.60f);
+ colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.05f, 0.06f, 0.07f, 1.00f);
- style.WindowPadding = ImVec2(8.0f, 8.0f);
- style.FramePadding = ImVec2(6.0f, 4.0f);
- style.ItemSpacing = ImVec2(8.0f, 4.0f);
- style.ItemInnerSpacing = ImVec2(4.0f, 4.0f);
+ colors[ImGuiCol_TextSelectedBg] = ImVec4(0.33f, 0.63f, 0.98f, 0.25f);
+ colors[ImGuiCol_NavHighlight] = accent;
+ colors[ImGuiCol_TableHeaderBg] = ImVec4(0.15f, 0.17f, 0.20f, 1.00f);
+
+ style.WindowRounding = 5.0f;
+ style.ChildRounding = 5.0f;
+ style.FrameRounding = 6.0f;
+ style.PopupRounding = 6.0f;
+ style.ScrollbarRounding = 10.0f;
+ style.GrabRounding = 6.0f;
+ style.TabRounding = 6.0f;
+
+ style.WindowPadding = ImVec2(10.0f, 10.0f);
+ style.FramePadding = ImVec2(9.0f, 5.0f);
+ style.ItemSpacing = ImVec2(10.0f, 6.0f);
+ style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
+ style.IndentSpacing = 18.0f;
style.WindowBorderSize = 1.0f;
- style.FrameBorderSize = 0.0f;
+ style.FrameBorderSize = 1.0f;
style.PopupBorderSize = 1.0f;
+ style.TabBorderSize = 1.0f;
}
void setupDockspace() {
diff --git a/src/Engine.cpp b/src/Engine.cpp
index 1af0fb8..d28bf43 100644
--- a/src/Engine.cpp
+++ b/src/Engine.cpp
@@ -87,6 +87,22 @@ SceneObject* Engine::getSelectedObject() {
return (it != sceneObjects.end()) ? &(*it) : nullptr;
}
+Camera Engine::makeCameraFromObject(const SceneObject& obj) const {
+ Camera cam;
+ cam.position = obj.position;
+ glm::quat q = glm::quat(glm::radians(obj.rotation));
+ glm::mat3 rot = glm::mat3_cast(q);
+ cam.front = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f));
+ cam.up = glm::normalize(rot * glm::vec3(0.0f, 1.0f, 0.0f));
+ if (!std::isfinite(cam.front.x) || glm::length(cam.front) < 1e-3f) {
+ cam.front = glm::vec3(0.0f, 0.0f, -1.0f);
+ }
+ if (!std::isfinite(cam.up.x) || glm::length(cam.up) < 1e-3f) {
+ cam.up = glm::vec3(0.0f, 1.0f, 0.0f);
+ }
+ return cam;
+}
+
void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) {
pos = glm::vec3(matrix[3]);
scale.x = glm::length(glm::vec3(matrix[0]));
@@ -204,6 +220,17 @@ void Engine::run() {
continue;
}
+ // Enforce cursor lock state every frame to avoid backends restoring it.
+ int desiredMode = cursorLocked ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL;
+ if (glfwGetInputMode(editorWindow, GLFW_CURSOR) != desiredMode) {
+ glfwSetInputMode(editorWindow, GLFW_CURSOR, desiredMode);
+ if (cursorLocked && glfwRawMouseMotionSupported()) {
+ glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
+ } else if (!cursorLocked && glfwRawMouseMotionSupported()) {
+ glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
+ }
+ }
+
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
@@ -223,6 +250,17 @@ void Engine::run() {
camera.firstMouse = true;
}
+ // Scroll-wheel speed adjustment while freelook is active
+ if (viewportController.isViewportFocused() && cursorLocked) {
+ float wheel = ImGui::GetIO().MouseWheel;
+ if (std::abs(wheel) > 0.0001f) {
+ float factor = std::pow(1.12f, wheel);
+ float ratio = (camera.moveSpeed > 0.001f) ? (camera.sprintSpeed / camera.moveSpeed) : 2.0f;
+ camera.moveSpeed = std::clamp(camera.moveSpeed * factor, 0.5f, 100.0f);
+ camera.sprintSpeed = std::clamp(camera.moveSpeed * ratio, 0.5f, 200.0f);
+ }
+ }
+
if (viewportController.isViewportFocused() && cursorLocked) {
camera.processKeyboard(deltaTime, editorWindow);
}
@@ -234,7 +272,7 @@ void Engine::run() {
glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE);
renderer.beginRender(view, proj, camera.position);
- renderer.renderScene(camera, sceneObjects);
+ renderer.renderScene(camera, sceneObjects, selectedObjectId);
renderer.endRender();
}
@@ -504,23 +542,39 @@ void Engine::handleKeyboardShortcuts() {
ctrlNPressed = false;
}
- if (!cursorLocked) {
+ bool cameraActive = cursorLocked || viewportController.isViewportFocused() && cursorLocked;
+ if (!cameraActive) {
if (ImGui::IsKeyPressed(ImGuiKey_Q)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_W)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) mCurrentGizmoOperation = ImGuizmo::SCALE;
- if (ImGui::IsKeyPressed(ImGuiKey_R)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
+ if (ImGui::IsKeyPressed(ImGuiKey_R)) mCurrentGizmoOperation = ImGuizmo::BOUNDS;
+ if (ImGui::IsKeyPressed(ImGuiKey_T)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
- if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
+ if (ImGui::IsKeyPressed(ImGuiKey_U)) {
mCurrentGizmoMode = (mCurrentGizmoMode == ImGuizmo::LOCAL) ? ImGuizmo::WORLD : ImGuizmo::LOCAL;
}
}
static bool snapPressed = false;
- if (ImGui::IsKeyPressed(ImGuiKey_X) && !snapPressed) {
- useSnap = !useSnap;
- snapPressed = true;
+ static bool snapHeldByCtrl = false;
+ static bool snapStateBeforeCtrl = false;
+
+ if (!snapHeldByCtrl && ctrlDown) {
+ snapStateBeforeCtrl = useSnap;
+ snapHeldByCtrl = true;
+ useSnap = true;
+ } else if (snapHeldByCtrl && !ctrlDown) {
+ useSnap = snapStateBeforeCtrl;
+ snapHeldByCtrl = false;
}
- if (ImGui::IsKeyReleased(ImGuiKey_X)) {
+
+ if (!cameraActive) {
+ if (ImGui::IsKeyPressed(ImGuiKey_Y) && !snapPressed) {
+ useSnap = !useSnap;
+ snapPressed = true;
+ }
+ }
+ if (ImGui::IsKeyReleased(ImGuiKey_Y)) {
snapPressed = false;
}
@@ -544,16 +598,37 @@ void Engine::handleKeyboardShortcuts() {
}
void Engine::OpenProjectPath(const std::string& path) {
- if (projectManager.loadProject(path)) {
- if (!initRenderer()) {
- addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
- } else {
- showLauncher = false;
+ try {
+ if (projectManager.loadProject(path)) {
+ // Make sure project folders exist even for older/minimal projects
+ if (!fs::exists(projectManager.currentProject.assetsPath)) {
+ fs::create_directories(projectManager.currentProject.assetsPath);
+ }
+ if (!fs::exists(projectManager.currentProject.scenesPath)) {
+ fs::create_directories(projectManager.currentProject.scenesPath);
+ }
+
+ if (!initRenderer()) {
+ addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
+ showLauncher = true;
+ return;
+ }
+
loadRecentScenes();
+ fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
+ fileBrowser.currentPath = projectManager.currentProject.assetsPath;
+ fileBrowser.needsRefresh = true;
+ showLauncher = false;
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
+ } else {
+ addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
}
- } else {
- addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
+ } catch (const std::exception& e) {
+ addConsoleMessage(std::string("Exception opening project: ") + e.what(), ConsoleMessageType::Error);
+ showLauncher = true;
+ } catch (...) {
+ addConsoleMessage("Unknown exception opening project", ConsoleMessageType::Error);
+ showLauncher = true;
}
}
@@ -578,6 +653,7 @@ void Engine::createNewProject(const char* name, const char* location) {
addObject(ObjectType::Cube, "Cube");
+ fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
fileBrowser.needsRefresh = true;
@@ -614,6 +690,7 @@ void Engine::loadRecentScenes() {
}
recordState("sceneLoaded");
+ fileBrowser.setProjectRoot(projectManager.currentProject.assetsPath);
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
fileBrowser.needsRefresh = true;
}
@@ -702,6 +779,13 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
sceneObjects.back().light.range = 10.0f;
sceneObjects.back().light.intensity = 3.0f;
sceneObjects.back().light.size = glm::vec2(2.0f, 2.0f);
+ } else if (type == ObjectType::PostFXNode) {
+ sceneObjects.back().postFx.enabled = true;
+ sceneObjects.back().postFx.bloomEnabled = true;
+ sceneObjects.back().postFx.colorAdjustEnabled = true;
+ } else if (type == ObjectType::Camera) {
+ sceneObjects.back().camera.type = SceneCameraType::Player;
+ sceneObjects.back().camera.fov = 60.0f;
}
selectedObjectId = id;
if (projectManager.currentProject.isLoaded) {
@@ -732,6 +816,8 @@ void Engine::duplicateSelected() {
newObj.fragmentShaderPath = it->fragmentShaderPath;
newObj.useOverlay = it->useOverlay;
newObj.light = it->light;
+ newObj.camera = it->camera;
+ newObj.postFx = it->postFx;
sceneObjects.push_back(newObj);
selectedObjectId = id;
diff --git a/src/Engine.h b/src/Engine.h
index e1a70a4..dc5a9ef 100644
--- a/src/Engine.h
+++ b/src/Engine.h
@@ -82,6 +82,10 @@ private:
float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range
bool showEnvironmentWindow = true;
bool showCameraWindow = true;
+ bool isPlaying = false;
+ bool isPaused = false;
+ bool showViewOutput = true;
+ int previewCameraId = -1;
// Private methods
SceneObject* getSelectedObject();
@@ -107,6 +111,7 @@ private:
void renderViewport();
void renderDialogs();
void renderProjectBrowserPanel();
+ Camera makeCameraFromObject(const SceneObject& obj) const;
void renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb();
diff --git a/src/EnginePanels.cpp b/src/EnginePanels.cpp
index 6c9fb1a..ff13bed 100644
--- a/src/EnginePanels.cpp
+++ b/src/EnginePanels.cpp
@@ -1,7 +1,10 @@
#include "Engine.h"
#include "ModelLoader.h"
#include
+#include
#include
+#include
+#include
#ifdef _WIN32
#include
@@ -294,6 +297,12 @@ namespace FileIcons {
void Engine::renderFileBrowserPanel() {
ImGui::Begin("Project", &showFileBrowser);
+ ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4 accent = style.Colors[ImGuiCol_CheckMark];
+ ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
+ toolbarBg.x = std::min(toolbarBg.x + 0.02f, 1.0f);
+ toolbarBg.y = std::min(toolbarBg.y + 0.02f, 1.0f);
+ toolbarBg.z = std::min(toolbarBg.z + 0.02f, 1.0f);
if (fileBrowser.needsRefresh) {
fileBrowser.refresh();
@@ -314,59 +323,56 @@ void Engine::renderFileBrowserPanel() {
default: return IM_COL32(150, 150, 150, 255); // Dark gray
}
};
-
- // === TOOLBAR ===
- ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
-
- // Navigation buttons
+ ImGui::PushStyleColor(ImGuiCol_ChildBg, toolbarBg);
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 3.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 3.0f));
+ ImGui::BeginChild("ProjectToolbar", ImVec2(0, 44), true, ImGuiWindowFlags_NoScrollbar);
+
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 3.0f));
bool canGoBack = fileBrowser.historyIndex > 0;
bool canGoForward = fileBrowser.historyIndex < (int)fileBrowser.pathHistory.size() - 1;
- bool canGoUp = fileBrowser.currentPath != fileBrowser.projectRoot &&
+ bool canGoUp = fileBrowser.currentPath != fileBrowser.projectRoot &&
fileBrowser.currentPath.has_parent_path();
-
+
ImGui::BeginDisabled(!canGoBack);
- if (ImGui::Button("<##Back", ImVec2(24, 0))) {
- fileBrowser.navigateBack();
- }
+ ImGui::Button("<##Back", ImVec2(26, 0));
+ if (ImGui::IsItemActivated()) fileBrowser.navigateBack();
ImGui::EndDisabled();
- if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
- ImGui::SetTooltip("Back");
- }
-
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Back");
+
ImGui::SameLine();
-
ImGui::BeginDisabled(!canGoForward);
- if (ImGui::Button(">##Forward", ImVec2(24, 0))) {
- fileBrowser.navigateForward();
- }
+ ImGui::Button(">##Forward", ImVec2(26, 0));
+ if (ImGui::IsItemActivated()) fileBrowser.navigateForward();
ImGui::EndDisabled();
- if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
- ImGui::SetTooltip("Forward");
- }
-
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Forward");
+
ImGui::SameLine();
-
ImGui::BeginDisabled(!canGoUp);
- if (ImGui::Button("^##Up", ImVec2(24, 0))) {
- fileBrowser.navigateUp();
- }
+ ImGui::Button("^##Up", ImVec2(26, 0));
+ if (ImGui::IsItemActivated()) fileBrowser.navigateUp();
ImGui::EndDisabled();
- if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
- ImGui::SetTooltip("Up one folder");
- }
-
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Up one folder");
+ ImGui::PopStyleVar();
+
ImGui::SameLine();
- ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
- ImGui::SameLine();
-
- // Breadcrumb path
+
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.5f));
-
- fs::path relativePath = fs::relative(fileBrowser.currentPath, fileBrowser.projectRoot);
+
+ fs::path relativePath;
+ if (fileBrowser.projectRoot.empty()) {
+ relativePath = fileBrowser.currentPath.filename();
+ } else {
+ try {
+ relativePath = fs::relative(fileBrowser.currentPath, fileBrowser.projectRoot);
+ } catch (...) {
+ relativePath = fileBrowser.currentPath.filename();
+ }
+ }
std::vector pathParts;
fs::path accumulated = fileBrowser.projectRoot;
-
+
pathParts.push_back(fileBrowser.projectRoot);
for (const auto& part : relativePath) {
if (part != ".") {
@@ -374,68 +380,68 @@ void Engine::renderFileBrowserPanel() {
pathParts.push_back(accumulated);
}
}
-
- for (size_t i = 0; i < pathParts.size(); i++) {
- std::string name = (i == 0) ? "Project" : pathParts[i].filename().string();
+
+ struct Breadcrumb {
+ std::string label;
+ fs::path target;
+ };
+ std::vector crumbs;
+ if (pathParts.size() <= 4) {
+ for (size_t i = 0; i < pathParts.size(); ++i) {
+ std::string name = (i == 0) ? "Project" : pathParts[i].filename().string();
+ crumbs.push_back({name, pathParts[i]});
+ }
+ } else {
+ crumbs.push_back({"Project", pathParts.front()});
+ crumbs.push_back({"..", pathParts[pathParts.size() - 3]});
+ crumbs.push_back({pathParts[pathParts.size() - 2].filename().string(), pathParts[pathParts.size() - 2]});
+ crumbs.push_back({pathParts.back().filename().string(), pathParts.back()});
+ }
+
+ for (size_t i = 0; i < crumbs.size(); i++) {
ImGui::PushID(static_cast(i));
- if (ImGui::SmallButton(name.c_str())) {
- fileBrowser.navigateTo(pathParts[i]);
+ if (ImGui::SmallButton(crumbs[i].label.c_str())) {
+ fileBrowser.navigateTo(crumbs[i].target);
}
ImGui::PopID();
- if (i < pathParts.size() - 1) {
+ if (i < crumbs.size() - 1) {
ImGui::SameLine(0, 2);
ImGui::TextDisabled("/");
ImGui::SameLine(0, 2);
}
}
-
+
ImGui::PopStyleColor(2);
-
- ImGui::PopStyleVar();
-
- // === SECOND ROW: Search, Scale Slider, View Mode ===
- ImGui::Spacing();
-
- // Search box
- ImGui::SetNextItemWidth(150);
+
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(140);
if (ImGui::InputTextWithHint("##Search", "Search...", fileBrowserSearch, sizeof(fileBrowserSearch))) {
fileBrowser.searchFilter = fileBrowserSearch;
fileBrowser.needsRefresh = true;
}
-
+
ImGui::SameLine();
- ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
- ImGui::SameLine();
-
bool isGridMode = fileBrowser.viewMode == FileBrowserViewMode::Grid;
if (isGridMode) {
- ImGui::Text("Size:");
+ ImGui::TextDisabled("Size");
ImGui::SameLine();
- ImGui::SetNextItemWidth(100);
- ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.5f, 2.0f, "%.1fx");
- if (ImGui::IsItemHovered()) {
- ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
- }
- ImGui::SameLine();
- ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
+ ImGui::SetNextItemWidth(90);
+ ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx");
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
ImGui::SameLine();
}
-
- // View mode toggle
- if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(50, 0))) {
+
+ if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
}
- if (ImGui::IsItemHovered()) {
- ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
- }
-
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
+
ImGui::SameLine();
-
- if (ImGui::Button("Refresh", ImVec2(60, 0))) {
+ if (ImGui::Button("Refresh", ImVec2(68, 0))) {
fileBrowser.needsRefresh = true;
}
ImGui::SameLine();
- if (ImGui::Button("New Mat", ImVec2(70, 0))) {
+ if (ImGui::Button("New Mat", ImVec2(78, 0))) {
fs::path target = fileBrowser.currentPath / "NewMaterial.mat";
int counter = 1;
while (fs::exists(target)) {
@@ -446,11 +452,20 @@ void Engine::renderFileBrowserPanel() {
saveMaterialToFile(temp);
fileBrowser.needsRefresh = true;
}
-
- ImGui::Separator();
-
+
+ ImGui::EndChild();
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor();
+
+ ImGui::Spacing();
+
// === FILE CONTENT AREA ===
- ImGui::BeginChild("FileContent", ImVec2(0, 0), false);
+ ImVec4 contentBg = style.Colors[ImGuiCol_WindowBg];
+ contentBg.x = std::min(contentBg.x + 0.01f, 1.0f);
+ contentBg.y = std::min(contentBg.y + 0.01f, 1.0f);
+ contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
+ ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
+ ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
ImDrawList* drawList = ImGui::GetWindowDrawList();
@@ -742,6 +757,7 @@ void Engine::renderFileBrowserPanel() {
}
ImGui::EndChild();
+ ImGui::PopStyleColor();
ImGui::End();
}
@@ -1068,6 +1084,11 @@ void Engine::renderOpenProjectDialog() {
void Engine::renderMainMenuBar() {
if (ImGui::BeginMainMenuBar()) {
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(14.0f, 8.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 6.0f));
+ ImVec4 accent = ImGui::GetStyleColorVec4(ImGuiCol_CheckMark);
+ ImVec4 subtle = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
+
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
showNewSceneDialog = true;
@@ -1109,9 +1130,10 @@ void Engine::renderMainMenuBar() {
ImGui::MenuItem("Inspector", nullptr, &showInspector);
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
ImGui::MenuItem("Console", nullptr, &showConsole);
- ImGui::MenuItem("Project", nullptr, &showProjectBrowser);
+ ImGui::MenuItem("Project Manager", nullptr, &showProjectBrowser);
ImGui::MenuItem("Environment", nullptr, &showEnvironmentWindow);
ImGui::MenuItem("Camera", nullptr, &showCameraWindow);
+ ImGui::MenuItem("View Output", nullptr, &showViewOutput);
ImGui::Separator();
if (ImGui::MenuItem("Fullscreen Viewport", "F11", viewportFullscreen)) {
viewportFullscreen = !viewportFullscreen;
@@ -1123,10 +1145,12 @@ void Engine::renderMainMenuBar() {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
+ if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera");
if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
if (ImGui::MenuItem("Point Light")) addObject(ObjectType::PointLight, "Point Light");
if (ImGui::MenuItem("Spot Light")) addObject(ObjectType::SpotLight, "Spot Light");
if (ImGui::MenuItem("Area Light")) addObject(ObjectType::AreaLight, "Area Light");
+ if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
ImGui::EndMenu();
}
@@ -1137,6 +1161,51 @@ void Engine::renderMainMenuBar() {
ImGui::EndMenu();
}
+ ImGui::Separator();
+ ImGui::TextColored(subtle, "Project");
+ ImGui::SameLine();
+ std::string projectLabel = projectManager.currentProject.name.empty() ?
+ "New Project" : projectManager.currentProject.name;
+ ImGui::TextColored(accent, "%s", projectLabel.c_str());
+ ImGui::SameLine();
+ ImGui::TextColored(subtle, "|");
+ ImGui::SameLine();
+ std::string sceneLabel = projectManager.currentProject.currentSceneName.empty() ?
+ "No Scene Loaded" : projectManager.currentProject.currentSceneName;
+ ImGui::TextUnformatted(sceneLabel.c_str());
+
+ ImGui::SameLine();
+ ImGui::Dummy(ImVec2(14.0f, 0.0f));
+ ImGui::SameLine();
+
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 4.0f));
+ bool playPressed = ImGui::Button(isPlaying ? "Stop" : "Play");
+ ImGui::SameLine(0.0f, 6.0f);
+ bool pausePressed = ImGui::Button(isPaused ? "Resume" : "Pause");
+ ImGui::PopStyleVar();
+
+ if (playPressed) {
+ isPlaying = !isPlaying;
+ if (!isPlaying) {
+ isPaused = false;
+ }
+ }
+ if (pausePressed) {
+ isPaused = !isPaused;
+ if (isPaused) isPlaying = true; // placeholder: pausing implies we’re in play mode
+ }
+
+ float rightX = ImGui::GetWindowWidth() - 220.0f;
+ if (rightX > ImGui::GetCursorPosX()) {
+ ImGui::SameLine(rightX);
+ } else {
+ ImGui::SameLine();
+ }
+ ImGui::TextColored(subtle, "Viewport");
+ ImGui::SameLine();
+ ImGui::TextColored(accent, viewportFullscreen ? "Fullscreen" : "Docked");
+
+ ImGui::PopStyleVar(2);
ImGui::EndMainMenuBar();
}
}
@@ -1145,10 +1214,26 @@ void Engine::renderHierarchyPanel() {
ImGui::Begin("Hierarchy", &showHierarchy);
static char searchBuffer[128] = "";
+ ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4 headerBg = style.Colors[ImGuiCol_MenuBarBg];
+ headerBg.x = std::min(headerBg.x + 0.02f, 1.0f);
+ headerBg.y = std::min(headerBg.y + 0.02f, 1.0f);
+ headerBg.z = std::min(headerBg.z + 0.02f, 1.0f);
+ ImVec4 headerText = style.Colors[ImGuiCol_CheckMark];
+ ImVec4 listBg = style.Colors[ImGuiCol_WindowBg];
+ listBg.x = std::min(listBg.x + 0.01f, 1.0f);
+ listBg.y = std::min(listBg.y + 0.01f, 1.0f);
+ listBg.z = std::min(listBg.z + 0.01f, 1.0f);
+
+ ImGui::PushStyleColor(ImGuiCol_ChildBg, headerBg);
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 4.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
+ ImGui::BeginChild("HierarchyHeader", ImVec2(0, 50), true, ImGuiWindowFlags_NoScrollbar);
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##Search", "Search...", searchBuffer, sizeof(searchBuffer));
-
- ImGui::Separator();
+ ImGui::EndChild();
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor();
std::string filter = searchBuffer;
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
@@ -1161,7 +1246,10 @@ void Engine::renderHierarchyPanel() {
ImGui::EndDragDropTarget();
}
- ImGui::BeginChild("HierarchyList", ImVec2(0, 0), false);
+ ImGui::PushStyleColor(ImGuiCol_ChildBg, listBg);
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.0f, 2.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 2.0f));
+ ImGui::BeginChild("HierarchyList", ImVec2(0, 0), true);
for (size_t i = 0; i < sceneObjects.size(); i++) {
if (sceneObjects[i].parentId != -1)
@@ -1174,20 +1262,42 @@ void Engine::renderHierarchyPanel() {
ImGuiPopupFlags_MouseButtonRight |
ImGuiPopupFlags_NoOpenOverItems))
{
- if (ImGui::BeginMenu("Create")) {
- if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
- if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
- if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
- if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
- if (ImGui::MenuItem("Point Light")) addObject(ObjectType::PointLight, "Point Light");
- if (ImGui::MenuItem("Spot Light")) addObject(ObjectType::SpotLight, "Spot Light");
- if (ImGui::MenuItem("Area Light")) addObject(ObjectType::AreaLight, "Area Light");
+ if (ImGui::BeginMenu("Create"))
+ {
+ // ── Primitives ─────────────────────────────
+ if (ImGui::BeginMenu("Primitives"))
+ {
+ if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
+ if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
+ if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
+ ImGui::EndMenu();
+ }
+
+ // ── Lights ────────────────────────────────
+ if (ImGui::BeginMenu("Lights"))
+ {
+ if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
+ if (ImGui::MenuItem("Point Light")) addObject(ObjectType::PointLight, "Point Light");
+ if (ImGui::MenuItem("Spot Light")) addObject(ObjectType::SpotLight, "Spot Light");
+ if (ImGui::MenuItem("Area Light")) addObject(ObjectType::AreaLight, "Area Light");
+ ImGui::EndMenu();
+ }
+
+ // ── Other / Effects ───────────────────────
+ if (ImGui::BeginMenu("Effects"))
+ {
+ if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
+ ImGui::EndMenu();
+ }
+
ImGui::EndMenu();
}
ImGui::EndPopup();
}
ImGui::EndChild();
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor();
ImGui::End();
}
@@ -1214,10 +1324,12 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
case ObjectType::Capsule: icon = "[|]"; break;
case ObjectType::OBJMesh: icon = "[M]"; break;
case ObjectType::Model: icon = "[A]"; break;
+ case ObjectType::Camera: icon = "(C)"; break;
case ObjectType::DirectionalLight: icon = "(D)"; break;
case ObjectType::PointLight: icon = "(P)"; break;
case ObjectType::SpotLight: icon = "(S)"; break;
case ObjectType::AreaLight: icon = "(L)"; break;
+ case ObjectType::PostFXNode: icon = "(FX)"; break;
}
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
@@ -1524,10 +1636,12 @@ void Engine::renderInspectorPanel() {
case ObjectType::Capsule: typeLabel = "Capsule"; break;
case ObjectType::OBJMesh: typeLabel = "OBJ Mesh"; break;
case ObjectType::Model: typeLabel = "Model"; break;
+ case ObjectType::Camera: typeLabel = "Camera"; break;
case ObjectType::DirectionalLight: typeLabel = "Directional Light"; break;
case ObjectType::PointLight: typeLabel = "Point Light"; break;
case ObjectType::SpotLight: typeLabel = "Spot Light"; break;
case ObjectType::AreaLight: typeLabel = "Area Light"; break;
+ case ObjectType::PostFXNode: typeLabel = "Post FX Node"; break;
}
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeLabel);
@@ -1545,6 +1659,10 @@ void Engine::renderInspectorPanel() {
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
+ if (obj.type == ObjectType::PostFXNode) {
+ ImGui::TextDisabled("Transform is ignored for post-processing nodes.");
+ }
+
ImGui::Text("Position");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Position", &obj.position.x, 0.1f)) {
@@ -1584,8 +1702,104 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor();
+ if (obj.type == ObjectType::Camera) {
+ ImGui::Spacing();
+ ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f));
+ if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
+ ImGui::Indent(10.0f);
+ const char* cameraTypes[] = { "Scene", "Player" };
+ int camType = static_cast(obj.camera.type);
+ if (ImGui::Combo("Type", &camType, cameraTypes, IM_ARRAYSIZE(cameraTypes))) {
+ obj.camera.type = static_cast(camType);
+ projectManager.currentProject.hasUnsavedChanges = true;
+ }
+
+ if (ImGui::SliderFloat("FOV", &obj.camera.fov, 20.0f, 120.0f, "%.0f deg")) {
+ projectManager.currentProject.hasUnsavedChanges = true;
+ }
+ if (ImGui::DragFloat("Near Clip", &obj.camera.nearClip, 0.01f, 0.01f, obj.camera.farClip - 0.01f, "%.3f")) {
+ obj.camera.nearClip = std::max(0.01f, std::min(obj.camera.nearClip, obj.camera.farClip - 0.01f));
+ projectManager.currentProject.hasUnsavedChanges = true;
+ }
+ if (ImGui::DragFloat("Far Clip", &obj.camera.farClip, 0.1f, obj.camera.nearClip + 0.05f, 1000.0f, "%.1f")) {
+ obj.camera.farClip = std::max(obj.camera.nearClip + 0.05f, obj.camera.farClip);
+ projectManager.currentProject.hasUnsavedChanges = true;
+ }
+ ImGui::Unindent(10.0f);
+ }
+ ImGui::PopStyleColor();
+ }
+
+ if (obj.type == ObjectType::PostFXNode) {
+ ImGui::Spacing();
+ ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f));
+ if (ImGui::CollapsingHeader("Post Processing", ImGuiTreeNodeFlags_DefaultOpen)) {
+ ImGui::Indent(10.0f);
+ bool changed = false;
+
+ if (ImGui::Checkbox("Enable Node", &obj.postFx.enabled)) {
+ changed = true;
+ }
+
+ ImGui::Separator();
+ ImGui::TextDisabled("Bloom");
+ if (ImGui::Checkbox("Bloom Enabled", &obj.postFx.bloomEnabled)) {
+ changed = true;
+ }
+ ImGui::BeginDisabled(!obj.postFx.bloomEnabled);
+ if (ImGui::SliderFloat("Threshold", &obj.postFx.bloomThreshold, 0.0f, 3.0f, "%.2f")) {
+ changed = true;
+ }
+ if (ImGui::SliderFloat("Intensity", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) {
+ changed = true;
+ }
+ if (ImGui::SliderFloat("Spread", &obj.postFx.bloomRadius, 0.5f, 3.5f, "%.2f")) {
+ changed = true;
+ }
+ ImGui::EndDisabled();
+
+ ImGui::Separator();
+ ImGui::TextDisabled("Color Adjustments");
+ if (ImGui::Checkbox("Enable Color Adjust", &obj.postFx.colorAdjustEnabled)) {
+ changed = true;
+ }
+ ImGui::BeginDisabled(!obj.postFx.colorAdjustEnabled);
+ if (ImGui::SliderFloat("Exposure (EV)", &obj.postFx.exposure, -5.0f, 5.0f, "%.2f")) {
+ changed = true;
+ }
+ if (ImGui::SliderFloat("Contrast", &obj.postFx.contrast, 0.0f, 2.5f, "%.2f")) {
+ changed = true;
+ }
+ if (ImGui::SliderFloat("Saturation", &obj.postFx.saturation, 0.0f, 2.5f, "%.2f")) {
+ changed = true;
+ }
+ if (ImGui::ColorEdit3("Color Filter", &obj.postFx.colorFilter.x)) {
+ changed = true;
+ }
+ ImGui::EndDisabled();
+
+ ImGui::Separator();
+ ImGui::TextDisabled("Motion Blur");
+ if (ImGui::Checkbox("Enable Motion Blur", &obj.postFx.motionBlurEnabled)) {
+ changed = true;
+ }
+ ImGui::BeginDisabled(!obj.postFx.motionBlurEnabled);
+ if (ImGui::SliderFloat("Strength", &obj.postFx.motionBlurStrength, 0.0f, 0.95f, "%.2f")) {
+ changed = true;
+ }
+ ImGui::EndDisabled();
+
+ if (changed) {
+ projectManager.currentProject.hasUnsavedChanges = true;
+ }
+ ImGui::TextDisabled("Nodes stack in hierarchy order; latest node overrides previous settings.");
+ ImGui::Unindent(10.0f);
+ }
+ ImGui::PopStyleColor();
+ }
+
// Material section (skip for pure light objects)
- if (obj.type != ObjectType::DirectionalLight && obj.type != ObjectType::PointLight && obj.type != ObjectType::SpotLight && obj.type != ObjectType::AreaLight) {
+ if (obj.type != ObjectType::DirectionalLight && obj.type != ObjectType::PointLight && obj.type != ObjectType::SpotLight && obj.type != ObjectType::AreaLight && obj.type != ObjectType::Camera && obj.type != ObjectType::PostFXNode) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f));
@@ -1962,6 +2176,238 @@ void Engine::renderConsolePanel() {
ImGui::End();
}
+namespace GizmoToolbar {
+ enum class Icon {
+ Translate,
+ Rotate,
+ Scale,
+ Bounds,
+ Universal
+ };
+
+ static ImVec4 ScaleColor(const ImVec4& c, float s) {
+ return ImVec4(
+ std::clamp(c.x * s, 0.0f, 1.0f),
+ std::clamp(c.y * s, 0.0f, 1.0f),
+ std::clamp(c.z * s, 0.0f, 1.0f),
+ c.w
+ );
+ }
+
+ static bool TextButton(const char* label, bool active, const ImVec2& size, ImU32 base, ImU32 hover, ImU32 activeCol, ImU32 accent, ImU32 textColor) {
+ ImGui::PushStyleColor(ImGuiCol_Button, active ? accent : base);
+ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, active ? accent : hover);
+ ImGui::PushStyleColor(ImGuiCol_ButtonActive, active ? accent : activeCol);
+ ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(textColor));
+ bool pressed = ImGui::Button(label, size);
+ ImGui::PopStyleColor(4);
+ return pressed;
+ }
+
+ static void DrawTranslateIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
+ float len = (max.x - min.x) * 0.3f;
+ float head = len * 0.5f;
+
+ drawList->AddLine(ImVec2(center.x - len, center.y), ImVec2(center.x + len, center.y), lineColor, 2.4f);
+ drawList->AddLine(ImVec2(center.x, center.y - len), ImVec2(center.x, center.y + len), lineColor, 2.4f);
+
+ drawList->AddTriangleFilled(ImVec2(center.x + len, center.y),
+ ImVec2(center.x + len - head, center.y - head * 0.6f),
+ ImVec2(center.x + len - head, center.y + head * 0.6f),
+ accentColor);
+ drawList->AddTriangleFilled(ImVec2(center.x - len, center.y),
+ ImVec2(center.x - len + head, center.y - head * 0.6f),
+ ImVec2(center.x - len + head, center.y + head * 0.6f),
+ accentColor);
+ drawList->AddTriangleFilled(ImVec2(center.x, center.y - len),
+ ImVec2(center.x - head * 0.6f, center.y - len + head),
+ ImVec2(center.x + head * 0.6f, center.y - len + head),
+ accentColor);
+ drawList->AddTriangleFilled(ImVec2(center.x, center.y + len),
+ ImVec2(center.x - head * 0.6f, center.y + len - head),
+ ImVec2(center.x + head * 0.6f, center.y + len - head),
+ accentColor);
+
+ drawList->AddCircleFilled(center, head * 0.35f, lineColor, 16);
+ }
+
+ static void DrawRotateIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
+ float radius = (max.x - min.x) * 0.28f;
+ float start = -IM_PI * 0.25f;
+ float end = IM_PI * 1.1f;
+
+ drawList->PathArcTo(center, radius, start, end, 32);
+ drawList->PathStroke(lineColor, false, 2.4f);
+
+ ImVec2 arrow = ImVec2(center.x + cosf(end) * radius, center.y + sinf(end) * radius);
+ ImVec2 dir = ImVec2(cosf(end), sinf(end));
+ ImVec2 ortho = ImVec2(-dir.y, dir.x);
+ float head = radius * 0.5f;
+
+ ImVec2 a = ImVec2(arrow.x - dir.x * head + ortho.x * head * 0.55f, arrow.y - dir.y * head + ortho.y * head * 0.55f);
+ ImVec2 b = ImVec2(arrow.x - dir.x * head - ortho.x * head * 0.55f, arrow.y - dir.y * head - ortho.y * head * 0.55f);
+ drawList->AddTriangleFilled(arrow, a, b, accentColor);
+ }
+
+ static void DrawScaleIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ ImVec2 pad = ImVec2((max.x - min.x) * 0.2f, (max.y - min.y) * 0.2f);
+ ImVec2 rMin = ImVec2(min.x + pad.x, min.y + pad.y);
+ ImVec2 rMax = ImVec2(max.x - pad.x, max.y - pad.y);
+
+ drawList->AddRect(rMin, rMax, lineColor, 3.0f, 0, 2.1f);
+
+ ImVec2 center = ImVec2((rMin.x + rMax.x) * 0.5f, (rMin.y + rMax.y) * 0.5f);
+ ImVec2 offsets[] = {
+ ImVec2(-1, -1),
+ ImVec2(1, -1),
+ ImVec2(1, 1),
+ ImVec2(-1, 1)
+ };
+ float arrowLen = pad.x * 0.65f;
+ float head = arrowLen * 0.5f;
+ for (const ImVec2& off : offsets) {
+ ImVec2 dir = ImVec2(off.x * 0.7f, off.y * 0.7f);
+ ImVec2 tip = ImVec2(center.x + dir.x * arrowLen, center.y + dir.y * arrowLen);
+ ImVec2 base = ImVec2(center.x + dir.x * (arrowLen * 0.45f), center.y + dir.y * (arrowLen * 0.45f));
+ ImVec2 ortho = ImVec2(-dir.y, dir.x);
+ ImVec2 a = ImVec2(base.x + ortho.x * head * 0.35f, base.y + ortho.y * head * 0.35f);
+ ImVec2 b = ImVec2(base.x - ortho.x * head * 0.35f, base.y - ortho.y * head * 0.35f);
+ drawList->AddTriangleFilled(tip, a, b, accentColor);
+ drawList->AddLine(center, tip, lineColor, 2.0f);
+ }
+ }
+
+ static void DrawBoundsIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ ImVec2 pad = ImVec2((max.x - min.x) * 0.2f, (max.y - min.y) * 0.22f);
+ ImVec2 rMin = ImVec2(min.x + pad.x, min.y + pad.y);
+ ImVec2 rMax = ImVec2(max.x - pad.x, max.y - pad.y);
+
+ drawList->AddRect(rMin, rMax, lineColor, 4.0f, 0, 2.0f);
+
+ float handle = pad.x * 0.6f;
+ ImVec2 handles[] = {
+ rMin,
+ ImVec2((rMin.x + rMax.x) * 0.5f, rMin.y),
+ ImVec2(rMax.x, rMin.y),
+ ImVec2(rMax.x, (rMin.y + rMax.y) * 0.5f),
+ rMax,
+ ImVec2((rMin.x + rMax.x) * 0.5f, rMax.y),
+ ImVec2(rMin.x, rMax.y),
+ ImVec2(rMin.x, (rMin.y + rMax.y) * 0.5f)
+ };
+
+ for (const ImVec2& h : handles) {
+ drawList->AddRectFilled(
+ ImVec2(h.x - handle * 0.32f, h.y - handle * 0.32f),
+ ImVec2(h.x + handle * 0.32f, h.y + handle * 0.32f),
+ accentColor,
+ 4.0f
+ );
+ }
+ }
+
+ static void DrawUniversalIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
+ float radius = (max.x - min.x) * 0.28f;
+
+ drawList->AddCircle(center, radius, lineColor, 20, 2.0f);
+
+ float len = radius * 0.95f;
+ ImVec2 axes[] = {
+ ImVec2(1, 0), ImVec2(-1, 0), ImVec2(0, 1), ImVec2(0, -1)
+ };
+ float head = radius * 0.45f;
+ for (const ImVec2& dir : axes) {
+ ImVec2 tip = ImVec2(center.x + dir.x * len, center.y + dir.y * len);
+ drawList->AddLine(center, tip, accentColor, 2.0f);
+ ImVec2 ortho = ImVec2(-dir.y, dir.x);
+ ImVec2 a = ImVec2(tip.x - dir.x * head + ortho.x * head * 0.35f, tip.y - dir.y * head + ortho.y * head * 0.35f);
+ ImVec2 b = ImVec2(tip.x - dir.x * head - ortho.x * head * 0.35f, tip.y - dir.y * head - ortho.y * head * 0.35f);
+ drawList->AddTriangleFilled(tip, a, b, accentColor);
+ }
+
+ drawList->AddCircleFilled(center, radius * 0.24f, lineColor, 16);
+ }
+
+ static void DrawIcon(Icon icon, ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
+ switch (icon) {
+ case Icon::Translate: DrawTranslateIcon(drawList, min, max, lineColor, accentColor); break;
+ case Icon::Rotate: DrawRotateIcon(drawList, min, max, lineColor, accentColor); break;
+ case Icon::Scale: DrawScaleIcon(drawList, min, max, lineColor, accentColor); break;
+ case Icon::Bounds: DrawBoundsIcon(drawList, min, max, lineColor, accentColor); break;
+ case Icon::Universal: DrawUniversalIcon(drawList, min, max, lineColor, accentColor); break;
+ }
+ }
+
+ static bool IconButton(const char* id, Icon icon, bool active, const ImVec2& size,
+ ImU32 baseColor, ImU32 hoverColor, ImU32 activeColor,
+ ImU32 accentColor, ImU32 iconColor) {
+ ImGui::PushID(id);
+ ImGui::InvisibleButton("##btn", size);
+ bool hovered = ImGui::IsItemHovered();
+ bool pressed = ImGui::IsItemClicked();
+ ImVec2 min = ImGui::GetItemRectMin();
+ ImVec2 max = ImGui::GetItemRectMax();
+ float rounding = 9.0f;
+
+ ImDrawList* drawList = ImGui::GetWindowDrawList();
+ ImU32 bg = active ? activeColor : (hovered ? hoverColor : baseColor);
+
+ ImVec4 bgCol = ImGui::ColorConvertU32ToFloat4(bg);
+ ImU32 top = ImGui::GetColorU32(ScaleColor(bgCol, 1.07f));
+ ImU32 bottom = ImGui::GetColorU32(ScaleColor(bgCol, 0.93f));
+ drawList->AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
+ drawList->AddRect(min, max, ImGui::GetColorU32(ImVec4(1, 1, 1, active ? 0.35f : 0.18f)), rounding);
+
+ DrawIcon(icon, drawList, min, max, iconColor, accentColor);
+
+ ImGui::PopID();
+ return pressed;
+ }
+
+ static bool TextButton(const char* id, const char* label, bool active, const ImVec2& size,
+ ImU32 baseColor, ImU32 hoverColor, ImU32 activeColor, ImU32 borderColor, ImVec4 textColor) {
+ ImGui::PushID(id);
+ ImGui::InvisibleButton("##btn", size);
+ bool hovered = ImGui::IsItemHovered();
+ bool pressed = ImGui::IsItemClicked();
+ ImVec2 min = ImGui::GetItemRectMin();
+ ImVec2 max = ImGui::GetItemRectMax();
+ float rounding = 8.0f;
+
+ ImDrawList* drawList = ImGui::GetWindowDrawList();
+ ImU32 bg = active ? activeColor : (hovered ? hoverColor : baseColor);
+
+ ImVec4 bgCol = ImGui::ColorConvertU32ToFloat4(bg);
+ ImU32 top = ImGui::GetColorU32(ScaleColor(bgCol, 1.06f));
+ ImU32 bottom = ImGui::GetColorU32(ScaleColor(bgCol, 0.94f));
+ drawList->AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
+ drawList->AddRect(min, max, borderColor, rounding);
+
+ ImVec2 textSize = ImGui::CalcTextSize(label);
+ ImVec2 textPos = ImVec2(
+ min.x + (size.x - textSize.x) * 0.5f,
+ min.y + (size.y - textSize.y) * 0.5f - 1.0f
+ );
+ drawList->AddText(textPos, ImGui::GetColorU32(textColor), label);
+
+ ImGui::PopID();
+ return pressed;
+ }
+
+ static bool ModeButton(const char* label, bool active, const ImVec2& size, ImVec4 baseColor, ImVec4 activeColor, ImVec4 textColor) {
+ ImGui::PushStyleColor(ImGuiCol_Button, active ? activeColor : baseColor);
+ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, active ? activeColor : baseColor);
+ ImGui::PushStyleColor(ImGuiCol_ButtonActive, active ? activeColor : baseColor);
+ ImGui::PushStyleColor(ImGuiCol_Text, textColor);
+ bool pressed = ImGui::Button(label, size);
+ ImGui::PopStyleColor(4);
+ return pressed;
+ }
+}
+
void Engine::renderViewport() {
ImGuiWindowFlags viewportFlags = ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar;
@@ -1979,7 +2425,7 @@ void Engine::renderViewport() {
ImVec2 fullAvail = ImGui::GetContentRegionAvail();
- const float toolbarHeight = 32.0f;
+ const float toolbarHeight = 0.0f;
ImVec2 imageSize = fullAvail;
imageSize.y = ImMax(1.0f, imageSize.y - toolbarHeight);
@@ -2003,7 +2449,7 @@ void Engine::renderViewport() {
glm::mat4 view = camera.getViewMatrix();
renderer.beginRender(view, proj, camera.position);
- renderer.renderScene(camera, sceneObjects);
+ renderer.renderScene(camera, sceneObjects, selectedObjectId);
unsigned int tex = renderer.getViewportTexture();
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
@@ -2012,8 +2458,18 @@ void Engine::renderViewport() {
ImVec2 imageMax = ImGui::GetItemRectMax();
mouseOverViewportImage = ImGui::IsItemHovered();
+ auto projectToScreen = [&](const glm::vec3& p) -> std::optional {
+ glm::vec4 clip = proj * view * glm::vec4(p, 1.0f);
+ if (clip.w <= 0.0f) return std::nullopt;
+ glm::vec3 ndc = glm::vec3(clip) / clip.w;
+ ImVec2 screen;
+ screen.x = imageMin.x + (ndc.x * 0.5f + 0.5f) * (imageMax.x - imageMin.x);
+ screen.y = imageMin.y + (1.0f - (ndc.y * 0.5f + 0.5f)) * (imageMax.y - imageMin.y);
+ return screen;
+ };
+
SceneObject* selectedObj = getSelectedObject();
- if (selectedObj) {
+ if (selectedObj && selectedObj->type != ObjectType::PostFXNode) {
ImGuizmo::BeginFrame();
ImGuizmo::Enable(true);
ImGuizmo::SetOrthographic(false);
@@ -2044,6 +2500,63 @@ void Engine::renderViewport() {
}
}
+ glm::vec3 gizmoBoundsMin(-0.5f);
+ glm::vec3 gizmoBoundsMax(0.5f);
+
+ switch (selectedObj->type) {
+ case ObjectType::Cube:
+ gizmoBoundsMin = glm::vec3(-0.5f);
+ gizmoBoundsMax = glm::vec3(0.5f);
+ break;
+ case ObjectType::Sphere:
+ gizmoBoundsMin = glm::vec3(-0.5f);
+ gizmoBoundsMax = glm::vec3(0.5f);
+ break;
+ case ObjectType::Capsule:
+ gizmoBoundsMin = glm::vec3(-0.35f, -0.9f, -0.35f);
+ gizmoBoundsMax = glm::vec3(0.35f, 0.9f, 0.35f);
+ break;
+ case ObjectType::OBJMesh: {
+ const auto* info = g_objLoader.getMeshInfo(selectedObj->meshId);
+ if (info && info->boundsMin.x < info->boundsMax.x) {
+ gizmoBoundsMin = info->boundsMin;
+ gizmoBoundsMax = info->boundsMax;
+ }
+ break;
+ }
+ case ObjectType::Model: {
+ const auto* info = getModelLoader().getMeshInfo(selectedObj->meshId);
+ if (info && info->boundsMin.x < info->boundsMax.x) {
+ gizmoBoundsMin = info->boundsMin;
+ gizmoBoundsMax = info->boundsMax;
+ }
+ break;
+ }
+ case ObjectType::Camera:
+ gizmoBoundsMin = glm::vec3(-0.3f);
+ gizmoBoundsMax = glm::vec3(0.3f);
+ break;
+ case ObjectType::DirectionalLight:
+ case ObjectType::PointLight:
+ case ObjectType::SpotLight:
+ case ObjectType::AreaLight:
+ gizmoBoundsMin = glm::vec3(-0.3f);
+ gizmoBoundsMax = glm::vec3(0.3f);
+ break;
+ case ObjectType::PostFXNode:
+ gizmoBoundsMin = glm::vec3(-0.25f);
+ gizmoBoundsMax = glm::vec3(0.25f);
+ break;
+ }
+
+ float bounds[6] = {
+ gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z,
+ gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z
+ };
+ float boundsSnap[3] = { snapValue[0], snapValue[1], snapValue[2] };
+ const float* boundsPtr = (mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? bounds : nullptr;
+ const float* boundsSnapPtr = (useSnap && mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? boundsSnap : nullptr;
+
ImGuizmo::Manipulate(
glm::value_ptr(view),
glm::value_ptr(proj),
@@ -2051,9 +2564,44 @@ void Engine::renderViewport() {
mCurrentGizmoMode,
glm::value_ptr(modelMatrix),
nullptr,
- snapPtr
+ snapPtr,
+ boundsPtr,
+ boundsSnapPtr
);
+ std::array corners = {
+ glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
+ glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
+ glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
+ glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
+ glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
+ glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
+ glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
+ glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
+ };
+
+ std::array projected{};
+ bool allProjected = true;
+ for (size_t i = 0; i < corners.size(); ++i) {
+ glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(corners[i], 1.0f));
+ auto p = projectToScreen(world);
+ if (!p.has_value()) { allProjected = false; break; }
+ projected[i] = *p;
+ }
+
+ if (allProjected) {
+ ImDrawList* dl = ImGui::GetWindowDrawList();
+ ImU32 col = ImGui::GetColorU32(ImVec4(1.0f, 0.93f, 0.35f, 0.45f));
+ const int edges[12][2] = {
+ {0,1},{1,2},{2,3},{3,0},
+ {4,5},{5,6},{6,7},{7,4},
+ {0,4},{1,5},{2,6},{3,7}
+ };
+ for (auto& e : edges) {
+ dl->AddLine(projected[e[0]], projected[e[1]], col, 2.0f);
+ }
+ }
+
if (ImGuizmo::IsUsing()) {
if (!gizmoHistoryCaptured) {
recordState("gizmo");
@@ -2072,26 +2620,128 @@ void Engine::renderViewport() {
}
}
+ auto drawCameraDirection = [&](const SceneObject& camObj) {
+ glm::quat q = glm::quat(glm::radians(camObj.rotation));
+ glm::mat3 rot = glm::mat3_cast(q);
+ glm::vec3 forward = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f));
+ glm::vec3 upDir = glm::normalize(rot * glm::vec3(0.0f, 1.0f, 0.0f));
+ if (!std::isfinite(forward.x) || glm::length(forward) < 1e-3f) return;
+
+ auto start = projectToScreen(camObj.position);
+ auto end = projectToScreen(camObj.position + forward * 1.4f);
+ auto upTip = projectToScreen(camObj.position + upDir * 0.6f);
+ if (start && end) {
+ ImDrawList* dl = ImGui::GetWindowDrawList();
+ ImU32 lineCol = ImGui::GetColorU32(ImVec4(0.3f, 0.8f, 1.0f, 0.9f));
+ ImU32 headCol = ImGui::GetColorU32(ImVec4(0.9f, 1.0f, 1.0f, 0.95f));
+ dl->AddLine(*start, *end, lineCol, 2.5f);
+ ImVec2 dir = ImVec2(end->x - start->x, end->y - start->y);
+ float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
+ if (len > 1.0f) {
+ ImVec2 normDir = ImVec2(dir.x / len, dir.y / len);
+ ImVec2 left = ImVec2(-normDir.y, normDir.x);
+ float head = 10.0f;
+ ImVec2 tip = *end;
+ ImVec2 p1 = ImVec2(tip.x - normDir.x * head + left.x * head * 0.6f, tip.y - normDir.y * head + left.y * head * 0.6f);
+ ImVec2 p2 = ImVec2(tip.x - normDir.x * head - left.x * head * 0.6f, tip.y - normDir.y * head - left.y * head * 0.6f);
+ dl->AddTriangleFilled(tip, p1, p2, headCol);
+ }
+ if (upTip) {
+ dl->AddCircleFilled(*upTip, 3.0f, ImGui::GetColorU32(ImVec4(0.8f, 1.0f, 0.6f, 0.8f)));
+ }
+ }
+ };
+
+ for (const auto& obj : sceneObjects) {
+ if (obj.type == ObjectType::Camera) {
+ drawCameraDirection(obj);
+ }
+ }
+
// Toolbar
- ImGui::SetCursorPos(ImVec2(10, imageSize.y + 6));
+ const float toolbarPadding = 6.0f;
+ const float toolbarSpacing = 5.0f;
+ const ImVec2 gizmoButtonSize(60.0f, 24.0f);
+ const float toolbarWidthEstimate = 520.0f;
+ const float toolbarHeightEstimate = 42.0f; // rough height to keep toolbar on-screen when anchoring bottom
+ ImVec2 desiredBottomLeft = ImVec2(imageMin.x + 12.0f, imageMax.y - 12.0f);
- if (ImGui::RadioButton("Move", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
- ImGui::SameLine();
- if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
- ImGui::SameLine();
- if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) mCurrentGizmoOperation = ImGuizmo::SCALE;
- ImGui::SameLine();
- if (ImGui::RadioButton("Uni", mCurrentGizmoOperation == ImGuizmo::UNIVERSAL)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
+ float minX = imageMin.x + 12.0f;
+ float maxX = imageMax.x - 12.0f;
+ float toolbarLeft = desiredBottomLeft.x;
+ if (toolbarLeft + toolbarWidthEstimate > maxX) toolbarLeft = maxX - toolbarWidthEstimate;
+ if (toolbarLeft < minX) toolbarLeft = minX;
- ImGui::SameLine();
- ImGui::Text("|");
- ImGui::SameLine();
+ float minY = imageMin.y + 12.0f;
+ float toolbarTop = desiredBottomLeft.y - toolbarHeightEstimate;
+ if (toolbarTop < minY) toolbarTop = minY;
- if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) mCurrentGizmoMode = ImGuizmo::LOCAL;
- ImGui::SameLine();
- if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) mCurrentGizmoMode = ImGuizmo::WORLD;
+ ImVec2 toolbarPos = ImVec2(toolbarLeft, toolbarTop);
- ImGui::SameLine();
+ const ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4 bgCol = style.Colors[ImGuiCol_PopupBg];
+ bgCol.w = 0.78f;
+ ImVec4 baseCol = style.Colors[ImGuiCol_FrameBg];
+ baseCol.w = 0.85f;
+ ImVec4 hoverCol = style.Colors[ImGuiCol_ButtonHovered];
+ hoverCol.w = 0.95f;
+ ImVec4 activeCol = style.Colors[ImGuiCol_ButtonActive];
+ activeCol.w = 1.0f;
+ ImVec4 accentCol = style.Colors[ImGuiCol_HeaderActive];
+ accentCol.w = 1.0f;
+ ImVec4 textCol = style.Colors[ImGuiCol_Text];
+
+ ImU32 baseBtn = ImGui::GetColorU32(baseCol);
+ ImU32 hoverBtn = ImGui::GetColorU32(GizmoToolbar::ScaleColor(hoverCol, 1.05f));
+ ImU32 activeBtn = ImGui::GetColorU32(GizmoToolbar::ScaleColor(activeCol, 1.08f));
+ ImU32 accent = ImGui::GetColorU32(accentCol);
+ ImU32 iconColor = ImGui::GetColorU32(ImVec4(0.95f, 0.98f, 1.0f, 0.95f));
+ ImU32 toolbarBg = ImGui::GetColorU32(bgCol);
+ ImU32 toolbarOutline = ImGui::GetColorU32(ImVec4(1, 1, 1, 0.0f));
+
+ ImDrawList* toolbarDrawList = ImGui::GetWindowDrawList();
+ ImDrawListSplitter splitter;
+ splitter.Split(toolbarDrawList, 2);
+ splitter.SetCurrentChannel(toolbarDrawList, 1);
+
+ ImVec2 contentStart = ImVec2(toolbarPos.x + toolbarPadding, toolbarPos.y + toolbarPadding);
+ ImVec2 windowPos = ImGui::GetWindowPos();
+ ImVec2 contentStartLocal = ImVec2(contentStart.x - windowPos.x, contentStart.y - windowPos.y);
+ ImGui::SetCursorPos(contentStartLocal);
+ ImVec2 contentStartScreen = ImGui::GetCursorScreenPos();
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(toolbarSpacing, toolbarSpacing));
+ ImGui::BeginGroup();
+
+ auto gizmoButton = [&](const char* label, ImGuizmo::OPERATION op, const char* tooltip) {
+ if (GizmoToolbar::TextButton(label, mCurrentGizmoOperation == op, gizmoButtonSize, baseBtn, hoverBtn, activeBtn, accent, iconColor)) {
+ mCurrentGizmoOperation = op;
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("%s", tooltip);
+ }
+ };
+
+ gizmoButton("Move", ImGuizmo::TRANSLATE, "Translate");
+ ImGui::SameLine(0.0f, toolbarSpacing);
+ gizmoButton("Rotate", ImGuizmo::ROTATE, "Rotate");
+ ImGui::SameLine(0.0f, toolbarSpacing);
+ gizmoButton("Scale", ImGuizmo::SCALE, "Scale");
+ ImGui::SameLine(0.0f, toolbarSpacing);
+ gizmoButton("Rect", ImGuizmo::BOUNDS, "Rect scale");
+ ImGui::SameLine(0.0f, toolbarSpacing);
+ gizmoButton("Uni", ImGuizmo::UNIVERSAL, "Universal");
+
+ ImGui::SameLine(0.0f, toolbarSpacing * 1.25f);
+ ImVec2 modeSize(56.0f, 24.0f);
+ if (GizmoToolbar::ModeButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL, modeSize, baseCol, accentCol, textCol)) {
+ mCurrentGizmoMode = ImGuizmo::LOCAL;
+ }
+ ImGui::SameLine(0.0f, toolbarSpacing * 0.8f);
+ if (GizmoToolbar::ModeButton("World", mCurrentGizmoMode == ImGuizmo::WORLD, modeSize, baseCol, accentCol, textCol)) {
+ mCurrentGizmoMode = ImGuizmo::WORLD;
+ }
+
+ ImGui::SameLine(0.0f, toolbarSpacing);
ImGui::Checkbox("Snap", &useSnap);
if (useSnap) {
@@ -2105,6 +2755,20 @@ void Engine::renderViewport() {
}
}
+ ImGui::EndGroup();
+ ImGui::PopStyleVar();
+
+ ImVec2 groupMax = ImGui::GetItemRectMax();
+
+ splitter.SetCurrentChannel(toolbarDrawList, 0);
+ float rounding = 10.0f;
+ ImVec2 bgMin = ImVec2(contentStartScreen.x - toolbarPadding, contentStartScreen.y - toolbarPadding);
+ ImVec2 bgMax = ImVec2(groupMax.x + toolbarPadding, groupMax.y + toolbarPadding);
+ toolbarDrawList->AddRectFilled(bgMin, bgMax, toolbarBg, rounding, ImDrawFlags_RoundCornersAll);
+ toolbarDrawList->AddRect(bgMin, bgMax, toolbarOutline, rounding, ImDrawFlags_RoundCornersAll, 1.5f);
+
+ splitter.Merge(toolbarDrawList);
+
// Left-click picking inside viewport
if (mouseOverViewportImage &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
@@ -2163,6 +2827,26 @@ void Engine::renderViewport() {
return true;
};
+ auto rayTriangle = [](const glm::vec3& orig, const glm::vec3& dir, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& tHit) {
+ const float EPSILON = 1e-6f;
+ glm::vec3 e1 = v1 - v0;
+ glm::vec3 e2 = v2 - v0;
+ glm::vec3 pvec = glm::cross(dir, e2);
+ float det = glm::dot(e1, pvec);
+ if (fabs(det) < EPSILON) return false;
+ float invDet = 1.0f / det;
+ glm::vec3 tvec = orig - v0;
+ float u = glm::dot(tvec, pvec) * invDet;
+ if (u < 0.0f || u > 1.0f) return false;
+ glm::vec3 qvec = glm::cross(tvec, e1);
+ float v = glm::dot(dir, qvec) * invDet;
+ if (v < 0.0f || u + v > 1.0f) return false;
+ float t = glm::dot(e2, qvec) * invDet;
+ if (t < 0.0f) return false;
+ tHit = t;
+ return true;
+ };
+
auto ray = makeRay(mousePos);
float closest = FLT_MAX;
int hitId = -1;
@@ -2200,7 +2884,24 @@ void Engine::renderViewport() {
aabbMin = info->boundsMin;
aabbMax = info->boundsMax;
}
- hit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
+ bool aabbHit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
+ if (aabbHit && info && !info->triangleVertices.empty()) {
+ float triBest = FLT_MAX;
+ for (size_t i = 0; i + 2 < info->triangleVertices.size(); i += 3) {
+ float triT = 0.0f;
+ if (rayTriangle(localOrigin, localDir, info->triangleVertices[i], info->triangleVertices[i + 1], info->triangleVertices[i + 2], triT)) {
+ if (triT < triBest && triT >= 0.0f) triBest = triT;
+ }
+ }
+ if (triBest < FLT_MAX) {
+ hit = true;
+ hitT = triBest;
+ } else {
+ hit = false;
+ }
+ } else {
+ hit = aabbHit;
+ }
break;
}
case ObjectType::Model: {
@@ -2209,15 +2910,38 @@ void Engine::renderViewport() {
aabbMin = info->boundsMin;
aabbMax = info->boundsMax;
}
- hit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
+ bool aabbHit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
+ if (aabbHit && info && !info->triangleVertices.empty()) {
+ float triBest = FLT_MAX;
+ for (size_t i = 0; i + 2 < info->triangleVertices.size(); i += 3) {
+ float triT = 0.0f;
+ if (rayTriangle(localOrigin, localDir, info->triangleVertices[i], info->triangleVertices[i + 1], info->triangleVertices[i + 2], triT)) {
+ if (triT < triBest && triT >= 0.0f) triBest = triT;
+ }
+ }
+ if (triBest < FLT_MAX) {
+ hit = true;
+ hitT = triBest;
+ } else {
+ hit = false;
+ }
+ } else {
+ hit = aabbHit;
+ }
break;
}
+ case ObjectType::Camera:
+ hit = raySphere(localOrigin, localDir, 0.3f, hitT);
+ break;
case ObjectType::DirectionalLight:
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight:
hit = raySphere(localOrigin, localDir, 0.3f, hitT);
break;
+ case ObjectType::PostFXNode:
+ hit = false;
+ break;
}
if (hit && hitT < closest && hitT >= 0.0f) {
@@ -2238,17 +2962,85 @@ void Engine::renderViewport() {
viewportController.setFocused(true);
cursorLocked = true;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
+ if (glfwRawMouseMotionSupported()) {
+ glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
+ }
camera.firstMouse = true;
}
if (cursorLocked && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
cursorLocked = false;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
+ if (glfwRawMouseMotionSupported()) {
+ glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
+ }
camera.firstMouse = true;
}
if (cursorLocked) {
viewportController.setFocused(true);
}
+
+ if (isPlaying && showViewOutput) {
+ std::vector playerCams;
+ for (const auto& obj : sceneObjects) {
+ if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) {
+ playerCams.push_back(&obj);
+ }
+ }
+
+ if (playerCams.empty()) {
+ previewCameraId = -1;
+ } else {
+ auto findCamById = [&](int id) -> const SceneObject* {
+ auto it = std::find_if(playerCams.begin(), playerCams.end(), [id](const SceneObject* o) { return o->id == id; });
+ return (it != playerCams.end()) ? *it : nullptr;
+ };
+ const SceneObject* previewCam = findCamById(previewCameraId);
+ if (!previewCam) {
+ previewCam = playerCams.front();
+ previewCameraId = previewCam->id;
+ }
+
+ int previewWidth = static_cast(imageSize.x * 0.28f);
+ previewWidth = std::clamp(previewWidth, 180, 420);
+ int previewHeight = static_cast(previewWidth / 16.0f * 9.0f);
+ unsigned int previewTex = renderer.renderScenePreview(
+ makeCameraFromObject(*previewCam),
+ sceneObjects,
+ previewWidth,
+ previewHeight,
+ previewCam->camera.fov,
+ previewCam->camera.nearClip,
+ previewCam->camera.farClip
+ );
+
+ if (previewTex != 0) {
+ ImVec2 overlaySize(previewWidth + 20.0f, previewHeight + 64.0f);
+ ImVec2 overlayPos = ImVec2(imageMax.x - overlaySize.x - 12.0f, imageMax.y - overlaySize.y - 12.0f);
+ ImVec2 winPos = ImGui::GetWindowPos();
+ ImVec2 localPos = ImVec2(overlayPos.x - winPos.x, overlayPos.y - winPos.y);
+ ImGui::SetCursorPos(localPos);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f));
+ ImGui::BeginChild("ViewOutputOverlay", overlaySize, true, ImGuiWindowFlags_NoScrollbar);
+ ImGui::TextDisabled("View Output");
+ if (ImGui::BeginCombo("##ViewOutputCamera", previewCam->name.c_str())) {
+ for (const auto* cam : playerCams) {
+ bool selected = cam->id == previewCameraId;
+ if (ImGui::Selectable(cam->name.c_str(), selected)) {
+ previewCameraId = cam->id;
+ }
+ if (selected) ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+ ImGui::Image((void*)(intptr_t)previewTex, ImVec2((float)previewWidth, (float)previewHeight), ImVec2(0, 1), ImVec2(1, 0));
+ ImGui::EndChild();
+ ImGui::PopStyleVar();
+ }
+ }
+ } else {
+ previewCameraId = -1;
+ }
}
// Overlay hint
@@ -2432,11 +3224,21 @@ void Engine::renderDialogs() {
}
void Engine::renderProjectBrowserPanel() {
- ImGui::Begin("Project", &showProjectBrowser);
+ ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f);
+ ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f);
+ ImGui::PushStyleColor(ImGuiCol_Header, headerCol);
+ ImGui::PushStyleColor(ImGuiCol_HeaderHovered, headerColActive);
+ ImGui::PushStyleColor(ImGuiCol_HeaderActive, headerColActive);
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
+
+ ImGui::Begin("Project Manager", &showProjectBrowser);
if (!projectManager.currentProject.isLoaded) {
ImGui::TextDisabled("No project loaded");
ImGui::End();
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor(3);
return;
}
@@ -2547,6 +3349,8 @@ void Engine::renderProjectBrowserPanel() {
}
ImGui::End();
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor(3);
}
void Engine::renderEnvironmentWindow() {
diff --git a/src/ModelLoader.cpp b/src/ModelLoader.cpp
index a426ac0..d544a72 100644
--- a/src/ModelLoader.cpp
+++ b/src/ModelLoader.cpp
@@ -119,6 +119,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
glm::vec3 boundsMin(FLT_MAX);
glm::vec3 boundsMax(-FLT_MAX);
+ std::vector triPositions;
// Process all meshes in the scene
std::vector vertices;
@@ -128,7 +129,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
result.hasTangents = false;
// Process the root node recursively
- processNode(scene->mRootNode, scene, vertices, boundsMin, boundsMax);
+ processNode(scene->mRootNode, scene, aiMatrix4x4(), vertices, triPositions, boundsMin, boundsMax);
// Check mesh properties
for (unsigned int i = 0; i < scene->mNumMeshes; i++) {
@@ -157,6 +158,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
loaded.boundsMin = boundsMin;
loaded.boundsMax = boundsMax;
+ loaded.triangleVertices = std::move(triPositions);
loadedMeshes.push_back(std::move(loaded));
@@ -171,20 +173,33 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
return result;
}
-void ModelLoader::processNode(aiNode* node, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax) {
+static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
+ return glm::mat4(
+ m.a1, m.b1, m.c1, m.d1,
+ m.a2, m.b2, m.c2, m.d2,
+ m.a3, m.b3, m.c3, m.d3,
+ m.a4, m.b4, m.c4, m.d4
+ );
+}
+
+void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, std::vector& vertices, std::vector& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax) {
+ aiMatrix4x4 currentTransform = parentTransform * node->mTransformation;
// Process all meshes in this node
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
- processMesh(mesh, scene, vertices, boundsMin, boundsMax);
+ processMesh(mesh, currentTransform, vertices, triPositions, boundsMin, boundsMax);
}
// Process children nodes
for (unsigned int i = 0; i < node->mNumChildren; i++) {
- processNode(node->mChildren[i], scene, vertices, boundsMin, boundsMax);
+ processNode(node->mChildren[i], scene, currentTransform, vertices, triPositions, boundsMin, boundsMax);
}
}
-void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax) {
+void ModelLoader::processMesh(aiMesh* mesh, const aiMatrix4x4& transform, std::vector& vertices, std::vector& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax) {
+ glm::mat4 gTransform = aiToGlm(transform);
+ glm::mat3 normalMat = glm::transpose(glm::inverse(glm::mat3(gTransform)));
+
// Process each face
for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
@@ -193,23 +208,34 @@ void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vectormVertices[index].x);
- vertices.push_back(mesh->mVertices[index].y);
- vertices.push_back(mesh->mVertices[index].z);
+ glm::vec3 pos(mesh->mVertices[index].x,
+ mesh->mVertices[index].y,
+ mesh->mVertices[index].z);
+ glm::vec4 transformed = gTransform * glm::vec4(pos, 1.0f);
+ glm::vec3 finalPos = glm::vec3(transformed) / (transformed.w == 0.0f ? 1.0f : transformed.w);
- boundsMin.x = std::min(boundsMin.x, mesh->mVertices[index].x);
- boundsMin.y = std::min(boundsMin.y, mesh->mVertices[index].y);
- boundsMin.z = std::min(boundsMin.z, mesh->mVertices[index].z);
- boundsMax.x = std::max(boundsMax.x, mesh->mVertices[index].x);
- boundsMax.y = std::max(boundsMax.y, mesh->mVertices[index].y);
- boundsMax.z = std::max(boundsMax.z, mesh->mVertices[index].z);
+ vertices.push_back(finalPos.x);
+ vertices.push_back(finalPos.y);
+ vertices.push_back(finalPos.z);
+
+ triPositions.push_back(finalPos);
+
+ boundsMin.x = std::min(boundsMin.x, finalPos.x);
+ boundsMin.y = std::min(boundsMin.y, finalPos.y);
+ boundsMin.z = std::min(boundsMin.z, finalPos.z);
+ boundsMax.x = std::max(boundsMax.x, finalPos.x);
+ boundsMax.y = std::max(boundsMax.y, finalPos.y);
+ boundsMax.z = std::max(boundsMax.z, finalPos.z);
// Normal
if (mesh->mNormals) {
- vertices.push_back(mesh->mNormals[index].x);
- vertices.push_back(mesh->mNormals[index].y);
- vertices.push_back(mesh->mNormals[index].z);
+ glm::vec3 n(mesh->mNormals[index].x,
+ mesh->mNormals[index].y,
+ mesh->mNormals[index].z);
+ n = glm::normalize(normalMat * n);
+ vertices.push_back(n.x);
+ vertices.push_back(n.y);
+ vertices.push_back(n.z);
} else {
vertices.push_back(0.0f);
vertices.push_back(1.0f);
diff --git a/src/ModelLoader.h b/src/ModelLoader.h
index a11255f..318c09a 100644
--- a/src/ModelLoader.h
+++ b/src/ModelLoader.h
@@ -64,8 +64,8 @@ private:
ModelLoader& operator=(const ModelLoader&) = delete;
// Process Assimp scene
- void processNode(aiNode* node, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax);
- void processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax);
+ void processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, std::vector& vertices, std::vector& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax);
+ void processMesh(aiMesh* mesh, const aiMatrix4x4& transform, std::vector& vertices, std::vector& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax);
// Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure)
std::vector loadedMeshes;
diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp
index 276681f..057a7c3 100644
--- a/src/ProjectManager.cpp
+++ b/src/ProjectManager.cpp
@@ -236,7 +236,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false;
file << "# Scene File\n";
- file << "version=2\n";
+ file << "version=4\n";
file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n";
file << "\n";
@@ -269,6 +269,24 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
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";
+ file << "cameraType=" << static_cast(obj.camera.type) << "\n";
+ file << "cameraFov=" << obj.camera.fov << "\n";
+ file << "cameraNear=" << obj.camera.nearClip << "\n";
+ file << "cameraFar=" << obj.camera.farClip << "\n";
+ if (obj.type == ObjectType::PostFXNode) {
+ file << "postEnabled=" << (obj.postFx.enabled ? 1 : 0) << "\n";
+ file << "postBloomEnabled=" << (obj.postFx.bloomEnabled ? 1 : 0) << "\n";
+ file << "postBloomThreshold=" << obj.postFx.bloomThreshold << "\n";
+ file << "postBloomIntensity=" << obj.postFx.bloomIntensity << "\n";
+ file << "postBloomRadius=" << obj.postFx.bloomRadius << "\n";
+ file << "postColorAdjustEnabled=" << (obj.postFx.colorAdjustEnabled ? 1 : 0) << "\n";
+ file << "postExposure=" << obj.postFx.exposure << "\n";
+ file << "postContrast=" << obj.postFx.contrast << "\n";
+ file << "postSaturation=" << obj.postFx.saturation << "\n";
+ file << "postColorFilter=" << obj.postFx.colorFilter.r << "," << obj.postFx.colorFilter.g << "," << obj.postFx.colorFilter.b << "\n";
+ file << "postMotionBlurEnabled=" << (obj.postFx.motionBlurEnabled ? 1 : 0) << "\n";
+ file << "postMotionBlurStrength=" << obj.postFx.motionBlurStrength << "\n";
+ }
if ((obj.type == ObjectType::OBJMesh || obj.type == ObjectType::Model) && !obj.meshPath.empty()) {
file << "meshPath=" << obj.meshPath << "\n";
@@ -332,6 +350,9 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
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 (currentObj->type == ObjectType::Camera) {
+ currentObj->camera.type = SceneCameraType::Scene;
+ }
} else if (key == "parentId") {
currentObj->parentId = std::stoi(value);
} else if (key == "position") {
@@ -395,6 +416,41 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
¤tObj->light.size.y);
} else if (key == "lightEnabled") {
currentObj->light.enabled = (std::stoi(value) != 0);
+ } else if (key == "cameraType") {
+ currentObj->camera.type = static_cast(std::stoi(value));
+ } else if (key == "cameraFov") {
+ currentObj->camera.fov = std::stof(value);
+ } else if (key == "cameraNear") {
+ currentObj->camera.nearClip = std::stof(value);
+ } else if (key == "cameraFar") {
+ currentObj->camera.farClip = std::stof(value);
+ } else if (key == "postEnabled") {
+ currentObj->postFx.enabled = (std::stoi(value) != 0);
+ } else if (key == "postBloomEnabled") {
+ currentObj->postFx.bloomEnabled = (std::stoi(value) != 0);
+ } else if (key == "postBloomThreshold") {
+ currentObj->postFx.bloomThreshold = std::stof(value);
+ } else if (key == "postBloomIntensity") {
+ currentObj->postFx.bloomIntensity = std::stof(value);
+ } else if (key == "postBloomRadius") {
+ currentObj->postFx.bloomRadius = std::stof(value);
+ } else if (key == "postColorAdjustEnabled") {
+ currentObj->postFx.colorAdjustEnabled = (std::stoi(value) != 0);
+ } else if (key == "postExposure") {
+ currentObj->postFx.exposure = std::stof(value);
+ } else if (key == "postContrast") {
+ currentObj->postFx.contrast = std::stof(value);
+ } else if (key == "postSaturation") {
+ currentObj->postFx.saturation = std::stof(value);
+ } else if (key == "postColorFilter") {
+ sscanf(value.c_str(), "%f,%f,%f",
+ ¤tObj->postFx.colorFilter.r,
+ ¤tObj->postFx.colorFilter.g,
+ ¤tObj->postFx.colorFilter.b);
+ } else if (key == "postMotionBlurEnabled") {
+ currentObj->postFx.motionBlurEnabled = (std::stoi(value) != 0);
+ } else if (key == "postMotionBlurStrength") {
+ currentObj->postFx.motionBlurStrength = std::stof(value);
} else if (key == "meshPath") {
currentObj->meshPath = value;
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
diff --git a/src/Rendering.cpp b/src/Rendering.cpp
index 2e58892..25c068e 100644
--- a/src/Rendering.cpp
+++ b/src/Rendering.cpp
@@ -298,6 +298,7 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
glm::vec3 boundsMin(FLT_MAX);
glm::vec3 boundsMax(-FLT_MAX);
+ std::vector triPositions;
for (const auto& shape : shapes) {
size_t indexOffset = 0;
@@ -360,6 +361,7 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
const TempVertex* tri[3] = { &faceVerts[0], &faceVerts[v], &faceVerts[v+1] };
for (int i = 0; i < 3; i++) {
+ triPositions.push_back(tri[i]->pos);
vertices.push_back(tri[i]->pos.x);
vertices.push_back(tri[i]->pos.y);
vertices.push_back(tri[i]->pos.z);
@@ -390,6 +392,7 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
loaded.hasTexCoords = !attrib.texcoords.empty();
loaded.boundsMin = boundsMin;
loaded.boundsMax = boundsMax;
+ loaded.triangleVertices = std::move(triPositions);
loadedMeshes.push_back(std::move(loaded));
return static_cast(loadedMeshes.size() - 1);
@@ -420,9 +423,29 @@ Renderer::~Renderer() {
delete sphereMesh;
delete capsuleMesh;
delete skybox;
+ delete postShader;
+ delete brightShader;
+ delete blurShader;
+ if (previewTarget.fbo) glDeleteFramebuffers(1, &previewTarget.fbo);
+ if (previewTarget.texture) glDeleteTextures(1, &previewTarget.texture);
+ if (previewTarget.rbo) glDeleteRenderbuffers(1, &previewTarget.rbo);
+ if (postTarget.fbo) glDeleteFramebuffers(1, &postTarget.fbo);
+ if (postTarget.texture) glDeleteTextures(1, &postTarget.texture);
+ if (postTarget.rbo) glDeleteRenderbuffers(1, &postTarget.rbo);
+ if (historyTarget.fbo) glDeleteFramebuffers(1, &historyTarget.fbo);
+ if (historyTarget.texture) glDeleteTextures(1, &historyTarget.texture);
+ if (historyTarget.rbo) glDeleteRenderbuffers(1, &historyTarget.rbo);
+ if (bloomTargetA.fbo) glDeleteFramebuffers(1, &bloomTargetA.fbo);
+ if (bloomTargetA.texture) glDeleteTextures(1, &bloomTargetA.texture);
+ if (bloomTargetA.rbo) glDeleteRenderbuffers(1, &bloomTargetA.rbo);
+ if (bloomTargetB.fbo) glDeleteFramebuffers(1, &bloomTargetB.fbo);
+ if (bloomTargetB.texture) glDeleteTextures(1, &bloomTargetB.texture);
+ if (bloomTargetB.rbo) glDeleteRenderbuffers(1, &bloomTargetB.rbo);
if (framebuffer) glDeleteFramebuffers(1, &framebuffer);
if (viewportTexture) glDeleteTextures(1, &viewportTexture);
if (rbo) glDeleteRenderbuffers(1, &rbo);
+ if (quadVBO) glDeleteBuffers(1, &quadVBO);
+ if (quadVAO) glDeleteVertexArrays(1, &quadVAO);
}
Texture* Renderer::getTexture(const std::string& path) {
@@ -448,6 +471,36 @@ void Renderer::initialize() {
shader = nullptr;
throw std::runtime_error("Shader error");
}
+ postShader = new Shader(postVertPath.c_str(), postFragPath.c_str());
+ if (!postShader || postShader->ID == 0) {
+ std::cerr << "PostFX shader compilation failed!\n";
+ delete postShader;
+ postShader = nullptr;
+ } else {
+ postShader->use();
+ postShader->setInt("sceneTex", 0);
+ postShader->setInt("bloomTex", 1);
+ postShader->setInt("historyTex", 2);
+ }
+ brightShader = new Shader(postVertPath.c_str(), postBrightFragPath.c_str());
+ if (!brightShader || brightShader->ID == 0) {
+ std::cerr << "Bright-pass shader compilation failed!\n";
+ delete brightShader;
+ brightShader = nullptr;
+ } else {
+ brightShader->use();
+ brightShader->setInt("sceneTex", 0);
+ }
+
+ blurShader = new Shader(postVertPath.c_str(), postBlurFragPath.c_str());
+ if (!blurShader || blurShader->ID == 0) {
+ std::cerr << "Blur shader compilation failed!\n";
+ delete blurShader;
+ blurShader = nullptr;
+ } else {
+ blurShader->use();
+ blurShader->setInt("image", 0);
+ }
ShaderEntry entry;
entry.shader.reset(defaultShader);
entry.vertPath = defaultVertPath;
@@ -470,6 +523,12 @@ void Renderer::initialize() {
skybox = new Skybox();
setupFBO();
+ ensureRenderTarget(postTarget, currentWidth, currentHeight);
+ ensureRenderTarget(historyTarget, currentWidth, currentHeight);
+ ensureRenderTarget(bloomTargetA, currentWidth, currentHeight);
+ ensureRenderTarget(bloomTargetB, currentWidth, currentHeight);
+ ensureQuad();
+ clearHistory();
glEnable(GL_DEPTH_TEST);
}
@@ -573,6 +632,93 @@ void Renderer::setupFBO() {
std::cerr << "Framebuffer setup failed!\n";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ displayTexture = viewportTexture;
+}
+
+void Renderer::ensureRenderTarget(RenderTarget& target, int w, int h) {
+ if (w <= 0 || h <= 0) return;
+
+ if (target.fbo == 0) {
+ glGenFramebuffers(1, &target.fbo);
+ glGenTextures(1, &target.texture);
+ glGenRenderbuffers(1, &target.rbo);
+ }
+
+ if (target.width == w && target.height == h) return;
+
+ target.width = w;
+ target.height = h;
+
+ glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
+
+ glBindTexture(GL_TEXTURE_2D, target.texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, target.width, target.height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
+
+ glBindRenderbuffer(GL_RENDERBUFFER, target.rbo);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, target.width, target.height);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, target.rbo);
+
+ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ std::cerr << "Preview framebuffer setup failed!\n";
+ }
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+void Renderer::ensureQuad() {
+ if (quadVAO != 0) return;
+
+ float quadVertices[] = {
+ // positions // texcoords
+ -1.0f, 1.0f, 0.0f, 1.0f,
+ -1.0f, -1.0f, 0.0f, 0.0f,
+ 1.0f, -1.0f, 1.0f, 0.0f,
+
+ -1.0f, 1.0f, 0.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f, 0.0f,
+ 1.0f, 1.0f, 1.0f, 1.0f
+ };
+
+ glGenVertexArrays(1, &quadVAO);
+ glGenBuffers(1, &quadVBO);
+ glBindVertexArray(quadVAO);
+ glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+ glBindVertexArray(0);
+}
+
+void Renderer::drawFullscreenQuad() {
+ if (quadVAO == 0) ensureQuad();
+ glBindVertexArray(quadVAO);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glBindVertexArray(0);
+}
+
+void Renderer::clearHistory() {
+ historyValid = false;
+ if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) {
+ glBindFramebuffer(GL_FRAMEBUFFER, historyTarget.fbo);
+ glViewport(0, 0, historyTarget.width, historyTarget.height);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ }
+}
+
+void Renderer::clearTarget(RenderTarget& target) {
+ if (target.fbo == 0 || target.width <= 0 || target.height <= 0) return;
+ glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
+ glViewport(0, 0, target.width, target.height);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Renderer::resize(int w, int h) {
@@ -591,6 +737,13 @@ void Renderer::resize(int w, int h) {
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Framebuffer incomplete after resize!\n";
}
+
+ ensureRenderTarget(postTarget, currentWidth, currentHeight);
+ ensureRenderTarget(historyTarget, currentWidth, currentHeight);
+ ensureRenderTarget(bloomTargetA, currentWidth, currentHeight);
+ ensureRenderTarget(bloomTargetB, currentWidth, currentHeight);
+ clearHistory();
+ displayTexture = viewportTexture;
}
void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos) {
@@ -598,6 +751,7 @@ void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj, const g
glViewport(0, 0, currentWidth, currentHeight);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ displayTexture = viewportTexture;
shader->use();
shader->setMat4("view", view);
@@ -698,11 +852,16 @@ void Renderer::renderObject(const SceneObject& obj) {
case ObjectType::DirectionalLight:
// Not rendered as geometry
break;
+ case ObjectType::Camera:
+ // Cameras are editor helpers only
+ break;
+ case ObjectType::PostFXNode:
+ break;
}
}
-void Renderer::renderScene(const Camera& camera, const std::vector& sceneObjects) {
- if (!defaultShader) return;
+void Renderer::renderSceneInternal(const Camera& camera, const std::vector& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane) {
+ if (!defaultShader || width <= 0 || height <= 0) return;
struct LightUniform {
int type = 0; // 0 dir,1 point,2 spot
@@ -774,8 +933,8 @@ void Renderer::renderScene(const Camera& camera, const std::vector&
}
for (const auto& obj : sceneObjects) {
- // Skip light gizmo-only types
- if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight) {
+ // Skip light gizmo-only types and camera helpers
+ if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) {
continue;
}
@@ -785,7 +944,7 @@ void Renderer::renderScene(const Camera& camera, const std::vector&
shader->use();
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(fovDeg), (float)width / (float)height, nearPlane, farPlane));
shader->setVec3("viewPos", camera.position);
shader->setVec3("ambientColor", ambientColor);
shader->setVec3("ambientColor", ambientColor);
@@ -862,14 +1021,153 @@ void Renderer::renderScene(const Camera& camera, const std::vector&
if (skybox) {
glm::mat4 view = camera.getViewMatrix();
- glm::mat4 proj = glm::perspective(glm::radians(FOV),
- (float)currentWidth / currentHeight,
- NEAR_PLANE, FAR_PLANE);
+ glm::mat4 proj = glm::perspective(glm::radians(fovDeg),
+ (float)width / height,
+ nearPlane, farPlane);
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
}
+ if (unbindFramebuffer) {
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ }
+}
+
+PostFXSettings Renderer::gatherPostFX(const std::vector& sceneObjects) const {
+ PostFXSettings combined;
+ combined.enabled = false;
+ for (const auto& obj : sceneObjects) {
+ if (obj.type != ObjectType::PostFXNode) continue;
+ if (!obj.postFx.enabled) continue;
+ combined = obj.postFx; // Last enabled node wins for now
+ combined.enabled = true;
+ }
+ return combined;
+}
+
+void Renderer::applyPostProcessing(const std::vector& sceneObjects) {
+ PostFXSettings settings = gatherPostFX(sceneObjects);
+ bool wantsEffects = settings.enabled && (settings.bloomEnabled || settings.colorAdjustEnabled || settings.motionBlurEnabled);
+
+ if (!wantsEffects || !postShader || currentWidth <= 0 || currentHeight <= 0) {
+ displayTexture = viewportTexture;
+ clearHistory();
+ return;
+ }
+
+ ensureRenderTarget(postTarget, currentWidth, currentHeight);
+ ensureRenderTarget(historyTarget, currentWidth, currentHeight);
+ if (postTarget.fbo == 0 || postTarget.texture == 0) {
+ displayTexture = viewportTexture;
+ clearHistory();
+ return;
+ }
+
+ // --- Bloom using bright pass + separable blur (inspired by ProcessingPostFX) ---
+ unsigned int bloomTex = 0;
+ if (settings.bloomEnabled && brightShader && blurShader) {
+ // Bright pass
+ glDisable(GL_DEPTH_TEST);
+ brightShader->use();
+ brightShader->setFloat("threshold", settings.bloomThreshold);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, viewportTexture);
+ glBindFramebuffer(GL_FRAMEBUFFER, bloomTargetA.fbo);
+ glViewport(0, 0, currentWidth, currentHeight);
+ glClear(GL_COLOR_BUFFER_BIT);
+ drawFullscreenQuad();
+
+ // Blur ping-pong
+ blurShader->use();
+ float sigma = glm::max(settings.bloomRadius * 2.5f, 0.1f);
+ int radius = static_cast(glm::clamp(settings.bloomRadius * 4.0f, 2.0f, 12.0f));
+ blurShader->setFloat("sigma", sigma);
+ blurShader->setInt("radius", radius);
+
+ bool horizontal = true;
+ unsigned int pingTex = bloomTargetA.texture;
+ RenderTarget* writeTarget = &bloomTargetB;
+ for (int i = 0; i < 4; ++i) {
+ blurShader->setBool("horizontal", horizontal);
+ blurShader->setVec2("texelSize", glm::vec2(1.0f / currentWidth, 1.0f / currentHeight));
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, pingTex);
+ glBindFramebuffer(GL_FRAMEBUFFER, writeTarget->fbo);
+ glViewport(0, 0, currentWidth, currentHeight);
+ glClear(GL_COLOR_BUFFER_BIT);
+ drawFullscreenQuad();
+
+ // swap
+ pingTex = writeTarget->texture;
+ writeTarget = (writeTarget == &bloomTargetA) ? &bloomTargetB : &bloomTargetA;
+ horizontal = !horizontal;
+ }
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glEnable(GL_DEPTH_TEST);
+ bloomTex = pingTex;
+ } else {
+ bloomTex = 0;
+ clearTarget(bloomTargetA);
+ clearTarget(bloomTargetB);
+ }
+
+ glDisable(GL_DEPTH_TEST);
+ postShader->use();
+ postShader->setBool("enableBloom", settings.bloomEnabled && bloomTex != 0);
+ postShader->setFloat("bloomIntensity", settings.bloomIntensity);
+ postShader->setBool("enableColorAdjust", settings.colorAdjustEnabled);
+ postShader->setFloat("exposure", settings.exposure);
+ postShader->setFloat("contrast", settings.contrast);
+ postShader->setFloat("saturation", settings.saturation);
+ postShader->setVec3("colorFilter", settings.colorFilter);
+ postShader->setBool("enableMotionBlur", settings.motionBlurEnabled);
+ postShader->setFloat("motionBlurStrength", settings.motionBlurStrength);
+ postShader->setBool("hasHistory", historyValid);
+
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, viewportTexture);
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, bloomTex ? bloomTex : viewportTexture);
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, historyTarget.texture);
+
+ glBindFramebuffer(GL_FRAMEBUFFER, postTarget.fbo);
+ glViewport(0, 0, currentWidth, currentHeight);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ drawFullscreenQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glEnable(GL_DEPTH_TEST);
+
+ displayTexture = postTarget.texture;
+
+ if (settings.motionBlurEnabled && historyTarget.fbo != 0) {
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, postTarget.fbo);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, historyTarget.fbo);
+ glBlitFramebuffer(0, 0, currentWidth, currentHeight, 0, 0, currentWidth, currentHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ historyValid = true;
+ } else {
+ clearHistory();
+ }
+}
+
+void Renderer::renderScene(const Camera& camera, const std::vector& sceneObjects, int /*selectedId*/, float fovDeg, float nearPlane, float farPlane) {
+ renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane);
+ applyPostProcessing(sceneObjects);
+}
+
+unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane) {
+ ensureRenderTarget(previewTarget, width, height);
+ if (previewTarget.fbo == 0) return 0;
+
+ glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo);
+ glViewport(0, 0, width, height);
+ glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane);
+ return previewTarget.texture;
}
void Renderer::endRender() {
diff --git a/src/Rendering.h b/src/Rendering.h
index 82995a3..49489e3 100644
--- a/src/Rendering.h
+++ b/src/Rendering.h
@@ -39,6 +39,7 @@ public:
bool hasTexCoords = false;
glm::vec3 boundsMin = glm::vec3(FLT_MAX);
glm::vec3 boundsMax = glm::vec3(-FLT_MAX);
+ std::vector triangleVertices; // positions duplicated per-triangle for picking
};
private:
@@ -59,8 +60,23 @@ class Renderer {
private:
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
int currentWidth = 800, currentHeight = 600;
+ struct RenderTarget {
+ unsigned int fbo = 0;
+ unsigned int texture = 0;
+ unsigned int rbo = 0;
+ int width = 0;
+ int height = 0;
+ };
+ RenderTarget previewTarget;
+ RenderTarget postTarget;
+ RenderTarget historyTarget;
+ RenderTarget bloomTargetA;
+ RenderTarget bloomTargetB;
Shader* shader = nullptr;
Shader* defaultShader = nullptr;
+ Shader* postShader = nullptr;
+ Shader* brightShader = nullptr;
+ Shader* blurShader = nullptr;
Texture* texture1 = nullptr;
Texture* texture2 = nullptr;
std::unordered_map> textureCache;
@@ -74,14 +90,30 @@ private:
std::unordered_map shaderCache;
std::string defaultVertPath = "Resources/Shaders/vert.glsl";
std::string defaultFragPath = "Resources/Shaders/frag.glsl";
+ std::string postVertPath = "Resources/Shaders/postfx_vert.glsl";
+ std::string postFragPath = "Resources/Shaders/postfx_frag.glsl";
+ std::string postBrightFragPath = "Resources/Shaders/postfx_bright_frag.glsl";
+ std::string postBlurFragPath = "Resources/Shaders/postfx_blur_frag.glsl";
bool autoReloadShaders = true;
glm::vec3 ambientColor = glm::vec3(0.2f, 0.2f, 0.2f);
Mesh* cubeMesh = nullptr;
Mesh* sphereMesh = nullptr;
Mesh* capsuleMesh = nullptr;
Skybox* skybox = nullptr;
+ unsigned int quadVAO = 0;
+ unsigned int quadVBO = 0;
+ unsigned int displayTexture = 0;
+ bool historyValid = false;
void setupFBO();
+ void ensureRenderTarget(RenderTarget& target, int w, int h);
+ void ensureQuad();
+ void drawFullscreenQuad();
+ void clearHistory();
+ void clearTarget(RenderTarget& target);
+ void renderSceneInternal(const Camera& camera, const std::vector& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane);
+ void applyPostProcessing(const std::vector& sceneObjects);
+ PostFXSettings gatherPostFX(const std::vector& sceneObjects) const;
public:
Renderer() = default;
@@ -100,9 +132,10 @@ public:
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 renderObject(const SceneObject& obj);
- void renderScene(const Camera& camera, const std::vector& sceneObjects);
+ void renderScene(const Camera& camera, const std::vector& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE);
+ unsigned int renderScenePreview(const Camera& camera, const std::vector& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void endRender();
Skybox* getSkybox() { return skybox; }
- unsigned int getViewportTexture() const { return viewportTexture; }
+ unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; }
};
diff --git a/src/SceneObject.h b/src/SceneObject.h
index 7960c79..3cc89e8 100644
--- a/src/SceneObject.h
+++ b/src/SceneObject.h
@@ -11,7 +11,9 @@ enum class ObjectType {
DirectionalLight,
PointLight,
SpotLight,
- AreaLight
+ AreaLight,
+ Camera,
+ PostFXNode
};
struct MaterialProperties {
@@ -42,6 +44,33 @@ struct LightComponent {
bool enabled = true;
};
+enum class SceneCameraType {
+ Scene = 0,
+ Player = 1
+};
+
+struct CameraComponent {
+ SceneCameraType type = SceneCameraType::Scene;
+ float fov = FOV;
+ float nearClip = NEAR_PLANE;
+ float farClip = FAR_PLANE;
+};
+
+struct PostFXSettings {
+ bool enabled = true;
+ bool bloomEnabled = true;
+ float bloomThreshold = 1.1f;
+ float bloomIntensity = 0.8f;
+ float bloomRadius = 1.5f;
+ bool colorAdjustEnabled = false;
+ float exposure = 0.0f; // in EV stops
+ float contrast = 1.0f;
+ float saturation = 1.0f;
+ glm::vec3 colorFilter = glm::vec3(1.0f);
+ bool motionBlurEnabled = false;
+ float motionBlurStrength = 0.15f; // 0..1 blend with previous frame
+};
+
enum class ConsoleMessageType {
Info,
Warning,
@@ -71,6 +100,8 @@ public:
std::string fragmentShaderPath;
bool useOverlay = false;
LightComponent light; // Only used when type is a light
+ CameraComponent camera; // Only used when type is camera
+ PostFXSettings postFx; // Only used when type is PostFXNode
SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}