فهرست منبع

Merge branch 'master' into localization-reference-checker

Krzysztof Krysiński 3 هفته پیش
والد
کامیت
8c61b9f062

+ 163 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -18,6 +18,11 @@ public class NoiseNode : RenderNode
     private NoiseType previousNoiseType = Nodes.NoiseType.FractalPerlin;
     private int previousOctaves = -1;
     private VecD previousOffset = new VecD(0d, 0d);
+    private VoronoiFeature previousVoronoiFeature = Nodes.VoronoiFeature.F1;
+    private double previousRandomness = double.NaN;
+    private double previousAngleOffset = double.NaN;
+    
+    private Shader voronoiShader;
 
     private Paint paint = new();
 
@@ -25,7 +30,7 @@ public class NoiseNode : RenderNode
         ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
 
     public InputProperty<NoiseType> NoiseType { get; }
-
+    
     public InputProperty<VecD> Offset { get; }
     
     public InputProperty<double> Scale { get; }
@@ -33,6 +38,12 @@ public class NoiseNode : RenderNode
     public InputProperty<int> Octaves { get; }
 
     public InputProperty<double> Seed { get; }
+    
+    public InputProperty<VoronoiFeature> VoronoiFeature { get; }
+    
+    public InputProperty<double> Randomness { get; }
+    
+    public InputProperty<double> AngleOffset { get; }
 
     public NoiseNode()
     {
@@ -45,6 +56,13 @@ public class NoiseNode : RenderNode
             .WithRules(validator => validator.Min(1));
 
         Seed = CreateInput(nameof(Seed), "SEED", 0d);
+        
+        VoronoiFeature = CreateInput(nameof(VoronoiFeature), "VORONOI_FEATURE", Nodes.VoronoiFeature.F1);
+        
+        Randomness = CreateInput(nameof(Randomness), "RANDOMNESS", 1d)
+            .WithRules(v => v.Min(0d).Max(1d));
+        
+        AngleOffset = CreateInput(nameof(AngleOffset), "ANGLE_OFFSET", 0d);
     }
 
     protected override void OnPaint(RenderContext context, DrawingSurface target)
@@ -54,6 +72,9 @@ public class NoiseNode : RenderNode
             || previousOctaves != Octaves.Value
             || previousNoiseType != NoiseType.Value
             || previousOffset != Offset.Value
+            || previousVoronoiFeature != VoronoiFeature.Value
+            || Math.Abs(previousRandomness - Randomness.Value) > 0.000001
+            || Math.Abs(previousAngleOffset - AngleOffset.Value) > 0.000001
             || double.IsNaN(previousScale))
         {
             if (Scale.Value < 0.000001)
@@ -67,6 +88,10 @@ public class NoiseNode : RenderNode
                 return;
             }
 
+            if (paint.Shader != voronoiShader)
+            {
+                paint?.Shader?.Dispose();
+            }
             paint.Shader = shader;
 
             // Define a grayscale color filter to apply to the image
@@ -76,6 +101,9 @@ public class NoiseNode : RenderNode
             previousSeed = Seed.Value;
             previousOctaves = Octaves.Value;
             previousNoiseType = NoiseType.Value;
+            previousVoronoiFeature = VoronoiFeature.Value;
+            previousRandomness = Randomness.Value;
+            previousAngleOffset = AngleOffset.Value;
         }
 
         RenderNoise(target);
@@ -101,7 +129,11 @@ public class NoiseNode : RenderNode
         {
             return false;
         }
-        
+
+        if (paint.Shader != voronoiShader)
+        {
+            paint?.Shader?.Dispose();
+        }
         paint.Shader = shader;
         paint.ColorFilter = grayscaleFilter;
         
@@ -122,17 +154,145 @@ public class NoiseNode : RenderNode
                 (float)(1d / Scale.Value),
                 (float)(1d / Scale.Value),
                 octaves, (float)Seed.Value),
