Browse Source

Add blur filter, implement in GL3 renderer

Michael Ragazzon 2 years ago
parent
commit
17246f5ea2

+ 238 - 7
Backends/RmlUi_Renderer_GL3.cpp

@@ -53,7 +53,12 @@
 	#include "RmlUi_Include_GL3.h"
 #endif
 
+// Determines the anti-aliasing quality when creating layers. Enables better-looking visuals, especially when transforms are applied.
+static constexpr int NUM_MSAA_SAMPLES = 2;
+
 #define RMLUI_PREMULTIPLIED_ALPHA 1
+#define BLUR_SIZE 7
+#define BLUR_NUM_WEIGHTS ((BLUR_SIZE + 1) / 2)
 
 #define RMLUI_STRINGIFY_IMPL(x) #x
 #define RMLUI_STRINGIFY(x) RMLUI_STRINGIFY_IMPL(x)
@@ -62,9 +67,6 @@
 	RMLUI_SHADER_HEADER_VERSION \
 	"#define RMLUI_PREMULTIPLIED_ALPHA " RMLUI_STRINGIFY(RMLUI_PREMULTIPLIED_ALPHA) "\n"
 
-// Determines the anti-aliasing quality when creating layers. Enables better-looking visuals, especially when transforms are applied.
-static constexpr int NUM_MSAA_SAMPLES = 2;
-
 static const char* shader_vert_main = RMLUI_SHADER_HEADER R"(
 uniform vec2 _translate;
 uniform mat4 _transform;
@@ -147,17 +149,53 @@ void main() {
 }
 )";
 
