Browse Source

obs-filters: Add HDR Tonemap filter

Allow per-source tonemapping to override default tonemapping.
jpark37 3 years ago
parent
commit
535d4141cb

+ 3 - 0
plugins/obs-filters/CMakeLists.txt

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

+ 50 - 0
plugins/obs-filters/data/color.effect

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

+ 76 - 0
plugins/obs-filters/data/hdr_tonemap_filter.effect

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

+ 7 - 0
plugins/obs-filters/data/locale/en-US.ini

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

+ 234 - 0
plugins/obs-filters/hdr-tonemap-filter.c

@@ -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,
+};

+ 2 - 0
plugins/obs-filters/obs-filters.c

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