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) {}