Skip to main content
Version: 2.0.0-preview

Implementing Effects

A FilterEffect is an EngineObject that post-processes a render target. The shape of the code is the same as in Implementing a Drawing Object: declare a partial class, expose IExpressionProperty<T> members, and read snapshot values from the auto-generated Resource inside ApplyTo.

If you have not read the EngineObject reference yet, start there. The property system and Resource lifecycle are explained on that page.

1. The minimal effect​

using Beutl.Graphics.Effects;
using Beutl.Graphics.Rendering;

namespace MyExtension;

public sealed partial class MyFilterEffect : FilterEffect
{
public MyFilterEffect()
{
ScanProperties<MyFilterEffect>();
}

public override void ApplyTo(FilterEffectContext context, FilterEffect.Resource resource)
{
// Add filter operations to context here.
}
}

Two differences compared to Drawable:

  • ApplyTo receives a FilterEffectContext and the auto-generated Resource. Cast resource to your derived Resource type to read property values.
  • There is no MeasureCore or OnDraw. The editor pipes the render target through FilterEffectContext for you, so you only describe how to transform it.

2. Combining existing effects​

The simplest custom effects layer Beutl's built-in operations through the methods on FilterEffectContext. Bounds expansion (e.g. blur enlarging the surface) is handled internally, so you rarely need to adjust sizes yourself.

using System.ComponentModel.DataAnnotations;
using Beutl;
using Beutl.Engine;
using Beutl.Graphics;
using Beutl.Graphics.Effects;
using Beutl.Graphics.Rendering;
using Beutl.Media;

namespace MyExtension;

public sealed partial class HighContrastBlur : FilterEffect
{
public HighContrastBlur()
{
ScanProperties<HighContrastBlur>();
}

[Display(Name = "Blur Sigma")]
[Range(0, float.MaxValue)]
public IExpressionProperty<float> BlurSigma { get; } = Property.CreateAnimatable<float>(20);

[Display(Name = "Contrast")]
[Range(-100, 100)]
public IExpressionProperty<float> Contrast { get; } = Property.CreateAnimatable<float>(20);

public override void ApplyTo(FilterEffectContext context, FilterEffect.Resource resource)
{
var r = (Resource)resource;
context.Blur(new Size(r.BlurSigma, r.BlurSigma));
context.HighContrast(
grayscale: false,
invertStyle: HighContrastInvertStyle.NoInvert,
contrast: r.Contrast / 100f);
}
}

FilterEffectContext exposes a method for each built-in operation: Blur, DropShadow, InnerShadow, Erode, Dilate, ColorMatrix, Saturate, HueRotate, Brightness, HighContrast, Lighting, MatrixConvolution, Transform, BlendMode, and more. See FilterEffectContext.cs and FilterEffectContext.Operations.cs for the complete list.

Crop(Rect, GradientSpreadMethod = Decal) crops in the coordinates of Bounds. The default Decal makes pixels outside the rectangle transparent and shrinks the bounds. Pad extends edge pixels, Repeat tiles, and Reflect mirrors the crop while retaining the original bounds.

For GLSL with multiple inputs or multiple passes, also see C# Script.

3. Processing the render target directly​

See FlatShadow.cs, ColorShift.cs, and GLSLScriptEffect.cs as references.

CustomEffect(data, action, transformBounds) receives explicit state and computes output bounds from that state. If output bounds cannot be known during recording, the overload without transformBounds leaves them symbolic until the owning target's finite domain is available. Keep metadata callbacks independent of mutable editing objects.

FilterEffectContext.Shader(ShaderDescription) and Geometry(GeometryDescription) record deferred operations with explicit bounds and hit-test contracts. Use RenderHitTestContract.None, OutputBounds, AnyInput, or Custom(...) to state how the operation participates in selection; default(RenderHitTestContract) is invalid. A custom callback can receive state through Custom(state, static (state, context, point) => ...).

An AppendSkiaFilter factory must only produce its filter. It must not re-enter its FilterEffectExecutor through Activate, Flush, or related methods. Put target drawing in a CustomEffect action instead.

Metadata callback diagnostics​

If the compiler reports BESG004, check the values read by the metadata callback and any helpers it calls. Pass those values through explicit state or bound render resources instead of reading mutable static state.

4. Register the effect in your extension​

using Beutl.Extensibility;
using Beutl.Services;

namespace MyExtension;

[Export]
public sealed class MyEffectExtension : Extension
{
public override string Name => "My Effects";
public override string DisplayName => "My Effects";

public override void Load()
{
LibraryService.Current.Register<HighContrastBlur>(
KnownLibraryItemFormats.FilterEffect,
"High-Contrast Blur");
}
}

If you want to bundle several effects together, use the RegisterGroup helper described in the EngineObject reference to organize them under a single library category.

Reference files in the Beutl source​

FileWhat it teaches
src/Beutl.Engine/Graphics/FilterEffects/HighContrast.csA minimal effect that delegates to a single FilterEffectContext call.
src/Beutl.Engine/Graphics/FilterEffects/FlatShadow.csCustom drawing through CustomEffect, with transformBounds to expand the surface.
src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.csThe full list of chainable built-in operations.