Browse Source

Kernel Node WIP

CPKreuz 1 year ago
parent
commit
fc14406c08

+ 50 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/KernelPropertyViewModel.cs

@@ -0,0 +1,50 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class KernelPropertyViewModel : NodePropertyViewModel<Kernel?>
+{
+    public ObservableCollection<List<KernelVmReference>> ReferenceCollections { get; }
+    
+    public KernelPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+        ReferenceCollections = new ObservableCollection<List<KernelVmReference>>();
+        PropertyChanged += OnPropertyChanged;
+    }
+
+    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(Value) || Value == null)
+            return;
+
+        ReferenceCollections.Clear();
+        
+        for (int y = -Value.RadiusY; y <= Value.RadiusY; y++)
+        {
+            var collection = new List<KernelVmReference>();
+            
+            for (int x = -Value.RadiusX; x <= Value.RadiusX; x++)
+            {
+                collection.Add(new KernelVmReference(this, x, y));
+            }
+
+            ReferenceCollections.Add(collection);
+        }
+    }
+
+    public class KernelVmReference(KernelPropertyViewModel viewModel, int x, int y) : PixiObservableObject
+    {
+        public float Value
+        {
+            get => viewModel.Value[x, y];
+            set
+            {
+                viewModel.Value[x, y] = value;
+                ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((viewModel.Node, viewModel.PropertyName, viewModel.Value));
+                OnPropertyChanged();
+            }
+        }
+    }
+}

+ 34 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/KernelPropertyView.axaml

@@ -0,0 +1,34 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.KernelPropertyView">
+    
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock ui:Translator.Key="{Binding DisplayName}" />
+        
+        <ItemsControl ItemsSource="{Binding ReferenceCollections}" HorizontalAlignment="Stretch">
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <ItemsControl ItemsSource="{Binding .}">
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                <input:NumberInput Value="{Binding Value, Mode=TwoWay}"/>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch"/>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                    </ItemsControl>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+        </ItemsControl>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/KernelPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class KernelPropertyView : NodePropertyView
+{
+    public KernelPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 58 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KernelNode.cs

@@ -0,0 +1,58 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class KernelFilterNode : Node
+{
+    private readonly Paint _paint = new();
+    
+    public OutputProperty<Surface> Transformed { get; }
+    
+    public InputProperty<Surface?> Image { get; }
+    
+    public InputProperty<Kernel> Kernel { get; }
+    
+    public InputProperty<double> Gain { get; }
+
+    public InputProperty<double> Bias { get; }
+
+    public InputProperty<TileMode> Tile { get; }
+
+    public InputProperty<bool> OnAlpha { get; }
+
+    public KernelFilterNode()
+    {
+        Transformed = CreateOutput<Surface>(nameof(Transformed), "TRANSFORMED", null);
+        Image = CreateInput<Surface>(nameof(Image), "IMAGE", null);
+        Kernel = CreateInput(nameof(Kernel), "KERNEL", new Kernel(3, 3));
+        Gain = CreateInput(nameof(Gain), "GAIN", 1d);
+        Bias = CreateInput(nameof(Bias), "BIAS", 0d);
+        Tile = CreateInput(nameof(Tile), "TILE_MODE", TileMode.Clamp);
+        OnAlpha = CreateInput(nameof(OnAlpha), "ON_ALPHA", false);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        var input = Image.Value;
+        var kernel = Kernel.Value;
+        var workingSurface = new Surface(input.Size);
+
+        var kernelOffset = new VecI(kernel.RadiusX, kernel.RadiusY);
+        using var imageFilter = ImageFilter.CreateMatrixConvolution(kernel, (float)Gain.Value, (float)Bias.Value, kernelOffset, Tile.Value, OnAlpha.Value);
+
+        _paint.ImageFilter = imageFilter;
+        workingSurface.DrawingSurface.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0, _paint);
+        
+        Transformed.Value = workingSurface;
+        
+        return workingSurface;
+    }
+
+    public override bool Validate() => Image.Value != null;
+
+    public override Node CreateCopy() => new KernelFilterNode();
+}

+ 2 - 1
src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs

@@ -17,7 +17,8 @@ namespace PixiEditor.DrawingApi.Core.Bridge
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IImgDataImplementation ImgDataImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
-        public IColorFilterImplementation ColorFilterImplementation { get; set; }
+        public IColorFilterImplementation ColorFilterImplementation { get; }
+        public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
     }
 }

+ 14 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IImageFilterImplementation.cs

@@ -0,0 +1,14 @@
+using System;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+
+public interface IImageFilterImplementation
+{
+    IntPtr CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset, TileMode mode, bool convolveAlpha);
+
+    object GetNativeImageFilter(IntPtr objPtr);
+    
+    void DisposeObject(IntPtr objPtr);
+}

+ 5 - 1
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IPaintImplementation.cs

@@ -24,9 +24,13 @@ namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl
         public void SetStrokeCap(Paint paint, StrokeCap value);
         public float GetStrokeWidth(Paint paint);
         public void SetStrokeWidth(Paint paint, float value);
