Browse Source

Merge pull request #634 from PixiEditor/hsv-hsl-support

Hsv/Hsl Support for Combine/Separate Color nodes
Krzysztof Krysiński 11 months ago
parent
commit
38e6707c37
21 changed files with 545 additions and 63 deletions
  1. 64 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  2. 46 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  3. 37 26
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  4. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  5. 46 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NodeVariableAttachments.cs
  6. 149 0
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/BuiltInFunctions.cs
  7. 6 3
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Float1.cs
  8. 39 0
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half3.cs
  9. 26 0
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half3Float1Accessor.cs
  10. 12 6
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4.cs
  11. 27 0
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4Float1Accessor.cs
  12. 19 10
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/ShaderBuilder.cs
  13. BIN
      src/PixiEditor/Data/BetaExampleFiles/Island.pixi
  14. BIN
      src/PixiEditor/Data/BetaExampleFiles/Pond.pixi
  15. BIN
      src/PixiEditor/Data/BetaExampleFiles/Stars.pixi
  16. BIN
      src/PixiEditor/Data/BetaExampleFiles/Tree.pixi
  17. 13 0
      src/PixiEditor/Helpers/Extensions/CombineSeparateColorModeExtensions.cs
  18. 1 1
      src/PixiEditor/Models/Events/NodePropertyValueChanged.cs
  19. 5 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs
  20. 53 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineSeparateColorNodeViewModel.cs
  21. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs

+ 64 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs

@@ -1,5 +1,6 @@
 using System.Linq.Expressions;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
@@ -98,10 +99,10 @@ public class FuncContext
         if (!HasContext && r is Float1 firstFloat && g is Float1 secondFloat && b is Float1 thirdFloat && a is Float1 fourthFloat)
         {
             Half4 constantHalf4 = new Half4("");
-            byte rByte = (byte)firstFloat.ConstantValue;
-            byte gByte = (byte)secondFloat.ConstantValue;
-            byte bByte = (byte)thirdFloat.ConstantValue;
-            byte aByte = (byte)fourthFloat.ConstantValue;
+            byte rByte = firstFloat.AsConstantColorByte();
+            byte gByte = secondFloat.AsConstantColorByte();
+            byte bByte = thirdFloat.AsConstantColorByte();
+            byte aByte = fourthFloat.AsConstantColorByte();
             constantHalf4.ConstantValue = new Color(rByte, gByte, bByte, aByte);
             return constantHalf4;
         }
@@ -109,6 +110,65 @@ public class FuncContext
         return Builder.ConstructHalf4(r, g, b, a);
     }
 
