From 939bf95a81557dde754bd4c07c1a4bd607e7bdc0 Mon Sep 17 00:00:00 2001 From: itycodes Date: Mon, 19 Aug 2024 18:58:38 +0200 Subject: [PATCH] Initial commit --- Noise/PerlinNoise.cs | 224 +++++++++++++++++++++++++++++++++++++++++++ PerlinNoise.csproj | 10 ++ Program.cs | 42 ++++++++ 3 files changed, 276 insertions(+) create mode 100644 Noise/PerlinNoise.cs create mode 100644 PerlinNoise.csproj create mode 100644 Program.cs diff --git a/Noise/PerlinNoise.cs b/Noise/PerlinNoise.cs new file mode 100644 index 0000000..3b6492d --- /dev/null +++ b/Noise/PerlinNoise.cs @@ -0,0 +1,224 @@ +namespace Noise; + +using System; + +// Used for the hashing example that's commented out +//using System.Text; +//using System.Security.Cryptography; + +// Based on https://adrianb.io/2014/08/09/perlinnoise.html + +public class PerlinNoise { + // Hash lookup table as defined by Ken Perlin. This is a randomly + // arranged array of all numbers from 0-255 inclusive. + // Works as the seed. + private static readonly int[] permutation = { 151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 + }; + + // Later on, in the hash function, access similar to p[p[_]+255+1] is performed + // p contains two copies of permutation, len 512 in total. + // The max number p[n] can return is 255, and 255+255+1 is 511, + // which is the largest index in the 512 long array. + private static readonly int[] p; + + // Controls intentional noise repeating + // Do note the noise will repeat each 256 anyway, due to the hashing function + // (to be exact, the preprocessing of the input for the hashing function) + private static readonly int repeat = 0; + + static PerlinNoise() { + p = new int[512]; + for(int i=0;i<512;i++) { + p[i] = permutation[i%256]; + } + } + + // TODO figure out how the vectors were chosen + // TODO figure out whether there is bias due to a few vectors being repeated + // TODO experiment with other vectors + // + // Choose a random vector from + // + // The 12 vectors + // (1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0), + // (1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1), + // (0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1) + // + // Then calculate the dot product. + // Take as an example the first one, (1,1,0) + // The dot product would thus be 1x + 1y + 0z. Thus, x + y + // + // The equations below are just simplified dot products. + // + // We have 12 vectors but 16 cases, so some of the vectors repeat. + // + // The vectors represent the middles of edges of a cube. + // A cube has 12 edges, so 12 vectors. + public static double grad(int hash, double x, double y, double z) { + switch(hash & 0xF) + { + // Unique + case 0x0: return x + y; // 1, 1, 0 A + case 0x1: return -x + y; // -1, 1, 0 B + case 0x2: return -y + z; // 0, -1, 1 C + case 0x3: return -x - y; // -1, -1, 0 + case 0x4: return x + z; // 1, 0, 1 + case 0x5: return -x + z; // -1, 0, 1 + case 0x6: return x - z; // 1, 0, -1 + case 0x7: return -x - z; // -1, 0, -1 + case 0x8: return y + z; // 0, 1, 1 + case 0x9: return x - y; // 1, -1, 0 + case 0xA: return y - z; // 0, 1, -1 + case 0xB: return -y - z; // 0, -1, -1 + // Repetitions + case 0xC: return y + x; // 1, 1, 0 A + case 0xD: return -y + z; // 0, -1, 1 C + case 0xE: return y - x; // -1, 1, 0 B + case 0xF: return -y - z; // 0, -1, 1 C + default: return 0; // never happens + } + } + + // TODO Unsure of other properties it needs to satisfy + // TODO Unsure why it's required, but without it, + // edges between cells/cubes become visible + // TODO There are indeed other properties. + // 1 / (1 + Math.Exp(-12 * (x - 0.5))) + // satisfies the 3 defined ones, but does not give good results. + // + // A magical easing function + // + // Intended range is 0..1 -> 0..1 + // It's a simple sigmoid that satisfies + // f(0) = 0 + // f(1) = 1 + // + // Fade function as defined by Ken Perlin. This eases coordinate values + // so that they will ease towards integral values. This ends up smoothing + // the final output. + public static double fade(double t) { + // 6t^5 - 15t^4 + 10t^3 + return t * t * t * (t * (t * 6 - 15) + 10); + } + + // Incrementing function that wraps around based on `repeat` + public static int inc(int num) { + num++; + if (repeat > 0) num %= repeat; + + return num; + } + + // Linear Interpolate + public static double lerp(double a, double b, double x) { + return a + x * (b - a); + } + + // Hash function + // Any hash function can be utilized (for example sha256 works), though this one is fast and simple. + // This hash function performs 3 random "jumps" in the permutations array, + // seeded & offset by the coordinates and offset by the number it landed on. + private static int hash(int xi, int yi, int zi) { + return p[p[p[xi]+yi]+zi]; + } + + // Here is a Sha256 replacement, to show that the permutations are literally just a prng with nothing special +//private static int hash(int xi, int yi, int zi) { +// string inputString = $"{xi},{yi},{zi}"; +// byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(inputString)); +// return BitConverter.ToInt32(hashBytes, 0); +//} + + // TODO figure out why n.0 always returns 0.5, and n.5 always returns numbers like + // 0.75, 0.5 or 0.375. Overall, the noise seems to break down when it is not "zoomed in" enough. + public static double perlin(double x, double y, double z) { + // TODO this is probably useless + // If we have any repeat on, change the coordinates to their "local" repetitions + if(repeat > 0) { + x = x%repeat; + y = y%repeat; + z = z%repeat; + } + + // Calculate the "unit cube" that the point asked will be located in + // The left bound is ( |_x_|,|_y_|,|_z_| ) and the right bound is that + // plus 1. Next we calculate the location (from 0.0 to 1.0) in that cube. + + // This is just a singular corner of the cube. + // Modulos with 256 - this causes integers to repeat every 256. + // Thus, 0.25 = 256.25, 3.6 = 259.6... + // And yes, this causes a visual repeating pattern. + // The edge is not visible under normal conditions, though. + int xi = (int)x & 255; + int yi = (int)y & 255; + int zi = (int)z & 255; + + // 0..1 coordinates within the cube + double xf = x-(int)x; + double yf = y-(int)y; + double zf = z-(int)z; + + // TODO figure out why the lerping coordinates are eased rather than anything else. + // Coordinates used for the lerping + double u = fade(xf); + double v = fade(yf); + double w = fade(zf); + + // Magic hash + // Compute 8 hashes for each vertex of the cube, from each coordinate + int aaa, aba, aab, abb, baa, bba, bab, bbb; + aaa = hash( xi, yi, zi); + aba = hash( xi, inc(yi), zi); + aab = hash( xi, yi, inc(zi)); + abb = hash( xi, inc(yi), inc(zi)); + baa = hash(inc(xi), yi, zi); + bba = hash(inc(xi), inc(yi), zi); + bab = hash(inc(xi), yi, inc(zi)); + bbb = hash(inc(xi), inc(yi), inc(zi)); + + // TODO figure out how the -1s work + // TODO the u, v, and w being used in this order matters. Why? + // TODO figure out why lerping, and this particular way works + // + // This is then used as the seed for the grad() function that uses it to + // pick a random edge vector and compute the dot product between it and the input vector, + // offset into 8 corners, where one is the origin corner of the cube + // (This works fine because the origin corner of the cube is also the origin of the vectors) + // Results in vectors pointing from the corners to the input point specified by the input vector. + // + // The gradient function calculates the dot product between a pseudorandom + // gradient vector and the vector from the input coordinate to the 8 + // surrounding points in its unit cube. + // This is all then lerped together as a sort of weighted average based on the faded (u,v,w) + // values we made earlier. + double x1, x2, x3, x4, y1, y2; + x1 = lerp(grad(aaa, xf , yf , zf ), + grad(baa, xf-1, yf , zf ), u); + x2 = lerp(grad(aba, xf , yf-1, zf ), + grad(bba, xf-1, yf-1, zf ), u); + y1 = lerp(x1, x2, v); + + x3 = lerp(grad(aab, xf , yf , zf-1), + grad(bab, xf-1, yf , zf-1), u); + x4 = lerp(grad(abb, xf , yf-1, zf-1), + grad(bbb, xf-1, yf-1, zf-1), u); + y2 = lerp(x3, x4, v); + + // For convenience we bind the result to 0 - 1 (theoretical min/max before is [-1, 1]) + double res = (lerp (y1, y2, w)+1)/2; + + return res; + } +} diff --git a/PerlinNoise.csproj b/PerlinNoise.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/PerlinNoise.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c56d007 --- /dev/null +++ b/Program.cs @@ -0,0 +1,42 @@ +using Noise; +using System.IO; + +long width = 1024; +long height = 1024; +double scaleX = 128.0; +double scaleY = 128.0; + +byte[] resBytes = new byte[width*height*3]; + +// Console.WriteLine(PerlinNoise.perlin(3.25 , 0, 0)); +// Console.WriteLine(PerlinNoise.perlin(3.25+256, 0, 0)); +// Console.WriteLine(PerlinNoise.perlin(3.25+512, 0, 0)); + +for(int i = 0; i