chrisdill / raylib-cs

C# bindings for raylib, a simple and easy-to-use library to learn videogames programming

Home Page:http://www.raylib.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Improve utils for mesh generation and processing

chrisdill opened this issue · comments

New issue continuing #35. I want it to be easier to build and process meshes as that is one of the few areas that is a pain to manage with unsafe code even if you are careful.

Initially this starts with adding span wrappers/checks to help change the existing mesh but I would like to extend that to make the initial mesh creation/building process easier.

Since .NET 6 we have the static class NativeMemory:
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.nativememory?view=net-6.0

NativeMemory.AllocZeroed allocates zeroed memory on the heap (like calloc in C does).

Here is an example:

using System.Numerics;
using System.Runtime.InteropServices;
using Raylib_cs;

Raylib.InitWindow(800, 450, "Test");

var mesh = new Mesh() { triangleCount = 1, vertexCount = 3 };

Span<float> vertices, normals, texcoords;

unsafe
{
    mesh.vertices = (float*)NativeMemory.AllocZeroed(9, sizeof(float));
    mesh.normals = (float*)NativeMemory.AllocZeroed(9, sizeof(float));
    mesh.texcoords = (float*)NativeMemory.AllocZeroed(6, sizeof(float));

    vertices = new Span<float>(mesh.vertices, 9);
    normals = new Span<float>(mesh.normals, 9);
    texcoords = new Span<float>(mesh.texcoords, 6);
}

vertices[3] = 1;
vertices[5] = 2;
vertices[6] = 2;

normals[1] = 1;
normals[4] = 1;
normals[7] = 1;

texcoords[2] = .5f;
texcoords[3] = 1;
texcoords[4] = 1;

Raylib.UploadMesh(ref mesh, false);
var model = Raylib.LoadModelFromMesh(mesh);

var camera = new Camera3D() { position = new(5, 5, 5), up = new() { Y = 1 }, fovy = 45 };

while (!Raylib.WindowShouldClose())
{
    Raylib.UpdateCamera(ref camera, CameraMode.CAMERA_ORBITAL);

    Raylib.BeginDrawing();

    Raylib.ClearBackground(Color.WHITE);
    Raylib.BeginMode3D(camera);
    Raylib.DrawModel(model, Vector3.Zero, 1, Color.RED);
    Raylib.DrawGrid(10, 1);
    Raylib.EndMode3D();

    Raylib.EndDrawing();
}

Raylib.UnloadModel(model);
Raylib.CloseWindow();

Screenshot_20230804_185820

We don't need to call NativeMemory.Free in the end, because unload Raylib.UnloadModel frees the mesh as well.

Taking @JupiterRider's idea you could add a constructor that looks something like this:

/// <summary>Generate mesh</summary>
public Mesh(int triangleCount, int vertexCount)
{
    this.triangleCount = triangleCount;
    this.vertexCount = vertexCount;
    vertices = (float*)NativeMemory.AllocZeroed((nuint)(3 * this.vertexCount), sizeof(float));
}

Also add extention method's to utils:

/// <summary>Access mesh triangles</summary>
public static Span<T> TrianglesAs<T>(this Mesh mesh) where T : unmanaged
{
    return new(mesh.vertices, 3 * mesh.vertexCount * sizeof(float) / sizeof(T));
}

Utilizing these would look something like this:

using Raylib_cs;
using System.Numerics;

Raylib.InitWindow(1280, 960, "Triangle");
Raylib.SetTargetFPS(60);

Camera3D camera = new(Vector3.One, Vector3.Zero, Vector3.UnitY, 90f, CameraProjection.CAMERA_PERSPECTIVE);
Mesh mesh = new(1, 3);
Span<Vector3> vertices = mesh.TrianglesAs<Vector3>();
vertices[0] = Vector3.UnitZ;
vertices[1] = Vector3.UnitX;
vertices[2] = Vector3.Zero;
Raylib.UploadMesh(ref mesh, false);
Model model = Raylib.LoadModelFromMesh(mesh);