+    public Half4 HsvaToRgba(Expression h, Expression s, Expression v, Expression a)
+    {
+        if (!HasContext && h is Float1 firstFloat && s is Float1 secondFloat && v is Float1 thirdFloat && a is Float1 fourthFloat)
+        {
+            Half4 constantHalf4 = new Half4("");
+            var hValue = firstFloat.ConstantValue * 360;
+            var sValue = secondFloat.ConstantValue * 100;
+            var vValue = thirdFloat.ConstantValue * 100;
+            byte aByte = fourthFloat.AsConstantColorByte();
+            constantHalf4.ConstantValue = Color.FromHsv((float)hValue, (float)sValue, (float)vValue, aByte);
+            return constantHalf4;
+        }
+
+        return Builder.AssignNewHalf4(Builder.Functions.GetHslToRgb(h, s, v, a));
+    }
+
+    public Half4 HslaToRgba(Expression h, Expression s, Expression l, Expression a)
+    {
+        if (!HasContext && h is Float1 firstFloat && s is Float1 secondFloat && l is Float1 thirdFloat && a is Float1 fourthFloat)
+        {
+            Half4 constantHalf4 = new Half4("");
+            var hValue = firstFloat.ConstantValue * 360;
+            var sValue = secondFloat.ConstantValue * 100;
+            var lValue = thirdFloat.ConstantValue * 100;
+            byte aByte = fourthFloat.AsConstantColorByte();
+            constantHalf4.ConstantValue = Color.FromHsl((float)hValue, (float)sValue, (float)lValue, aByte);
+            return constantHalf4;
+        }
+
+        return Builder.AssignNewHalf4(Builder.Functions.GetHslToRgb(h, s, l, a));
+    }
+    
+    public Half4 RgbaToHsva(Expression color)
+    {
+        if (!HasContext && color is Half4 constantColor)
+        {
+            var variable = new Half4(string.Empty);
+            constantColor.ConstantValue.ToHsv(out float h, out float s, out float l);
+            variable.ConstantValue = new Color((byte)(h * 255), (byte)(s * 255), (byte)(l * 255), constantColor.ConstantValue.A);
+            
+            return variable;
+        }
+
+        return Builder.AssignNewHalf4(Builder.Functions.GetRgbToHsv(color));
+    }
+    
+    public Half4 RgbaToHsla(Expression color)
+    {
+        if (!HasContext && color is Half4 constantColor)
+        {
+            var variable = new Half4(string.Empty);
+            constantColor.ConstantValue.ToHsl(out float h, out float s, out float l);
+            variable.ConstantValue = new Color((byte)(h * 255), (byte)(s * 255), (byte)(l * 255), constantColor.ConstantValue.A);
+            
+            return variable;
+        }
+
+        return Builder.AssignNewHalf4(Builder.Functions.GetRgbToHsl(color));
+    }
 
     public Half4 NewHalf4(Expression assignment)
     {

+ 46 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs

@@ -13,11 +13,20 @@ public class CombineColorNode : Node
 
     public InputProperty<CombineSeparateColorMode> Mode { get; }
 
-    public FuncInputProperty<Float1> R { get; }
+    /// <summary>
+    /// Represents either Red 'R' or Hue 'H' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncInputProperty<Float1> V1 { get; }
 
-    public FuncInputProperty<Float1> G { get; }
+    /// <summary>
+    /// Represents either Green 'G' or Saturation 'S' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncInputProperty<Float1> V2 { get; }
 
-    public FuncInputProperty<Float1> B { get; }
+    /// <summary>
+    /// Represents either Blue 'B', Value 'V' or Lightness 'L' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncInputProperty<Float1> V3 { get; }
 
     public FuncInputProperty<Float1> A { get; }
 
@@ -26,22 +35,49 @@ public class CombineColorNode : Node
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
         Mode = CreateInput("Mode", "MODE", CombineSeparateColorMode.RGB);
 
-        R = CreateFuncInput<Float1>("R", "R", 0d);
-        G = CreateFuncInput<Float1>("G", "G", 0d);
-        B = CreateFuncInput<Float1>("B", "B", 0d);
+        V1 = CreateFuncInput<Float1>("R", "R", 0d);
+        V2 = CreateFuncInput<Float1>("G", "G", 0d);
+        V3 = CreateFuncInput<Float1>("B", "B", 0d);
         A = CreateFuncInput<Float1>("A", "A", 0d);
     }
 
-    private Half4 GetColor(FuncContext ctx)
+    private Half4 GetColor(FuncContext ctx) =>
+        Mode.Value switch
+        {
+            CombineSeparateColorMode.RGB => GetRgb(ctx),
+            CombineSeparateColorMode.HSV => GetHsv(ctx),
+            CombineSeparateColorMode.HSL => GetHsl(ctx)
+        };
+
+    private Half4 GetRgb(FuncContext ctx)
     {
-        var r = ctx.GetValue(R);
-        var g = ctx.GetValue(G);
-        var b = ctx.GetValue(B);
+        var r = ctx.GetValue(V1);
+        var g = ctx.GetValue(V2);
+        var b = ctx.GetValue(V3);
         var a = ctx.GetValue(A);
 
         return ctx.NewHalf4(r, g, b, a); 
     }
 
