Added WhiteBalance to RenderingWindow and Display View

Render output frame to output window using a Shader implementing white balance correction. Adjusting parameters of white balance from Display View with color picker (white) and slider (temperature). GLSL filter for white balance created from ShaderToy.
This commit is contained in:
Bruno Herbelin
2023-03-01 23:24:26 +01:00
parent 35507e7fbb
commit 1c309b2c89
10 changed files with 428 additions and 82 deletions

View File

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

Binary file not shown.

View File

@@ -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);
}

View File

@@ -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<float, float, float> 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<float, float, float> 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), &current_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<Node *, glm::vec2> 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<Node *, glm::vec2> 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

View File

@@ -80,6 +80,7 @@ private:
std::vector<WindowPreview> windows_;
int current_window_;
Group *current_window_status_;
glm::vec4 current_window_whitebalance;
bool show_window_menu_;
// Group *window_;

View File

@@ -19,18 +19,14 @@
#include <ctime>
#include <algorithm>
#include <glib.h>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
#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<std::string> *ret = nullptr);
};
ImageFilteringShader::ImageFilteringShader(): ImageShader()
{

View File

@@ -7,8 +7,10 @@
#include <string>
#include <future>
#include <glib.h>
#include <glm/glm.hpp>
#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<std::string> *ret = nullptr);
};
class ImageFilter : public FrameBufferFilter
{

View File

@@ -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<std::string, std::string> 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();

View File

@@ -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

View File

@@ -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();