obs-filters: Add HDR Tonemap filter

Allow per-source tonemapping to override default tonemapping.
master
jpark37 2022-08-11 02:19:20 -07:00 committed by Jim
parent 94189402c2
commit 535d4141cb
6 changed files with 372 additions and 0 deletions

View File

@ -121,6 +121,7 @@ target_sources(
color-correction-filter.c
async-delay-filter.c
gpu-delay.c
hdr-tonemap-filter.c
crop-filter.c
scale-filter.c
scroll-filter.c
@ -145,11 +146,13 @@ if(NOT OS_MACOS)
data/blend_sub_filter.effect
data/chroma_key_filter.effect
data/chroma_key_filter_v2.effect
data/color.effect
data/color_correction_filter.effect
data/color_grade_filter.effect
data/color_key_filter.effect
data/color_key_filter_v2.effect
data/crop_filter.effect
data/hdr_tonemap_filter.effect
data/luma_key_filter.effect
data/luma_key_filter_v2.effect
data/mask_alpha_filter.effect

View File

@ -34,3 +34,53 @@ float3 reinhard(float3 rgb)
return float3(reinhard_channel(rgb.r), reinhard_channel(rgb.g), reinhard_channel(rgb.b));
}
float linear_to_st2084_channel(float x)
{
float c = pow(abs(x), 0.1593017578);
return pow((0.8359375 + 18.8515625 * c) / (1. + 18.6875 * c), 78.84375);
}
float st2084_to_linear_channel(float u)
{
float c = pow(abs(u), 1. / 78.84375);
return pow(abs(max(c - 0.8359375, 0.) / (18.8515625 - 18.6875 * c)), 1. / 0.1593017578);
}
float eetf_0_Lmax(float maxRGB1_pq, float Lw, float Lmax)
{
float Lw_pq = linear_to_st2084_channel(Lw / 10000.);
float E1 = saturate(maxRGB1_pq / Lw_pq); // Ensure normalization in case Lw is a lie
float maxLum = linear_to_st2084_channel(Lmax / 10000.) / Lw_pq;
float KS = (1.5 * maxLum) - 0.5;
float E2 = E1;
if (E1 > KS)
{
float T = (E1 - KS) / (1. - KS);
float Tsquared = T * T;
float Tcubed = Tsquared * T;
float P = (2. * Tcubed - 3. * Tsquared + 1.) * KS + (Tcubed - 2. * Tsquared + T) * (1. - KS) + (-2. * Tcubed + 3. * Tsquared) * maxLum;
E2 = P;
}
float E3 = E2;
float E4 = E3 * Lw_pq;
return E4;
}
float3 maxRGB_eetf_internal(float3 rgb_linear, float maxRGB1_linear, float maxRGB1_pq, float Lw, float Lmax)
{
float maxRGB2_pq = eetf_0_Lmax(maxRGB1_pq, Lw, Lmax);
float maxRGB2_linear = st2084_to_linear_channel(maxRGB2_pq);
// avoid divide-by-zero possibility
maxRGB1_linear = max(6.10352e-5, maxRGB1_linear);
rgb_linear *= maxRGB2_linear / maxRGB1_linear;
return rgb_linear;
}
float3 maxRGB_eetf_linear_to_linear(float3 rgb_linear, float Lw, float Lmax)
{
float maxRGB1_linear = max(max(rgb_linear.r, rgb_linear.g), rgb_linear.b);
float maxRGB1_pq = linear_to_st2084_channel(maxRGB1_linear);
return maxRGB_eetf_internal(rgb_linear, maxRGB1_linear, maxRGB1_pq, Lw, Lmax);
}

View File