+        
         public ColorFilter GetColorFilter(Paint paint);
-
         public void SetColorFilter(Paint paint, ColorFilter value);
+        
+        public ImageFilter GetImageFilter(Paint paint);
+        public void SetImageFilter(Paint paint, ImageFilter value);
+        
         public object GetNativePaint(IntPtr objectPointer);
         public Shader GetShader(Paint paint);
         public void SetShader(Paint paint, Shader shader);

+ 40 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/ImageFilter.cs

@@ -0,0 +1,40 @@
+using System;
+using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+
+public class ImageFilter : NativeObject
+{
+    public ImageFilter(IntPtr objPtr) : base(objPtr)
+    {
+    }
+
+    public static ImageFilter CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset,
+        TileMode tileMode, bool convolveAlpha)
+    {
+        var filter = new ImageFilter(DrawingBackendApi.Current.ImageFilterImplementation.CreateMatrixConvolution(
+            size,
+            kernel,
+            gain,
+            bias,
+            kernelOffset,
+            tileMode,
+            convolveAlpha));
+
+        return filter;
+    }
+
+    public static ImageFilter CreateMatrixConvolution(Kernel kernel, float gain, float bias, VecI kernelOffset, TileMode tileMode, bool convolveAlpha) =>
+        CreateMatrixConvolution(new VecI(kernel.Width, kernel.Height), kernel.AsSpan(), gain, bias, kernelOffset, tileMode, convolveAlpha);
+
+    public static ImageFilter CreateMatrixConvolution(KernelArray kernel, float gain, float bias, VecI kernelOffset, TileMode tileMode, bool convolveAlpha) =>
+        CreateMatrixConvolution(new VecI(kernel.Width, kernel.Height), kernel.AsSpan(), gain, bias, kernelOffset, tileMode, convolveAlpha);
+
+    public override object Native => DrawingBackendApi.Current.ImageFilterImplementation.GetNativeImageFilter(ObjectPointer);
+    
+    public override void Dispose()
+    {
+        DrawingBackendApi.Current.ImageFilterImplementation.DisposeObject(ObjectPointer);
+    }
+}

+ 11 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/Paint.cs

@@ -9,6 +9,7 @@ namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl
     /// </summary>
     public class Paint : NativeObject
     {
+        private ImageFilter? imageFilter;
         private ColorFilter? colorFilter;
         private Shader? shader;
         
@@ -66,6 +67,16 @@ namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl
             }
         }
 
+        public ImageFilter ImageFilter
+        {
+            get => imageFilter ??= DrawingBackendApi.Current.PaintImplementation.GetImageFilter(this);
+            set
+            {
+                DrawingBackendApi.Current.PaintImplementation.SetImageFilter(this, value);
+                imageFilter = value;
+            }
+        }
+
         public Shader Shader
         {
             get => shader ??= DrawingBackendApi.Current.PaintImplementation.GetShader(this);

+ 16 - 0
src/PixiEditor.DrawingApi.Core/Surface/TileMode.cs

@@ -0,0 +1,16 @@
+namespace PixiEditor.DrawingApi.Core.Surface;
+
+public enum TileMode
+{
+    /// <summary>Replicate the edge color.</summary>
+    Clamp,
+    
+    /// <summary>Repeat the shader's image horizontally and vertically.</summary>
+    Repeat,
+    
+    /// <summary>Repeat the shader's image horizontally and vertically, alternating mirror images so that adjacent images always seam.</summary>
+    Mirror,
+    
+    /// <summary>To be added.</summary>
+    Decal,
+}

+ 30 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageFilterImplementation.cs

@@ -0,0 +1,30 @@
+using System;
+using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+using SkiaSharp;
+
+namespace PixiEditor.DrawingApi.Skia.Implementations
+{
+    public class SkiaImageFilterImplementation : SkObjectImplementation<SKImageFilter>, IImageFilterImplementation
+    {
+        public IntPtr CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset, TileMode mode, bool convolveAlpha)
+        {
+            var skImageFilter = SKImageFilter.CreateMatrixConvolution(
+                new SKSizeI(size.X, size.Y),
+                kernel,
+                gain,
+                bias,
+                new SKPointI(kernelOffset.X, kernelOffset.Y),
+                (SKShaderTileMode)mode,
+                convolveAlpha);
+
+            ManagedInstances[skImageFilter.Handle] = skImageFilter;
+            return skImageFilter.Handle;
+        }
+
+        public object GetNativeImageFilter(IntPtr objPtr) => ManagedInstances[objPtr];
+    }
+}

+ 16 - 2
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPaintImplementation.cs

