You also aren't actually generating Perlin noise. The Hugo Elias article gets that wrong.
/pedanticdickweed
Random isn't designed to be used this way. If I remember correctly, it uses Knuth's subtractive generator under the hood, and I believe the subtractive generator uses a state table that is 56 entries long, which is iterated and the values set every time a seed is set. A seed is set twice every time InterpolateNoise is called, so that is a
lot of redundant state setting going on in your inner loop. Random is fine to use in an application where you seed it once then just generate a sequence of random numbers; Perlin noise doesn't work that way, however.
For performance reasons, you should implement some sort of hash or folding algorithm instead. The
reference implementation of Perlin noise performs hashing by using a table to "fold" bytes. The table is static and the random generation is stateless, meaning it is designed to not have an explicit seed method. Other possibilities include algorithms such as the
FNV hash. These hash algorithms generate a random-seeming output based on an input, without storing any internal state or iterating any seed table.
Note that the noise that is implemented in the Hugo Elias article is commonly called "value noise", given that explicit values are generated at lattice points. It has the failing that it generates a
lot of lattice based artifacts. Real Perlin noise is implemented as a set of wavelet functions centered upon lattice points; the weighted sum of wavelets has the effect of moving the highs and lows off of the lattice grid, serving to significantly reduce the occurrence of grid-based artifacts.
I highly recommend the paper
"Simplex noise demystified"; it is mainly about Perlin's later algorithm, simplex noise, but it also includes a fine explanation of standard Perlin noise.