+    private Half4 GetHsv(FuncContext ctx)
+    {
+        var h = ctx.GetValue(V1);
+        var s = ctx.GetValue(V2);
+        var v = ctx.GetValue(V3);
+        var a = ctx.GetValue(A);
+
+        return ctx.HsvaToRgba(h, s, v, a);
+    }
+    
+    private Half4 GetHsl(FuncContext ctx)
+    {
+        var h = ctx.GetValue(V1);
+        var s = ctx.GetValue(V2);
+        var l = ctx.GetValue(V3);
+        var a = ctx.GetValue(A);
+
+        return ctx.HslaToRgba(h, s, l, a);
+    }
 
     protected override Texture? OnExecute(RenderingContext context)
     {

+ 37 - 26
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
@@ -9,27 +10,37 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 [NodeInfo("SeparateColor")]
 public class SeparateColorNode : Node
 {
+    private readonly NodeVariableAttachments contextVariables = new();
+    
     public FuncInputProperty<Half4> Color { get; }
     
-    public FuncOutputProperty<Float1> R { get; }
+    public InputProperty<CombineSeparateColorMode> Mode { get; }
+
+    /// <summary>
+    /// Represents either Red 'R' or Hue 'H' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncOutputProperty<Float1> V1 { get; }
     
-    public FuncOutputProperty<Float1> G { get; }
+    /// <summary>
+    /// Represents either Green 'G' or Saturation 'S' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncOutputProperty<Float1> V2 { get; }
     
-    public FuncOutputProperty<Float1> B { get; }
+    /// <summary>
+    /// Represents either Blue 'B', Value 'V' or Lightness 'L' depending on <see cref="Mode"/>
+    /// </summary>
+    public FuncOutputProperty<Float1> V3 { get; }
     
     public FuncOutputProperty<Float1> A { get; }
     
-    
-    private FuncContext lastContext;
-    private Half4 lastColor;
-
     public SeparateColorNode()
     {
+        V1 = CreateFuncOutput<Float1>("R", "R", ctx => GetColor(ctx).R);
+        V2 = CreateFuncOutput<Float1>("G", "G", ctx => GetColor(ctx).G);
+        V3 = CreateFuncOutput<Float1>("B", "B", ctx => GetColor(ctx).B);
+        A = CreateFuncOutput<Float1>("A", "A", ctx => GetColor(ctx).A);
+        Mode = CreateInput("Mode", "MODE", CombineSeparateColorMode.RGB);
         Color = CreateFuncInput<Half4>(nameof(Color), "COLOR", new Color());
-        R = CreateFuncOutput<Float1>(nameof(R), "R", ctx => GetColor(ctx).R);
-        G = CreateFuncOutput<Float1>(nameof(G), "G", ctx => GetColor(ctx).G);
-        B = CreateFuncOutput<Float1>(nameof(B), "B", ctx => GetColor(ctx).B);
-        A = CreateFuncOutput<Float1>(nameof(A), "A", ctx => GetColor(ctx).A);
     }
 
     protected override Texture? OnExecute(RenderingContext context)
@@ -37,22 +48,22 @@ public class SeparateColorNode : Node
         return null;
     }
     
-    private Half4 GetColor(FuncContext ctx)
-    {
-        Half4 target = null;
-        if (lastContext == ctx)
+    private Half4 GetColor(FuncContext ctx) =>
+        Mode.Value switch
         {
-            target = lastColor;
-        }
-        else
-        {
-            target = Color.Value(ctx);
-        }
-        
-        lastColor = target;
-        lastContext = ctx;
-        return lastColor;
-    }
+            CombineSeparateColorMode.RGB => GetRgba(ctx),
+            CombineSeparateColorMode.HSV => GetHsva(ctx),
+            CombineSeparateColorMode.HSL => GetHsla(ctx)
+        };
+
+    private Half4 GetRgba(FuncContext ctx) => 
+        contextVariables.GetOrAttachNew(ctx, Color, () => Color.Value(ctx));
+
+    private Half4 GetHsva(FuncContext ctx) =>
+        contextVariables.GetOrAttachNew(ctx, Color, () => ctx.RgbaToHsva(ctx.GetValue(Color)));
+
+    private Half4 GetHsla(FuncContext ctx) =>
+        contextVariables.GetOrAttachNew(ctx, Color, () => ctx.RgbaToHsla(ctx.GetValue(Color)));
 
     public override Node CreateCopy() => new SeparateColorNode();
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -77,7 +77,7 @@ public class ModifyImageRightNode : Node, IPairNode, ICustomShaderNode
         }
         else
         {
-            ShaderBuilder builder = new();
+            ShaderBuilder builder = new(size);
             FuncContext context = new(renderingContext, builder);
 
             if (Coordinate.Connection != null)

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NodeVariableAttachments.cs

@@ -0,0 +1,46 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.Shaders.Generation;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+internal class NodeVariableAttachments
+{
+    private readonly List<(INodeProperty key, ShaderExpressionVariable variable)> _variables = new();
+    
+    public FuncContext LastContext { get; private set; }
+
+    /// <summary>
+    /// Tries getting a shader variable associated to the property. Otherwise, it creates a new one from the variable factory
+    /// </summary>
+    /// <typeparam name="T">The shader variable type. i.e. Half4, Float1</typeparam>
+    /// <returns>The attached or new variable</returns>
+    public T GetOrAttachNew<T>(FuncContext context, INodeProperty property, Func<T> variable) where T : ShaderExpressionVariable
+    {
+        if (LastContext != context)
+        {
+            LastContext = context;
+            _variables.Clear();
+            
+            return AttachNew(property, variable);
+        }
+
+        var existing = _variables.FirstOrDefault(x => x.key == property);
+
+        if (existing == default)
+        {
+            existing.variable = AttachNew(property, variable);
+        }
+
+        return (T)existing.variable;
+    }
+
+    private T AttachNew<T>(INodeProperty property, Func<T> variable) where T : ShaderExpressionVariable
+    {
+        var newVariable = variable();
+        
+        _variables.Add((property, newVariable));
+
+        return newVariable;
+    }
+}

+ 149 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Generation/BuiltInFunctions.cs

@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.Text;
+using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation;
+
+public class BuiltInFunctions
+{
+    private readonly List<IBuiltInFunction> usedFunctions = new(6);
+
+    public Expression GetRgbToHsv(Expression rgba) => Call(RgbToHsv, rgba);
+
+    public Expression GetRgbToHsl(Expression rgba) => Call(RgbToHsl, rgba);
+
+    public Expression GetHsvToRgb(Expression hsva) => Call(HsvToRgb, hsva);
+
+    public Expression GetHsvToRgb(Expression h, Expression s, Expression v, Expression a) =>
+        GetHsvToRgb(Half4Float1Accessor.GetOrConstructorExpressionHalf4(h, s, v, a));
+
+    public Expression GetHslToRgb(Expression hsla) => Call(HslToRgb, hsla);
+
+    public Expression GetHslToRgb(Expression h, Expression s, Expression l, Expression a) =>
+        GetHslToRgb(Half4Float1Accessor.GetOrConstructorExpressionHalf4(h, s, l, a));
+
+    public string BuildFunctions()
+    {
+        var builder = new StringBuilder();
+
+        foreach (var function in usedFunctions)
+        {
+            builder.AppendLine(function.FullSource);
+        }
+
+        return builder.ToString();
+    }
+
+    private Expression Call(IBuiltInFunction function, Expression expression)
+    {
+        Require(function);
+
+        return new Expression(function.Call(expression.ExpressionValue));
+    }
+
+    private void Require(IBuiltInFunction function)
+    {
+        if (usedFunctions.Contains(function))
+        {
+            return;
+        }
+
+        foreach (var dependency in function.Dependencies)
+        {
+            Require(dependency);
+        }
+
+        usedFunctions.Add(function);
+    }
+
+    // Taken from here https://www.shadertoy.com/view/4dKcWK
+    private static readonly BuiltInFunction<Half3> HueToRgb = new(
+        "float hue",
+        nameof(HueToRgb),
+        """
+        half3 rgb = abs(hue * 6. - half3(3, 2, 4)) * half3(1, -1, -1) + half3(-1, 2, 2);
+        return clamp(rgb, 0., 1.);
+        """);
+
+    private static readonly BuiltInFunction<Half3> RgbToHcv = new(
+        "half3 rgba",
+        nameof(RgbToHcv),
+        """
+        half4 p = (rgba.g < rgba.b) ? half4(rgba.bg, -1., 2. / 3.) : half4(rgba.gb, 0., -1. / 3.);
+        half4 q = (rgba.r < p.x) ? half4(p.xyw, rgba.r) : half4(rgba.r, p.yzx);
+        float c = q.x - min(q.w, q.y);
+        float h = abs((q.w - q.y) / (6. * c) + q.z);
+        return half3(h, c, q.x);
+        """);
+
+    private static readonly BuiltInFunction<Half4> RgbToHsv = new(
+        "half4 rgba",
+        nameof(RgbToHsv),
+        $"""
+         half3 hcv = {RgbToHcv.Call("rgba.rgb")};
+         float s = hcv.y / (hcv.z);
+         return half4(hcv.x, s, hcv.z, rgba.w);
+         """,
+        RgbToHcv);
+
+    private static readonly BuiltInFunction<Half4> HsvToRgb = new(
+        "half4 hsva",
+        nameof(HsvToRgb),
+        $"""
+         half3 rgb = {HueToRgb.Call("hsva.r")};
+         return half4(((rgb - 1.) * hsva.y + 1.) * hsva.z, hsva.w);
+         """,
+        HueToRgb);
+
+    private static readonly BuiltInFunction<Half4> RgbToHsl = new(
+        "half4 rgba", 
+        nameof(RgbToHsl), 
+        $"""
+         half3 hcv = {RgbToHcv.Call("rgba.rgb")};
+         half z = hcv.z - hcv.y * 0.5;
+         half s = hcv.y / (1. - abs(z * 2. - 1.));
+         return half4(hcv.x, s, z, rgba.w);
+         """,
+        RgbToHcv);
+
+    private static readonly BuiltInFunction<Half4> HslToRgb = new(
+        "half4 hsla",
+        nameof(HslToRgb),
+        $"""
+         half3 rgb = {HueToRgb.Call("hsla.r")};
+         float c = (1. - abs(2. * hsla.z - 1.)) * hsla.y;
+         return half4((rgb - 0.5) * c + hsla.z, hsla.w);
+         """,
+        HueToRgb);
+
+    private class BuiltInFunction<TReturn>(string argumentList, string name, string body, params IBuiltInFunction[] dependencies) : IBuiltInFunction where TReturn : ShaderExpressionVariable
+    {
+        public string ArgumentList { get; } = argumentList;
+
+        public string Name { get; } = name;
+
+        public string Body { get; } = body;
+
+        public IBuiltInFunction[] Dependencies { get; } = dependencies;
+
+        public string FullSource =>
+         $$"""
+          {{typeof(TReturn).Name.ToLower()}} {{Name}}({{ArgumentList}}) {
+          {{Body}}
+          }
+          """;
+
+        public string Call(string arguments) => $"{Name}({arguments})";
+    }
+    
+    private interface IBuiltInFunction
+    {
+        IBuiltInFunction[] Dependencies { get; }
+        
+        string Name { get; }
+        
+        string FullSource { get; }
+
+        string Call(string arguments);
+    }
+}

+ 6 - 3
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Float1.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+using System;
+
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 /// <summary>
 ///     This is a shader type that represents a high precision floating point value. For medium precision see Short type.
@@ -12,8 +14,9 @@ public class Float1(string name) : ShaderExpressionVariable<double>(name)
 
     public override Expression? OverrideExpression { get; set; }
 
-    public static implicit operator Float1(double value) => new Float1("") { ConstantValue = value };
+    public static implicit operator Float1(double value) => new("") { ConstantValue = value };
 
     public static explicit operator double(Float1 value) => value.ConstantValue;
-    
+
+    public byte AsConstantColorByte() => (byte)(Math.Clamp(ConstantValue, 0, 1) * 255);
 }

