Auburn / FastNoiseLite

Fast Portable Noise Library - C# C++ C Java HLSL GLSL JavaScript Rust Go
http://auburn.github.io/FastNoiseLite/
MIT License
2.73k stars 320 forks source link

Added Go implementation #116

Closed ForeverZer0 closed 1 year ago

ForeverZer0 commented 1 year ago

This PR adds Go support to the library. It is based on the C/C# versions, with no significant changes other than employing Go naming conventions and making the public API idiomatic for Go.

Auburn commented 1 year ago

Thanks for working on this, I don't really know anything about Go but I had a scan through the code and it looks like it uses some form of templating to support float32 and 64?

The only thing I would change is using float64 for the constant arrays. I'd probably make them all float32, they don't need the precision and the smaller size is more cache friendly.

Also the FastNoise ASCII art in the header looks a bit wonky 😄

Thanks!

ForeverZer0 commented 1 year ago

it looks like it uses some form of templating to support float32 and 64?

Exactly, it is the Go equivalent of generics. If the C# implementation supported generics, the following two examples would be the idiomatic equivalents for each language:

noise := fastnoise.New[float32]()
var noise = new FastNoise<float>()

The only thing I would change is using float64 for the constant arrays. I'd probably make them all float32, they don't need the precision and the smaller size is more cache friendly.

Will do. I was torn on which to use, and actually went back and forth a few times. I can't even remember what led me to finally settle on 64-bit, but disregarding caching was an oversight on my part of, and a good reason to use 32-bit.

Also the FastNoise ASCII art in the header looks a bit wonk

Ugh, auto-formatters are a double-edged sword...

One question: should I include the tests in the repository? I omitted them for the simple reason that I didn't see any others have them included. They also rely upon a small C# project to generate the "control" values to test against, so not sure how helpful they would be to the average Go consumer of the library, who may not also be familiar with C# tooling.

Auburn commented 1 year ago

Sorry for the long delay, this fell off my radar. Thanks for making those changes!

With regards to the tests, I've looked into it before but I couldn't settle on a good method of doing it that would work for all languages. If possible could you put the test setup in another repo and link to it in the go readme, I'd be interested to see how you set it up.

ForeverZer0 commented 1 year ago

Sure, I will clean it up a bit to a be more universal and make a repo for it; perhaps others might be able to make use of it as well. I have just been changing the configured values (type, seed, etc) in the editor and re-running when needed, but it would be trivial to add some proper command-line args, and output to JSON or some other common format that most languages have built-in support for.

Auburn commented 1 year ago

I wouldn't worry too much about cleaning it up, I'm more just interested to see your approach

ForeverZer0 commented 1 year ago

I can make a repo if it helps, but it is really just a few methods that I can show in its entirety here.

On the C# side, I simply generate some values and output them to a file as rudimentary JSON, one for 2D noise, and another for 3D noise.

Program.cs

using System.Text.Json;

const int numSamples = 32;

var rand = new Random();
var noise = new FastNoiseLite();

using var file2D = File.Open("noise2D.json", FileMode.Create, FileAccess.Write);
using var file3D = File.Open("noise3D.json", FileMode.Create, FileAccess.Write);

var opts = new JsonWriterOptions { Indented = true };
var writer2D = new Utf8JsonWriter(file2D, opts);
var writer3D = new Utf8JsonWriter(file3D, opts);

writer2D.WriteStartObject();
writer3D.WriteStartObject();

foreach (var type in Enum.GetValues<FastNoiseLite.NoiseType>())
{
    noise.SetNoiseType(type);
    writer2D.WriteStartArray(Enum.GetName(type)!);
    writer3D.WriteStartArray(Enum.GetName(type)!);

    int x, y, z;
    for (var i = 0; i < numSamples; i++)
    {
        writer2D.WriteStartObject();
        writer3D.WriteStartObject();

        x = rand.Next(int.MinValue, int.MaxValue);
        y = rand.Next(int.MinValue, int.MaxValue);
        z = rand.Next(int.MinValue, int.MaxValue);

        writer2D.WriteNumber("x", x);
        writer2D.WriteNumber("y", y);

        writer3D.WriteNumber("x", x);
        writer3D.WriteNumber("y", y);
        writer3D.WriteNumber("z", z);

        writer2D.WriteNumber("value", noise.GetNoise(x, y));
        writer3D.WriteNumber("value", noise.GetNoise(x, y, z));

        writer2D.WriteEndObject();
        writer3D.WriteEndObject();
    }

    writer2D.WriteEndArray();
    writer3D.WriteEndArray();
}

writer2D.WriteEndObject();
writer3D.WriteEndObject();

writer2D.Flush();
writer3D.Flush();

Then in a Go test, deserialize those values and test them against the actual output from the Go implementation. You can probably figure whats going on in the Go code without being familiar with the language, but just ask if you want any clarification on something.

noise_test.go

package fastnoise

import (
    "encoding/json"
    "os"
    "testing"
)

type NoiseTest2D struct {
    X     int     `json:"x"`
    Y     int     `json:"y"`
    Value float32 `json:"value"`
}

type NoiseTest3D struct {
    NoiseTest2D
    Z     int     `json:"z"`
}

func unmarshal[T NoiseTest2D | NoiseTest3D](t *testing.T, path string) map[string][]T {
    result := make(map[string][]T)
    if data, err := os.ReadFile(path); err != nil {
        t.Fatal(err)
    } else if err = json.Unmarshal(data, &result); err != nil {
        t.Fatal(err)
    }
    return result
}

func setType(t *testing.T, noise *State[float32], name string) {
    switch name {
    case "OpenSimplex2":
        noise.NoiseType(OpenSimplex2)
    case "OpenSimplex2S":
        noise.NoiseType(OpenSimplex2S)
    case "Cellular":
        noise.NoiseType(Cellular)
    case "Perlin":
        noise.NoiseType(Perlin)
    case "ValueCubic":
        noise.NoiseType(ValueCubic)
    case "Value":
        noise.NoiseType(Value)
    default:
        t.Fatal("unknown noise type")
    }
}

func assertValue(t *testing.T, actual, expected float32) {
    const epsilon = 0.00001
    if fastAbs(actual-expected) > epsilon {
        t.Errorf("Expected: %f, Actual: %f", expected, actual)
    }
}

func TestRanges(t *testing.T) {
    expected2D := unmarshal[NoiseTest2D](t, "/path/to/noise2D.json")
    expected3D := unmarshal[NoiseTest3D](t, "/path/to/noise3D.json")

    noise := New[float32]()
    for name, values := range expected2D {
        setType(t, noise, name)
        for _, value := range values {
            assertValue(t, noise.Noise2D(value.X, value.Y), value.Value)
        }
    }

    for name, values := range expected3D {
        setType(t, noise, name)
        for _, value := range values {
            assertValue(t, noise.Noise3D(value.X, value.Y, value.Z), value.Value)
        }
    }
}
Auburn commented 1 year ago

Thanks for your work on this and posting the test code!