+            Nodes.NoiseType.Voronoi => GetVoronoiShader((float)(1d / (Scale.Value)), octaves, (float)Seed.Value, (int)VoronoiFeature.Value, (float)Randomness.Value, (float)AngleOffset.Value),
             _ => null
         };
 
         return shader;
     }
 
+    private Shader GetVoronoiShader(float frequency, int octaves, float seed, int feature, float randomness, float angleOffset)
+    {
+        string voronoiShaderCode = """
+                                   uniform float iSeed;
+                                   uniform float iFrequency;
+                                   uniform int iOctaves;
+                                   uniform float iRandomness;
+                                   uniform int iFeature;
+                                   uniform float iAngleOffset;
+                                   
+                                   const int MAX_OCTAVES = 8;
+                                   const float LARGE_NUMBER = 1e9;
+                                   const float FEATURE_SEED_SCALE = 10.0;
+                                   const float PI = 3.14159265;
+                                   
+                                   float hashPoint(float2 p, float seed) {
+                                       p = fract(p * float2(0.3183099, 0.3678794) + seed);
+                                       p += dot(p, p.yx + 19.19);
+                                       return fract(p.x * p.y);
+                                   }
+                                   
+                                   float2 getFeaturePoint(float2 cell, float seed, float randomness, float angleOffset) {
+                                       float2 randomCellOffset = float2(
+                                           hashPoint(cell, seed),
+                                           hashPoint(cell, seed + 17.0)
+                                       );
+                                       
+                                       float2 featurePoint = mix(float2(0.5, 0.5), randomCellOffset, randomness);
+                                       
+                                       float angle = hashPoint(cell, seed + 53.0) * PI * 2.0;
+                                       angle += angleOffset;
+                                       
+                                       float2 dir = float2(cos(angle), sin(angle));
+                                       float offsetAmount = 0.15;
+                                       featurePoint += dir * offsetAmount * randomness;
+                                       
+                                       featurePoint = clamp(featurePoint, 0.0, 1.0);
+                                       
+                                       return featurePoint;
+                                   }
+                                   
+                                   float2 getVoronoiDistances(float2 pos, float seed) {
+                                       float2 cell = floor(pos);
+                                       float minDist = LARGE_NUMBER;
+                                       float secondMinDist = LARGE_NUMBER;
+                                   
+                                       for (int y = -1; y <= 1; y++) {
+                                           for (int x = -1; x <= 1; x++) {
+                                               float2 neighborCell = cell + float2(float(x), float(y));
+                                               float2 featurePoint = getFeaturePoint(neighborCell, seed, iRandomness, iAngleOffset);
+                                               float2 delta = pos - (neighborCell + featurePoint);
+                                               float dist = length(delta);
+                                               
+                                               if (dist < minDist) {
+                                                   secondMinDist = minDist;
+                                                   minDist = dist;
+                                               } else if (dist < secondMinDist) {
+                                                   secondMinDist = dist;
+                                               }
+                                           }
+                                       }
+                                       return float2(minDist, secondMinDist);
+                                   }
+                                   
+                                   half4 main(float2 uv) {
+                                       float noiseSum = 0.0;
+                                       float amplitude = 1.0;
+                                       float amplitudeSum = 0.0;
+                                   
+                                       for (int octave = 0; octave < MAX_OCTAVES; octave++) {
+                                           if (octave >= iOctaves) break;
+                                   
+                                           float freq = iFrequency * exp2(float(octave));
+                                           float2 samplePos = uv * freq;
+                                           
+                                           float dist = 0.0;
+                                           float2 distances = getVoronoiDistances(samplePos, iSeed + float(octave) * FEATURE_SEED_SCALE);
+                                           float f1 = distances.x;
+                                           float f2 = distances.y;
+                                           
+                                           if (iFeature == 0) {
+                                               dist = f1;
+                                           }
+                                           else if (iFeature == 1) {
+                                               dist = f2;
+                                           }
+                                           else if (iFeature == 2) {
+                                               dist = f2 - f1;
+                                           }
+                                   
+                                           noiseSum += dist * amplitude;
+                                           amplitudeSum += amplitude;
+                                           amplitude *= 0.5;
+                                       }
+                                   
+                                       return half4(noiseSum / amplitudeSum);
+                                   }
+                                   """;
+        
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iSeed", new Uniform("iSeed", seed));
+        uniforms.Add("iFrequency", new Uniform("iFrequency", frequency));
+        uniforms.Add("iOctaves", new Uniform("iOctaves", octaves));
+        uniforms.Add("iRandomness", new Uniform("iRandomness", randomness));
+        uniforms.Add("iFeature", new Uniform("iFeature", feature));
+        uniforms.Add("iAngleOffset", new Uniform("iAngleOffset", angleOffset));
+
+        if (voronoiShader == null)
+        {
+            voronoiShader = Shader.Create(voronoiShaderCode, uniforms, out _);
+        }
+        else
+        {
+            voronoiShader = voronoiShader.WithUpdatedUniforms(uniforms);
+        }
+        
+        return voronoiShader;
+    }
+
     public override Node CreateCopy() => new NoiseNode();
 }
 
 public enum NoiseType
 {
     TurbulencePerlin,
-    FractalPerlin
+    FractalPerlin,
+    Voronoi
+}
+
+public enum VoronoiFeature
+{
+    F1 = 0, // Distance to the closest feature point
+    F2 = 1, // Distance to the second-closest feature point
+    F2MinusF1 = 2
 }