+ 39 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half3.cs

@@ -0,0 +1,39 @@
+using System;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+
+public class Half3(string name) : ShaderExpressionVariable<VecD3>(name), IMultiValueVariable
+{
+    private Expression? _overrideExpression;
+    public override string ConstantValueString => $"half3({ConstantValue.X}, {ConstantValue.Y}, {ConstantValue.Z})";
+    
+    public Float1 R => new Half3Float1Accessor(this, 'r') { ConstantValue = ConstantValue.X, OverrideExpression = _overrideExpression};
+    public Float1 G => new Half3Float1Accessor(this, 'g') { ConstantValue = ConstantValue.X, OverrideExpression = _overrideExpression};
+    public Float1 B => new Half3Float1Accessor(this, 'b') { ConstantValue = ConstantValue.Z, OverrideExpression = _overrideExpression};
+
+    public override Expression? OverrideExpression
+    {
+        get => _overrideExpression;
+        set
+        {
+            _overrideExpression = value;
+        }
+    }
+
+    public ShaderExpressionVariable GetValueAt(int index)
+    {
+        return index switch
+        {
+            0 => R,
+            1 => G,
+            2 => B,
+            _ => throw new IndexOutOfRangeException()
+        };
+    }
+
+    public static string ConstructorText(Expression r, Expression g, Expression b) =>
+        $"half3({r.ExpressionValue}, {g.ExpressionValue}, {b.ExpressionValue})";
+
+    public static Expression Constructor(Expression r, Expression g, Expression b) => new(ConstructorText(r, g, b));
+}