@ -0,0 +1,76 @@
#include "color.effect"
uniform float4x4 ViewProj;
uniform texture2d image;
uniform float multiplier;
uniform float hdr_input_maximum_nits;
uniform float hdr_output_maximum_nits;
sampler_state textureSampler {
Filter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
struct VertData {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
struct VertOut {
float2 uv : TEXCOORD0;
float4 pos : POSITION;
};
struct FragData {
float2 uv : TEXCOORD0;
};
VertOut VSHdrTonemap(VertData v_in)
{
VertOut vert_out;
vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj);
vert_out.uv = v_in.uv;
return vert_out;
}
float4 PSReinhard(FragData f_in) : TARGET
{
float4 rgba = image.Sample(textureSampler, f_in.uv);
rgba.rgb *= multiplier;
rgba.rgb = rec709_to_rec2020(rgba.rgb);
rgba.rgb = reinhard(rgba.rgb);
rgba.rgb = rec2020_to_rec709(rgba.rgb);
return rgba;
}
float4 PSMaxrgb(FragData f_in) : TARGET
{
float4 rgba = image.Sample(textureSampler, f_in.uv);
rgba.rgb *= multiplier;
rgba.rgb = rec709_to_rec2020(rgba.rgb);
rgba.rgb = maxRGB_eetf_linear_to_linear(rgba.rgb, hdr_input_maximum_nits, hdr_output_maximum_nits);
rgba.rgb = rec2020_to_rec709(rgba.rgb);
float multiplier_i = 1. / multiplier;
rgba.rgb *= multiplier_i;
return rgba;
}
technique Reinhard
{
pass
{
vertex_shader = VSHdrTonemap(v_in);
pixel_shader = PSReinhard(f_in);
}
}
technique MaxRGB
{
pass
{
vertex_shader = VSHdrTonemap(v_in);
pixel_shader = PSMaxrgb(f_in);
}
}

View File

@ -3,6 +3,7 @@ ColorGradeFilter="Apply LUT"
MaskFilter="Image Mask/Blend"
AsyncDelayFilter="Video Delay (Async)"
CropFilter="Crop/Pad"
HdrTonemapFilter="HDR Tone Mapping (Override)"
ScrollFilter="Scroll"
ChromaKeyFilter="Chroma Key"
ColorKeyFilter="Color Key"
@ -45,6 +46,12 @@ Crop.Bottom="Bottom"
Crop.Width="Width"
Crop.Height="Height"
Crop.Relative="Relative"
HdrTonemap.ToneTransform="Tone Transform"
HdrTonemap.SdrReinhard="SDR: Reinhard"
HdrTonemap.HdrMaxrgb="HDR: maxRGB"
HdrTonemap.SdrWhiteLevel="SDR White Level"
HdrTonemap.HdrInputMaximum="HDR Input Maximum"
HdrTonemap.HdrOutputMaximum="HDR Output Maximum"
ScrollFilter.SpeedX="Horizontal Speed"
ScrollFilter.SpeedY="Vertical Speed"
ScrollFilter.LimitWidth="Limit Width"

View File

@ -0,0 +1,234 @@
#include <obs-module.h>
enum hdr_tonemap_transform {
TRANSFORM_SDR_REINHARD,
TRANSFORM_HDR_MAXRGB,
};
struct hdr_tonemap_filter_data {
obs_source_t *context;
gs_effect_t *effect;
gs_eparam_t *param_multiplier;
gs_eparam_t *param_hdr_input_maximum_nits;
gs_eparam_t *param_hdr_output_maximum_nits;
enum hdr_tonemap_transform transform;
float sdr_white_level_nits_i;
float hdr_input_maximum_nits;
float hdr_output_maximum_nits;
};
static const char *hdr_tonemap_filter_get_name(void *unused)
{
UNUSED_PARAMETER(unused);
return obs_module_text("HdrTonemapFilter");
}
static void *hdr_tonemap_filter_create(obs_data_t *settings,
obs_source_t *context)
{
struct hdr_tonemap_filter_data *filter = bzalloc(sizeof(*filter));
char *effect_path = obs_module_file("hdr_tonemap_filter.effect");
filter->context = context;
obs_enter_graphics();
filter->effect = gs_effect_create_from_file(effect_path, NULL);
obs_leave_graphics();
bfree(effect_path);
if (!filter->effect) {
bfree(filter);
return NULL;
}
filter->param_multiplier =
gs_effect_get_param_by_name(filter->effect, "multiplier");
filter->param_hdr_input_maximum_nits = gs_effect_get_param_by_name(
filter->effect, "hdr_input_maximum_nits");
filter->param_hdr_output_maximum_nits = gs_effect_get_param_by_name(
filter->effect, "hdr_output_maximum_nits");
obs_source_update(context, settings);
return filter;
}
static void hdr_tonemap_filter_destroy(void *data)
{
struct hdr_tonemap_filter_data *filter = data;
obs_enter_graphics();
gs_effect_destroy(filter->effect);
obs_leave_graphics();
bfree(filter);
}
static void hdr_tonemap_filter_update(void *data, obs_data_t *settings)
{
struct hdr_tonemap_filter_data *filter = data;
filter->transform = obs_data_get_int(settings, "transform");
filter->sdr_white_level_nits_i =
1.f / (float)obs_data_get_int(settings, "sdr_white_level_nits");
filter->hdr_input_maximum_nits =
(float)obs_data_get_int(settings, "hdr_input_maximum_nits");
filter->hdr_output_maximum_nits =
(float)obs_data_get_int(settings, "hdr_output_maximum_nits");
}
static bool transform_changed(obs_properties_t *props, obs_property_t *p,
obs_data_t *settings)
{
enum hdr_tonemap_transform transform =
obs_data_get_int(settings, "transform");
const bool reinhard = transform == TRANSFORM_SDR_REINHARD;
const bool maxrgb = transform == TRANSFORM_HDR_MAXRGB;
obs_property_set_visible(
obs_properties_get(props, "sdr_white_level_nits"), reinhard);
obs_property_set_visible(
obs_properties_get(props, "hdr_input_maximum_nits"), maxrgb);
obs_property_set_visible(
obs_properties_get(props, "hdr_output_maximum_nits"), maxrgb);
UNUSED_PARAMETER(p);
return true;
}
static obs_properties_t *hdr_tonemap_filter_properties(void *data)
{
obs_properties_t *props = obs_properties_create();
obs_property_t *p = obs_properties_add_list(
props, "transform", obs_module_text("HdrTonemap.ToneTransform"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(p, obs_module_text("HdrTonemap.SdrReinhard"),
TRANSFORM_SDR_REINHARD);
obs_property_list_add_int(p, obs_module_text("HdrTonemap.HdrMaxrgb"),
TRANSFORM_HDR_MAXRGB);
obs_property_set_modified_callback(p, transform_changed);
p = obs_properties_add_int(props, "sdr_white_level_nits",
obs_module_text("HdrTonemap.SdrWhiteLevel"),
80, 480, 1);
obs_property_int_set_suffix(p, " nits");
p = obs_properties_add_int(
props, "hdr_input_maximum_nits",
obs_module_text("HdrTonemap.HdrInputMaximum"), 0, 10000, 1);
obs_property_int_set_suffix(p, " nits");
p = obs_properties_add_int(
props, "hdr_output_maximum_nits",
obs_module_text("HdrTonemap.HdrOutputMaximum"), 0, 10000, 1);
obs_property_int_set_suffix(p, " nits");
UNUSED_PARAMETER(data);
return props;
}
static void hdr_tonemap_filter_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, "transform", TRANSFORM_SDR_REINHARD);
obs_data_set_default_int(settings, "sdr_white_level_nits", 300);
obs_data_set_default_int(settings, "hdr_input_maximum_nits", 4000);
obs_data_set_default_int(settings, "hdr_output_maximum_nits", 1000);
}
static void hdr_tonemap_filter_render(void *data, gs_effect_t *effect)
{
UNUSED_PARAMETER(effect);
struct hdr_tonemap_filter_data *filter = data;
const enum gs_color_space preferred_spaces[] = {
GS_CS_SRGB,
GS_CS_SRGB_16F,
GS_CS_709_EXTENDED,
};
enum gs_color_space source_space = obs_source_get_color_space(
obs_filter_get_target(filter->context),
OBS_COUNTOF(preferred_spaces), preferred_spaces);
if (source_space == GS_CS_709_EXTENDED) {
float multiplier = obs_get_video_sdr_white_level();
multiplier *= (filter->transform == TRANSFORM_SDR_REINHARD)
? filter->sdr_white_level_nits_i
: 0.0001f;
const enum gs_color_format format =
gs_get_format_from_space(source_space);
if (obs_source_process_filter_begin_with_color_space(
filter->context, format, source_space,
OBS_NO_DIRECT_RENDERING)) {
gs_effect_set_float(filter->param_multiplier,
multiplier);
gs_effect_set_float(
filter->param_hdr_input_maximum_nits,
filter->hdr_input_maximum_nits);
gs_effect_set_float(
filter->param_hdr_output_maximum_nits,
filter->hdr_output_maximum_nits);
gs_blend_state_push();
gs_blend_function(GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
const char *const tech_name =
(filter->transform == TRANSFORM_HDR_MAXRGB)
? "MaxRGB"
: "Reinhard";
obs_source_process_filter_tech_end(filter->context,
filter->effect, 0, 0,
tech_name);
gs_blend_state_pop();
}
} else {
obs_source_skip_video_filter(filter->context);
}
}
static enum gs_color_space
hdr_tonemap_filter_get_color_space(void *data, size_t count,
const enum gs_color_space *preferred_spaces)
{
const enum gs_color_space potential_spaces[] = {
GS_CS_SRGB,
GS_CS_SRGB_16F,
GS_CS_709_EXTENDED,
};
struct hdr_tonemap_filter_data *const filter = data;
const enum gs_color_space source_space = obs_source_get_color_space(
obs_filter_get_target(filter->context),
OBS_COUNTOF(potential_spaces), potential_spaces);
enum gs_color_space space = source_space;
if ((source_space == GS_CS_709_EXTENDED) &&
(filter->transform == TRANSFORM_SDR_REINHARD)) {
space = GS_CS_SRGB;
for (size_t i = 0; i < count; ++i) {
if (preferred_spaces[i] != GS_CS_SRGB) {
space = GS_CS_SRGB_16F;
break;
}
}
}
return space;
}
struct obs_source_info hdr_tonemap_filter = {
.id = "hdr_tonemap_filter",
.type = OBS_SOURCE_TYPE_FILTER,
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_SRGB,
.get_name = hdr_tonemap_filter_get_name,
.create = hdr_tonemap_filter_create,
.destroy = hdr_tonemap_filter_destroy,
.update = hdr_tonemap_filter_update,
.get_properties = hdr_tonemap_filter_properties,
.get_defaults = hdr_tonemap_filter_defaults,
.video_render = hdr_tonemap_filter_render,
.video_get_color_space = hdr_tonemap_filter_get_color_space,
};

View File

@ -11,6 +11,7 @@ extern struct obs_source_info mask_filter;
extern struct obs_source_info mask_filter_v2;
extern struct obs_source_info crop_filter;
extern struct obs_source_info gain_filter;
extern struct obs_source_info hdr_tonemap_filter;
extern struct obs_source_info color_filter;
extern struct obs_source_info color_filter_v2;
extern struct obs_source_info scale_filter;
@ -49,6 +50,7 @@ bool obs_module_load(void)
obs_register_source(&mask_filter_v2);
obs_register_source(&crop_filter);
obs_register_source(&gain_filter);
obs_register_source(&hdr_tonemap_filter);
obs_register_source(&color_filter);
obs_register_source(&color_filter_v2);
obs_register_source(&scale_filter);