gfx/lut: Add a simple but efficient LUT producer and consumer

For simple image and video editing, LUTs (Look-Up Tables) are vastly superior to running the entire editing operation on each pixel - especially if all the processing can be done inside a single shader.

Due to the post-processing requirements for our LUTs, we are limited to 8 bits per channel - though clever use of the unused Alpha channel may result in additional space. For our purposes however, this is definitely enough.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2021-02-02 22:25:06 +01:00
parent 73f9257633
commit eba0a467d5
10 changed files with 578 additions and 0 deletions

View file

@ -1315,6 +1315,23 @@ if(REQUIRE_PART_SHADER)
)
endif()
# LUT
if(REQUIRE_LUT)
list(APPEND PROJECT_PRIVATE_SOURCE
"source/gfx/lut/gfx-lut.hpp"
"source/gfx/lut/gfx-lut.cpp"
"source/gfx/lut/gfx-lut-consumer.hpp"
"source/gfx/lut/gfx-lut-consumer.cpp"
"source/gfx/lut/gfx-lut-producer.hpp"
"source/gfx/lut/gfx-lut-producer.cpp"
)
list(APPEND PROJECT_DATA
"data/effects/lut.effect"
"data/effects/lut-consumer.effect"
"data/effects/lut-producer.effect"
)
endif()
# Windows
if(D_PLATFORM_WINDOWS)
list(APPEND PROJECT_PRIVATE_SOURCE

View file

@ -0,0 +1,25 @@
#include "shared.effect"
#include "lut.effect"
//------------------------------------------------------------------------------
// Uniforms
//------------------------------------------------------------------------------
uniform texture2d image;
uniform texture2d lut;
uniform int4 lut_params_0; // [size, grid_size, texture_size, 0]
uniform float4 lut_params_1; // [inverse_size, inverse_grid_size, inverse_texture_size, half_texel]
//------------------------------------------------------------------------------
// Functionality
//------------------------------------------------------------------------------
float4 PSConsumeLUT(VertexData vtx) : TARGET {
float4 c = image.Sample(LinearClampSampler, vtx.uv);
return float4(sample_lut2(c.rgb, lut, lut_params_0, lut_params_1), c.a);
};
technique Draw {
pass {
vertex_shader = DefaultVertexShader(vtx);
pixel_shader = PSConsumeLUT(vtx);
}
}

View file

@ -0,0 +1,21 @@
#include "shared.effect"
#include "lut.effect"
//------------------------------------------------------------------------------
// Uniforms
//------------------------------------------------------------------------------
uniform int4 lut_params_0;
//------------------------------------------------------------------------------
// Functionality
//------------------------------------------------------------------------------
float4 PSProduceLUT(VertexData vtx) : TARGET {
return generate_lut2(vtx.uv, lut_params_0);
};
technique Draw {
pass {
vertex_shader = DefaultVertexShader(vtx);
pixel_shader = PSProduceLUT(vtx);
};
};

129
data/effects/lut.effect Normal file
View file

@ -0,0 +1,129 @@
sampler_state __LUTSampler {
Filter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
float4 generate_lut(uint bit_depth, float2 uv) {
uint size = pow(2, bit_depth);
uint z_size = pow(2, bit_depth / 2);
uint container_size = pow(2, bit_depth + (bit_depth / 2));
uint2 xy = uint2(floor(uv * container_size));
uint2 rg = xy % size;
uint2 bb = xy / size;
return float4(
rg.xy / float(size - 1),
((bb.y * z_size) + bb.x) / float(size - 1),
1.
);
};
float4 generate_lut2(float2 uv, uint4 params0) {
uint size = params0.r;
uint z_size = params0.g;
uint container_size = params0.b;
uint2 xy = uint2(floor(uv * container_size));
uint2 rg = xy % size;
uint2 bb = xy / size;
return float4(
rg.xy / float(size - 1),
((bb.y * z_size) + bb.x) / float(size - 1),
1.
);
};
float3 sample_lut(float3 color, uint bit_depth, texture2D lut_texture) {
uint size = pow(2, bit_depth);
uint z_size = pow(2, bit_depth / 2);
uint container_size = pow(2, bit_depth + (bit_depth / 2));
float inverse_size = 1. / size;
float inverse_z_size = 1. / z_size;
float inverse_container_size = 1. / container_size;
float half_texel = inverse_container_size / 2.; // Linear sampling is weird.
// Due to our LUT not actually being a cube but a plane pretending to be a cube,
// we have to do some conversion into the grid structure in order to be successful.
// 1. Clamp everything to a reasonable range.
color = saturate(color);
// 2. Rescale everything into 0..(size - 1)
color *= (size - 1);
// 3. Convert red and green into initial grid cell UVs.
float2 xy_uv = color.xy * inverse_container_size;
// 4. Figure out the high and low parts for interpolation.
uint z_lo = floor(color.z);
uint z_hi = z_lo + 1;
// 5. Figure out the X location of the cell in the grid.
uint z_lo_x = z_lo % z_size;
uint z_hi_x = z_hi % z_size;
// 6. Figure out the Y location of the cell in the grid.
uint z_lo_y = z_lo / z_size;
uint z_hi_y = z_hi / z_size;
// 7. Convert the X and Y locations into UV coordinates.
float2 z_lo_uv = float2(z_lo_x, z_lo_y) * inverse_z_size;
float2 z_hi_uv = float2(z_hi_x, z_hi_y) * inverse_z_size;
// 8. Sample both low and high points.
float3 c_lo = lut_texture.Sample(__LUTSampler, xy_uv + z_lo_uv + half_texel).rgb;
float3 c_hi = lut_texture.Sample(__LUTSampler, xy_uv + z_hi_uv + half_texel).rgb;
// 9. Return an interpolated version based on the fraction of Z.
return lerp(c_lo, c_hi, frac(color.z));
};
float3 sample_lut2(float3 color, texture2D lut_texture, int4 params0, float4 params1) {
uint size = params0.r;
uint z_size = params0.g;
uint container_size = params0.b;
float inverse_size = params1.r;
float inverse_z_size = params1.g;
float inverse_container_size = params1.b;
float half_texel = params1.a;
// Due to our LUT not actually being a cube but a plane pretending to be a cube,
// we have to do some conversion into the grid structure in order to be successful.
// 1. Clamp everything to a reasonable range.
color = saturate(color);
// 2. Rescale everything into 0..(size - 1)
color *= (size - 1);
// 3. Convert red and green into initial grid cell UVs.
float2 xy_uv = color.xy * inverse_container_size;
// 4. Figure out the high and low parts for interpolation.
uint z_lo = floor(color.z);
uint z_hi = z_lo + 1;
// 5. Figure out the X location of the cell in the grid.
uint z_lo_x = z_lo % z_size;
uint z_hi_x = z_hi % z_size;
// 6. Figure out the Y location of the cell in the grid.
uint z_lo_y = z_lo / z_size;
uint z_hi_y = z_hi / z_size;
// 7. Convert the X and Y locations into UV coordinates.
float2 z_lo_uv = float2(z_lo_x, z_lo_y) * inverse_z_size;
float2 z_hi_uv = float2(z_hi_x, z_hi_y) * inverse_z_size;
// 8. Sample both low and high points.
float3 c_lo = lut_texture.Sample(__LUTSampler, xy_uv + z_lo_uv + half_texel).rgb;
float3 c_hi = lut_texture.Sample(__LUTSampler, xy_uv + z_hi_uv + half_texel).rgb;
// 9. Return an interpolated version based on the fraction of Z.
return lerp(c_lo, c_hi, frac(color.z));
};

View file

@ -0,0 +1,79 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#include "gfx-lut-consumer.hpp"
#include "obs/gs/gs-helper.hpp"
gfx::lut::consumer::consumer()
{
_data = gfx::lut::data::instance();
if (!_data->consumer_effect())
throw std::runtime_error("Unable to get LUT consumer effect.");
}
gfx::lut::consumer::~consumer() {}
std::shared_ptr<gs::effect> gfx::lut::consumer::prepare(gfx::lut::color_depth depth, std::shared_ptr<gs::texture> lut)
{
auto gctx = gs::context();
auto effect = _data->consumer_effect();
int32_t idepth = static_cast<int32_t>(depth);
int32_t size = static_cast<int32_t>(pow(2l, idepth));
int32_t grid_size = static_cast<int32_t>(pow(2l, (idepth / 2)));
int32_t container_size = static_cast<int32_t>(pow(2l, (idepth + (idepth / 2))));
if (gs::effect_parameter efp = effect->get_parameter("lut_params_0"); efp) {
efp.set_int4(size, grid_size, container_size, 0l);
}
if (gs::effect_parameter efp = effect->get_parameter("lut_params_1"); efp) {
float inverse_size = 1.f / static_cast<float>(size);
float inverse_z_size = 1.f / static_cast<float>(grid_size);
float inverse_container_size = 1.f / static_cast<float>(container_size);
float half_texel = inverse_container_size / 2.f;
efp.set_float4(inverse_size, inverse_z_size, inverse_container_size, half_texel);
}
if (gs::effect_parameter efp = effect->get_parameter("lut"); efp) {
efp.set_texture(lut);
}
return effect;
}
void gfx::lut::consumer::consume(gfx::lut::color_depth depth, std::shared_ptr<gs::texture> lut,
std::shared_ptr<gs::texture> texture)
{
auto gctx = gs::context();
auto effect = prepare(depth, lut);
if (gs::effect_parameter efp = effect->get_parameter("image"); efp) {
efp.set_texture(texture->get_object());
}
// Draw a simple quad.
while (gs_effect_loop(effect->get_object(), "Draw")) {
gs_draw_sprite(nullptr, 0, 1, 1);
}
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#pragma once
#include <memory>
#include "gfx-lut.hpp"
#include "obs/gs/gs-effect.hpp"
#include "obs/gs/gs-texture.hpp"
namespace gfx::lut {
class consumer {
std::shared_ptr<gfx::lut::data> _data;
public:
consumer();
~consumer();
std::shared_ptr<gs::effect> prepare(gfx::lut::color_depth depth, std::shared_ptr<gs::texture> lut);
void consume(gfx::lut::color_depth depth, std::shared_ptr<gs::texture> lut,
std::shared_ptr<gs::texture> texture);
};
} // namespace gfx::lut

View file

@ -0,0 +1,90 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#include "gfx-lut-producer.hpp"
#include "obs/gs/gs-helper.hpp"
gs_color_format format_from_depth(gfx::lut::color_depth depth)
{
switch (depth) {
case gfx::lut::color_depth::_2:
case gfx::lut::color_depth::_4:
case gfx::lut::color_depth::_6:
case gfx::lut::color_depth::_8:
return gs_color_format::GS_RGBA;
case gfx::lut::color_depth::_10:
return gs_color_format::GS_R10G10B10A2;
case gfx::lut::color_depth::_12:
case gfx::lut::color_depth::_14:
case gfx::lut::color_depth::_16:
return gs_color_format::GS_RGBA16;
}
return GS_RGBA32F;
}
gfx::lut::producer::producer()
{
_data = gfx::lut::data::instance();
if (!_data->producer_effect())
throw std::runtime_error("Unable to get LUT producer effect.");
}
gfx::lut::producer::~producer() {}
std::shared_ptr<gs::texture> gfx::lut::producer::produce(gfx::lut::color_depth depth)
{
auto gctx = gs::context();
if (!_rt || (_rt->get_color_format() != format_from_depth((depth)))) {
_rt = std::make_shared<gs::rendertarget>(format_from_depth(depth), GS_ZS_NONE);
}
auto effect = _data->producer_effect();
int32_t idepth = static_cast<int32_t>(depth);
int32_t size = static_cast<int32_t>(pow(2l, idepth));
int32_t grid_size = static_cast<int32_t>(pow(2l, (idepth / 2)));
int32_t container_size = static_cast<int32_t>(pow(2l, (idepth + (idepth / 2))));
{
auto op = _rt->render(static_cast<uint32_t>(container_size), static_cast<uint32_t>(container_size));
gs_blend_state_push();
gs_enable_color(true, true, true, false);
gs_enable_blending(false);
gs_enable_stencil_test(false);
gs_enable_stencil_write(false);
gs_ortho(0, 1, 0, 1, 0, 1);
if (gs::effect_parameter efp = effect->get_parameter("lut_params_0"); efp) {
efp.set_int4(size, grid_size, container_size, 0l);
}
while (gs_effect_loop(effect->get_object(), "Draw")) {
streamfx::gs_draw_fullscreen_tri();
}
gs_enable_color(true, true, true, true);
gs_blend_state_pop();
}
return _rt->get_texture();
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#pragma once
#include <memory>
#include "gfx-lut.hpp"
#include "obs/gs/gs-effect.hpp"
#include "obs/gs/gs-rendertarget.hpp"
namespace gfx::lut {
class producer {
std::shared_ptr<gfx::lut::data> _data;
std::shared_ptr<gs::rendertarget> _rt;
public:
producer();
~producer();
std::shared_ptr<gs::texture> produce(gfx::lut::color_depth depth);
};
} // namespace gfx::lut

View file

@ -0,0 +1,75 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#include "gfx-lut.hpp"
#include <mutex>
#include "obs/gs/gs-helper.hpp"
#include "plugin.hpp"
using namespace streamfx;
#define LOCAL_PREFIX "<gfx::lut::data> "
std::shared_ptr<gfx::lut::data> gfx::lut::data::instance()
{
static std::weak_ptr<gfx::lut::data> _instance;
static std::mutex _mutex;
std::lock_guard<std::mutex> lock(_mutex);
auto reference = _instance.lock();
if (!reference) {
reference = std::shared_ptr<gfx::lut::data>(new gfx::lut::data());
_instance = reference;
}
return reference;
}
gfx::lut::data::data() : _producer_effect(), _consumer_effect()
{
auto gctx = gs::context();
std::filesystem::path lut_producer_path = streamfx::data_file_path("effects/lut-producer.effect");
if (std::filesystem::exists(lut_producer_path)) {
try {
_producer_effect = std::make_shared<gs::effect>(lut_producer_path);
} catch (std::exception const& ex) {
DLOG_ERROR(LOCAL_PREFIX "Loading LUT Producer effect failed: %s", ex.what());
}
}
std::filesystem::path lut_consumer_path = streamfx::data_file_path("effects/lut-consumer.effect");
if (std::filesystem::exists(lut_consumer_path)) {
try {
_consumer_effect = std::make_shared<gs::effect>(lut_consumer_path);
} catch (std::exception const& ex) {
DLOG_ERROR(LOCAL_PREFIX "Loading LUT Consumer effect failed: %s", ex.what());
}
}
}
gfx::lut::data::~data()
{
auto gctx = gs::context();
_producer_effect.reset();
_consumer_effect.reset();
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2021 Michael Fabian Dirks <info@xaymar.com>
//
// 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.
#pragma once
#include <memory>
#include "obs/gs/gs-effect.hpp"
namespace gfx::lut {
class data {
std::shared_ptr<gs::effect> _producer_effect;
std::shared_ptr<gs::effect> _consumer_effect;
public:
static std::shared_ptr<data> instance();
private:
data();
public:
~data();
inline std::shared_ptr<gs::effect> producer_effect()
{
return _producer_effect;
};
inline std::shared_ptr<gs::effect> consumer_effect()
{
return _consumer_effect;
};
};
enum class color_depth {
Invalid = 0,
_2 = 2,
_4 = 4,
_6 = 6,
_8 = 8,
_10 = 10,
_12 = 12,
_14 = 14,
_16 = 16,
};
} // namespace gfx::lut