+ 26 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half3Float1Accessor.cs

@@ -0,0 +1,26 @@
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+
+public class Half3Float1Accessor : Float1
+{
+    public Half3Float1Accessor(Half3 accessTo, char name) : base(string.IsNullOrEmpty(accessTo.VariableName) ? string.Empty : $"{accessTo.VariableName}.{name}")
+    {
+        Accesses = accessTo;
+    }
+    
+    public Half3 Accesses { get; }
+
+    public static bool AllAccessSame(Expression r, Expression g, Expression b, out Half3? half3)
+    {
+        if (r is Half3Float1Accessor rA && g is Half3Float1Accessor gA && b is Half3Float1Accessor bA &&
+            rA.Accesses == gA.Accesses && rA.Accesses == bA.Accesses)
+        {
+            half3 = rA.Accesses;
+            return true;
+        }
+
+        half3 = null;
+        return false;
+    }
+    
+    public static Expression GetOrConstructorExpressionHalf3(Expression r, Expression g, Expression b) => AllAccessSame(r, g, b, out var value) ? value : Half3.Constructor(r, g, b);
+}

+ 12 - 6
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4.cs

@@ -8,12 +8,12 @@ public class Half4(string name) : ShaderExpressionVariable<Color>(name), IMultiV
     private Expression? _overrideExpression;
     public override string ConstantValueString => $"half4({ConstantValue.R}, {ConstantValue.G}, {ConstantValue.B}, {ConstantValue.A})";
     