+#define RMLUI_SHADER_BLUR_HEADER \
+	RMLUI_SHADER_HEADER "\n#define BLUR_SIZE " RMLUI_STRINGIFY(BLUR_SIZE) "\n#define BLUR_NUM_WEIGHTS " RMLUI_STRINGIFY(BLUR_NUM_WEIGHTS)
+
+static const char* shader_vert_blur = RMLUI_SHADER_BLUR_HEADER R"(
+uniform vec2 _texelOffset;
+
+in vec3 inPosition;
+in vec2 inTexCoord0;
+
+out vec2 fragTexCoord[BLUR_SIZE];
+
+void main() {
+	for(int i = 0; i < BLUR_SIZE; i++)
+		fragTexCoord[i] = inTexCoord0 - float(i - BLUR_NUM_WEIGHTS + 1) * _texelOffset;
+    gl_Position = vec4(inPosition, 1.0);
+}
+)";
+static const char* shader_frag_blur = RMLUI_SHADER_BLUR_HEADER R"(
+uniform sampler2D _tex;
+uniform float _weights[BLUR_NUM_WEIGHTS];
+uniform vec2 _texCoordMin;
+uniform vec2 _texCoordMax;
+
+in vec2 fragTexCoord[BLUR_SIZE];
+out vec4 finalColor;
+
+void main() {    
+	vec4 color = vec4(0.0, 0.0, 0.0, 0.0);
+	for(int i = 0; i < BLUR_SIZE; i++)
+		color += texture(_tex, clamp(fragTexCoord[i], _texCoordMin, _texCoordMax)) * _weights[abs(i - BLUR_NUM_WEIGHTS + 1)];
+	finalColor = color;
+}
+)";
+
 enum class ProgramId {
 	None,
 	Color,
 	Texture,
 	Passthrough,
 	ColorMatrix,
+	Blur,
 	Count,
 };
 enum class VertShaderId {
 	Main,
 	Passthrough,
+	Blur,
 	Count,
 };
 enum class FragShaderId {
@@ -165,6 +203,7 @@ enum class FragShaderId {
 	Texture,
 	Passthrough,
 	ColorMatrix,
+	Blur,
 	Count,
 };
 enum class UniformId {
@@ -172,12 +211,17 @@ enum class UniformId {
 	Transform,
 	Tex,
 	ColorMatrix,
+	TexelOffset,
+	TexCoordMin,
+	TexCoordMax,
+	Weights,
 	Count,
 };
 
 namespace Gfx {
 
-static const char* const program_uniform_names[(size_t)UniformId::Count] = {"_translate", "_transform", "_tex", "_color_matrix"};
+static const char* const program_uniform_names[(size_t)UniformId::Count] = {"_translate", "_transform", "_tex", "_color_matrix", "_texelOffset",
+	"_texCoordMin", "_texCoordMax", "_weights[0]"};
 
 enum class VertexAttribute { Position, Color0, TexCoord0, Count };
 static const char* const vertex_attribute_names[(size_t)VertexAttribute::Count] = {"inPosition", "inColor0", "inTexCoord0"};
@@ -203,18 +247,21 @@ struct ProgramDefinition {
 static const VertShaderDefinition vert_shader_definitions[] = {
 	{VertShaderId::Main,        "main",         shader_vert_main},
 	{VertShaderId::Passthrough, "passthrough",  shader_vert_passthrough},
+	{VertShaderId::Blur,        "blur",         shader_vert_blur},
 };
 static const FragShaderDefinition frag_shader_definitions[] = {
 	{FragShaderId::Color,       "color",        shader_frag_color},
 	{FragShaderId::Texture,     "texture",      shader_frag_texture},
 	{FragShaderId::Passthrough, "passthrough",  shader_frag_passthrough},
 	{FragShaderId::ColorMatrix, "color_matrix", shader_frag_color_matrix},
+	{FragShaderId::Blur,        "blur",         shader_frag_blur},
 };
 static const ProgramDefinition program_definitions[] = {
 	{ProgramId::Color,       "color",        VertShaderId::Main,        FragShaderId::Color},
 	{ProgramId::Texture,     "texture",      VertShaderId::Main,        FragShaderId::Texture},
 	{ProgramId::Passthrough, "passthrough",  VertShaderId::Passthrough, FragShaderId::Passthrough},
 	{ProgramId::ColorMatrix, "color_matrix", VertShaderId::Passthrough, FragShaderId::ColorMatrix},
+	{ProgramId::Blur,        "blur",         VertShaderId::Blur,        FragShaderId::Blur},
 };
 // clang-format on
 
@@ -1063,6 +1110,164 @@ void RenderInterface_GL3::DrawFullscreenQuad()
 	RenderCompiledGeometry(fullscreen_quad_geometry, {}, RenderInterface_GL3::TexturePostprocess);
 }
 
+void RenderInterface_GL3::DrawFullscreenQuad(Rml::Vector2f uv_offset, Rml::Vector2f uv_scaling)
+{
+	Rml::Vertex vertices[4];
+	int indices[6];
+	Rml::GeometryUtilities::GenerateQuad(vertices, indices, Rml::Vector2f(-1), Rml::Vector2f(2), {});
+	if (uv_offset != Rml::Vector2f() || uv_scaling != Rml::Vector2f(1.f))
+	{
+		for (Rml::Vertex& vertex : vertices)
+			vertex.tex_coord = (vertex.tex_coord * uv_scaling) + uv_offset;
+	}
+	RenderGeometry(vertices, 4, indices, 6, RenderInterface_GL3::TexturePostprocess, {});
+}
+
+static void SigmaToParameters(const float desired_sigma, int& out_pass_level, float& out_sigma)
+{
+	constexpr int max_num_passes = 10;
+	static_assert(max_num_passes < 31, "");
+	constexpr float max_single_pass_sigma = 3.0f;
+	out_pass_level = Rml::Math::Clamp(Rml::Math::Log2(int(desired_sigma * (2.f / max_single_pass_sigma))), 0, max_num_passes);
+	out_sigma = Rml::Math::Clamp(desired_sigma / float(1 << out_pass_level), 0.0f, max_single_pass_sigma);
+}
+
+static void SetTexCoordLimits(GLint tex_coord_min_location, GLint tex_coord_max_location, Rml::Rectanglei rectangle_flipped,
+	Rml::Vector2i framebuffer_size)
+{
+	// Offset by half-texel values so that texture lookups are clamped to fragment centers, thereby avoiding color
+	// bleeding from neighboring texels due to bilinear interpolation.
+	const Rml::Vector2f min = (Rml::Vector2f(rectangle_flipped.p0) + Rml::Vector2f(0.5f)) / Rml::Vector2f(framebuffer_size);
+	const Rml::Vector2f max = (Rml::Vector2f(rectangle_flipped.p1) - Rml::Vector2f(0.5f)) / Rml::Vector2f(framebuffer_size);
+
+	glUniform2f(tex_coord_min_location, min.x, min.y);
+	glUniform2f(tex_coord_max_location, max.x, max.y);
+}
+
+static void SetBlurWeights(GLint weights_location, float sigma)
+{
+	constexpr int num_weights = BLUR_NUM_WEIGHTS;
+	float weights[num_weights];
+	float normalization = 0.0f;
+	for (int i = 0; i < num_weights; i++)
+	{
+		if (Rml::Math::Absolute(sigma) < 0.1f)
+			weights[i] = float(i == 0);
+		else
+			weights[i] = Rml::Math::Exp(-float(i * i) / (2.0f * sigma * sigma)) / (Rml::Math::SquareRoot(2.f * Rml::Math::RMLUI_PI) * sigma);
+
+		normalization += (i == 0 ? 1.f : 2.0f) * weights[i];
+	}
+	for (int i = 0; i < num_weights; i++)
+		weights[i] /= normalization;
+
+	glUniform1fv(weights_location, (GLsizei)num_weights, &weights[0]);
+}
+
+void RenderInterface_GL3::RenderBlur(float sigma, const Gfx::FramebufferData& source_destination, const Gfx::FramebufferData& temp,
+	const Rml::Rectanglei window_flipped)
+{
+	RMLUI_ASSERT(&source_destination != &temp && source_destination.width == temp.width && source_destination.height == temp.height);
+	RMLUI_ASSERT(window_flipped.Valid());
+
+	int pass_level = 0;
+	SigmaToParameters(sigma, pass_level, sigma);
+
+	const Rml::Rectanglei original_scissor = scissor_state;
+
+	// Begin by downscaling so that the blur pass can be done at a reduced resolution for large sigma.
+	Rml::Rectanglei scissor = window_flipped;
+
+	UseProgram(ProgramId::Passthrough);
+	SetScissor(scissor, true);
+
+	// Downscale by iterative half-scaling with bilinear filtering, to reduce aliasing.
+	glViewport(0, 0, source_destination.width / 2, source_destination.height / 2);
+
+	// Scale UVs if we have even dimensions, such that texture fetches align perfectly between texels, thereby producing a 50% blend of
+	// neighboring texels.
+	const Rml::Vector2f uv_scaling = {(source_destination.width % 2 == 1) ? (1.f - 1.f / float(source_destination.width)) : 1.f,
+		(source_destination.height % 2 == 1) ? (1.f - 1.f / float(source_destination.height)) : 1.f};
+
+	for (int i = 0; i < pass_level; i++)
+	{
+		scissor.p0 = (scissor.p0 + Rml::Vector2i(1)) / 2;
+		scissor.p1 = Rml::Math::Max(scissor.p1 / 2, scissor.p0);
+		const bool from_source = (i % 2 == 0);
+		Gfx::BindTexture(from_source ? source_destination : temp);
+		glBindFramebuffer(GL_FRAMEBUFFER, (from_source ? temp : source_destination).framebuffer);
+		SetScissor(scissor, true);
+
+		DrawFullscreenQuad({}, uv_scaling);
+	}
+
+	glViewport(0, 0, source_destination.width, source_destination.height);
+
+	// Ensure texture data end up in the temp buffer. Depending on the last downscaling, we might need to move it from the source_destination buffer.
+	const bool transfer_to_temp_buffer = (pass_level % 2 == 0);
+	if (transfer_to_temp_buffer)
+	{
+		Gfx::BindTexture(source_destination);
+		glBindFramebuffer(GL_FRAMEBUFFER, temp.framebuffer);
+		DrawFullscreenQuad();
+	}
+
+	// Set up uniforms.
+	UseProgram(ProgramId::Blur);
+	SetBlurWeights(GetUniformLocation(UniformId::Weights), sigma);
+	SetTexCoordLimits(GetUniformLocation(UniformId::TexCoordMin), GetUniformLocation(UniformId::TexCoordMax), scissor,
+		{source_destination.width, source_destination.height});
+
+	const GLint texel_offset_location = GetUniformLocation(UniformId::TexelOffset);
+	auto SetTexelOffset = [texel_offset_location](Rml::Vector2f blur_direction, int texture_dimension) {
+		const Rml::Vector2f texel_offset = blur_direction * (1.0f / float(texture_dimension));
+		glUniform2f(texel_offset_location, texel_offset.x, texel_offset.y);
+	};
+
+	// Blur render pass - vertical.
+	Gfx::BindTexture(temp);
+	glBindFramebuffer(GL_FRAMEBUFFER, source_destination.framebuffer);
+
+	SetTexelOffset({0.f, 1.f}, temp.height);
+	DrawFullscreenQuad();
+
+	// Blur render pass - horizontal.
+	Gfx::BindTexture(source_destination);
+	glBindFramebuffer(GL_FRAMEBUFFER, temp.framebuffer);
+
+	SetTexelOffset({1.f, 0.f}, source_destination.width);
+	DrawFullscreenQuad();
+
+	// Blit the blurred image to the scissor region with upscaling.
+	SetScissor(window_flipped, true);
+	glBindFramebuffer(GL_READ_FRAMEBUFFER, temp.framebuffer);
+	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, source_destination.framebuffer);
+
+	const Rml::Vector2i src_min = scissor.p0;
+	const Rml::Vector2i src_max = scissor.p1;
+	const Rml::Vector2i dst_min = window_flipped.p0;
+	const Rml::Vector2i dst_max = window_flipped.p1;
+	glBlitFramebuffer(src_min.x, src_min.y, src_max.x, src_max.y, dst_min.x, dst_min.y, dst_max.x, dst_max.y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+
+	// The above upscale blit might be jittery at low resolutions (large pass levels). This is especially noticable when moving an element with
+	// backdrop blur around or when trying to click/hover an element within a blurred region since it may be rendered at an offset. For more stable
+	// and accurate rendering we next upscale the blur image by an exact power-of-two. However, this may not fill the edges completely so we need to
+	// do the above first. Note that this strategy may sometimes result in visible seams. Alternatively, we could try to enlargen the window to the
+	// next power-of-two size and then downsample and blur that.
+	const Rml::Vector2i target_min = src_min * (1 << pass_level);
+	const Rml::Vector2i target_max = src_max * (1 << pass_level);
+	if (target_min != dst_min || target_max != dst_max)
+	{
+		glBlitFramebuffer(src_min.x, src_min.y, src_max.x, src_max.y, target_min.x, target_min.y, target_max.x, target_max.y, GL_COLOR_BUFFER_BIT,
+			GL_LINEAR);
+	}
+
+	// Restore render state.
+	SetScissor(original_scissor);
+
+	Gfx::CheckGLError("Blur");
+}
+
 void RenderInterface_GL3::ReleaseTexture(Rml::TextureHandle texture_handle)
 {
 	glDeleteTextures(1, (GLuint*)&texture_handle);
@@ -1074,13 +1279,16 @@ void RenderInterface_GL3::SetTransform(const Rml::Matrix4f* new_transform)
 	program_transform_dirty.set();
 }
 
-enum class FilterType { Invalid = 0, Passthrough, ColorMatrix };
+enum class FilterType { Invalid = 0, Passthrough, Blur, ColorMatrix };
 struct CompiledFilter {
 	FilterType type;
 
 	// Passthrough
 	float blend_factor;
 
+	// Blur
+	float sigma;
+
 	// ColorMatrix
 	Rml::Matrix4f color_matrix;
 };
@@ -1094,6 +1302,11 @@ Rml::CompiledFilterHandle RenderInterface_GL3::CompileFilter(const Rml::String&
 		filter.type = FilterType::Passthrough;
 		filter.blend_factor = Rml::Get(parameters, "value", 1.0f);
 	}
+	else if (name == "blur")
+	{
+		filter.type = FilterType::Blur;
+		filter.sigma = 0.5f * Rml::Get(parameters, "radius", 1.0f);
+	}
 	else if (name == "brightness")
 	{
 		filter.type = FilterType::ColorMatrix;
@@ -1227,6 +1440,19 @@ void RenderInterface_GL3::RenderFilters(const Rml::FilterHandleList& filter_hand
 			glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
 		}
 		break;
+		case FilterType::Blur:
+		{
+			glDisable(GL_BLEND);
+
+			const Gfx::FramebufferData& source_destination = render_layers.GetPostprocessPrimary();
+			const Gfx::FramebufferData& temp = render_layers.GetPostprocessSecondary();
+
+			const Rml::Rectanglei window_flipped = VerticallyFlipped(scissor_state, viewport_height);
+			RenderBlur(filter.sigma, source_destination, temp, window_flipped);
+
+			glEnable(GL_BLEND);
+		}
+		break;
 		case FilterType::ColorMatrix:
 		{
 			UseProgram(ProgramId::ColorMatrix);
@@ -1321,6 +1547,11 @@ void RenderInterface_GL3::UseProgram(ProgramId program_id)
 	}
 }
 
+int RenderInterface_GL3::GetUniformLocation(UniformId uniform_id) const
+{
+	return program_data->uniforms.Get(active_program, uniform_id);
+}
+
 void RenderInterface_GL3::SubmitTransformUniform(Rml::Vector2f translation)
 {
 	static_assert((size_t)ProgramId::Count < MaxNumPrograms, "Maximum number of programs exceeded.");
@@ -1328,11 +1559,11 @@ void RenderInterface_GL3::SubmitTransformUniform(Rml::Vector2f translation)
 
 	if (program_transform_dirty.test(program_index))
 	{
-		glUniformMatrix4fv(program_data->uniforms.Get(active_program, UniformId::Transform), 1, false, transform.data());
+		glUniformMatrix4fv(GetUniformLocation(UniformId::Transform), 1, false, transform.data());
 		program_transform_dirty.set(program_index, false);
 	}
 
-	glUniform2fv(program_data->uniforms.Get(active_program, UniformId::Translate), 1, &translation.x);
+	glUniform2fv(GetUniformLocation(UniformId::Translate), 1, &translation.x);
 
 	Gfx::CheckGLError("SubmitTransformUniform");
 }

+ 5 - 0
Backends/RmlUi_Renderer_GL3.h

@@ -34,6 +34,7 @@
 #include <bitset>
 
 enum class ProgramId;
+enum class UniformId;
 class RenderLayerStack;
 namespace Gfx {
 struct ProgramData;
@@ -90,6 +91,7 @@ public:
 
 private:
 	void UseProgram(ProgramId program_id);
+	int GetUniformLocation(UniformId uniform_id) const;
 	void SubmitTransformUniform(Rml::Vector2f translation);
 
 	void BlitTopLayerToPostprocessPrimary();
@@ -98,6 +100,9 @@ private:
 	void SetScissor(Rml::Rectanglei region, bool vertically_flip = false);
 
 	void DrawFullscreenQuad();
+	void DrawFullscreenQuad(Rml::Vector2f uv_offset, Rml::Vector2f uv_scaling);
+
+	void RenderBlur(float sigma, const Gfx::FramebufferData& source_destination, const Gfx::FramebufferData& temp, Rml::Rectanglei window_flipped);
 
 	static constexpr size_t MaxNumPrograms = 32;
 	std::bitset<MaxNumPrograms> program_transform_dirty;

+ 2 - 0
CMake/FileList.cmake

@@ -48,6 +48,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/EventSpecification.h
     ${PROJECT_SOURCE_DIR}/Source/Core/FileInterfaceDefault.h
     ${PROJECT_SOURCE_DIR}/Source/Core/FilterBasic.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/FilterBlur.h
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectBlur.h
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectGlow.h
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectOutline.h
@@ -305,6 +306,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/FileInterfaceDefault.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Filter.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FilterBasic.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/FilterBlur.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffect.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectBlur.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/FontEffectGlow.cpp

+ 9 - 5
Samples/basic/effect/data/effect.rml

@@ -45,15 +45,14 @@
 
 .box.window {
 	position: absolute;
-	bottom: 50dp;
-	left: 50dp;
+	left: 30dp;
 	margin: 0;
-	cursor: move;
 }
 .box.window handle {
 	position: absolute;
 	top: 0; right: 0; bottom: 0; left: 0;
 	display: block;
+	cursor: move;
 }
 .box.big {
 	width: 500dp;
@@ -74,6 +73,9 @@
 .invert { filter: invert(100%); }
 .opacity_low { filter: opacity(0.2); }
 
+.blur { filter: blur(25px); }
+.back_blur { backdrop-filter: blur(15px);  }
+
 </style>
 </head>
 <body
@@ -101,6 +103,7 @@
 			<tr><td>Contrast</td><td><input type="range" min="0" max="2" step="0.02" data-value="contrast"/></td><td>{{ contrast*100 }} %</td></tr>
 			<tr><td>Hue</td><td><input type="range" min="0" max="360" step="1" data-value="hue_rotate"/></td><td>{{ hue_rotate }} deg</td></tr>
 			<tr><td>Invert</td><td><input type="range" min="0" max="1" step="0.01" data-value="invert"/></td><td>{{ invert*100 }} %</td></tr>
+			<tr><td>Blur</td><td><input type="range" min="0" max="150" step="1" data-value="blur"/></td><td>{{ blur }} px</td></tr>
 		</tbody>
 		<tbody data-if="submenu == 'transform'">
 			<tr><td>Scale</td><td><input type="range" min="0.1" max="2.0" step="0.1" data-value="scale"/></td><td>{{ scale | format(1) }}x</td></tr>
@@ -117,7 +120,7 @@
 </div>
 <div class="filter"
 	data-style-transform="'scale(' + scale + ') rotateX(' + rotate_x + 'deg) rotateY(' + rotate_y + 'deg) rotateZ(' + rotate_z + 'deg)'"
-	data-style-filter="'opacity(' + opacity + ') sepia(' + sepia + ') grayscale(' + grayscale + ') saturate(' + saturate + ') brightness(' + brightness + ') contrast(' + contrast + ') hue-rotate(' + hue_rotate + 'deg) invert(' + invert + ')'"
+	data-style-filter="'opacity(' + opacity + ') sepia(' + sepia + ') grayscale(' + grayscale + ') saturate(' + saturate + ') brightness(' + brightness + ') contrast(' + contrast + ') hue-rotate(' + hue_rotate + 'deg) invert(' + invert + ') blur(' + blur + 'px)'"
 	data-class-transform_all="transform_all"
 >
 
@@ -133,7 +136,8 @@
 <div class="box contrast"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box dropshadow"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 
-<div class="box window sepia" id="sepia" style="bottom: 150dp"><handle move_target="sepia"/><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box window sepia" style="top: 375dp"><handle move_target="#parent"/><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box window back_blur" style="top: 475dp"><handle move_target="#parent"/><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 
 <div class="box grayscale"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box opacity_low"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>

+ 1 - 1
Samples/basic/effect/data/effect_style.rcss

@@ -81,7 +81,7 @@ handle.size:hover, handle.size:active {
 	right: 25dp;
 	box-sizing: border-box;
 	width: 400dp;
-	height: 415dp;
+	height: 440dp;
 	overflow: auto;
 
 	background: #fffc;

+ 2 - 0
Samples/basic/effect/src/main.cpp

@@ -87,6 +87,7 @@ int main(int /*argc*/, char** /*argv*/)
 			float contrast = 1.0f;
 			float hue_rotate = 0.0f;
 			float invert = 0.0f;
+			float blur = 0.0f;
 		} filter;
 
 		struct Transform {
@@ -111,6 +112,7 @@ int main(int /*argc*/, char** /*argv*/)
 		constructor.Bind("contrast", &data.filter.contrast);
 		constructor.Bind("hue_rotate", &data.filter.hue_rotate);
 		constructor.Bind("invert", &data.filter.invert);
+		constructor.Bind("blur", &data.filter.blur);
 
 		constructor.Bind("scale", &data.transform.scale);
 		constructor.Bind("rotate_x", &data.transform.rotate.x);

+ 4 - 0
Source/Core/Factory.cpp

@@ -63,6 +63,7 @@
 #include "Elements/XMLNodeHandlerTextArea.h"
 #include "EventInstancerDefault.h"
 #include "FilterBasic.h"
+#include "FilterBlur.h"
 #include "FontEffectBlur.h"
 #include "FontEffectGlow.h"
 #include "FontEffectOutline.h"
@@ -156,6 +157,7 @@ struct DefaultInstancers {
 	FilterBasicInstancer filter_hue_rotate = {FilterBasicInstancer::ValueType::Angle, "0rad"};
 	FilterBasicInstancer filter_basic_d0 = {FilterBasicInstancer::ValueType::NumberPercent, "0"};
 	FilterBasicInstancer filter_basic_d1 = {FilterBasicInstancer::ValueType::NumberPercent, "1"};
+	FilterBlurInstancer filter_blur;
 
 	// Font effects
 	FontEffectBlurInstancer font_effect_blur;
@@ -249,6 +251,8 @@ bool Factory::Initialise()
 	RegisterFilterInstancer("saturate", &default_instancers->filter_basic_d1);
 	RegisterFilterInstancer("sepia", &default_instancers->filter_basic_d0);
 
+	RegisterFilterInstancer("blur", &default_instancers->filter_blur);
+
 	// Font effect instancers
 	RegisterFontEffectInstancer("blur", &default_instancers->font_effect_blur);
 	RegisterFontEffectInstancer("glow", &default_instancers->font_effect_glow);

+ 2 - 2
Source/Core/FilterBasic.cpp

@@ -63,9 +63,9 @@ FilterBasicInstancer::FilterBasicInstancer(ValueType value_type, const char* def
 	RegisterShorthand("filter", "value", ShorthandType::FallThrough);
 }
 
-SharedPtr<Filter> FilterBasicInstancer::InstanceFilter(const String& name, const PropertyDictionary& properties_)
+SharedPtr<Filter> FilterBasicInstancer::InstanceFilter(const String& name, const PropertyDictionary& properties)
 {
-	const Property* p_value = properties_.GetProperty(ids.value);
+	const Property* p_value = properties.GetProperty(ids.value);
 	if (!p_value)
 		return nullptr;
 

+ 81 - 0
Source/Core/FilterBlur.cpp

@@ -0,0 +1,81 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019-2023 The RmlUi Team, and contributors
+ *
+ * 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 "FilterBlur.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/PropertyDefinition.h"
+#include "../../Include/RmlUi/Core/PropertyDictionary.h"
+#include "../../Include/RmlUi/Core/RenderInterface.h"
+
+namespace Rml {
+
+bool FilterBlur::Initialise(NumericValue in_radius)
+{
+	radius_value = in_radius;
+	return Any(in_radius.unit & Unit::LENGTH);
+}
+
+CompiledFilterHandle FilterBlur::CompileFilter(Element* element) const
+{
+	const float radius = element->ResolveLength(radius_value);
+	CompiledFilterHandle handle = GetRenderInterface()->CompileFilter("blur", Dictionary{{"radius", Variant(radius)}});
+	return handle;
+}
+
+void FilterBlur::ReleaseCompiledFilter(Element* /*element*/, CompiledFilterHandle filter_handle) const
+{
+	GetRenderInterface()->ReleaseCompiledFilter(filter_handle);
+}
+
+void FilterBlur::ExtendInkOverflow(Element* element, Rectanglef& scissor_region) const
+{
+	const float radius = element->ResolveLength(radius_value);
+	const float blur_extent = 1.5f * Math::Max(radius, 1.f);
+	scissor_region.Extend(blur_extent);
+}
+
+FilterBlurInstancer::FilterBlurInstancer()
+{
+	ids.radius = RegisterProperty("radius", "0px").AddParser("length").GetId();
+	RegisterShorthand("filter", "radius", ShorthandType::FallThrough);
+}
+
+SharedPtr<Filter> FilterBlurInstancer::InstanceFilter(const String& /*name*/, const PropertyDictionary& properties)
+{
+	const Property* p_radius = properties.GetProperty(ids.radius);
+	if (!p_radius)
+		return nullptr;
+
+	auto decorator = MakeShared<FilterBlur>();
+	if (decorator->Initialise(p_radius->GetNumericValue()))
+		return decorator;
+
+	return nullptr;
+}
+
+} // namespace Rml

+ 66 - 0
Source/Core/FilterBlur.h

@@ -0,0 +1,66 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019-2023 The RmlUi Team, and contributors
+ *
+ * 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.
+ *
+ */
+
+#ifndef RMLUI_CORE_FILTERBLUR_H
+#define RMLUI_CORE_FILTERBLUR_H
+
+#include "../../Include/RmlUi/Core/Filter.h"
+#include "../../Include/RmlUi/Core/ID.h"
+#include "../../Include/RmlUi/Core/NumericValue.h"
+
+namespace Rml {
+
+class FilterBlur : public Filter {
+public:
+	bool Initialise(NumericValue radius);
+
+	CompiledFilterHandle CompileFilter(Element* element) const override;
+
+	void ReleaseCompiledFilter(Element* element, CompiledFilterHandle filter_handle) const override;
+
+	void ExtendInkOverflow(Element* element, Rectanglef& scissor_region) const override;
+
+private:
+	NumericValue radius_value;
+};
+
+class FilterBlurInstancer : public FilterInstancer {
+public:
+	FilterBlurInstancer();
+
+	SharedPtr<Filter> InstanceFilter(const String& name, const PropertyDictionary& properties) override;
+
+private:
+	struct PropertyIds {
+		PropertyId radius;
+	};
+	PropertyIds ids;
+};
+
+} // namespace Rml
+#endif