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