while (!Raylib.WindowShouldClose())
{
    Raylib.UpdateCamera(ref camera, CameraMode.CAMERA_ORBITAL);
    Raylib.BeginDrawing();
    Raylib.ClearBackground(Color.BLACK);
    Raylib.BeginMode3D(camera);
    Raylib.DrawModel(model, Vector3.Zero, 1f, Color.PINK);
    Raylib.DrawGrid(2, 1);
    Raylib.EndMode3D();
    Raylib.EndDrawing();
}
Raylib.UnloadModel(model);
Raylib.CloseWindow();

@JupiterRider Unsure if it is better to use MemAlloc, MemFree via Raylib instead. Raylib can be recompiled to change how allocation works internally so using the built in versions can make sure alloc/free calls work with each other correctly.

@n77y Interesting idea! How could this be applied to other mesh fields?

@ChrisDill Doesn't matter. They both use calloc in stdlib.h anyways:
https://github.com/raysan5/raylib/blob/master/src/utils.c#L163
https://github.com/raysan5/raylib/blob/master/src/raylib.h#L124

https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeMemory.Unix.cs,149
https://source.dot.net/#System.Private.CoreLib/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MemAlloc.cs,20
https://github.com/dotnet/runtime/blob/main/src/native/libs/System.Native/pal_memory.c#L63

I don't think its a good Idea for maintainability compiling our own version of raylib or writing to many layers of abstraction.

Raylib-cs should better be a binding only. For everything beyond we could create an additional project called Raylib-cs.Extras or something.

@ChrisDill, @JupiterRider, would it be possible and sensible to add an syntax suger allocator like this?

/// <summary>C++ style memory allocator</summary>
public static T* New<T>(int count) where T : unmanaged
{
    return (T*)MemAlloc(count * sizeof(T));
}

or this:

/// <summary>C++ style memory allocator</summary>
public static T* New<T>(int count) where T : unmanaged
{
    return (T*)NativeMemory.AllocZeroed((nuint)count, (nuint)sizeof(T));
}

The method name can be changed if its not disirable. This could help with changing:

mesh.vertices = (float*)NativeMemory.AllocZeroed((nuint)(3 * mesh.vertexCount), sizeof(float));

To instead look like this:

mesh.vertices = New<float>(3 * mesh.vertexCount);

We could simplify the constructor to:

public Mesh(int triangleCount, int vertexCount)
{
    this.triangleCount = triangleCount;
    this.vertexCount = vertexCount;
}

And have all of our allocations be in seperate extention methods (utils) so we can pick and choose which ones we are going to use when generating custom meshes:

/// <summary>Allocate mesh vertices</summary>
public static void AllocVertices(ref this Mesh mesh)
{
    mesh.vertices = New<float>(3 * mesh.vertexCount);
}

/// <summary>Allocate mesh texcoords</summary>
public static void AllocTexcoords(ref this Mesh mesh)
{
    mesh.texcoords = New<float>(2 * mesh.vertexCount);
}

/// <summary>Allocate mesh colors</summary>
public static void AllocColors(ref this Mesh mesh)
{
    mesh.colors = New<byte>(4 * mesh.vertexCount);
}

/// <summary>Allocate mesh indices</summary>
public static void AllocIndices(ref this Mesh mesh)
{
    mesh.indices = New<ushort>(3 * mesh.triangleCount);
}

All of the accessors can be more utils:

/// <summary>Access mesh vertices</summary>
public static Span<T> VerticesAs<T>(this Mesh mesh) where T : unmanaged
{
    return new(mesh.vertices, 3 * mesh.vertexCount * sizeof(float) / sizeof(T));
}

/// <summary>Access mesh texcoords</summary>
public static Span<T> TexcoordsAs<T>(this Mesh mesh) where T : unmanaged
{
    return new(mesh.texcoords, 2 * mesh.vertexCount * sizeof(float) / sizeof(T));
}

/// <summary>Access mesh colors</summary>
public static Span<T> ColorsAs<T>(this Mesh mesh) where T : unmanaged
{
    return new(mesh.colors, 4 * mesh.vertexCount * sizeof(byte) / sizeof(T));
}

/// <summary>Access mesh indices</summary>
public static Span<T> IndicesAs<T>(this Mesh mesh) where T : unmanaged
{
    return new(mesh.indices, 3 * mesh.triangleCount * sizeof(ushort) / sizeof(T));
}