+ 7 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -946,6 +946,13 @@
   "RAW_COLOR_SAMPLE_MODE": "Raw",
   "FRACTAL_PERLIN_NOISE_TYPE": "Perlin",
   "TURBULENCE_PERLIN_NOISE_TYPE": "Turbulence",
+  "VORONOI_NOISE_TYPE": "Voronoi",
+  "VORONOI_FEATURE": "Feature",
+  "F1_VORONOI_FEATURE": "F1",
+  "F2_VORONOI_FEATURE": "F2",
+  "F2_MINUS_F1_VORONOI_FEATURE": "F2-F1",
+  "RANDOMNESS": "Randomness",
+  "ANGLE_OFFSET": "Angle Offset",
   "INHERIT_COLOR_SPACE_TYPE": "Inherit",
   "SRGB_COLOR_SPACE_TYPE": "sRGB",
   "LINEAR_SRGB_COLOR_SPACE_TYPE": "Linear sRGB",

+ 29 - 1
src/PixiEditor/ViewModels/Document/Nodes/NoiseNodeViewModel.cs

@@ -1,8 +1,36 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 [NodeViewModel("NOISE_NODE", "IMAGE", PixiPerfectIcons.Noise)]
-internal class NoiseNodeViewModel : NodeViewModel<NoiseNode>;
+internal class NoiseNodeViewModel : NodeViewModel<NoiseNode>
+{
+    private GenericEnumPropertyViewModel Type { get; set; }
+    private GenericEnumPropertyViewModel VoronoiFeature { get; set; }
+    private NodePropertyViewModel Randomness { get; set; }
+    private NodePropertyViewModel AngleOffset { get; set; }
+
+    public override void OnInitialized()
+    {
+        Type = FindInputProperty("NoiseType") as GenericEnumPropertyViewModel;
+        VoronoiFeature = FindInputProperty("VoronoiFeature") as GenericEnumPropertyViewModel;
+        Randomness = FindInputProperty("Randomness");
+        AngleOffset = FindInputProperty("AngleOffset");
+
+        Type.ValueChanged += (_, _) => UpdateInputVisibility();
+        UpdateInputVisibility();
+    }
+
+    private void UpdateInputVisibility()
+    {
+        if (Type.Value is not NoiseType type)
+            return;
+        
+        Randomness.IsVisible = type == NoiseType.Voronoi;
+        VoronoiFeature.IsVisible = type == NoiseType.Voronoi;
+        AngleOffset.IsVisible = type == NoiseType.Voronoi;
+    }
+}