-    public Float1 R => new Float1(string.IsNullOrEmpty(VariableName) ? string.Empty : $"{VariableName}.r") { ConstantValue = ConstantValue.R, OverrideExpression = _overrideExpression};
-    public Float1 G => new Float1(string.IsNullOrEmpty(VariableName) ? string.Empty : $"{VariableName}.g") { ConstantValue = ConstantValue.G, OverrideExpression = _overrideExpression};
-    public Float1 B => new Float1(string.IsNullOrEmpty(VariableName) ? string.Empty : $"{VariableName}.b") { ConstantValue = ConstantValue.B, OverrideExpression = _overrideExpression};
-    public Float1 A => new Float1(string.IsNullOrEmpty(VariableName) ? string.Empty : $"{VariableName}.a") { ConstantValue = ConstantValue.A, OverrideExpression = _overrideExpression};
-    
-    public static implicit operator Half4(Color value) => new Half4("") { ConstantValue = value };
+    public Float1 R => new Half4Float1Accessor(this, 'r') { ConstantValue = ConstantValue.R, OverrideExpression = _overrideExpression};
+    public Float1 G => new Half4Float1Accessor(this, 'g') { ConstantValue = ConstantValue.G, OverrideExpression = _overrideExpression};
+    public Float1 B => new Half4Float1Accessor(this, 'b') { ConstantValue = ConstantValue.B, OverrideExpression = _overrideExpression};
+    public Float1 A => new Half4Float1Accessor(this, 'a') { ConstantValue = ConstantValue.A, OverrideExpression = _overrideExpression};
+
+    public static implicit operator Half4(Color value) => new("") { ConstantValue = value };
     public static explicit operator Color(Half4 value) => value.ConstantValue;
 
     public override Expression? OverrideExpression
