diff --git a/CMakeLists.txt b/CMakeLists.txt index ed74e46..20fae6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,7 @@ set(VMIX_RSC_FILES ./rsc/shaders/filters/resample_quarter.glsl ./rsc/shaders/filters/earlybird.glsl ./rsc/shaders/filters/logo.glsl + ./rsc/shaders/filters/whitebalance.glsl ) cmrc_add_resource_library(vmix-resources ALIAS vmix::rc NAMESPACE vmix WHENCE rsc ${VMIX_RSC_FILES}) diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index 6654a7b..4c190c2 100644 Binary files a/rsc/images/icons.dds and b/rsc/images/icons.dds differ diff --git a/rsc/shaders/filters/whitebalance.glsl b/rsc/shaders/filters/whitebalance.glsl new file mode 100644 index 0000000..01117cc --- /dev/null +++ b/rsc/shaders/filters/whitebalance.glsl @@ -0,0 +1,262 @@ +// From https://www.shadertoy.com/view/Ml2BRD +// and https://www.shadertoy.com/view/MtcfDr +// By Tynach. +// Adapted by Bruno Herbelin for vimix + +precision highp float; + +/* + * Settings + */ + +// Minimum temperature in the range (defined in the standard as 4000) +const float minTemp = 4000.0; + +// Maximum temperature in the range (defined in the standard as 25000) +const float maxTemp = 9000.0; + +// [0 1] value slider between min and max Temp +uniform float Temperature; + +// Color for picked 'grey' +uniform float Red; +uniform float Green; +uniform float Blue; + + +// Parameters for transfer characteristics (gamma curves) +struct transfer { + // Exponent used to linearize the signal + float power; + + // Offset from 0.0 for the exponential curve + float off; + + // Slope of linear segment near 0 + float slope; + + // Values below this are divided by slope during linearization + float cutoffToLinear; + + // Values below this are multiplied by slope during gamma correction + float cutoffToGamma; +}; + +// Parameters for a colorspace +struct rgb_space { + // Chromaticity coordinates (xyz) for Red, Green, and Blue primaries + mat4 primaries; + + // Chromaticity coordinates (xyz) for white point + vec4 white; + + // Linearization and gamma correction parameters + transfer trc; +}; + + +/* + * Preprocessor 'functions' that help build colorspaces as constants + */ + +// Turns 6 chromaticity coordinates into a 3x3 matrix +#define Primaries(r1, r2, g1, g2, b1, b2)\ + mat4(\ + (r1), (r2), 1.0 - (r1) - (r2), 0,\ + (g1), (g2), 1.0 - (g1) - (g2), 0,\ + (b1), (b2), 1.0 - (b1) - (b2), 0,\ + 0, 0, 0, 1) + +// Creates a whitepoint's xyz chromaticity coordinates from the given xy coordinates +#define White(x, y)\ + vec4(vec3((x), (y), 1.0 - (x) - (y))/(y), 1) + +// Automatically calculate the slope and cutoffs for transfer characteristics +#define Transfer(po, of)\ +transfer(\ + (po),\ + (of),\ + (pow((po)*(of)/((po) - 1.0), 1.0 - (po))*pow(1.0 + (of), (po)))/(po),\ + (of)/((po) - 1.0),\ + (of)/((po) - 1.0)*(po)/(pow((po)*(of)/((po) - 1.0), 1.0 - (po))*pow(1.0 + (of), (po)))\ +) + +// Creates a scaling matrix using a vec4 to set the xyzw scalars +#define diag(v)\ + mat4(\ + (v).x, 0, 0, 0,\ + 0, (v).y, 0, 0,\ + 0, 0, (v).z, 0,\ + 0, 0, 0, (v).w) + +// Creates a conversion matrix that turns RGB colors into XYZ colors +#define rgbToXyz(space)\ + (space.primaries*diag(inverse((space).primaries)*(space).white)) + +// Creates a conversion matrix that turns XYZ colors into RGB colors +#define xyzToRgb(space)\ + inverse(rgbToXyz(space)) + + +/* + * Chromaticities for RGB primaries + */ + + +// Rec. 709 (HDTV) and sRGB primaries +const mat4 primaries709 = Primaries( + 0.64, 0.33, + 0.3, 0.6, + 0.15, 0.06 +); + + +// Same as above, but in fractional form +const mat4 primariesLms = Primaries( + 194735469.0/263725741.0, 68990272.0/263725741.0, + 141445123.0/106612934.0, -34832189.0/106612934.0, + 36476327.0/229961670.0, 0.0 +); + + +/* + * Chromaticities for white points + */ + +// Standard illuminant E (also known as the 'equal energy' white point) +const vec4 whiteE = White(1.0/3.0, 1.0/3.0); + +// Standard Illuminant D65 according to the Rec. 709 and sRGB standards +const vec4 whiteD65S = White(0.3127, 0.3290); + +/* + * Gamma curve parameters + */ +// Linear gamma +const transfer gam10 = transfer(1.0, 0.0, 1.0, 0.0, 0.0); +// Gamma for sRGB +const transfer gamSrgb = transfer(2.4, 0.055, 12.92, 0.04045, 0.0031308); + +/* + * RGB Colorspaces + */ +// sRGB (mostly the same as Rec. 709, but different gamma and full range values) +const rgb_space Srgb = rgb_space(primaries709, whiteD65S, gamSrgb); + +// Lms primaries, balanced against equal energy white point +const rgb_space LmsRgb = rgb_space(primariesLms, whiteE, gam10); + + +/* + * Colorspace conversion functions + */ + +// Converts RGB colors to a linear light scale +vec4 toLinear(vec4 color, const transfer trc) +{ + bvec4 cutoff = lessThan(color, vec4(trc.cutoffToLinear)); + bvec4 negCutoff = lessThanEqual(color, vec4(-1.0*trc.cutoffToLinear)); + vec4 higher = pow((color + trc.off)/(1.0 + trc.off), vec4(trc.power)); + vec4 lower = color/trc.slope; + vec4 neg = -1.0*pow((color - trc.off)/(-1.0 - trc.off), vec4(trc.power)); + + vec4 result = mix(higher, lower, cutoff); + return mix(result, neg, negCutoff); +} + +// Gamma-corrects RGB colors to be sent to a display +vec4 toGamma(vec4 color, const transfer trc) +{ + bvec4 cutoff = lessThan(color, vec4(trc.cutoffToGamma)); + bvec4 negCutoff = lessThanEqual(color, vec4(-1.0*trc.cutoffToGamma)); + vec4 higher = (1.0 + trc.off)*pow(color, vec4(1.0/trc.power)) - trc.off; + vec4 lower = color*trc.slope; + vec4 neg = (-1.0 - trc.off)*pow(-1.0*color, vec4(1.0/trc.power)) + trc.off; + + vec4 result = mix(higher, lower, cutoff); + return mix(result, neg, negCutoff); +} + +// Scales a color to the closest in-gamut representation of that color +vec4 gamutScale(vec4 color, float luma) +{ + float low = min(color.r, min(color.g, min(color.b, 0.0))); + float high = max(color.r, max(color.g, max(color.b, 1.0))); + + float lowScale = low/(low - luma); + float highScale = max((high - 1.0)/(high - luma), 0.0); + float scale = max(lowScale, highScale); + color.rgb += scale*(luma - color.rgb); + + return color; +} +// Calculate Standard Illuminant Series D light source XYZ values +vec4 temperatureToXyz(float temperature) +{ + // Calculate terms to be added up. Since only the coefficients aren't + // known ahead of time, they're the only thing determined by mix() + float x = dot(mix( + vec4(0.244063, 99.11, 2967800.0, -4607000000.0), + vec4(0.23704, 247.48, 1901800.0, -2006400000.0), + bvec4(temperature > 7000.0) + )/vec4(1, temperature, pow(temperature, 2.0), pow(temperature, 3.0)), vec4(1)); + + return White(x, -3.0*pow(x, 2.0) + 2.87*x - 0.275); +} + + +// XYZ conversion matrices for the display colorspace +const mat4 toXyz = rgbToXyz(Srgb); +const mat4 toRgb = xyzToRgb(Srgb); + +// LMS conversion matrices for white point adaptation +const mat4 toLms = xyzToRgb(LmsRgb); +const mat4 frLms = rgbToXyz(LmsRgb); + + +// Converts from one RGB colorspace to another, output as linear light +vec4 convert(vec4 color, vec4 whiteNew) +{ + color = toXyz*color; + + color= toLms*color; + color = diag(((toLms*Srgb.white)/(toLms*whiteNew)))*color; + color = inverse(toLms)*color; + float luma = color.y; + + color = toRgb*color; + color = gamutScale(color, luma); + + return color; +} + +// Main function +void mainImage(out vec4 color, in vec2 coord) +{ + vec2 uv = coord.xy / iResolution.xy; + vec4 texColor = texture(iChannel0, uv); + + texColor = toLinear(texColor, Srgb.trc); + + // Calculate temperature conversion + vec4 convWhite = temperatureToXyz(Temperature*(maxTemp - minTemp) + minTemp); + mat4 adapt = toRgb*frLms*diag((toLms*convWhite)/(toLms*Srgb.white))*toLms*toXyz; + texColor = adapt*texColor; + + // Darken values to fit within gamut (for texture) + vec4 newWhite = adapt*vec4(1); + texColor.rgb /= max(newWhite.r, max(newWhite.g, newWhite.b)); + color = texColor; + + // color balance by color picker + vec4 whiteNew = vec4( Red, Green, Blue, 1.0); + whiteNew = toXyz*whiteNew; + whiteNew /= dot(whiteNew, vec4(1.0)); + whiteNew /= whiteNew.y; + color = convert(color, whiteNew); + + // Convert to display gamma curve + color = toGamma(color, Srgb.trc); +} + + diff --git a/src/DisplaysView.cpp b/src/DisplaysView.cpp index 4799f86..7b41655 100644 --- a/src/DisplaysView.cpp +++ b/src/DisplaysView.cpp @@ -119,8 +119,8 @@ DisplaysView::DisplaysView() : View(DISPLAYS) // initial behavior: no window selected, no menu show_window_menu_ = false; current_window_ = -1; - current_window_status_ = new Group; - + current_window_status_ = new Group; + current_window_whitebalance = glm::vec4(1.f, 1.f, 1.f, 0.5f); draw_pending_ = false; // display actions : 0 = move output, 1 paint, 2 erase @@ -377,15 +377,19 @@ void DisplaysView::draw() // Add / Remove windows ImGui::SameLine(); if ( Settings::application.num_output_windows < MAX_OUTPUT_WINDOW) { - if (ImGuiToolkit::IconButton(18, 4, "More windows")) + if (ImGuiToolkit::IconButton(18, 4, "More windows")) { ++Settings::application.num_output_windows; + current_window_ = Settings::application.num_output_windows-1; + } } else ImGuiToolkit::Icon(18, 4, false); ImGui::SameLine(); if ( Settings::application.num_output_windows > 0 ) { - if (ImGuiToolkit::IconButton(19, 4, "Less windows")) + if (ImGuiToolkit::IconButton(19, 4, "Less windows")) { --Settings::application.num_output_windows; + current_window_ = -1; + } } else ImGuiToolkit::Icon(19, 4, false); @@ -393,47 +397,73 @@ void DisplaysView::draw() // Modify current window if (current_window_ > -1) { - // Pattern output - ImGui::SameLine(0, 50); + ImGuiContext& g = *GImGui; + + // title output + ImGui::SameLine(0, 5.f * g.Style.FramePadding.x); + ImGui::Text("Output %d", current_window_ + 1); + + // Output options + ImGui::SameLine(0, 2.f * g.Style.FramePadding.x); + ImGuiToolkit::ButtonIconToggle(8,5,9,5, &Settings::application.windows[1+current_window_].scaled, "Scaled to window"); + + ImGui::SameLine(0, g.Style.FramePadding.x); ImGuiToolkit::ButtonIconToggle(10,1,11,1, &Settings::application.windows[1+current_window_].show_pattern, "Test pattern"); -// // White ballance -// static DialogToolkit::ColorPickerDialog whitedialog; -// ImGui::SameLine(0, 30); -// ImGuiToolkit::Icon(5, 4); -// static ImVec4 white = ImVec4(1.f, 1.f, 1.f, 1.f); -// ImGui::SameLine(); -// ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); -// if (ImGui::ColorButton("White", white, ImGuiColorEditFlags_NoAlpha)) { -// whitedialog.setRGB( std::make_tuple(white.x, white.y, white.z) ); -// whitedialog.open(); -// } -// ImGui::PopFont(); -// // get picked color if dialog finished -// if (whitedialog.closed()){ -// std::tuple c = whitedialog.RGB(); -// white.x = std::get<0>(c); -// white.y = std::get<1>(c); -// white.z = std::get<2>(c); -// } + // White ballance color + static DialogToolkit::ColorPickerDialog whitebalancedialog; -// ImGui::SameLine(); -// ImGuiToolkit::Icon(3,4); -// static ImVec4 grey = ImVec4(0.5f, 0.5f, 0.5f, 1.f); -// ImGui::SameLine(); -// ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); -// ImGui::ColorButton("Grey", grey, ImGuiColorEditFlags_NoAlpha); -// ImGui::PopFont(); + ImGui::SameLine(0, 1.5f * g.Style.FramePadding.x); + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); -// ImGui::SameLine(); -// ImGuiToolkit::Icon(4,4); -// static ImVec4 black = ImVec4(0.f, 0.f, 0.f, 1.f); -// ImGui::SameLine(); -// ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); -// ImGui::ColorButton("Black", black, ImGuiColorEditFlags_NoAlpha); -// ImGui::PopFont(); + // hack to re-align color button to text + ImGuiWindow* window = ImGui::GetCurrentWindow(); + window->DC.CursorPos.y += g.Style.FramePadding.y; + if (ImGui::ColorButton("White balance", ImVec4(current_window_whitebalance.x, + current_window_whitebalance.y, + current_window_whitebalance.z, 1.f), + ImGuiColorEditFlags_NoAlpha)) { + whitebalancedialog.setRGB( std::make_tuple(current_window_whitebalance.x, + current_window_whitebalance.y, + current_window_whitebalance.z) ); + whitebalancedialog.open(); + } + ImGui::PopFont(); + // get picked color if dialog finished + if (whitebalancedialog.closed()){ + std::tuple c = whitebalancedialog.RGB(); + current_window_whitebalance.x = std::get<0>(c); + current_window_whitebalance.y = std::get<1>(c); + current_window_whitebalance.z = std::get<2>(c); + + // set White Balance Color matrix to shader + Rendering::manager().outputWindow(current_window_).setWhiteBalance( current_window_whitebalance ); + } + + // White ballance temperature + ImGui::SameLine(); + window->DC.CursorPos.y -= g.Style.FramePadding.y; + if (ImGui::Button(ICON_FA_THERMOMETER_HALF ICON_FA_SORT_DOWN )) + ImGui::OpenPopup("temperature_popup"); + if (ImGui::BeginPopup("temperature_popup", ImGuiWindowFlags_NoMove)) + { + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); + ImGuiToolkit::Indication("9000 K", " " ICON_FA_THERMOMETER_FULL); + if ( ImGui::VSliderFloat("##Temperature", ImVec2(30,260), ¤t_window_whitebalance.w, 0.0, 1.0, "") ){ + + Rendering::manager().outputWindow(current_window_).setWhiteBalance( current_window_whitebalance ); + } + if (ImGui::IsItemHovered() || ImGui::IsItemActive() ) { + ImGui::BeginTooltip(); + ImGui::Text("%d K", 4000 + (int) ceil(current_window_whitebalance.w * 5000.f)); + ImGui::EndTooltip(); + } + ImGuiToolkit::Indication("4000 K", " " ICON_FA_THERMOMETER_EMPTY); + ImGui::PopFont(); + ImGui::EndPopup(); + } } @@ -482,7 +512,7 @@ void DisplaysView::draw() Rendering::manager().outputWindow(current_window_).setDecoration(!_borderless); } - if (ImGui::MenuItem( ICON_FA_EXPAND_ALT " Buffer aspect ratio" , nullptr, false, _windowed )){ + if (ImGui::MenuItem( ICON_FA_EXPAND_ALT " Reset aspect ratio" , nullptr, false, _windowed )){ // reset aspect ratio glm::ivec4 rect = windowCoordinates(current_window_); float ar = Mixer::manager().session()->frame()->aspectRatio(); @@ -493,7 +523,7 @@ void DisplaysView::draw() Rendering::manager().outputWindow(current_window_).setCoordinates( rect ); } - if (ImGui::MenuItem( ICON_FA_COMPRESS " Buffer size", nullptr, false, _windowed )){ + if (ImGui::MenuItem( ICON_FA_RULER_COMBINED " Reset to pixel size", nullptr, false, _windowed )){ // reset resolution to 1:1 glm::ivec4 rect = windowCoordinates(current_window_); rect.p = Mixer::manager().session()->frame()->width(); @@ -517,7 +547,17 @@ void DisplaysView::draw() } ImGui::Separator(); - ImGui::MenuItem( ICON_FA_EXPAND_ARROWS_ALT " Scaled content", nullptr, &Settings::application.windows[1].scaled ); + if ( ImGui::MenuItem( ICON_FA_REPLY " Reset") ) { + glm::ivec4 rect (0, 0, 800, 600); + rect.p = Mixer::manager().session()->frame()->width(); + rect.q = Mixer::manager().session()->frame()->height(); + Rendering::manager().outputWindow(current_window_).setDecoration(true); + Rendering::manager().outputWindow(current_window_).setWhiteBalance( glm::vec4(1.f, 1.f, 1.f, 0.5f) ); + if (Settings::application.windows[current_window_+1].fullscreen) + Rendering::manager().outputWindow(current_window_).exitFullscreen(); + else + Rendering::manager().outputWindow(current_window_).setCoordinates( rect ); + } ImGui::PopStyleColor(2); ImGui::EndPopup(); @@ -538,6 +578,8 @@ std::pair DisplaysView::pick(glm::vec2 P) // test all windows current_window_ = -1; + current_window_whitebalance = glm::vec4(1.f, 1.f, 1.f, 0.5f); + for (int i = 0; i < Settings::application.num_output_windows; ++i) { // ignore pick on render surface: it's the same as output surface @@ -555,6 +597,7 @@ std::pair DisplaysView::pick(glm::vec2 P) (pick.first == windows_[i].handles_) || (pick.first == windows_[i].menu_) ) { current_window_ = i; + current_window_whitebalance = Rendering::manager().outputWindow(current_window_).whiteBalance(); windows_[i].overlays_->setActive(1); } else diff --git a/src/DisplaysView.h b/src/DisplaysView.h index c34e214..a793fdc 100644 --- a/src/DisplaysView.h +++ b/src/DisplaysView.h @@ -80,6 +80,7 @@ private: std::vector windows_; int current_window_; Group *current_window_status_; + glm::vec4 current_window_whitebalance; bool show_window_menu_; // Group *window_; diff --git a/src/ImageFilter.cpp b/src/ImageFilter.cpp index f7551c0..093d1e5 100644 --- a/src/ImageFilter.cpp +++ b/src/ImageFilter.cpp @@ -19,18 +19,14 @@ #include #include -#include #include #include #include "defines.h" #include "Resource.h" -#include "ImageShader.h" #include "Visitor.h" #include "FrameBuffer.h" -#include "ImageShader.h" #include "Primitives.h" -#include "Log.h" #include "ImageFilter.h" @@ -162,38 +158,6 @@ bool FilteringProgram::operator!= (const FilteringProgram& other) const /// //// //////////////////////////////////////// -class ImageFilteringShader : public ImageShader -{ - // GLSL Program - ShadingProgram custom_shading_; - - // fragment shader GLSL code - std::string shader_code_; - std::string code_; - -public: - // for iTimedelta - GTimer *timer_; - double iTime_; - uint iFrame_; - - // list of uniforms to control shader - std::map< std::string, float > uniforms_; - - ImageFilteringShader(); - ~ImageFilteringShader(); - - void update(float dt); - - void use() override; - void reset() override; - void copy(ImageFilteringShader const& S); - - // set the code of the filter - void setCode(const std::string &code, std::promise *ret = nullptr); - -}; - ImageFilteringShader::ImageFilteringShader(): ImageShader() { diff --git a/src/ImageFilter.h b/src/ImageFilter.h index 7426fd4..ba34418 100644 --- a/src/ImageFilter.h +++ b/src/ImageFilter.h @@ -7,8 +7,10 @@ #include #include +#include #include +#include "ImageShader.h" #include "FrameBufferFilter.h" class FilteringProgram @@ -65,7 +67,39 @@ public: class Surface; class FrameBuffer; -class ImageFilteringShader; + +class ImageFilteringShader : public ImageShader +{ + // GLSL Program + ShadingProgram custom_shading_; + + // fragment shader GLSL code + std::string shader_code_; + std::string code_; + +public: + // for iTimedelta + GTimer *timer_; + double iTime_; + uint iFrame_; + + // list of uniforms to control shader + std::map< std::string, float > uniforms_; + + ImageFilteringShader(); + ~ImageFilteringShader(); + + void update(float dt); + + void use() override; + void reset() override; + void copy(ImageFilteringShader const& S); + + // set the code of the filter + void setCode(const std::string &code, std::promise *ret = nullptr); + +}; + class ImageFilter : public FrameBufferFilter { diff --git a/src/RenderingManager.cpp b/src/RenderingManager.cpp index fb8e873..18f8646 100644 --- a/src/RenderingManager.cpp +++ b/src/RenderingManager.cpp @@ -69,6 +69,7 @@ #include "GstToolkit.h" #include "UserInterfaceManager.h" #include "ControlManager.h" +#include "ImageFilter.h" #include "RenderingManager.h" @@ -1042,6 +1043,7 @@ void RenderingWindow::terminate() // invalidate window_ = NULL; + shader_ = nullptr; surface_ = nullptr; fbo_ = 0; index_ = -1; @@ -1088,6 +1090,33 @@ void RenderingWindow::swap() } +FilteringProgram whitebalance("Whitebalance", "shaders/filters/whitebalance.glsl", "", { { "Red", 1.0}, { "Green", 1.0}, { "Blue", 1.0}, { "Temperature", 0.5} }); + + +void RenderingWindow::setWhiteBalance(glm::vec4 colorcorrection) +{ + if (shader_) + shader_->uniforms_ = std::map< std::string, float >{ + { "Red", colorcorrection.x}, + { "Green", colorcorrection.y}, + { "Blue", colorcorrection.z}, + { "Temperature", colorcorrection.w} + }; + +} + +glm::vec4 RenderingWindow::whiteBalance() const +{ + glm::vec4 ret(1.f, 1.f, 1.f, 0.5f); + + ret.x = shader_->uniforms_["Red"]; + ret.y = shader_->uniforms_["Green"]; + ret.z = shader_->uniforms_["Blue"]; + ret.w = shader_->uniforms_["Temperature"]; + + return ret; +} + bool RenderingWindow::draw(FrameBuffer *fb) { // cannot draw if there is no window or invalid framebuffer @@ -1119,8 +1148,15 @@ bool RenderingWindow::draw(FrameBuffer *fb) // VAO is not shared between multiple contexts of different windows // so we have to create a new VAO for rendering the surface in this window - if (surface_ == nullptr) - surface_ = new WindowSurface; + if (surface_ == nullptr) { + + const std::pair codes = whitebalance.code(); + shader_ = new ImageFilteringShader; + shader_->setCode( codes.first ); + shader_->uniforms_ = whitebalance.parameters(); + + surface_ = new WindowSurface(shader_); + } // calculate scaling factor of frame buffer inside window const float windowAspectRatio = aspectRatio(); diff --git a/src/RenderingManager.h b/src/RenderingManager.h index f39acdc..19bf015 100644 --- a/src/RenderingManager.h +++ b/src/RenderingManager.h @@ -40,6 +40,7 @@ class RenderingWindow uint fbo_; Stream *pattern_; class WindowSurface *surface_; + class ImageFilteringShader *shader_; protected: void setTitle(const std::string &title = ""); @@ -82,6 +83,10 @@ public: void setCoordinates(glm::ivec4 rect); void setDecoration (bool on); + // set color correction + void setWhiteBalance(glm::vec4 colorcorrection); + glm::vec4 whiteBalance() const; + // get width of rendering area int width(); // get height of rendering area diff --git a/src/TextureView.cpp b/src/TextureView.cpp index ed30a3e..d328efd 100644 --- a/src/TextureView.cpp +++ b/src/TextureView.cpp @@ -916,7 +916,7 @@ void TextureView::draw() s->touch(); Action::manager().store(s->name() + std::string(": Texture Reset rotation")); } - if (ImGui::Selectable( ICON_FA_EXPAND_ALT " Reset aspect ratio" )){ + if (ImGui::Selectable( ICON_FA_EXPAND_ALT " Reset aspect ratio" )){ s->group(mode_)->scale_.x = s->group(mode_)->scale_.y; s->group(mode_)->scale_.x *= s->group(mode_)->crop_.x / s->group(mode_)->crop_.y; s->touch();