From d843d54388c54ccae3a738ef4c28afc128b6b8b6 Mon Sep 17 00:00:00 2001 From: Antoine Pilote Date: Mon, 25 Sep 2023 02:21:22 -0400 Subject: [PATCH] Material Thumbnail, PostFX, more big commit + Depth of field + Barrel distortion + Vignette + Material thumbnail in filebrowser + Fullbright wad conversion turns into emissive --- Editor/barrel_distortion.cpp | 1 + Editor/barrel_distortion.h | 5 + .../Shaders/barrel_distortion.shader | 49 + Editor/resources/Shaders/deferred.shader | 29 +- Editor/resources/Shaders/dof.shader | 365 +++++ Editor/resources/Shaders/ssr.shader | 12 +- Editor/resources/Shaders/vignette.shader | 39 + .../ComponentsPanel/ParticleEmitterPanel.h | 2 +- Editor/src/Misc/ThumbnailManager.cpp | 128 ++ Editor/src/Misc/ThumbnailManager.h | 25 + Editor/src/Windows/EditorInterface.cpp | 165 ++- Editor/src/Windows/EditorSelectionPanel.cpp | 14 + Editor/src/Windows/FileSystemUI.cpp | 35 +- Nuake/src/Core/FileSystem.cpp | 9 +- Nuake/src/Core/FileSystem.h | 7 +- Nuake/src/Rendering/RenderList.h | 7 + Nuake/src/Rendering/Renderer.cpp | 132 +- Nuake/src/Rendering/Renderer.h | 3 + Nuake/src/Rendering/SceneRenderer.cpp | 203 ++- Nuake/src/Rendering/SceneRenderer.h | 6 +- Nuake/src/Rendering/Vertex.h | 4 +- Nuake/src/Scene/Lighting/Environment.h | 11 + Nuake/src/Scene/Systems/WadConverter.cpp | 43 +- Nuake/src/Scene/Systems/WadConverter.h | 3 +- Nuake/src/Vendors/filewatch/FileWatch.hpp | 1248 +++++++++++++++++ 25 files changed, 2495 insertions(+), 50 deletions(-) create mode 100644 Editor/barrel_distortion.cpp create mode 100644 Editor/barrel_distortion.h create mode 100644 Editor/resources/Shaders/barrel_distortion.shader create mode 100644 Editor/resources/Shaders/dof.shader create mode 100644 Editor/resources/Shaders/vignette.shader create mode 100644 Editor/src/Misc/ThumbnailManager.cpp create mode 100644 Editor/src/Misc/ThumbnailManager.h create mode 100644 Nuake/src/Vendors/filewatch/FileWatch.hpp diff --git a/Editor/barrel_distortion.cpp b/Editor/barrel_distortion.cpp new file mode 100644 index 00000000..56b1612a --- /dev/null +++ b/Editor/barrel_distortion.cpp @@ -0,0 +1 @@ +#include "barrel_distortion.h" diff --git a/Editor/barrel_distortion.h b/Editor/barrel_distortion.h new file mode 100644 index 00000000..5258939e --- /dev/null +++ b/Editor/barrel_distortion.h @@ -0,0 +1,5 @@ +#pragma once +class barrel_distortion +{ +}; + diff --git a/Editor/resources/Shaders/barrel_distortion.shader b/Editor/resources/Shaders/barrel_distortion.shader new file mode 100644 index 00000000..8fab23b9 --- /dev/null +++ b/Editor/resources/Shaders/barrel_distortion.shader @@ -0,0 +1,49 @@ +#shader vertex +#version 460 core + +layout(location = 0) in vec3 VertexPosition; +layout(location = 1) in vec2 UVPosition; + +out flat vec2 UV; + +void main() +{ + UV = UVPosition; + gl_Position = vec4(VertexPosition, 1.0f); +} + +#shader fragment +#version 460 core + +uniform sampler2D u_Source; // Input texture +uniform float u_Distortion; +uniform float u_DistortionEdge; +uniform float u_Scale; + +in vec2 UV; + +out vec4 FragColor; + +// k1: the main distortion +// positive = barrel, negative = pincushion +// k2 : tweaks the edges of distortion +// can be 0.0 +vec2 brownConradyDistortion(in vec2 uv, in float k1, in float k2) +{ + uv = uv * 2.0 - 1.0; // brown conrady takes [-1:1] + + // positive values of K1 give barrel distortion, negative give pincushion + float r2 = uv.x * uv.x + uv.y * uv.y; + uv *= 1.0 + k1 * r2 + k2 * r2 * r2; + + // tangential distortion (due to off center lens elements) + // is not modeled in this function, but if it was, the terms would go here + uv *= u_Scale; + uv = (uv * .5 + .5); // restore -> [0:1] + return uv; +} + +void main() +{ + FragColor = texture(u_Source, brownConradyDistortion(UV, u_Distortion, u_DistortionEdge)); +} \ No newline at end of file diff --git a/Editor/resources/Shaders/deferred.shader b/Editor/resources/Shaders/deferred.shader index cb0d755a..7fa0b64e 100644 --- a/Editor/resources/Shaders/deferred.shader +++ b/Editor/resources/Shaders/deferred.shader @@ -29,8 +29,7 @@ in mat4 InvProjection; in mat4 InvView; // Camera -uniform float u_Exposure; -uniform vec3 u_EyePosition; +uniform vec3 u_EyePosition; // GBuffer uniform sampler2D m_Depth; @@ -57,12 +56,14 @@ struct DirectionalLight float CascadeDepth[4]; mat4 LightTransforms[4]; int Volumetric; + int Shadow; }; uniform sampler2D ShadowMaps[4]; uniform Light Lights[MaxLight]; uniform DirectionalLight u_DirectionalLight; +uniform int u_DisableSSAO = 0; // Converts depth to World space coords. vec3 WorldPosFromDepth(float depth) { @@ -230,7 +231,17 @@ void main() float ao = materialSample.g; float roughness = materialSample.b; float unlit = materialSample.a; - float ssao = texture(m_SSAO, UV).r; + float ssao = 0.0f; + + if (u_DisableSSAO == 1) + { + ssao = 1.0f; + } + else + { + ssao = texture(m_SSAO, UV).r; + } + float emissive = texture(m_Emissive, UV).r; if (unlit > 0.1f) @@ -251,8 +262,12 @@ void main() vec3 Lo = vec3(0.0); vec3 fog = vec3(0.0); float shadow = 0.0f; - + if (u_DirectionalLight.Shadow < 0.1f) + { + shadow = 1.f; + } + if (true) { vec3 L = normalize(u_DirectionalLight.Direction); @@ -260,7 +275,11 @@ void main() float attenuation = 1.0f; L = normalize(u_DirectionalLight.Direction); - shadow += ShadowCalculation(worldPos, N); + + if(u_DirectionalLight.Shadow > 0.1f) + { + shadow += ShadowCalculation(worldPos, N); + } vec3 radiance = u_DirectionalLight.Color * attenuation * shadow; diff --git a/Editor/resources/Shaders/dof.shader b/Editor/resources/Shaders/dof.shader new file mode 100644 index 00000000..752646d4 --- /dev/null +++ b/Editor/resources/Shaders/dof.shader @@ -0,0 +1,365 @@ +#shader vertex +#version 460 core + +layout(location = 0) in vec3 VertexPosition; +layout(location = 1) in vec2 UVPosition; + +out flat vec2 texcoord; + +void main() +{ + texcoord = UVPosition; + gl_Position = vec4(VertexPosition, 1.0f); +} + +#shader fragment +#version 460 core + +/* +DoF with bokeh GLSL shader v2.4 +by Martins Upitis (martinsh) (devlog-martinsh.blogspot.com) +---------------------- +The shader is Blender Game Engine ready, but it should be quite simple to adapt for your engine. +This work is licensed under a Creative Commons Attribution 3.0 Unported License. +So you are free to share, modify and adapt it for your needs, and even use it for commercial use. +I would also love to hear about a project you are using it. +Have fun, +Martins +---------------------- +changelog: + +2.4: +- physically accurate DoF simulation calculated from "focalDepth" ,"focalLength", "f-stop" and "CoC" parameters. +- option for artist controlled DoF simulation calculated only from "focalDepth" and individual controls for near and far blur +- added "circe of confusion" (CoC) parameter in mm to accurately simulate DoF with different camera sensor or film sizes +- cleaned up the code +- some optimization +2.3: +- new and physically little more accurate DoF +- two extra input variables - focal length and aperture iris diameter +- added a debug visualization of focus point and focal range +2.1: +- added an option for pentagonal bokeh shape +- minor fixes +2.0: +- variable sample count to increase quality/performance +- option to blur depth buffer to reduce hard edges +- option to dither the samples with noise or pattern +- bokeh chromatic aberration/fringing +- bokeh bias to bring out bokeh edges +- image thresholding to bring out highlights when image is out of focus +*/ + +smooth in vec2 texcoord; + +uniform sampler2D renderTex; +uniform sampler2D depthTex; + +//uniform float renderTexWidth; +//uniform float renderTexHeight; + +#define PI 3.14159265 + +//float width = renderTexWidth; //texture width +//float height = renderTexHeight; //texture height +uniform float width = 900; //texture width +uniform float height = 600; //texture height + + +//uniform variables from external script + +/* +uniform float focalDepth; //focal distance value in meters, but you may use autofocus option below +uniform float focalLength; //focal length in mm +uniform float fstop; //f-stop value +uniform bool showFocus; //show debug focus point and focal range (red = focal point, green = focal range) +*/ +uniform float focalDepth = 1.5; +uniform float focalLength = 12.0; +uniform float fstop = 2.0; +uniform bool showFocus = false; + +/* +make sure that these two values are the same for your camera, otherwise distances will be wrong. +*/ + +uniform float znear = 0.1f; //camera clipping start +uniform float zfar = 1000.0; //camera clipping end + +//------------------------------------------ +//user variables + +uniform int samples = 3; //samples on the first ring +uniform int rings = 3; //ring count + +uniform bool manualdof = true; //manual dof calculation +uniform float ndofstart = 1.0; //near dof blur start +uniform float ndofdist = 2.0; //near dof blur falloff distance +uniform float fdofstart = 1.0; //far dof blur start +uniform float fdofdist = 3.0; //far dof blur falloff distance + +uniform float CoC = 0.03;//circle of confusion size in mm (35mm film = 0.03mm) + +uniform bool autofocus = false; //use autofocus in shader? disable if you use external focalDepth value +uniform vec2 focus = vec2(0.5, 0.5); // autofocus point on screen (0.0,0.0 - left lower corner, 1.0,1.0 - upper right) +uniform float maxblur = 0.0; //clamp value of max blur (0.0 = no blur,1.0 default) + +uniform float threshold = 0.7; //highlight threshold; +uniform float gain = 100.0; //highlight gain; + +uniform float bias = 0.5; //bokeh edge bias +uniform float fringe = 0.7; //bokeh chromatic aberration/fringing + +uniform bool noise = true; //use noise instead of pattern for sample dithering +uniform float namount = 0.0000001; //dither amount + +uniform bool depthblur = true; //blur the depth buffer? +uniform float dbsize = 1.25; //depthblursize + +/* +next part is experimental +not looking good with small sample and ring count +looks okay starting from samples = 4, rings = 4 +*/ + +uniform bool pentagon = true; //use pentagon as bokeh shape? +uniform float feather = 1.0; //pentagon shape feather + +//------------------------------------------ + + +float penta(vec2 coords) //pentagonal shape +{ + float scale = float(rings); + vec4 HS0 = vec4(1.0, 0.0, 0.0, 1.0); + vec4 HS1 = vec4(0.309016994, 0.951056516, 0.0, 1.0); + vec4 HS2 = vec4(-0.809016994, 0.587785252, 0.0, 1.0); + vec4 HS3 = vec4(-0.809016994, -0.587785252, 0.0, 1.0); + vec4 HS4 = vec4(0.309016994, -0.951056516, 0.0, 1.0); + vec4 HS5 = vec4(0.0, 0.0, 1.0, 1.0); + + vec4 one = vec4(1.0); + + vec4 P = vec4((coords), vec2(scale, scale)); + + vec4 dist = vec4(0.0); + float inorout = -4.0; + + dist.x = dot(P, HS0); + dist.y = dot(P, HS1); + dist.z = dot(P, HS2); + dist.w = dot(P, HS3); + + dist = smoothstep(-feather, feather, dist); + + inorout += dot(dist, one); + + dist.x = dot(P, HS4); + dist.y = HS5.w - abs(P.z); + + dist = smoothstep(-feather, feather, dist); + inorout += dist.x; + + return clamp(inorout, 0.0, 1.0); +} + +float bdepth(vec2 coords) //blurring depth +{ + float d = 0.0; + float kernel[9]; + vec2 offset[9]; + + vec2 texel = vec2(1.0 / width, 1.0 / height); + vec2 wh = vec2(texel.x, texel.y) * dbsize; + + offset[0] = vec2(-wh.x, -wh.y); + offset[1] = vec2(0.0, -wh.y); + offset[2] = vec2(wh.x - wh.y); + + offset[3] = vec2(-wh.x, 0.0); + offset[4] = vec2(0.0, 0.0); + offset[5] = vec2(wh.x, 0.0); + + offset[6] = vec2(-wh.x, wh.y); + offset[7] = vec2(0.0, wh.y); + offset[8] = vec2(wh.x, wh.y); + + kernel[0] = 1.0 / 16.0; kernel[1] = 2.0 / 16.0; kernel[2] = 1.0 / 16.0; + kernel[3] = 2.0 / 16.0; kernel[4] = 4.0 / 16.0; kernel[5] = 2.0 / 16.0; + kernel[6] = 1.0 / 16.0; kernel[7] = 2.0 / 16.0; kernel[8] = 1.0 / 16.0; + + + for (int i = 0; i < 9; i++) + { + float tmp = texture2D(depthTex, coords + offset[i]).r; + d += tmp * kernel[i]; + } + + return d; +} + + +vec3 color(vec2 coords, float blur) //processing the sample +{ + vec3 col = vec3(0.0); + + + vec2 texel = vec2(1.0 / width, 1.0 / height); + + col.r = texture2D(renderTex, coords + vec2(0.0, 1.0) * texel * fringe * blur).r; + col.g = texture2D(renderTex, coords + vec2(-0.866, -0.5) * texel * fringe * blur).g; + col.b = texture2D(renderTex, coords + vec2(0.866, -0.5) * texel * fringe * blur).b; + + vec3 lumcoeff = vec3(0.299, 0.587, 0.114); + float lum = dot(col.rgb, lumcoeff); + float thresh = max((lum - threshold) * gain, 0.0); + return col + mix(vec3(0.0), col, thresh * blur); +} + +vec2 rand(vec2 coord) //generating noise/pattern texture for dithering +{ + float noiseX = ((fract(1.0 - coord.s * (width / 2.0)) * 0.25) + (fract(coord.t * (height / 2.0)) * 0.75)) * 2.0 - 1.0; + float noiseY = ((fract(1.0 - coord.s * (width / 2.0)) * 0.75) + (fract(coord.t * (height / 2.0)) * 0.25)) * 2.0 - 1.0; + + if (noise) + { + noiseX = clamp(fract(sin(dot(coord, vec2(12.9898, 78.233))) * 43758.5453), 0.0, 1.0) * 2.0 - 1.0; + noiseY = clamp(fract(sin(dot(coord, vec2(12.9898, 78.233) * 2.0)) * 43758.5453), 0.0, 1.0) * 2.0 - 1.0; + } + return vec2(noiseX, noiseY); +} + +vec3 debugFocus(vec3 col, float blur, float depth) +{ + float edge = 0.002 * depth; //distance based edge smoothing + float m = clamp(smoothstep(0.0, edge, blur), 0.0, 1.0); + float e = clamp(smoothstep(1.0 - edge, 1.0, blur), 0.0, 1.0); + + col = mix(col, vec3(1.0, 0.5, 0.0), (1.0 - m) * 0.6); + col = mix(col, vec3(0.0, 0.5, 1.0), ((1.0 - e) - (1.0 - m)) * 0.2); + + return col; +} + +float linearize(float depth) +{ + return -zfar * znear / (depth * (zfar - znear) - zfar); +} + +out vec4 FragColor; + +void main() +{ + //scene depth calculation + + float depth = linearize(texture2D(depthTex, texcoord.xy).x); + + if (depthblur) + { + depth = linearize(bdepth(texcoord.xy)); + } + + //focal plane calculation + + float fDepth = focalDepth; + + if (autofocus) + { + fDepth = linearize(texture2D(depthTex, focus).x); + } + + //dof blur factor calculation + + float blur = 0.0; + + if (manualdof) + { + float a = depth - fDepth; //focal plane + float b = (a - fdofstart) / fdofdist; //far DoF + float c = (-a - ndofstart) / ndofdist; //near Dof + blur = (a > 0.0) ? b : c; + } + + else + { + float f = focalLength; //focal length in mm + float d = fDepth * 1000.0; //focal plane in mm + float o = depth * 1000.0; //depth in mm + + float a = (o * f) / (o - f); + float b = (d * f) / (d - f); + float c = (d - f) / (d * fstop * CoC); + + blur = abs(a - b) * c; + } + + blur = clamp(blur, 0.0, 1.0); + + // calculation of pattern for ditering + + vec2 noise = rand(texcoord.xy) * namount * blur; + + // getting blur x and y step factor + + float w = (1.0 / width) * blur * maxblur + noise.x; + float h = (1.0 / height) * blur * maxblur + noise.y; + + // calculation of final color + + vec3 col = vec3(0.0); + + if (blur < 0.05f) //some optimization thingy + { + col = texture(renderTex, texcoord.xy).rgb; + } + + else + { + col = texture(renderTex, texcoord.xy).rgb; + float s = 1.0f; + int ringsamples; + + for (int i = 1; i <= rings; i += 1) + { + ringsamples = i * samples; + + for (int j = 0; j < ringsamples; j += 1) + { + float step = PI * 2.0 / float(ringsamples); + float pw = (cos(float(j) * step) * float(i)); + float ph = (sin(float(j) * step) * float(i)); + float p = 1.0; + if (pentagon) + { + p = penta(vec2(pw, ph)); + } + col += color(texcoord.xy + vec2(pw * w, ph * h), blur) * mix(1.0, (float(i)) / (float(rings)), bias) * p; + s += 1.0 * mix(1.0, (float(i)) / (float(rings)), bias) * p; + } + } + col /= s; //divide by sample count + } + + if (showFocus) + { + col = debugFocus(col, blur, depth); + } + + //gl_FragColor.rgb = texture(renderTex, texcoord); + FragColor = vec4(col.rgb, 1.0f); + +} + +/* +uniform sampler2D renderTex; +uniform sampler2D depthTex; +out vec4 color; +smooth in vec2 texcoord; +void main() +{ + vec4 c = texture(renderTex, texcoord); + // grabbing values out of the depth buffer causes program to fail. + float z = texture(depthTex, texcoord).x; + color = texture(renderTex, texcoord) + (z * 0.000001); +} +*/ \ No newline at end of file diff --git a/Editor/resources/Shaders/ssr.shader b/Editor/resources/Shaders/ssr.shader index 9976ef02..2def635b 100644 --- a/Editor/resources/Shaders/ssr.shader +++ b/Editor/resources/Shaders/ssr.shader @@ -75,8 +75,16 @@ vec3 SSR(vec3 position, vec3 reflection) { delta = abs(marchingPosition.z) - depthFromScreen; float depth = textureLod(textureDepth, screenPosition, 2).r; - - if (abs(delta) < distanceBias || depthFromScreen > 1000.0f) { + if (depthFromScreen > 1000.0f) + { + vec2 dCoords = smoothstep(0.2, 0.6, abs(vec2(0.5, 0.5) - screenPosition.xy)); + float screenEdgefactor = clamp(1.0 - (dCoords.x + dCoords.y), 0.0, 1.0); + float ReflectionMultiplier = pow(Metallic, 3.0) * + screenEdgefactor * + -ReflectedVector.z; + return texture(textureFrame, screenPosition).xyz * ReflectionMultiplier; + } + if (abs(delta) < distanceBias) { vec3 color = vec3(1); if (debugDraw) color = vec3(0.5 + sign(delta) / 2, 0.3, 0.5 - sign(delta) / 2); diff --git a/Editor/resources/Shaders/vignette.shader b/Editor/resources/Shaders/vignette.shader new file mode 100644 index 00000000..8db3e90a --- /dev/null +++ b/Editor/resources/Shaders/vignette.shader @@ -0,0 +1,39 @@ +#shader vertex +#version 460 core + +layout(location = 0) in vec3 VertexPosition; +layout(location = 1) in vec2 UVPosition; + +out flat vec2 UV; + +void main() +{ + UV = UVPosition; + gl_Position = vec4(VertexPosition, 1.0f); +} + +#shader fragment +#version 460 core + +uniform sampler2D u_Source; + +uniform float u_Intensity; +uniform float u_Extend; + +in vec2 UV; + +out vec4 FragColor; + +void main() +{ + vec2 uv = UV; + + uv *= 1.0 - uv.yx; //vec2(1.0)- uv.yx; -> 1.-u.yx; Thanks FabriceNeyret ! + + float vig = uv.x * uv.y * u_Intensity; // multiply with sth for intensity + + vig = pow(vig, u_Extend); // change pow for modifying the extend of the vignette + vig = clamp(vig, 0, 1); + FragColor = vec4(texture(u_Source, UV).rgb * vig, 1.0) ; + +} \ No newline at end of file diff --git a/Editor/src/ComponentsPanel/ParticleEmitterPanel.h b/Editor/src/ComponentsPanel/ParticleEmitterPanel.h index b114b839..17d39d8e 100644 --- a/Editor/src/ComponentsPanel/ParticleEmitterPanel.h +++ b/Editor/src/ComponentsPanel/ParticleEmitterPanel.h @@ -30,7 +30,7 @@ public: ImGui::TableNextColumn(); std::string label = "Empty"; - if (!component.ParticleMaterial->Path.empty()) + if (component.ParticleMaterial && !component.ParticleMaterial->Path.empty()) { label = component.ParticleMaterial->Path; } diff --git a/Editor/src/Misc/ThumbnailManager.cpp b/Editor/src/Misc/ThumbnailManager.cpp new file mode 100644 index 00000000..31337ec5 --- /dev/null +++ b/Editor/src/Misc/ThumbnailManager.cpp @@ -0,0 +1,128 @@ +#include "ThumbnailManager.h" + +#include +#include +#include +#include + + +ThumbnailManager::ThumbnailManager() +{ + using namespace Nuake; + + m_Framebuffer = CreateRef(false, m_ThumbnailSize); + auto texture = CreateRef(m_ThumbnailSize, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT, GL_FLOAT); + m_Framebuffer->SetTexture(texture, GL_DEPTH_ATTACHMENT); + m_Framebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE), GL_COLOR_ATTACHMENT0); // Albedo + m_Framebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE), GL_COLOR_ATTACHMENT1); // + m_Framebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE), GL_COLOR_ATTACHMENT2); // Material + unlit + m_Framebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RED_INTEGER, GL_R32I, GL_INT), GL_COLOR_ATTACHMENT3); + m_Framebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RED, GL_R16F, GL_FLOAT), GL_COLOR_ATTACHMENT4); // Emissive + + //m_Framebuffer->QueueResize(m_ThumbnailSize); + + m_ShadedFramebuffer = CreateRef(true, m_ThumbnailSize); + m_ShadedFramebuffer->SetTexture(CreateRef(m_ThumbnailSize, GL_RGB, GL_RGB16F, GL_FLOAT)); +} + +ThumbnailManager& ThumbnailManager::Get() +{ + static ThumbnailManager instance; + return instance; +} + +bool ThumbnailManager::IsThumbnailLoaded(const std::string& path) const +{ + return m_Thumbnails.find(path) != m_Thumbnails.end(); +} + +Ref ThumbnailManager::GetThumbnail(const std::string& path) +{ + if (IsThumbnailLoaded(path)) + { + return m_Thumbnails[path]; + } + using namespace Nuake; + + // Generate Thumbnail + Ref thumbnail = CreateRef(m_ThumbnailSize, GL_RGB, GL_RGB16F, GL_FLOAT); + GenerateThumbnail(path, thumbnail); + + m_Thumbnails[path] = thumbnail; + return thumbnail; +} + +void ThumbnailManager::MarkThumbnailAsDirty(const std::string & path) +{ + if (IsThumbnailLoaded(path)) + { + m_Thumbnails.erase(path); + } +} + +Ref ThumbnailManager::GenerateThumbnail(const std::string& path, Ref texture) +{ + using namespace Nuake; + + const Matrix4 ortho = glm::orthoLH(-0.6f, 0.6f, -0.6f, 0.6f, -100.0f, 100.0f); + Matrix4 view = Matrix4(1.0f); + view = glm::lookAt(Vector3(1, -1.0, 0), Vector3(0, 0, 0), Vector3(0, 1, 0));; + + // Gbuffer pass + m_Framebuffer->Bind(); + { + RenderCommand::SetClearColor({ 0.2, 0.2, 0.2, 0.0f }); + m_Framebuffer->Clear(); + + RenderCommand::Disable(RendererEnum::BLENDING); + RenderCommand::Enable(RendererEnum::DEPTH_TEST); + auto shader = ShaderManager::GetShader("resources/Shaders/gbuffer.shader"); + shader->Bind(); + + auto cam = Engine::GetCurrentScene()->GetCurrentCamera(); + shader->SetUniformMat4f("u_View", view); + shader->SetUniformMat4f("u_Projection", ortho); + shader->SetUniformMat4f("u_Model", Matrix4(1.0f)); + Ref material = ResourceLoader::LoadMaterial(path); + material->Bind(shader); + Renderer::SphereMesh->Draw(shader, false); + } + m_Framebuffer->Unbind(); + m_ShadedFramebuffer->SetTexture(texture); + m_ShadedFramebuffer->Bind(); + { + //RenderCommand::Enable(RendererEnum::BLENDING); + RenderCommand::SetClearColor({ 0.2, 0.2, 0.2, 1 }); + m_ShadedFramebuffer->Clear(); + RenderCommand::Disable(RendererEnum::DEPTH_TEST); + RenderCommand::Disable(RendererEnum::FACE_CULL); + auto shader = ShaderManager::GetShader("resources/Shaders/deferred.shader"); + shader->Bind(); + shader->SetUniformVec3("u_EyePosition", Vector3(1, 0, 0)); + shader->SetUniform1i("LightCount", 0); + auto dir = Engine::GetCurrentScene()->GetEnvironment()->ProceduralSkybox->GetSunDirection(); + shader->SetUniform3f("u_DirectionalLight.Direction", 0.6, -0.6, 0.6); + shader->SetUniform3f("u_DirectionalLight.Color", 10.0f, 10.0f, 10.0f); + shader->SetUniform1i("u_DirectionalLight.Shadow", 0); + shader->SetUniform1i("u_DisableSSAO", 1); + shader->SetUniformMat4f("u_View", view); + shader->SetUniformMat4f("u_Projection", ortho); + + m_Framebuffer->GetTexture(GL_DEPTH_ATTACHMENT)->Bind(5); + m_Framebuffer->GetTexture(GL_COLOR_ATTACHMENT0)->Bind(6); + m_Framebuffer->GetTexture(GL_COLOR_ATTACHMENT1)->Bind(7); + m_Framebuffer->GetTexture(GL_COLOR_ATTACHMENT2)->Bind(8); + m_Framebuffer->GetTexture(GL_COLOR_ATTACHMENT4)->Bind(10); + + shader->SetUniform1i("m_Depth", 5); + shader->SetUniform1i("m_Albedo", 6); + shader->SetUniform1i("m_Normal", 7); + shader->SetUniform1i("m_Material", 8); + shader->SetUniform1i("m_Emissive", 10); + + Renderer::DrawQuad(Matrix4()); + } + m_ShadedFramebuffer->Unbind(); + + return m_ShadedFramebuffer->GetTexture(GL_COLOR_ATTACHMENT0); +} \ No newline at end of file diff --git a/Editor/src/Misc/ThumbnailManager.h b/Editor/src/Misc/ThumbnailManager.h new file mode 100644 index 00000000..8718a150 --- /dev/null +++ b/Editor/src/Misc/ThumbnailManager.h @@ -0,0 +1,25 @@ +#pragma once +#include "src/Core/Core.h" +#include "src/Rendering/Textures/Texture.h" +#include "src/Rendering/Buffers/Framebuffer.h" + +class ThumbnailManager +{ +private: + std::unordered_map> m_Thumbnails; + + Ref m_Framebuffer; + Ref m_ShadedFramebuffer; + const Nuake::Vector2 m_ThumbnailSize = { 128, 128 }; + +public: + ThumbnailManager(); + ~ThumbnailManager() = default; + + static ThumbnailManager& Get(); + + bool IsThumbnailLoaded(const std::string& path) const; + Ref GetThumbnail(const std::string& path); + void MarkThumbnailAsDirty(const std::string& path); + Ref GenerateThumbnail(const std::string& path, Ref texture); +}; \ No newline at end of file diff --git a/Editor/src/Windows/EditorInterface.cpp b/Editor/src/Windows/EditorInterface.cpp index 65c2357a..cc602673 100644 --- a/Editor/src/Windows/EditorInterface.cpp +++ b/Editor/src/Windows/EditorInterface.cpp @@ -682,8 +682,13 @@ namespace Nuake { ImGui::TableNextColumn(); int iteration = env->mBloom->GetIteration(); + int oldIteration = iteration; ImGui::DragInt("##quality", &iteration, 1.0f, 0, 4); - env->mBloom->SetIteration(iteration); + + if (oldIteration != iteration) + { + env->mBloom->SetIteration(iteration); + } ImGui::TableNextColumn(); // Reset button @@ -1046,6 +1051,164 @@ namespace Nuake { ImGui::EndTable(); } END_COLLAPSE_HEADER() + + + BEGIN_COLLAPSE_HEADER(DOF) + if (ImGui::BeginTable("EnvTable", 3, ImGuiTableFlags_BordersInner | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("name", 0, 0.3); + ImGui::TableSetupColumn("set", 0, 0.6); + ImGui::TableSetupColumn("reset", 0, 0.1); + + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("DOF Enabled"); + ImGui::TableNextColumn(); + + ImGui::Checkbox("##dofEnabled", &env->DOFEnabled); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetSSR = ICON_FA_UNDO + std::string("##resetrBarrelDistortionEnabled"); + if (ImGui::Button(resetSSR.c_str())) env->DOFEnabled = false; + ImGui::PopStyleColor(); + } + + ImGui::EndTable(); + } + END_COLLAPSE_HEADER() + + BEGIN_COLLAPSE_HEADER(BARREL_DISTORTION) + if (ImGui::BeginTable("EnvTable", 3, ImGuiTableFlags_BordersInner | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("name", 0, 0.3); + ImGui::TableSetupColumn("set", 0, 0.6); + ImGui::TableSetupColumn("reset", 0, 0.1); + + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Barrel Distortion"); + ImGui::TableNextColumn(); + + ImGui::Checkbox("##BarrelEnabled", &env->BarrelDistortionEnabled); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetSSR = ICON_FA_UNDO + std::string("##resetrBarrelDistortionEnabled"); + if (ImGui::Button(resetSSR.c_str())) env->BarrelDistortionEnabled = false; + ImGui::PopStyleColor(); + } + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Distortion"); + ImGui::TableNextColumn(); + ImGui::DragFloat("##distortion", &env->BarrelDistortion, 0.01f); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetVolumetric = ICON_FA_UNDO + std::string("##resetVolumetric"); + if (ImGui::Button(resetVolumetric.c_str())) env->BarrelDistortion = 0.0f; + ImGui::PopStyleColor(); + } + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Edge Distortion"); + ImGui::TableNextColumn(); + ImGui::DragFloat("##edgedistortion", &env->BarrelEdgeDistortion, 0.01f); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetVolumetric = ICON_FA_UNDO + std::string("##resetVolumetric"); + if (ImGui::Button(resetVolumetric.c_str())) env->BarrelEdgeDistortion = 0.0f; + ImGui::PopStyleColor(); + } + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Scale Adjustement"); + ImGui::TableNextColumn(); + ImGui::DragFloat("##barrelScale", &env->BarrelScale, 0.01f); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetVolumetric = ICON_FA_UNDO + std::string("##resetVolumetric"); + if (ImGui::Button(resetVolumetric.c_str())) env->BarrelScale = 1.0f; + ImGui::PopStyleColor(); + } + ImGui::EndTable(); + } + END_COLLAPSE_HEADER() + + BEGIN_COLLAPSE_HEADER(VIGNETTE) + if (ImGui::BeginTable("EnvTable", 3, ImGuiTableFlags_BordersInner | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("name", 0, 0.3); + ImGui::TableSetupColumn("set", 0, 0.6); + ImGui::TableSetupColumn("reset", 0, 0.1); + + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Vignette Enabled"); + ImGui::TableNextColumn(); + + ImGui::Checkbox("##VignetteEnabled", &env->VignetteEnabled); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetSSR = ICON_FA_UNDO + std::string("##resetrBarrelDistortionEnabled"); + if (ImGui::Button(resetSSR.c_str())) env->VignetteEnabled = false; + ImGui::PopStyleColor(); + } + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Intensity"); + ImGui::TableNextColumn(); + ImGui::DragFloat("##vignetteIntensity", &env->VignetteIntensity, 0.1f); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetVolumetric = ICON_FA_UNDO + std::string("##resetVolumetric"); + if (ImGui::Button(resetVolumetric.c_str())) env->VignetteIntensity = 0.0f; + ImGui::PopStyleColor(); + } + + { + ImGui::TableNextColumn(); + // Title + ImGui::Text("Extend"); + ImGui::TableNextColumn(); + ImGui::DragFloat("##vignetteExtend", &env->VignetteExtend, 0.01f, 0.0f); + ImGui::TableNextColumn(); + + // Reset button + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0)); + std::string resetVolumetric = ICON_FA_UNDO + std::string("##resetVolumetric"); + if (ImGui::Button(resetVolumetric.c_str())) env->VignetteExtend = 0.0f; + ImGui::PopStyleColor(); + } + ImGui::EndTable(); + } + END_COLLAPSE_HEADER() } ImGui::End(); diff --git a/Editor/src/Windows/EditorSelectionPanel.cpp b/Editor/src/Windows/EditorSelectionPanel.cpp index 660dad64..b06c5ca6 100644 --- a/Editor/src/Windows/EditorSelectionPanel.cpp +++ b/Editor/src/Windows/EditorSelectionPanel.cpp @@ -57,6 +57,20 @@ void EditorSelectionPanel::Draw(EditorSelection selection) ResolveFile(selection.File); } + if (!selection.File->IsValid()) + { + std::string text = "File is invaluid"; + auto windowWidth = ImGui::GetWindowSize().x; + auto windowHeight = ImGui::GetWindowSize().y; + + auto textWidth = ImGui::CalcTextSize(text.c_str()).x; + auto textHeight = ImGui::CalcTextSize(text.c_str()).y; + ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); + ImGui::SetCursorPosY((windowHeight - textHeight) * 0.5f); + + ImGui::TextColored({1, 0.1, 0.1, 1.0}, text.c_str()); + } + DrawFile(selection.File); break; } diff --git a/Editor/src/Windows/FileSystemUI.cpp b/Editor/src/Windows/FileSystemUI.cpp index fb53d409..e1163583 100644 --- a/Editor/src/Windows/FileSystemUI.cpp +++ b/Editor/src/Windows/FileSystemUI.cpp @@ -14,6 +14,7 @@ #include "../Misc/PopupHelper.h" #include "src/Scene/Systems/WadConverter.h" +#include "../Misc/ThumbnailManager.h" namespace Nuake { @@ -38,6 +39,10 @@ namespace Nuake void FileSystemUI::EditorInterfaceDrawFiletree(Ref dir) { + + + + ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_FramePadding; //if (is_selected) @@ -211,10 +216,31 @@ namespace Nuake icon = ICON_FA_FILE_IMAGE; std::string fullName = icon + std::string("##") + file->GetAbsolutePath(); - if (ImGui::Button(fullName.c_str(), ImVec2(100, 100))) + + bool pressed = false; + if (fileExtension == ".material") + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + pressed = ImGui::ImageButton(fullName.c_str(), (void*)ThumbnailManager::Get().GetThumbnail(file->GetRelativePath())->GetID(), ImVec2(100, 100), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::PopStyleVar(); + } + else + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + pressed = ImGui::Button(fullName.c_str(), ImVec2(100, 100)); + ImGui::PopStyleVar(); + } + + if (Editor->Selection.File == file) + { + ThumbnailManager::Get().MarkThumbnailAsDirty(file->GetRelativePath()); + } + + if(pressed) { Editor->Selection = EditorSelection(file); } + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { OS::OpenTrenchbroomMap(file->GetAbsolutePath()); @@ -528,6 +554,13 @@ namespace Nuake static float sz2 = 300; void FileSystemUI::DrawDirectoryExplorer() { + //if (ImGui::Begin("thumbnail debugger")) + //{ + // auto texture = ThumbnailManager::Get().GetThumbnail("Materials/prototype_1_2/64_blood_2.material"); + // ImGui::Image((void*)texture->GetID(), ImGui::GetContentRegionAvail(), ImVec2(0, 1), ImVec2(1, 0)); + //} + //ImGui::End(); + if (ImGui::Begin("File browser")) { Ref rootDirectory = FileSystem::GetFileTree(); diff --git a/Nuake/src/Core/FileSystem.cpp b/Nuake/src/Core/FileSystem.cpp index a3eb675b..617a9177 100644 --- a/Nuake/src/Core/FileSystem.cpp +++ b/Nuake/src/Core/FileSystem.cpp @@ -84,9 +84,10 @@ namespace Nuake } } - bool FileSystem::DirectoryExists(const std::string& path) + bool FileSystem::DirectoryExists(const std::string& path, bool absolute) { - return std::filesystem::exists(path) && std::filesystem::is_directory(path); + const std::string& finalPath = absolute ? path : Root + path; + return std::filesystem::exists(finalPath) && std::filesystem::is_directory(finalPath); } bool FileSystem::MakeDirectory(const std::string& path, bool absolute) @@ -97,9 +98,7 @@ namespace Nuake bool FileSystem::FileExists(const std::string& path, bool absolute) { std::string fullPath = absolute ? path : FileSystem::Root + path; - - std::ifstream f(fullPath.c_str()); - return f.good(); + return std::filesystem::exists(fullPath); } void FileSystem::SetRootDirectory(const std::string path) diff --git a/Nuake/src/Core/FileSystem.h b/Nuake/src/Core/FileSystem.h index 23767cb1..f58b47f5 100644 --- a/Nuake/src/Core/FileSystem.h +++ b/Nuake/src/Core/FileSystem.h @@ -39,7 +39,7 @@ namespace Nuake static void GetDirectories(); static bool MakeDirectory(const std::string& path, bool absolute = false); - static bool DirectoryExists(const std::string& path); + static bool DirectoryExists(const std::string& path, bool absolute = false); static bool FileExists(const std::string& path, bool absolute = false); static std::string ReadFile(const std::string& path, bool absolute = false); @@ -73,6 +73,11 @@ namespace Nuake return FileSystem::ReadFile(AbsolutePath); } + bool IsValid() + { + return FileSystem::FileExists(AbsolutePath, true); + } + File(Ref parentDir, const std::string& absolutePath, const std::string& name, const std::string& type) { AbsolutePath = absolutePath; diff --git a/Nuake/src/Rendering/RenderList.h b/Nuake/src/Rendering/RenderList.h index b1bf3562..54afb605 100644 --- a/Nuake/src/Rendering/RenderList.h +++ b/Nuake/src/Rendering/RenderList.h @@ -6,6 +6,8 @@ #include "src/Rendering/Textures/Material.h" #include "src/Rendering/Mesh/Mesh.h" #include "src/Rendering/Shaders/ShaderManager.h" +#include "src/Rendering/Textures/MaterialManager.h" + namespace Nuake { struct RenderMesh @@ -27,6 +29,11 @@ namespace Nuake { Ref material = mesh->GetMaterial(); + if (!material) + { + material = MaterialManager::Get()->GetMaterial("default"); + } + if (m_RenderList.find(material) == m_RenderList.end()) { m_RenderList[material] = std::vector(); diff --git a/Nuake/src/Rendering/Renderer.cpp b/Nuake/src/Rendering/Renderer.cpp index 7d79c2d0..d007c62f 100644 --- a/Nuake/src/Rendering/Renderer.cpp +++ b/Nuake/src/Rendering/Renderer.cpp @@ -25,6 +25,7 @@ namespace Nuake Ref Renderer::CubeMesh; Ref Renderer::QuadMesh; + Ref Renderer::SphereMesh; Shader* Renderer::m_Shader; Shader* Renderer::m_SkyboxShader; @@ -67,12 +68,12 @@ namespace Nuake std::vector QuadVertices { - { Vector3(-1.0f, 1.0f, 0.0f), Vector2(0.0f, 1.0f), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, - { Vector3(1.0f, 1.0f, 0.0f), Vector2(1.0f, 1.0f), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, - { Vector3(-1.0f, -1.0f, 0.0f), Vector2(0, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, - { Vector3(1.0f, -1.0f, 0.0f), Vector2(1.0f, 0.0f), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, - { Vector3(-1.0f, -1.0f, 0.0f), Vector2(0.0f, 0.0f), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, - { Vector3(1.0f, 1.0f, 0.0f), Vector2(1.0f, 1.0f), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(0, 1, 0) } + { Vector3(-1.0f, 1.0f, 0.0f), Vector2(0.0f, 1.0f), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, + { Vector3(1.0f, 1.0f, 0.0f), Vector2(1.0f, 1.0f), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, + { Vector3(-1.0f, -1.0f, 0.0f), Vector2(0, 0), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, + { Vector3(1.0f, -1.0f, 0.0f), Vector2(1.0f, 0.0f), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, + { Vector3(-1.0f, -1.0f, 0.0f), Vector2(0.0f, 0.0f), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) }, + { Vector3(1.0f, 1.0f, 0.0f), Vector2(1.0f, 1.0f), Vector3(0, 0, 1), Vector3(1, 0, 0), Vector3(0, 1, 0) } }; @@ -95,6 +96,8 @@ namespace Nuake QuadMesh = CreateRef(); QuadMesh->AddSurface(QuadVertices, { 0, 1, 2, 3, 4, 5 }); QuadMesh->SetMaterial(defaultMaterial); + + SphereMesh = CreateSphereMesh(); } void Renderer::LoadShaders() @@ -116,6 +119,122 @@ namespace Nuake m_RenderList.Flush(shader, depthOnly); } + Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c) + { + const float EPSILON = 0.000001f; + + Vector3 normal; // default return value (0,0,0) + float nx, ny, nz; + + // find 2 edge vectors: v1-v2, v1-v3 + float ex1 = b.x - a.x; + float ey1 = b.y - a.y; + float ez1 = b.z - a.z; + float ex2 = c.x - a.x; + float ey2 = c.y - a.y; + float ez2 = c.z - a.z; + + // cross product: e1 x e2 + nx = ez1 * ey2 - ey1 * ez2; + ny = ex1 * ez2 - ez1 * ex2; + nz = ey1 * ex2 - ex1 * ey2; + + // normalize only if the length is > 0 + float length = sqrtf(nx * nx + ny * ny + nz * nz); + if (length > EPSILON) + { + // normalize + float lengthInv = 1.0f / length; + normal.x = nx * lengthInv; + normal.y = ny * lengthInv; + normal.z = nz * lengthInv; + } + + return normal * -1.0f; + } + + Ref Renderer::CreateSphereMesh() + { + const float sectorCount = 36; + const float stackCount = 36; + const float radius = 0.5f; + const float PI = acos(-1.0f); + + // new + std::vector finalVertices; + + float x, y, z, xy; // vertex position + float nx, ny, nz, lengthInv = 1.0f / radius; // normal + float s, t; // texCoord + + float sectorStep = 2 * PI / sectorCount; + float stackStep = PI / stackCount; + float sectorAngle, stackAngle; + + for (int i = 0; i <= stackCount; ++i) + { + stackAngle = PI / 2 - i * stackStep; // starting from pi/2 to -pi/2 + xy = radius * cosf(stackAngle); // r * cos(u) + z = radius * sinf(stackAngle); // r * sin(u) + + // add (sectorCount+1) vertices per stack + // the first and last vertices have same position and normal, but different tex coords + for (int j = 0; j <= sectorCount; ++j) + { + sectorAngle = j * sectorStep; // starting from 0 to 2pi + + Vertex newVertex; + + x = xy * cosf(sectorAngle); // r * cos(u) * cos(v) + y = xy * sinf(sectorAngle); // r * cos(u) * sin(v) + newVertex.position = Vector3(x, y, z); + + nx = x * lengthInv; + ny = y * lengthInv; + nz = z * lengthInv; + newVertex.normal = Vector3(nx, ny, nz) * -1.0f; + // vertex position + + s = (float)j / sectorCount * 4.f; + t = (float)i / stackCount * 4.f; + newVertex.uv = { t, s }; + + finalVertices.push_back(newVertex); + } + } + + std::vector finalIndices; + unsigned int k1, k2; + for (int i = 0; i < stackCount; ++i) + { + k1 = i * (sectorCount + 1); // beginning of current stack + k2 = k1 + sectorCount + 1; // beginning of next stack + + for (int j = 0; j < sectorCount; ++j, ++k1, ++k2) + { + // 2 triangles per sector excluding 1st and last stacks + if (i != 0) + { + finalIndices.push_back(k1); + finalIndices.push_back(k2); + finalIndices.push_back(k1 + 1); + } + + if (i != (stackCount - 1)) + { + finalIndices.push_back(k1 + 1); + finalIndices.push_back(k2); + finalIndices.push_back(k2 + 1); + } + } + } + + Ref sphereMesh = CreateRef(); + sphereMesh->SetMaterial(CreateRef("resources/Images/nuake-logo.png")); + sphereMesh->AddSurface(std::move(finalVertices), std::move(finalIndices)); + return sphereMesh; + } + void Renderer::BeginDraw(Ref camera) { Shader* lineShader = ShaderManager::GetShader("resources/Shaders/line.shader"); @@ -153,6 +272,7 @@ namespace Nuake int shadowmapAmount = 0; if (light.Type == Directional) { + deferredShader->SetUniform1i("u_DirectionalLight.Shadow", light.CastShadows); if (light.CastShadows) { for (unsigned int i = 0; i < CSM_AMOUNT; i++) diff --git a/Nuake/src/Rendering/Renderer.h b/Nuake/src/Rendering/Renderer.h index 5f8508f9..dcaffe6a 100644 --- a/Nuake/src/Rendering/Renderer.h +++ b/Nuake/src/Rendering/Renderer.h @@ -54,6 +54,7 @@ namespace Nuake static Ref CubeMesh; static Ref QuadMesh; + static Ref SphereMesh; static void Init(); static void LoadShaders(); @@ -62,6 +63,8 @@ namespace Nuake static void SubmitCube(Matrix4 transform); static void Flush(Shader* shader, bool depthOnly = false); + static Ref CreateSphereMesh(); + // Drawing states static void BeginDraw(Ref camera); static void EndDraw(); diff --git a/Nuake/src/Rendering/SceneRenderer.cpp b/Nuake/src/Rendering/SceneRenderer.cpp index 68f07183..76a31818 100644 --- a/Nuake/src/Rendering/SceneRenderer.cpp +++ b/Nuake/src/Rendering/SceneRenderer.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace Nuake @@ -28,6 +29,15 @@ namespace Nuake mSSR = CreateScope(); mToneMapBuffer = CreateScope(false, defaultResolution); mToneMapBuffer->SetTexture(CreateRef(defaultResolution, GL_RGB), GL_COLOR_ATTACHMENT0); + + mBarrelDistortionBuffer = CreateScope(false, defaultResolution); + mBarrelDistortionBuffer->SetTexture(CreateRef(defaultResolution, GL_RGB), GL_COLOR_ATTACHMENT0); + + mVignetteBuffer = CreateScope(false, defaultResolution); + mVignetteBuffer->SetTexture(CreateRef(defaultResolution, GL_RGB), GL_COLOR_ATTACHMENT0); + + mDOFBuffer = CreateScope(false, defaultResolution); + mDOFBuffer->SetTexture(CreateRef(defaultResolution, GL_RGB), GL_COLOR_ATTACHMENT0); } void SceneRenderer::Cleanup() @@ -47,7 +57,7 @@ namespace Nuake /// /// Scene to render /// Framebuffer to render the scene to. Should be in the right size - void SceneRenderer::RenderScene(Scene& scene, FrameBuffer& framebuffer) + void SceneRenderer::RenderScene(Scene& scene, FrameBuffer& framebuffer) { // Renders all shadow maps ShadowPass(scene); @@ -88,12 +98,12 @@ namespace Nuake if (lc.Type == Directional && lc.IsVolumetric && lc.CastShadows) lightList.push_back(lc); } - + if (sceneEnv->VolumetricEnabled) { sceneEnv->mVolumetric->Resize(framebuffer.GetSize()); sceneEnv->mVolumetric->SetDepth(mGBuffer->GetTexture(GL_DEPTH_ATTACHMENT).get()); - sceneEnv->mVolumetric->Draw(mProjection , mView, mCamPos, lightList); + sceneEnv->mVolumetric->Draw(mProjection, mView, mCamPos, lightList); //finalOutput = mVolumetric->GetFinalOutput().get(); @@ -171,6 +181,180 @@ namespace Nuake framebuffer.Unbind(); } + static float focalDepth = 100.0f; + static float focalLength = 16.0f; + static float fstop = 6.0f; + static bool autoFocus = false; + static bool showFocus = false; + static bool manualdof = true; + static int samples = 3; + static int rings = 3; + static float ndofstart = 1.0f; + static float ndofDist = 2.0f; + static float fdofstart = 1.0f; + static float fdofdist = 3.0f; + static float coc = 0.03f; + static float maxBlue = 1.0f; + static float threshold = 0.7f; + static float gain = 100.0f; + static float biaos = 0.0f; + static float fringe = 0.0f; + static float nammount = 0.0001; + static float dbsize = 1.25f; + static float feather = 1.0f; + ImGui::Begin("DOF Setting"); + { + ImGui::DragFloat("focalDepth", &focalDepth, 0.01f); + ImGui::DragFloat("focalLength", &focalLength, 0.01f); + ImGui::DragFloat("fstop", &fstop, 0.01f); + ImGui::DragInt("samples", &samples, 0.01f); + ImGui::DragInt("rings", &rings, 0.01f); + ImGui::Checkbox("showFocus", &showFocus); + ImGui::Checkbox("manualDof", &manualdof); + ImGui::Checkbox("autoFocus", &autoFocus); + ImGui::DragInt("rings", &rings, 0.01f); + ImGui::DragFloat("ndofstart", &ndofstart, 0.01f); + ImGui::DragFloat("ndofDist", &ndofDist, 0.01f); + ImGui::DragFloat("fdofstart", &fdofstart, 0.01f); + ImGui::DragFloat("fdofdist", &fdofdist, 0.01f); + ImGui::DragFloat("coc", &coc, 0.01f); + ImGui::DragFloat("maxBlue", &maxBlue, 0.01f); + ImGui::DragFloat("threshold", &threshold, 0.01f); + ImGui::DragFloat("gain", &gain, 0.01f); + ImGui::DragFloat("biaos", &biaos, 0.01f); + ImGui::DragFloat("fringe", &fringe, 0.01f); + ImGui::DragFloat("nammount", &nammount, 0.01f); + ImGui::DragFloat("dbsize", &dbsize, 0.01f); + ImGui::DragFloat("feather", &feather, 0.01f); + } + ImGui::End(); + + mDOFBuffer->QueueResize(framebuffer.GetSize()); + mDOFBuffer->Bind(); + { + RenderCommand::Clear(); + Shader* shader = ShaderManager::GetShader("resources/Shaders/dof.shader"); + shader->Bind(); + + shader->SetUniform1f("focalDepth", focalDepth); + shader->SetUniform1f("focalLength", focalLength); + shader->SetUniform1f("fstop", fstop); + shader->SetUniform1i("showFocus", showFocus); + shader->SetUniform1i("autofocus", autoFocus); + shader->SetUniform1i("samples", samples); + shader->SetUniform1i("manualdof", manualdof); + shader->SetUniform1f("rings", rings); + shader->SetUniform1f("ndofstart", ndofstart); + shader->SetUniform1f("ndofdist", ndofDist); + shader->SetUniform1f("fdofstart", fdofstart); + shader->SetUniform1f("fdofdist", fdofdist); + shader->SetUniform1f("CoC", coc); + shader->SetUniform1f("maxblur", maxBlue); + shader->SetUniform1f("threshold", threshold); + shader->SetUniform1f("gain", gain); + shader->SetUniform1f("bias", biaos); + shader->SetUniform1f("fringe", fringe); + shader->SetUniform1f("namount", nammount); + shader->SetUniform1f("dbsize", dbsize); + shader->SetUniform1f("feather", feather); + shader->SetUniform1f("u_Distortion", sceneEnv->BarrelDistortion); + shader->SetUniform1f("height", finalOutput->GetHeight()); + shader->SetUniform1f("width", finalOutput->GetWidth()); + shader->SetUniformTex("depthTex", mGBuffer->GetTexture(GL_DEPTH_ATTACHMENT).get(), 0); + shader->SetUniformTex("renderTex", finalOutput.get(), 1); + Renderer::DrawQuad(); + } + mDOFBuffer->Unbind(); + + if (ImGui::Begin("DOF")) + { + ImGui::Image((void*)mDOFBuffer->GetTexture()->GetID(), ImGui::GetContentRegionAvail(), ImVec2(0, 1), ImVec2(1, 0)); + } + ImGui::End(); + + if (sceneEnv->BarrelDistortionEnabled) + { + mBarrelDistortionBuffer->QueueResize(framebuffer.GetSize()); + mBarrelDistortionBuffer->Bind(); + { + RenderCommand::Clear(); + Shader* shader = ShaderManager::GetShader("resources/Shaders/barrel_distortion.shader"); + shader->Bind(); + + shader->SetUniform1f("u_Distortion", sceneEnv->BarrelDistortion); + shader->SetUniform1f("u_DistortionEdge", sceneEnv->BarrelEdgeDistortion); + shader->SetUniform1f("u_Scale", sceneEnv->BarrelScale); + + if (sceneEnv->DOFEnabled) + { + shader->SetUniformTex("u_Source", mDOFBuffer->GetTexture().get(), 0); + } + else + { + shader->SetUniformTex("u_Source", finalOutput.get(), 0); + } + + + Renderer::DrawQuad(); + } + mBarrelDistortionBuffer->Unbind(); + + framebuffer.Bind(); + { + RenderCommand::Clear(); + Shader* shader = ShaderManager::GetShader("resources/Shaders/copy.shader"); + shader->Bind(); + + shader->SetUniformTex("u_Source", mBarrelDistortionBuffer->GetTexture().get(), 0); + Renderer::DrawQuad(); + } + framebuffer.Unbind(); + } + + if (sceneEnv->VignetteEnabled) + { + mVignetteBuffer->QueueResize(framebuffer.GetSize()); + mVignetteBuffer->Bind(); + { + RenderCommand::Clear(); + Shader* shader = ShaderManager::GetShader("resources/Shaders/vignette.shader"); + shader->Bind(); + + shader->SetUniform1f("u_Intensity", sceneEnv->VignetteIntensity); + shader->SetUniform1f("u_Extend", sceneEnv->VignetteExtend); + shader->SetUniformTex("u_Source", finalOutput.get(), 0); + Renderer::DrawQuad(); + } + mVignetteBuffer->Unbind(); + + framebuffer.Bind(); + { + RenderCommand::Clear(); + Shader* shader = ShaderManager::GetShader("resources/Shaders/copy.shader"); + shader->Bind(); + + shader->SetUniformTex("u_Source", mVignetteBuffer->GetTexture().get(), 0); + Renderer::DrawQuad(); + } + framebuffer.Unbind(); + } + + + // Barrel distortion + //mVignetteBuffer->Bind(); + //{ + // RenderCommand::Clear(); + // Shader* shader = ShaderManager::GetShader("resources/Shaders/vignette.shader"); + // shader->Bind(); + // + // shader->SetUniform1f("u_Intensity", sceneEnv->VignetteIntensity); + // shader->SetUniform1f("u_Extend", sceneEnv->VignetteExtend); + // shader->SetUniformTex("u_Source", mBarrelDistortionBuffer->GetTexture().get(), 0); + // Renderer::DrawQuad(); + //} + //mVignetteBuffer->Unbind(); + + RenderCommand::Enable(RendererEnum::DEPTH_TEST); Renderer::EndDraw(); } @@ -289,7 +473,7 @@ namespace Nuake if (mesh.ModelResource != nullptr && visibility.Visible) { auto& rootBoneNode = mesh.ModelResource->GetSkeletonRootNode(); - SetSkeletonBoneTransformRecursive(rootBoneNode, gBufferSkinnedMeshShader); + SetSkeletonBoneTransformRecursive(scene, rootBoneNode, gBufferSkinnedMeshShader); for (auto& m : mesh.ModelResource->GetMeshes()) { @@ -394,7 +578,7 @@ namespace Nuake { auto [transform, emitterComponent, visibility] = particleEmitterView.get(e); - if (!visibility.Visible) + if (!visibility.Visible || !emitterComponent.ParticleMaterial) continue; Renderer::QuadMesh->SetMaterial(emitterComponent.ParticleMaterial); @@ -459,7 +643,7 @@ namespace Nuake if (meshResource && visibility.Visible) { auto& rootBoneNode = meshResource->GetSkeletonRootNode(); - SetSkeletonBoneTransformRecursive(rootBoneNode, gBufferSkinnedMeshShader); + SetSkeletonBoneTransformRecursive(scene, rootBoneNode, gBufferSkinnedMeshShader); for (auto& m : mesh.ModelResource->GetMeshes()) { @@ -539,18 +723,17 @@ namespace Nuake { } - void SceneRenderer::SetSkeletonBoneTransformRecursive(SkeletonNode& skeletonNode, Shader* shader) + void SceneRenderer::SetSkeletonBoneTransformRecursive(Scene& scene, SkeletonNode& skeletonNode, Shader* shader) { - auto scene = Engine::GetCurrentScene(); for (auto& child : skeletonNode.Children) { - if (auto entity = scene->GetEntity(child.Name); entity.GetHandle() != -1) + if (auto entity = scene.GetEntity(child.Name); entity.GetHandle() != -1) { const std::string boneMatrixUniformName = "u_FinalBonesMatrice[" + std::to_string(child.Id) + "]"; shader->SetUniformMat4f(boneMatrixUniformName, child.FinalTransform); } - SetSkeletonBoneTransformRecursive(child, shader); + SetSkeletonBoneTransformRecursive(scene, child, shader); } } diff --git a/Nuake/src/Rendering/SceneRenderer.h b/Nuake/src/Rendering/SceneRenderer.h index 267359ad..0f791e22 100644 --- a/Nuake/src/Rendering/SceneRenderer.h +++ b/Nuake/src/Rendering/SceneRenderer.h @@ -33,13 +33,15 @@ namespace Nuake Scope mGBuffer; Scope mShadingBuffer; Scope mToneMapBuffer; - + Scope mBarrelDistortionBuffer; + Scope mVignetteBuffer; + Scope mDOFBuffer;; private: void ShadowPass(Scene& scene); void GBufferPass(Scene& scene); void ShadingPass(Scene& scene); void PostProcessPass(const Scene& scene); - void SetSkeletonBoneTransformRecursive(SkeletonNode& skeletonNode, Shader* shader); + void SetSkeletonBoneTransformRecursive(Scene& scene, SkeletonNode& skeletonNode, Shader* shader); }; } \ No newline at end of file diff --git a/Nuake/src/Rendering/Vertex.h b/Nuake/src/Rendering/Vertex.h index 6143e396..1bfc9e72 100644 --- a/Nuake/src/Rendering/Vertex.h +++ b/Nuake/src/Rendering/Vertex.h @@ -8,8 +8,8 @@ namespace Nuake Vector3 position; Vector2 uv; Vector3 normal; - Vector3 tangent; - Vector3 bitangent; + Vector3 tangent = Vector3(0, 1, 0); + Vector3 bitangent = Vector3(1, 0, 0); }; const uint32_t MAX_BONE_INFLUENCE = 4; diff --git a/Nuake/src/Scene/Lighting/Environment.h b/Nuake/src/Scene/Lighting/Environment.h index 55e41f07..d841a305 100644 --- a/Nuake/src/Scene/Lighting/Environment.h +++ b/Nuake/src/Scene/Lighting/Environment.h @@ -44,6 +44,17 @@ namespace Nuake bool SSREnabled = false; + bool DOFEnabled = false; + + bool BarrelDistortionEnabled = true; + float BarrelDistortion = 0.f; + float BarrelEdgeDistortion = 0.f; + float BarrelScale = 1.0f; + + bool VignetteEnabled = true; + float VignetteIntensity = 15.0f; + float VignetteExtend = 0.5f; + Vector3 ClearColor; glm::vec4 m_AmbientColor; diff --git a/Nuake/src/Scene/Systems/WadConverter.cpp b/Nuake/src/Scene/Systems/WadConverter.cpp index 238e7174..f2d99a4b 100644 --- a/Nuake/src/Scene/Systems/WadConverter.cpp +++ b/Nuake/src/Scene/Systems/WadConverter.cpp @@ -10,9 +10,15 @@ namespace Nuake { + struct ConvertedTexture + { + std::string path; + bool fullbright; + }; + std::string TargetDirectory = ""; std::string WadName = ""; - std::vector ConvertedTextures; + std::vector ConvertedTextures; unsigned char host_quakepal[768] = @@ -151,8 +157,8 @@ namespace Nuake } } - if (fullbright) - strcat(result, "_fbr"); + //if (fullbright) + // strcat(result, "_fbr"); strcat(result, ".png"); @@ -194,7 +200,7 @@ namespace Nuake return false; } - char *pixels = new char[width * height]; + unsigned char* pixels = new unsigned char[width * height]; if (!WAD2_ReadData(entry, 0, width * height, pixels)) { @@ -230,7 +236,7 @@ namespace Nuake return false; } - unsigned char* pixels = new unsigned char[width * height]; + char* pixels = new char[width * height]; if (!WAD2_ReadData(entry, (int)sizeof(pic), width * height, pixels)) { @@ -309,17 +315,20 @@ namespace Nuake { unsigned char pix = pixels[y * width + x]; + if (pix >= 256 - 32) + fullbright = true; + textureData[y * width + x] = COL_ReadPalette(pix); } } - const std::string lumpName = ExpandFileName(lump_name, false); + const std::string lumpName = ExpandFileName(lump_name, fullbright); const std::string finalFilePath = TargetDirectory + lumpName; stbi_write_png((FileSystem::Root + finalFilePath).c_str(), width, height, 4, textureData.data(), width * 4); delete[] pixels; - ConvertedTextures.push_back(std::string(lumpName)); + ConvertedTextures.push_back({ std::string(lumpName), fullbright }); return true; } @@ -339,16 +348,15 @@ namespace Nuake return; } - ConvertedTextures = std::vector(); + ConvertedTextures = std::vector(); auto pathSplits = String::Split(std::string(wadPath.begin(), wadPath.end() - 4), '\\'); WadName = pathSplits[std::size(pathSplits) - 1]; TargetDirectory = "/textures/" + WadName + "/"; - if (const std::string absoluteDirPath = FileSystem::RelativeToAbsolute(TargetDirectory); - !FileSystem::DirectoryExists(absoluteDirPath)) + if (!FileSystem::DirectoryExists(TargetDirectory)) { - FileSystem::MakeDirectory(absoluteDirPath); + FileSystem::MakeDirectory(TargetDirectory); } WadOpenRead(wadPath); @@ -425,10 +433,19 @@ namespace Nuake { Ref material = CreateRef(); material->IsEmbedded = false; - material->SetAlbedo(TextureManager::Get()->GetTexture(FileSystem::RelativeToAbsolute(TargetDirectory + t))); + + + if (t.fullbright) + { + material->SetUnlit(true); + material->data.u_Emissive = 2.0f; + } + + material->SetAlbedo(TextureManager::Get()->GetTexture(FileSystem::RelativeToAbsolute(TargetDirectory + t.path))); + auto jsonData = material->Serialize(); - const std::string materialFilePath = materialFolderPath + std::string(t.begin(), t.end() - 4) + ".material"; + const std::string materialFilePath = materialFolderPath + std::string(t.path.begin(), t.path.end() - 4) + ".material"; FileSystem::BeginWriteFile(materialFilePath); FileSystem::WriteLine(jsonData.dump(4)); diff --git a/Nuake/src/Scene/Systems/WadConverter.h b/Nuake/src/Scene/Systems/WadConverter.h index 9d235331..8f855063 100644 --- a/Nuake/src/Scene/Systems/WadConverter.h +++ b/Nuake/src/Scene/Systems/WadConverter.h @@ -7,7 +7,8 @@ namespace Nuake { -#define MAKE_RGB(r,g,b) (uint32_t)(((r) << 16) | ((g) << 8) | (b) | (255<<24)) +#define MAKE_RGB(r,g,b) (unsigned int)((255 << 24) | (b << 16) | (g << 8) | r) +#define MAKE_RGBA(r,g,b,a) (unsigned int)((a << 24) | (b << 16) | (g << 8) | r) // John Carmack said the quake palette.lmp can be considered public domain because it is not an important asset to id, so I include it here as a fallback if no external palette file is found. #define CMP_LZSS 1 diff --git a/Nuake/src/Vendors/filewatch/FileWatch.hpp b/Nuake/src/Vendors/filewatch/FileWatch.hpp new file mode 100644 index 00000000..329c410c --- /dev/null +++ b/Nuake/src/Vendors/filewatch/FileWatch.hpp @@ -0,0 +1,1248 @@ +// MIT License +// +// Copyright(c) 2017 Thomas Monkman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef FILEWATCHER_H +#define FILEWATCHER_H + +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include +#endif // WIN32 + +#if __unix__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif // __unix__ + +#ifdef __linux__ +#include +#endif + +#if defined(__APPLE__) || defined(__MACH__) +#include +#include +#include +#include +#include +#include +#define FILEWATCH_PLATFORM_MAC 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef FILEWATCH_PLATFORM_MAC +extern "C" int __getdirentries64(int, char *, int, long *); +#endif // FILEWATCH_PLATFORM_MAC + +namespace filewatch { + enum class Event { + added, + removed, + modified, + renamed_old, + renamed_new + }; + + template + struct IsWChar { + static constexpr bool value = false; + }; + + template<> + struct IsWChar { + static constexpr bool value = true; + }; + + template + struct Invokable { + static Fn make() { + return (Fn*)0; + } + + template + static T defaultValue() { + return *(T*)0; + } + + static void call(int) { + make()(defaultValue()); + } + + static int call(long value); + + static constexpr bool value = std::is_same::value; + }; + +#define _FILEWATCH_TO_STRING(x) #x +#define FILEWATCH_TO_STRING(x) _FILEWATCH_TO_STRING(x) + + [[maybe_unused]] static const char* event_to_string(Event event) { + switch (event) { + case Event::added: + return FILEWATCH_TO_STRING(Event::added); + case Event::removed: + return FILEWATCH_TO_STRING(Event::removed); + case Event::modified: + return FILEWATCH_TO_STRING(Event::modified); + case Event::renamed_old: + return FILEWATCH_TO_STRING(Event : renamed_old); + case Event::renamed_new: + return FILEWATCH_TO_STRING(Event::renamed_new); + } + assert(false); + } + + template + static typename std::enable_if::value, bool>::type + isParentOrSelfDirectory(const StringType& path) { + return path == L"." || path == L".."; + } + + template + static typename std::enable_if::value, bool>::type + isParentOrSelfDirectory(const StringType& path) { + return path == "." || path == ".."; + } + + /** + * \class FileWatch + * + * \brief Watches a folder or file, and will notify of changes via function callback. + * + * \author Thomas Monkman + * + */ + template + class FileWatch + { + typedef typename StringType::value_type C; + typedef std::basic_string> UnderpinningString; + typedef std::basic_regex> UnderpinningRegex; + + public: + + FileWatch(StringType path, UnderpinningRegex pattern, std::function callback) : + _path(absolute_path_of(path)), + _pattern(pattern), + _callback(callback), + _directory(get_directory(path)) + { + init(); + } + + FileWatch(StringType path, std::function callback) : + FileWatch(path, UnderpinningRegex(_regex_all), callback) {} + + ~FileWatch() { + destroy(); + } + + FileWatch(const FileWatch& other) : FileWatch(other._path, other._callback) {} + + FileWatch& operator=(const FileWatch& other) + { + if (this == &other) { return *this; } + + destroy(); + _path = other._path; + _callback = other._callback; + _directory = get_directory(other._path); + init(); + return *this; + } + + // Const memeber varibles don't let me implent moves nicely, if moves are really wanted std::unique_ptr should be used and move that. + FileWatch(FileWatch&&) = delete; + FileWatch& operator=(FileWatch&&) & = delete; + + private: + static constexpr C _regex_all[] = { '.', '*', '\0' }; + static constexpr C _this_directory[] = { '.', '/', '\0' }; + + struct PathParts + { + PathParts(StringType directory, StringType filename) : directory(directory), filename(filename) {} + StringType directory; + StringType filename; + }; + const StringType _path; + + UnderpinningRegex _pattern; + + static constexpr std::size_t _buffer_size = { 1024 * 256 }; + + // only used if watch a single file + StringType _filename; + + std::function _callback; + + std::thread _watch_thread; + + std::condition_variable _cv; + std::mutex _callback_mutex; + std::vector> _callback_information; + std::thread _callback_thread; + + std::promise _running; + std::atomic _destory = { false }; + bool _watching_single_file = { false }; + +#pragma mark "Platform specific data" +#ifdef _WIN32 + HANDLE _directory = { nullptr }; + HANDLE _close_event = { nullptr }; + + const DWORD _listen_filters = + FILE_NOTIFY_CHANGE_SECURITY | + FILE_NOTIFY_CHANGE_CREATION | + FILE_NOTIFY_CHANGE_LAST_ACCESS | + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_FILE_NAME; + + const std::unordered_map _event_type_mapping = { + { FILE_ACTION_ADDED, Event::added }, + { FILE_ACTION_REMOVED, Event::removed }, + { FILE_ACTION_MODIFIED, Event::modified }, + { FILE_ACTION_RENAMED_OLD_NAME, Event::renamed_old }, + { FILE_ACTION_RENAMED_NEW_NAME, Event::renamed_new } + }; +#endif // WIN32 + +#if __unix__ + struct FolderInfo { + int folder; + int watch; + }; + + FolderInfo _directory; + + const std::uint32_t _listen_filters = IN_MODIFY | IN_CREATE | IN_DELETE; + + const static std::size_t event_size = (sizeof(struct inotify_event)); +#endif // __unix__ + +#if FILEWATCH_PLATFORM_MAC + struct FileState + { + int fd; + uint32_t nlink; + time_t last_modification; + + FileState(int fd, uint32_t nlink, time_t lt) + : fd(fd), nlink(nlink), + last_modification(lt) + { + + } + FileState(const FileState&) = delete; + FileState& operator=(const FileState&) = delete; + FileState(FileState&& other) : fd(other.fd), nlink(other.nlink), last_modification(other.last_modification) + { + other.fd = -1; + } + + FileState invalidate_and_clone() { + int fd = this->fd; + + this->fd = -1; + return FileState{ fd, nlink, last_modification }; + } + + ~FileState() + { + if (fd != -1) { + close(fd); + } + } + }; + std::unordered_map _directory_snapshot{}; + bool _previous_event_is_rename = false; + CFRunLoopRef _run_loop = nullptr; + int _file_fd = -1; + struct timespec _last_modification_time = {}; + FSEventStreamRef _directory; + // fd for single file +#endif // FILEWATCH_PLATFORM_MAC + + void init() + { +#ifdef _WIN32 + _close_event = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!_close_event) { + throw std::system_error(GetLastError(), std::system_category()); + } +#endif // WIN32 + + _callback_thread = std::thread([this]() { + try { + callback_thread(); + } + catch (...) { + try { + _running.set_exception(std::current_exception()); + } + catch (...) {} // set_exception() may throw too + } + }); + + _watch_thread = std::thread([this]() { + try { + monitor_directory(); + } + catch (...) { + try { + _running.set_exception(std::current_exception()); + } + catch (...) {} // set_exception() may throw too + } + }); + + std::future future = _running.get_future(); + future.get(); //block until the monitor_directory is up and running + } + + void destroy() + { + _destory = true; + _running = std::promise(); + +#ifdef _WIN32 + SetEvent(_close_event); +#elif __unix__ + inotify_rm_watch(_directory.folder, _directory.watch); +#elif FILEWATCH_PLATFORM_MAC + if (_run_loop) { + CFRunLoopStop(_run_loop); + } +#endif // __unix__ + + _cv.notify_all(); + _watch_thread.join(); + _callback_thread.join(); + +#ifdef _WIN32 + CloseHandle(_directory); +#elif __unix__ + close(_directory.folder); +#elif FILEWATCH_PLATFORM_MAC + FSEventStreamStop(_directory); + FSEventStreamInvalidate(_directory); + FSEventStreamRelease(_directory); + _directory = nullptr; +#endif // FILEWATCH_PLATFORM_MAC + } + + const PathParts split_directory_and_file(const StringType& path) const + { + const auto predict = [](C character) { +#ifdef _WIN32 + return character == C('\\') || character == C('/'); +#elif __unix__ || FILEWATCH_PLATFORM_MAC + return character == C('/'); +#endif // __unix__ + }; + + UnderpinningString path_string = path; + const auto pivot = std::find_if(path_string.rbegin(), path_string.rend(), predict).base(); + //if the path is something like "test.txt" there will be no directory part, however we still need one, so insert './' + const StringType directory = [&]() { + const auto extracted_directory = UnderpinningString(path_string.begin(), pivot); + return (extracted_directory.size() > 0) ? extracted_directory : UnderpinningString(_this_directory); + }(); + const StringType filename = UnderpinningString(pivot, path_string.end()); + return PathParts(directory, filename); + } + + bool pass_filter(const UnderpinningString& file_path) + { + if (_watching_single_file) { + const UnderpinningString extracted_filename = { split_directory_and_file(file_path).filename }; + //if we are watching a single file, only that file should trigger action + return extracted_filename == _filename; + } + return std::regex_match(file_path, _pattern); + } + +#ifdef _WIN32 + template DWORD GetFileAttributesX(const char* lpFileName, Args... args) { + return GetFileAttributesA(lpFileName, args...); + } + template DWORD GetFileAttributesX(const wchar_t* lpFileName, Args... args) { + return GetFileAttributesW(lpFileName, args...); + } + + template HANDLE CreateFileX(const char* lpFileName, Args... args) { + return CreateFileA(lpFileName, args...); + } + template HANDLE CreateFileX(const wchar_t* lpFileName, Args... args) { + return CreateFileW(lpFileName, args...); + } + + HANDLE get_directory(const StringType& path) + { + auto file_info = GetFileAttributesX(path.c_str()); + + if (file_info == INVALID_FILE_ATTRIBUTES) + { + throw std::system_error(GetLastError(), std::system_category()); + } + _watching_single_file = (file_info & FILE_ATTRIBUTE_DIRECTORY) == false; + + const StringType watch_path = [this, &path]() { + if (_watching_single_file) + { + const auto parsed_path = split_directory_and_file(path); + _filename = parsed_path.filename; + return parsed_path.directory; + } + else + { + return path; + } + }(); + + HANDLE directory = CreateFileX( + watch_path.c_str(), // pointer to the file name + FILE_LIST_DIRECTORY, // access (read/write) mode + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode + nullptr, // security descriptor + OPEN_EXISTING, // how to create + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes + HANDLE(0)); // file with attributes to copy + + if (directory == INVALID_HANDLE_VALUE) + { + throw std::system_error(GetLastError(), std::system_category()); + } + return directory; + } + + void convert_wstring(const std::wstring& wstr, std::string& out) + { + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); + out.resize(size_needed, '\0'); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &out[0], size_needed, NULL, NULL); + } + + void convert_wstring(const std::wstring& wstr, std::wstring& out) + { + out = wstr; + } + + void monitor_directory() + { + std::vector buffer(_buffer_size); + DWORD bytes_returned = 0; + OVERLAPPED overlapped_buffer{ 0 }; + + overlapped_buffer.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!overlapped_buffer.hEvent) { + std::cerr << "Error creating monitor event" << std::endl; + } + + std::array handles{ overlapped_buffer.hEvent, _close_event }; + + auto async_pending = false; + _running.set_value(); + do { + std::vector> parsed_information; + ReadDirectoryChangesW( + _directory, + buffer.data(), static_cast(buffer.size()), + TRUE, + _listen_filters, + &bytes_returned, + &overlapped_buffer, NULL); + + async_pending = true; + + switch (WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE)) + { + case WAIT_OBJECT_0: + { + if (!GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE)) { + throw std::system_error(GetLastError(), std::system_category()); + } + async_pending = false; + + if (bytes_returned == 0) { + break; + } + + FILE_NOTIFY_INFORMATION *file_information = reinterpret_cast(&buffer[0]); + do + { + std::wstring changed_file_w{ file_information->FileName, file_information->FileNameLength / sizeof(file_information->FileName[0]) }; + UnderpinningString changed_file; + convert_wstring(changed_file_w, changed_file); + if (pass_filter(changed_file)) + { + parsed_information.emplace_back(StringType{ changed_file }, _event_type_mapping.at(file_information->Action)); + } + + if (file_information->NextEntryOffset == 0) { + break; + } + + file_information = reinterpret_cast(reinterpret_cast(file_information) + file_information->NextEntryOffset); + } while (true); + break; + } + case WAIT_OBJECT_0 + 1: + // quit + break; + case WAIT_FAILED: + break; + } + //dispatch callbacks + { + std::lock_guard lock(_callback_mutex); + _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end()); + } + _cv.notify_all(); + } while (_destory == false); + + if (async_pending) + { + //clean up running async io + CancelIo(_directory); + GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE); + } + } +#endif // WIN32 + +#if __unix__ + + bool is_file(const StringType& path) const + { + struct stat statbuf = {}; + if (stat(path.c_str(), &statbuf) != 0) + { + throw std::system_error(errno, std::system_category()); + } + return S_ISREG(statbuf.st_mode); + } + + FolderInfo get_directory(const StringType& path) + { + const auto folder = inotify_init(); + if (folder < 0) + { + throw std::system_error(errno, std::system_category()); + } + + _watching_single_file = is_file(path); + + const StringType watch_path = [this, &path]() { + if (_watching_single_file) + { + const auto parsed_path = split_directory_and_file(path); + _filename = parsed_path.filename; + return parsed_path.directory; + } + else + { + return path; + } + }(); + + const auto watch = inotify_add_watch(folder, watch_path.c_str(), IN_MODIFY | IN_CREATE | IN_DELETE); + if (watch < 0) + { + throw std::system_error(errno, std::system_category()); + } + return { folder, watch }; + } + + void monitor_directory() + { + std::vector buffer(_buffer_size); + + _running.set_value(); + while (_destory == false) + { + const auto length = read(_directory.folder, static_cast(buffer.data()), buffer.size()); + if (length > 0) + { + int i = 0; + std::vector> parsed_information; + while (i < length) + { + struct inotify_event *event = reinterpret_cast(&buffer[i]); // NOLINT + if (event->len) + { + const UnderpinningString changed_file{ event->name }; + if (pass_filter(changed_file)) + { + if (event->mask & IN_CREATE) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::added); + } + else if (event->mask & IN_DELETE) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::removed); + } + else if (event->mask & IN_MODIFY) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::modified); + } + } + } + i += event_size + event->len; + } + //dispatch callbacks + { + std::lock_guard lock(_callback_mutex); + _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end()); + } + _cv.notify_all(); + } + } + } +#endif // __unix__ + +#if FILEWATCH_PLATFORM_MAC + static StringType absolute_path_of(const StringType& path) { + char buf[PATH_MAX]; + int fd = open((const char*)path.c_str(), O_RDONLY); + const char* str = buf; + struct stat stat; + mbstate_t state; + + assert(fd != -1); + fcntl(fd, F_GETPATH, buf); + fstat(fd, &stat); + + if (stat.st_mode & S_IFREG || stat.st_mode & S_IFLNK) { + size_t len = strlen(buf); + + for (size_t i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + buf[i] = '\0'; + break; + } + } + } + close(fd); + + if (IsWChar::value) { + size_t needed = mbsrtowcs(nullptr, &str, 0, &state) + 1; + StringType s; + + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &str, s.size(), &state); + return s; + } + return StringType{ buf }; + } +#elif defined(__unix__) + static StringType absolute_path_of(const StringType& path) { + char buf[PATH_MAX]; + const char* str = buf; + struct stat stat; + mbstate_t state; + + realpath((const char*)path.c_str(), buf); + ::stat((const char*)path.c_str(), &stat); + + if (stat.st_mode & S_IFREG || stat.st_mode & S_IFLNK) { + size_t len = strlen(buf); + + for (size_t i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + buf[i] = '\0'; + break; + } + } + } + + if (IsWChar::value) { + size_t needed = mbsrtowcs(nullptr, &str, 0, &state) + 1; + StringType s; + + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &str, s.size(), &state); + return s; + } + return StringType{ buf }; + } +#elif _WIN32 + static StringType absolute_path_of(const StringType& path) { + constexpr size_t size = IsWChar::value ? MAX_PATH : 32767 * sizeof(wchar_t); + char buf[size]; + + DWORD length = IsWChar::value ? + GetFullPathNameW((LPCWSTR)path.c_str(), + size / sizeof(TCHAR), + (LPWSTR)buf, + nullptr) : + GetFullPathNameA((LPCSTR)path.c_str(), + size / sizeof(TCHAR), + buf, + nullptr); + return StringType{ (C*)buf, length }; + } +#endif + +#if FILEWATCH_PLATFORM_MAC + static StringType utf8StringToUtf32String(const char* buffer) { + mbstate_t state{}; + StringType s{}; + + size_t needed = mbsrtowcs(nullptr, &buffer, 0, &state) + 1; + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &buffer, s.size(), &state); + return s; + } + + template::value>> + static void walkDirectory(const StringType& path, Fn callback) { + int fd = open(path.c_str(), O_RDONLY); + char buf[1024]; + long basep = 0; + + if (fd == -1) { + return; + } + + int ret = __getdirentries64(fd, buf, sizeof(buf), &basep); + + while (ret > 0) { + char* current = buf; + int offset = 0; + + while (offset < ret) { + struct dirent* dirent = (struct dirent*)current; + StringType name = IsWChar::value ? + utf8StringToUtf32String(dirent->d_name) + : StringType(dirent->d_name); + + callback(std::move(name)); + current += dirent->d_reclen; + offset += dirent->d_reclen; + } + ret = __getdirentries64(fd, buf, sizeof(buf), &basep); + } + close(fd); + } + + static StringType nameofFd(int fd) { + size_t len = 0; + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + + len = strnlen(buf, MAXPATHLEN); + for (int i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + return StringType{ buf + i + 1, len - i - 1 }; + } + } + return StringType{ buf, len }; + } + + static StringType fullPathOfFd(int fd) { + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + return StringType{ (C*)buf }; + } + + static StringType pathOfFd(int fd) { + size_t len = 0; + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + + len = strnlen(buf, MAXPATHLEN); + for (int i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + return StringType{ buf, static_cast(i) }; + } + } + return StringType{ buf, len }; + } + + static bool fdIsRemoved(int fd) { + char buf[MAXPATHLEN]; + return fcntl(fd, F_GETPATH, buf) == -1; + } + + FileState makeFileState(const StringType& path) { + int fd = openFile(path); + struct stat stat; + + fstat(fd, &stat); + + return FileState{ + openFile(path), + stat.st_nlink, + stat.st_mtimespec.tv_sec + }; + } + + static StringType filenameOf(const StringType& file) { + for (int i = file.size() - 1; i >= 0; i--) { + if (file[i] == '/') { + return file.substr(i + 1); + } + } + return file; + } + + static bool isInDirectory(const StringType& file, const StringType& path) { + if (file.size() < path.size()) { + return false; + } + return strncmp(file.data(), path.data(), path.size()) == 0; + } + + PathParts splitPath(const StringType& path) { + PathParts split = split_directory_and_file(path); + + if (split.directory.size() > 0 && split.directory[split.directory.size() - 1] == '/') { + split.directory.erase(split.directory.size() - 1); + } + return split; + } + + StringType fullPathOf(const StringType& file) { + return _path + '/' + file; + } + + int openFile(const StringType& file) { + int fd = open(fullPathOf(file).c_str(), O_RDONLY); + assert(fd != -1); + return fd; + } + + void walkAndSeeChanges() { + struct RenamedPair { + StringType old; + StringType current; + }; + struct EventInfo { + StringType file; + struct timespec time; + Event event; + }; + std::unordered_map newSnapshot{}; + std::vector events{}; + + for (auto& entry : _directory_snapshot) { + struct stat stat; + + fstat(entry.second.fd, &stat); + if (fdIsRemoved(entry.second.fd)) { + events.push_back(EventInfo{ + .event = Event::removed, + .file = entry.first, + .time = stat.st_ctimespec + }); + continue; + } + + StringType fullPath = fullPathOfFd(entry.second.fd); + PathParts pathPair = splitPath(fullPath); + + if (pathPair.directory != _path) { + events.push_back(EventInfo{ + .event = Event::removed, + .file = entry.first, + .time = stat.st_ctimespec + }); + continue; + } + if (entry.first != pathPair.filename) { + events.push_back(EventInfo{ + .event = Event::renamed_old, + .file = entry.first, + .time = stat.st_ctimespec + }); + events.push_back(EventInfo{ + .event = Event::renamed_new, + .file = pathPair.filename, + .time = stat.st_ctimespec + }); + } + else { + if (stat.st_mtimespec.tv_sec > entry.second.last_modification) { + entry.second.last_modification = stat.st_mtimespec.tv_sec; + events.push_back(EventInfo{ + .event = Event::modified, + .file = pathPair.filename, + .time = stat.st_mtimespec + }); + } + } + newSnapshot.insert(std::make_pair(std::move(pathPair.filename), + std::move(entry.second.invalidate_and_clone()))); + } + + walkDirectory(_path, [&](StringType file) { + if (isParentOrSelfDirectory(file) || !std::regex_match(file, _pattern)) { + return; + } + if (newSnapshot.count(file) == 0) { + FileState state = makeFileState(file); + struct stat stat; + + fstat(state.fd, &stat); + events.push_back(EventInfo{ + .event = Event::added, + .file = file, + .time = stat.st_mtimespec + }); + newSnapshot.insert(std::make_pair(file, std::move(state))); + } + }); + + std::swap(_directory_snapshot, newSnapshot); + + std::sort(events.begin(), events.end(), [](const EventInfo& a, EventInfo& b) { + if (a.time.tv_sec == b.time.tv_sec) { + return a.time.tv_nsec < b.time.tv_nsec; + } + return a.time.tv_sec < b.time.tv_sec; + }); + + { + std::lock_guard lock(_callback_mutex); + + for (const auto& event : events) { + _callback_information.push_back(std::make_pair(event.file, event.event)); + } + } + _cv.notify_all(); + } + + void seeSingleFileChanges() { + struct EventInfo { + StringType file; + Event event; + }; + + int eventCount = 1; + EventInfo eventInfos[2]; + + if (fdIsRemoved(_file_fd)) { + eventInfos[0].event = Event::removed; + eventInfos[0].file = _filename; + } + else { + StringType absPath = pathOfFd(_file_fd); + PathParts split = splitPath(absPath); + + if (split.directory != _path) { + eventInfos[0].event = Event::removed; + eventInfos[0].file = _filename; + } + else if (split.filename != _filename) { + eventInfos[0].event = Event::renamed_old; + eventInfos[0].file = std::move(_filename); + eventInfos[1].event = Event::renamed_new; + eventInfos[1].file = split.filename; + eventCount = 2; + _filename = std::move(split.filename); + } + else { + struct stat stat; + + fstat(_file_fd, &stat); + + if (stat.st_mtimespec.tv_sec > _last_modification_time.tv_sec) { + eventInfos[0].event = Event::modified; + eventInfos[0].file = _filename; + _last_modification_time = stat.st_mtimespec; + } + else if (stat.st_mtimespec.tv_nsec > _last_modification_time.tv_nsec) { + eventInfos[0].event = Event::modified; + eventInfos[0].file = _filename; + _last_modification_time = stat.st_mtimespec; + } + else { + return; + } + } + } + + { + std::lock_guard lock(_callback_mutex); + for (int i = 0; i < eventCount; i++) { + _callback_information.push_back( + std::make_pair(eventInfos[i].file, eventInfos[i].event)); + } + } + _cv.notify_all(); + } + + void notify(CFStringRef path, const FSEventStreamEventFlags flags) { + CFIndex pathLength = CFStringGetLength(path); + CFIndex written = 0; + char buffer[PATH_MAX + 1]; + + CFStringGetBytes(path, + CFRange{ + .location = 0, + .length = pathLength, + }, + IsWChar::value ? kCFStringEncodingUTF32 : kCFStringEncodingUTF8, + 0, + false, + (UInt8*)buffer, + PATH_MAX, + &written); + + buffer[written] = 0; + + StringType absolutePath{ (const C*)buffer, static_cast(pathLength) }; + PathParts pathPair = splitPath(absolutePath); + + if (_watching_single_file && pathPair.filename != _filename) { + return; + } + if (pathPair.directory != _path || !std::regex_match(pathPair.filename, _pattern)) { + return; + } + + Event event = Event::modified; + if (_previous_event_is_rename) { + event = Event::renamed_new; + _directory_snapshot.insert(std::make_pair(pathPair.filename, + std::move(makeFileState(pathPair.filename)))); + _previous_event_is_rename = false; + } + else if (flags & kFSEventStreamEventFlagItemRenamed) { + const auto state = _directory_snapshot.find(pathPair.filename); + assert(state != _directory_snapshot.end()); + StringType fdPath = pathOfFd(state->second.fd); + + // moved/delete to Trash folder + if (!isInDirectory(absolutePath, fdPath)) { + event = Event::removed; + _directory_snapshot.erase(pathPair.filename); + } + else { + event = Event::renamed_old; + _previous_event_is_rename = true; + } + } + else if (flags & kFSEventStreamEventFlagItemCreated) { + _directory_snapshot.insert(std::make_pair(pathPair.filename, + std::move(makeFileState(pathPair.filename)))); + event = Event::added; + } + else if (flags & kFSEventStreamEventFlagItemRemoved) { + _directory_snapshot.erase(pathPair.filename); + event = Event::removed; + } + + { + std::lock_guard lock(_callback_mutex); + _callback_information.push_back(std::make_pair(std::move(pathPair.filename), event)); + } + _cv.notify_all(); + } + + static void handleFsEvent(__attribute__((unused)) ConstFSEventStreamRef streamFef, + void* clientCallBackInfo, + size_t numEvents, + CFArrayRef eventPaths, + const FSEventStreamEventFlags* eventFlags, + __attribute__((unused)) const FSEventStreamEventId* eventIds) { + FileWatch* self = (FileWatch*)clientCallBackInfo; + + for (size_t i = 0; i < numEvents; i++) { + FSEventStreamEventFlags flag = eventFlags[i]; + CFStringRef path = (CFStringRef)CFArrayGetValueAtIndex(eventPaths, i); + + if (self->_watching_single_file) { + self->seeSingleFileChanges(); + } + else if (flag & kFSEventStreamEventFlagMustScanSubDirs) { + self->walkAndSeeChanges(); + } + else { + self->notify(path, flag); + } + } + } + + FSEventStreamRef openStream(const StringType& directory) { + CFStringEncoding encoding = IsWChar::value ? + kCFStringEncodingUTF32 : kCFStringEncodingASCII; + CFStringRef path = CFStringCreateWithBytes(kCFAllocatorDefault, + (const UInt8*)directory.data(), + directory.size(), + encoding, + false); + CFArrayRef paths = CFArrayCreate( + kCFAllocatorDefault, + (const void**)&path, + 1, + nullptr); + FSEventStreamContext context{ + .info = (void*)this + }; + FSEventStreamRef event = FSEventStreamCreate( + kCFAllocatorDefault, + (FSEventStreamCallback)handleFsEvent, + &context, + paths, + kFSEventStreamEventIdSinceNow, + 0, + kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagUseCFTypes); + + CFRelease(path); + CFRelease(paths); + return event; + } + + FSEventStreamRef openStreamForDirectory(const StringType& directory) { + FSEventStreamRef stream = openStream(directory); + walkDirectory(directory, [this](StringType path) mutable { + if (!isParentOrSelfDirectory(path) && std::regex_match(path, _pattern)) { + _directory_snapshot.insert(std::make_pair(std::move(path), + std::move(makeFileState(path)))); + } + }); + return stream; + } + + FSEventStreamRef openStreamForFile(const StringType& file) { + PathParts split = splitPath(file); + + _watching_single_file = true; + _filename = std::move(split.filename); + _file_fd = openFile(file); + return openStreamForDirectory(split.directory); + } + + FSEventStreamRef get_directory(const StringType& directory) { + struct stat stat; + + ::stat((const char*)directory.c_str(), &stat); + if (stat.st_mode & S_IFDIR) { + return openStreamForDirectory(directory); + } + _last_modification_time = stat.st_mtimespec; + return openStreamForFile(directory); + } + + void monitor_directory() { + _run_loop = CFRunLoopGetCurrent(); + FSEventStreamScheduleWithRunLoop(_directory, + _run_loop, + kCFRunLoopDefaultMode); + FSEventStreamStart(_directory); + _running.set_value(); + CFRunLoopRun(); + } +#endif // FILEWATCH_PLATFORM_MAC + + void callback_thread() + { + while (_destory == false) { + std::unique_lock lock(_callback_mutex); + if (_callback_information.empty() && _destory == false) { + _cv.wait(lock, [this] { return _callback_information.size() > 0 || _destory; }); + } + decltype(_callback_information) callback_information = {}; + std::swap(callback_information, _callback_information); + lock.unlock(); + + for (const auto& file : callback_information) { + if (_callback) { + try + { + _callback(file.first, file.second); + } + catch (const std::exception&) + { + } + } + } + } + } + }; + + template constexpr typename FileWatch::C FileWatch::_regex_all[]; + template constexpr typename FileWatch::C FileWatch::_this_directory[]; +} +#endif \ No newline at end of file