@@ -36,4 +36,10 @@ public class Half4(string name) : ShaderExpressionVariable<Color>(name), IMultiV
             _ => throw new IndexOutOfRangeException()
         };
     }
+
+    public static string ConstructorText(Expression r, Expression g, Expression b, Expression a) =>
+        $"half4({r.ExpressionValue}, {g.ExpressionValue}, {b.ExpressionValue}, {a.ExpressionValue})";
+
+    public static Expression Constructor(Expression r, Expression g, Expression b, Expression a) =>
+        new Expression(ConstructorText(r, g, b, a));
 }

+ 27 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4Float1Accessor.cs

@@ -0,0 +1,27 @@
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+
+public class Half4Float1Accessor : Float1
+{
+    public Half4Float1Accessor(Half4 accessTo, char name) : base(string.IsNullOrEmpty(accessTo.VariableName) ? string.Empty : $"{accessTo.VariableName}.{name}")
+    {
+        Accesses = accessTo;
+    }
+    
+    public Half4 Accesses { get; }
+
+    public static bool AllAccessSame(Expression r, Expression g, Expression b, Expression a, out Half4? half4)
+    {
+        if (r is Half4Float1Accessor rA && g is Half4Float1Accessor gA &&
+            b is Half4Float1Accessor bA && a is Half4Float1Accessor aA &&
+            rA.Accesses == gA.Accesses && bA.Accesses == aA.Accesses && rA.Accesses == bA.Accesses)
+        {
+            half4 = rA.Accesses;
+            return true;
+        }
+
+        half4 = null;
+        return false;
+    }
+
+    public static Expression GetOrConstructorExpressionHalf4(Expression r, Expression g, Expression b, Expression a) => AllAccessSame(r, g, b, a, out var value) ? value : Half4.Constructor(r, g, b, a);
+}

+ 19 - 10
src/PixiEditor.DrawingApi.Core/Shaders/Generation/ShaderBuilder.cs

@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
@@ -16,6 +18,13 @@ public class ShaderBuilder
 
     private Dictionary<Texture, TextureSampler> _samplers = new Dictionary<Texture, TextureSampler>();
 
+    public BuiltInFunctions Functions { get; } = new();
+
+    public ShaderBuilder(VecI resolution)
+    {
+        AddUniform("iResolution", resolution);
+    }
+    
     public Shader BuildShader()
     {
         string generatedSksl = ToSkSl();
@@ -26,8 +35,12 @@ public class ShaderBuilder
     {
         StringBuilder sb = new StringBuilder();
         AppendUniforms(sb);
+
+        sb.AppendLine(Functions.BuildFunctions());
+        
         sb.AppendLine("half4 main(float2 coords)");
         sb.AppendLine("{");
+        sb.AppendLine("coords = coords / iResolution;");
         sb.Append(_bodyBuilder);
         sb.AppendLine("}");
 
@@ -64,7 +77,7 @@ public class ShaderBuilder
         string resultName = $"color_{GetUniqueNameNumber()}";
         Half4 result = new Half4(resultName);
         _variables.Add(result);
-        _bodyBuilder.AppendLine($"half4 {resultName} = sample({texName.VariableName}, {pos.VariableName});");
+        _bodyBuilder.AppendLine($"half4 {resultName} = sample({texName.VariableName}, {pos.VariableName} * iResolution);");
         return result;
     }
 
@@ -152,19 +165,15 @@ public class ShaderBuilder
         Half4 result = new Half4(name);
         _variables.Add(result);
 
-        string rExpression = r.ExpressionValue;
-        string gExpression = g.ExpressionValue;
-        string bExpression = b.ExpressionValue;
-        string aExpression = a.ExpressionValue;
-
-        _bodyBuilder.AppendLine($"half4 {name} = half4({rExpression}, {gExpression}, {bExpression}, {aExpression});");
+        _bodyBuilder.AppendLine($"half4 {name} = {Half4.ConstructorText(r, g, b, a)};");
         return result;
     }
 
 