Example of usage:

using Raylib_cs;
using System.Numerics;

Raylib.InitWindow(1280, 960, "Hello World!");
Raylib.SetTargetFPS(60);

Camera3D camera = new(Vector3.One * 1.5f, Vector3.Zero, Vector3.UnitY, 60f, CameraProjection.CAMERA_PERSPECTIVE);
Mesh tetrahedron = new(4, 4);
tetrahedron.AllocVertices();
tetrahedron.AllocTexcoords();
tetrahedron.AllocColors();
tetrahedron.AllocIndices();
Span<Vector3> vertices = tetrahedron.VerticesAs<Vector3>();
Span<Vector2> texcoords = tetrahedron.TexcoordsAs<Vector2>();
Span<Color> colors = tetrahedron.ColorsAs<Color>();
Span<ushort> indices = tetrahedron.IndicesAs<ushort>();

// Coordinates for a regular tetrahedron (wikipedia)
vertices[0] = new(float.Sqrt(8f / 9f), 0f, -1f / 3f);
vertices[1] = new(-float.Sqrt(2f / 9f), float.Sqrt(2f / 3f), -1f / 3f);
vertices[2] = new(-float.Sqrt(2f / 9f), -float.Sqrt(2f / 3f), -1f / 3f);
vertices[3] = Vector3.UnitZ;

texcoords[0] = Vector2.Zero;
texcoords[1] = Vector2.UnitX;
texcoords[2] = Vector2.UnitY;
texcoords[3] = Vector2.One;

colors[0] = Color.PINK;
colors[1] = Color.LIME;
colors[2] = Color.SKYBLUE;
colors[3] = Color.VIOLET;

indices[0] = 2;
indices[1] = 1;
indices[2] = 0;

indices[3] = 1;
indices[4] = 3;
indices[5] = 0;

indices[6] = 2;
indices[7] = 3;
indices[8] = 1;

indices[9] = 0;
indices[10] = 3;
indices[11] = 2;

float rotationAngle = 0f;
Raylib.UploadMesh(ref tetrahedron, false);
Model model = Raylib.LoadModelFromMesh(tetrahedron);

Image image = Raylib.GenImagePerlinNoise(16, 16, 0, 0, 1000f);
Raylib.ImageBlurGaussian(ref image, 2);
Raylib.ImageColorBrightness(ref image, 100);
Raylib.ImageDither(ref image, 4, 4, 4, 4);
Texture2D texture = Raylib.LoadTextureFromImage(image);
Raylib.UnloadImage(image);

Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.MATERIAL_MAP_DIFFUSE, ref texture);

while (!Raylib.WindowShouldClose())
{
    Raylib.UpdateCamera(ref camera, CameraMode.CAMERA_ORBITAL);
    rotationAngle = Raymath.Wrap(rotationAngle += 1f, 0f, 360f);

    Raylib.BeginDrawing();
    Raylib.ClearBackground(Color.BLACK);
    Raylib.BeginMode3D(camera);
    Raylib.DrawModelEx(model, Vector3.Zero, Vector3.UnitX, rotationAngle, Vector3.One, Color.WHITE);
    Raylib.EndMode3D();
    Raylib.EndDrawing();
}
Raylib.UnloadModel(model);
Raylib.UnloadTexture(texture);
Raylib.CloseWindow();

Hello World!

Nice work @n77y!
Now make a pull request for that and the pear is peeled :D

U can also apply an example for that here:
https://github.com/ChrisDill/Raylib-cs-Examples

Side notice:
NativeMemory requires at least .Net 6.

@n77y Trying out the idea further using your example and I changed my mind on it needing to be separate from the Mesh struct. Updated the pr with a few more suggestions. Once done I will merge it in and add your example for it.

@n77y @JupiterRider Also note that examples have been merged back into the main repo so further development on examples will be done here.

@JupiterRider @n77y The MeshDemo example has now been added and the utils seem to be working as expected. Plan to experiment more with this and maybe apply the same approach to other resource types in the future.

Thanks for all the help on this issue!