@@ -10,11 +10,13 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
     public class SkiaPaintImplementation : SkObjectImplementation<SKPaint>, IPaintImplementation
     {
         private readonly SkiaColorFilterImplementation colorFilterImplementation;
+        private readonly SkiaImageFilterImplementation imageFilterImplementation;
         private readonly SkiaShaderImplementation shaderImplementation;
  
-        public SkiaPaintImplementation(SkiaColorFilterImplementation colorFilterImpl, SkiaShaderImplementation shaderImpl)
+        public SkiaPaintImplementation(SkiaColorFilterImplementation colorFilterImpl, SkiaImageFilterImplementation imageFilterImpl, SkiaShaderImplementation shaderImpl)
         {
             colorFilterImplementation = colorFilterImpl;
+            imageFilterImplementation = imageFilterImpl;
             shaderImplementation = shaderImpl;
         }
 
@@ -148,7 +150,19 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
             skPaint.ColorFilter = colorFilterImplementation[value.ObjectPointer];
         }
-        
+
+        public ImageFilter GetImageFilter(Paint paint)
+        {
+            SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
+            return new ImageFilter(skPaint.ColorFilter.Handle);
+        }
+
+        public void SetImageFilter(Paint paint, ImageFilter value)
+        {
+            SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
+            skPaint.ImageFilter = imageFilterImplementation[value.ObjectPointer];
+        }
+
         public Shader GetShader(Paint paint)
         {
             SKPaint skPaint = ManagedInstances[paint.ObjectPointer];

+ 6 - 2
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -18,7 +18,8 @@ namespace PixiEditor.DrawingApi.Skia
         public ISurfaceImplementation SurfaceImplementation { get; }
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
-        public IColorFilterImplementation ColorFilterImplementation { get; set; }
+        public IColorFilterImplementation ColorFilterImplementation { get; }
+        public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
 
         public SkiaDrawingBackend()
@@ -30,11 +31,14 @@ namespace PixiEditor.DrawingApi.Skia
             
             SkiaColorFilterImplementation colorFilterImpl = new SkiaColorFilterImplementation();
             ColorFilterImplementation = colorFilterImpl;
+
+            SkiaImageFilterImplementation imageFilterImpl = new SkiaImageFilterImplementation();
+            ImageFilterImplementation = imageFilterImpl;
             
             SkiaShaderImplementation shader = new SkiaShaderImplementation();
             ShaderImplementation = shader;
             
-            SkiaPaintImplementation paintImpl = new SkiaPaintImplementation(colorFilterImpl, shader);
+            SkiaPaintImplementation paintImpl = new SkiaPaintImplementation(colorFilterImpl, imageFilterImpl, shader);
             PaintImplementation = paintImpl;
             
             SkiaPathImplementation pathImpl = new SkiaPathImplementation();

+ 56 - 0
src/PixiEditor.Numerics/Kernel.cs

@@ -0,0 +1,56 @@
+namespace PixiEditor.Numerics;
+
+public class Kernel
+{
+    private KernelArray _buffer;
+
+    public int Width { get; private set; }
+
+    public int Height { get; private set; }
+
+    public int RadiusX => Width / 2;
+    
+    public int RadiusY => Height / 2;
+
+    public float this[int x, int y]
+    {
+        get => _buffer[x, y];
+        set => _buffer[x, y] = value;
+    }
+
+    public Kernel(int width, int height)
+    {
+        if (width % 2 == 0)
+            throw new ArgumentException($"{width} must be odd", nameof(width));
+        
+        Width = width;
+        Height = height;
+        _buffer = new KernelArray(width, height);
+    }
+
+    public void Resize(int width, int height)
+    {
+        var old = _buffer;
+
+        _buffer = new KernelArray(width, height);
+        Width = width;
+        Height = height;
+
+        var oldRadiusX = old.RadiusX;
+        var oldRadiusY = old.RadiusY;
+        var newRadiusX = _buffer.RadiusX;
+        var newRadiusY = _buffer.RadiusY;
+        for (int y = -newRadiusY; y <= newRadiusY; y++)
+        {
+            for (int x = -newRadiusX; x <= newRadiusX; x++)
+            {
+                if (x < oldRadiusX || x > oldRadiusX || y < oldRadiusY || y > oldRadiusY)
+                    continue;
+
+                _buffer[x, y] = old[x, y];
+            }
+        }
+    }
+
+    public ReadOnlySpan<float> AsSpan() => _buffer.AsSpan();
+}

+ 40 - 0
src/PixiEditor.Numerics/KernelArray.cs

@@ -0,0 +1,40 @@
+namespace PixiEditor.Numerics;
+
+public class KernelArray
+{
+    private readonly float[] _buffer;
+    
+    public int Width { get; }
+    
+    public int Height { get; }
+
+    public int RadiusX => Width / 2;
+    
+    public int RadiusY => Height / 2;
+    
+    public KernelArray(int width, int height)
+    {
+        if (width % 2 == 0)
+            throw new ArgumentException($"{width} must be odd", nameof(width));
+        
+        Width = width;
+        Height = height;
+        _buffer = new float[width * height];
+    }
+
+    public float this[int x, int y]
+    {
+        get => _buffer[GetBufferIndex(x, y)];
+        set => _buffer[GetBufferIndex(x, y)] = value;
+    }
+
+    private int GetBufferIndex(int x, int y)
+    {
+        x += RadiusX;
+        y += RadiusY;
+
+        return y * Width + x;
+    }
+
+    public ReadOnlySpan<float> AsSpan() => _buffer.AsSpan();
+}