-    public Half4 AssignNewHalf4(Expression assignment)
+    public Half4 AssignNewHalf4(Expression assignment) => AssignNewHalf4($"color_{GetUniqueNameNumber()}", assignment);
+
+    public Half4 AssignNewHalf4(string name, Expression assignment)
     {
-        string name = $"color_{GetUniqueNameNumber()}";
         Half4 result = new Half4(name);
         _variables.Add(result);
 

BIN
src/PixiEditor/Data/BetaExampleFiles/Island.pixi


BIN
src/PixiEditor/Data/BetaExampleFiles/Pond.pixi


BIN
src/PixiEditor/Data/BetaExampleFiles/Stars.pixi


BIN
src/PixiEditor/Data/BetaExampleFiles/Tree.pixi


+ 13 - 0
src/PixiEditor/Helpers/Extensions/CombineSeparateColorModeExtensions.cs

@@ -0,0 +1,13 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+
+namespace PixiEditor.Helpers.Extensions;
+
+internal static class CombineSeparateColorModeExtensions
+{
+    public static (string v1, string v2, string v3) GetLocalizedColorStringNames(this CombineSeparateColorMode mode) => mode switch
+    {
+        CombineSeparateColorMode.RGB => ("R", "G", "B"),
+        CombineSeparateColorMode.HSV => ("H", "S", "V"),
+        CombineSeparateColorMode.HSL => ("H", "S", "L"),
+    };
+}

+ 1 - 1
src/PixiEditor/Models/Events/NodePropertyValueChanged.cs

@@ -4,4 +4,4 @@ namespace PixiEditor.Models.Events;
 
 public delegate void NodePropertyValueChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args);
 
-public record NodePropertyValueChangedArgs(object OldValue, object NewValue);
+public record NodePropertyValueChangedArgs(object? OldValue, object? NewValue);

+ 5 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs

@@ -1,7 +1,11 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Handlers;
 using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
 [NodeViewModel("COMBINE_COLOR_NODE", "COLOR", "\ue908")]
-internal class CombineColorNodeViewModel : NodeViewModel<CombineColorNode>;
+internal class CombineColorNodeViewModel() : CombineSeparateColorNodeViewModel<CombineColorNode>(true);

+ 53 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineSeparateColorNodeViewModel.cs

@@ -0,0 +1,53 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+internal abstract class CombineSeparateColorNodeViewModel<T>(bool isInput) : NodeViewModel<T> where T : Node
+{
+    private GenericEnumPropertyViewModel Mode { get; set; }
+    
+    private NodePropertyViewModel<double> V1 { get; set; }
+
+    private NodePropertyViewModel<double> V2 { get; set; }
+    
+    private NodePropertyViewModel<double> V3 { get; set; }
+
+    public override void OnInitialized()
+    {
+        Mode = FindInputProperty("Mode") as GenericEnumPropertyViewModel;
+        
+        Mode.ValueChanged += OnModeChanged;
+
+        if (isInput)
+        {
+            V1 = FindInputProperty<double>("R");
+            V2 = FindInputProperty<double>("G");
+            V3 = FindInputProperty<double>("B");
+        }
+        else
+        {
+            V1 = FindOutputProperty<double>("R");
+            V2 = FindOutputProperty<double>("G");
+            V3 = FindOutputProperty<double>("B");
+        }
+    }
+
+    private void OnModeChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args)
+    {
+        if (args.NewValue == null) return;
+        
+        var mode = (CombineSeparateColorMode)args.NewValue;
+
+        var (v1, v2, v3) = mode.GetLocalizedColorStringNames();
+
+        V1.DisplayName = v1;
+        V2.DisplayName = v2;
+        V3.DisplayName = v3;
+    }
+}

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs

@@ -4,4 +4,4 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
 [NodeViewModel("SEPARATE_COLOR_NODE", "COLOR", "\ue913")]
-internal class SeparateColorNodeViewModel : NodeViewModel<SeparateColorNode>;
+internal class SeparateColorNodeViewModel() : CombineSeparateColorNodeViewModel<SeparateColorNode>(false);