5
\$\begingroup\$

In order to improve my c# (which is fairly basic at best) I decided to try to serialize a simple mesh to a file using the JSON.net library.

Simple mesh in this case means an array of vertices and a number of arrays of triangles constructed from 3 vertices each. No materials or animations or anything fancy.

Vertices have a position, a normal, a tangent, 4 sets of texture coordinates and a color. Everything but the position is optional though.

As a starting point, I use the mesh class of the Unity3d. I convert this to my own mesh class and pass this into the JSON serializer. Deserializing works the same, just in the opposite direction.

I'm not too fussed about the Unity specific parts for now, I've put it in to make testing more convenient. I am planning to use JSON with a game project in Unity eventually but the program posted here is so far purely an educational thing.

using UnityEngine;
using Newtonsoft.Json;
using System;
using System.IO;

//naively represented geometry - nrg :)
[JsonConverter(typeof(VertexConverter))]
public class nrgVertex
{
    private float[] _d;

    public nrgVertex() { _d = new float[22]; }
    public nrgVertex (float[] data) : this()
    {
        for (int i = 0; i < data.Length && i < _d.Length; ++i) _d[i] = data[i];        
    }

    //expose data as array
    public float[] data { get { return (float[])_d.Clone(); } }

    //expose data as array without trailing zeros
    public float[] shortData {
        get
        {
            var temp = (float[])_d.Clone();
            Array.Resize(ref temp, 1 + Array.FindLastIndex(temp, v => ((v < -0.000001) || (v > 0.000001))));
            return temp;
        }
    }

    //expose data component wise
    //position xyz
    public float x { get { return _d[0]; } }
    public float y { get { return _d[1]; } }
    public float z { get { return _d[2]; } }

    //normal xyz
    public float nx { get { return _d[3]; } }
    public float ny { get { return _d[4]; } }
    public float nz { get { return _d[5]; } }

    //tangent xyz
    public float tx { get { return _d[6]; } }
    public float ty { get { return _d[7]; } }
    public float tz { get { return _d[8]; } }
    public float tw { get { return _d[9]; } }

    //uv1
    public float u { get { return _d[10]; } }
    public float v { get { return _d[11]; } }

    //uv2
    public float u2 { get { return _d[12]; } }
    public float v2 { get { return _d[13]; } }

    //uv3
    public float u3 { get { return _d[14]; } }
    public float v3 { get { return _d[15]; } }

    //uv4
    public float u4 { get { return _d[16]; } }
    public float v4 { get { return _d[17]; } }

    //vertex color rgba
    public float cr { get { return _d[18]; } }
    public float cg { get { return _d[19]; } }
    public float cb { get { return _d[20]; } }
    public float ca { get { return _d[21]; } }
}

class VertexConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(nrgVertex);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var d = new float[22];
        int i = 0;

        if (reader.TokenType == JsonToken.StartArray) {
            while (reader.Read() && (reader.TokenType != JsonToken.EndArray) && (i < d.Length))
            {
                d[i] = (float)(double)reader.Value;
                ++i;
            }
        }

        return new nrgVertex(d);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var v = (nrgVertex)value;

        writer.WriteRawValue(JsonConvert.SerializeObject(v.shortData, Formatting.None));
    }
}

[JsonConverter(typeof(FaceConverter))]
public class nrgFace
{
    private int[] _d;

    public nrgFace() { _d = new int[3]; }
    public nrgFace(int[] data) : this()
    {
        for (int i = 0; i < data.Length && i < _d.Length; ++i) _d[i] = data[i]; 
    }

    public int[] data { get { return (int[])_d.Clone(); } }

    public int a { get { return _d[0]; } }
    public int b { get { return _d[1]; } }
    public int c { get { return _d[2]; } }
}

class FaceConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(nrgFace);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var t = new int[3];
            int i = 0;

            if (reader.TokenType == JsonToken.StartArray) {
                while (reader.Read() && (reader.TokenType != JsonToken.EndArray) && (i < t.Length)) {
                    t[i] = (int)(Int64)reader.Value;
                    ++i;
                }
            }

            return new nrgFace(t);
        }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var t = (nrgFace)value;

        writer.WriteRawValue(JsonConvert.SerializeObject(t.data, Formatting.None));
    }
}

public class nrgMesh
{
    public nrgVertex[] Vertices;
    public nrgFace[][] Faces;

    static public nrgMesh ConvertUnityToNrgMesh(Mesh source)
    {
        nrgMesh nrg = new nrgMesh();
        int vertexCount = source.vertexCount;

        nrg.Vertices = new nrgVertex[vertexCount];
        for (int i = 0; i < vertexCount; ++i) {
            var d = new float[22] {
                (source.vertices != null && i < source.vertices.Length) ? source.vertices[i].x : 0.0f,
                (source.vertices != null && i < source.vertices.Length) ? source.vertices[i].y : 0.0f,
                (source.vertices != null && i < source.vertices.Length) ? source.vertices[i].z : 0.0f,
                (source.normals != null  && i < source.normals.Length)  ? source.normals[i].x  : 0.0f,
                (source.normals != null  && i < source.normals.Length)  ? source.normals[i].y  : 0.0f,
                (source.normals != null  && i < source.normals.Length)  ? source.normals[i].z  : 0.0f,
                (source.tangents != null && i < source.tangents.Length) ? source.tangents[i].x : 0.0f,
                (source.tangents != null && i < source.tangents.Length) ? source.tangents[i].y : 0.0f,
                (source.tangents != null && i < source.tangents.Length) ? source.tangents[i].z : 0.0f,
                (source.tangents != null && i < source.tangents.Length) ? source.tangents[i].w : 0.0f,
                (source.uv != null       && i < source.uv.Length)       ? source.uv[i].x       : 0.0f,
                (source.uv != null       && i < source.uv.Length)       ? source.uv[i].y       : 0.0f,
                (source.uv2 != null      && i < source.uv2.Length)      ? source.uv2[i].x      : 0.0f,
                (source.uv2 != null      && i < source.uv2.Length)      ? source.uv2[i].y      : 0.0f,
                (source.uv3 != null      && i < source.uv3.Length)      ? source.uv3[i].x      : 0.0f,
                (source.uv3 != null      && i < source.uv3.Length)      ? source.uv3[i].y      : 0.0f,
                (source.uv4 != null      && i < source.uv4.Length)      ? source.uv4[i].x      : 0.0f,
                (source.uv4 != null      && i < source.uv4.Length)      ? source.uv4[i].y      : 0.0f,
                (source.colors != null   && i < source.colors.Length)   ? source.colors[i].r   : 0.0f,
                (source.colors != null   && i < source.colors.Length)   ? source.colors[i].g   : 0.0f,
                (source.colors != null   && i < source.colors.Length)   ? source.colors[i].b   : 0.0f,
                (source.colors != null   && i < source.colors.Length)   ? source.colors[i].a   : 0.0f
            };

            nrg.Vertices[i] = new nrgVertex(d);
        }

        int submeshCount = source.subMeshCount;
        nrg.Faces = new nrgFace[submeshCount][];

        for (int i = 0; i < submeshCount; ++i) {
            int[] submesh = source.GetTriangles(i);

            int submeshLength = submesh.Length / 3;
            nrg.Faces[i] = new nrgFace[submeshLength];

            for (int j = 0; j < submeshLength; ++j)
            {
                nrg.Faces[i][j] = new nrgFace( new int[3] { submesh[(j*3)+0], submesh[(j*3)+1], submesh[(j*3)+2] } );
            }
        }

        return nrg;
    }

    static public Mesh ConvertNrgToUnityMesh(nrgMesh source)
    {
        Mesh mesh = new Mesh();
        int vertexCount = source.Vertices.Length;

        Vector3[] pos = new Vector3[vertexCount];
        Vector3[] nrm = new Vector3[vertexCount];
        Vector4[] tan = new Vector4[vertexCount];
        Vector2[] uv1 = new Vector2[vertexCount];
        Vector2[] uv2 = new Vector2[vertexCount];
        Vector2[] uv3 = new Vector2[vertexCount];
        Vector2[] uv4 = new Vector2[vertexCount];
        Color[] vcl = new Color[vertexCount];

        for (int i = 0; i < vertexCount; ++i) {
            nrgVertex v = source.Vertices[i];
            pos[i] = new Vector3(v.x, v.y, v.z);
            nrm[i] = new Vector3(v.nx, v.ny, v.nz);
            tan[i] = new Vector4(v.tx, v.ty, v.tz, v.tw);
            uv1[i] = new Vector2(v.u, v.v);
            uv2[i] = new Vector2(v.u2, v.v2);
            uv3[i] = new Vector2(v.u3, v.v3);
            uv4[i] = new Vector2(v.u4, v.v4);
            vcl[i] = new Color(v.cr, v.cg, v.cb, v.ca);
        }

        mesh.vertices = pos;
        mesh.normals = nrm;
        mesh.tangents = tan;
        mesh.uv = uv1;
        mesh.uv2 = uv2;
        mesh.uv3 = uv3;
        mesh.uv4 = uv4;
        mesh.colors = vcl;

        int submeshCount = source.Faces.Length;

        for (int i = 0; i < submeshCount; ++i) {
            int submeshLength = source.Faces[i].Length;
            int[] faces = new int[submeshLength*3];

            for (int j = 0; j < submeshLength; ++j) {
                var f = source.Faces[i][j].data;
                for (int k = 0; k < 3; ++k) faces[(j*3)+k] = f[k];
            }
            mesh.SetTriangles(faces, i);
        }

        return mesh;
    }

    public static void WriteToFile(string filename, nrgMesh mesh)
    {
        using (StreamWriter file = File.CreateText(filename))
        {
            JsonSerializer serializer = new JsonSerializer();
            serializer.Formatting = Formatting.Indented;
            serializer.Serialize(file, mesh);
        }
    }

    public static nrgMesh ReadFromFile(string filename)
    {
        using(StreamReader file = File.OpenText(filename))
        {
            JsonSerializer serializer = new JsonSerializer();
            return (nrgMesh)serializer.Deserialize(file, typeof(nrgMesh));
        }
    }
}

public class JSON_test : MonoBehaviour {

    public GameObject box;
    private Mesh testmesh;

    // Use this for initialization
    void Start () {

        testmesh = box.GetComponent<MeshFilter>().mesh;

        if (testmesh == null) {
            Debug.Log("no mesh.");
            return;
        }

        nrgMesh mymesh = nrgMesh.ConvertUnityToNrgMesh(testmesh);

        string filename = Application.dataPath + "/mesh.nrg";
        nrgMesh.WriteToFile(filename, mymesh);

        var mysecondmesh = nrgMesh.ReadFromFile(filename);

        Mesh secondtestmesh = nrgMesh.ConvertNrgToUnityMesh(mysecondmesh);
        box.GetComponent<MeshFilter>().mesh = secondtestmesh;

        filename = Application.dataPath + "/mesh2.nrg";
        nrgMesh.WriteToFile(filename, mysecondmesh);
    }
}

This produces files like this:

{
  "Vertices": [
    [0.5,-0.5,0.5,0.0,0.0,1.0,-1.0,0.0,0.0,-1.0,0.0,0.0,0.6909865,0.3471644],
    [-0.5,-0.5,0.5,0.0,0.0,1.0,-1.0,0.0,0.0,-1.0,1.0,0.0,0.9966581,0.3471644],
    [0.5,0.5,0.5,0.0,0.0,1.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.6909865,0.652835846],
    [-0.5,0.5,0.5,0.0,0.0,1.0,-1.0,0.0,0.0,-1.0,1.0,1.0,0.9966581,0.652835846],
    [0.5,0.5,-0.5,0.0,1.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.3097273,0.347164243],
    [-0.5,0.5,-0.5,0.0,1.0,0.0,-1.0,0.0,0.0,-1.0,1.0,1.0,0.00405576639,0.347164243],
    [0.5,-0.5,-0.5,0.0,0.0,-1.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.347164243,0.652835846],
    [-0.5,-0.5,-0.5,0.0,0.0,-1.0,-1.0,0.0,0.0,-1.0,1.0,1.0,0.652835846,0.652835846],
    [0.5,0.5,0.5,0.0,1.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,0.3097273,0.652835846],
    [-0.5,0.5,0.5,0.0,1.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,0.00405576639,0.652835846],
    [0.5,0.5,-0.5,0.0,0.0,-1.0,-1.0,0.0,0.0,-1.0,0.0,0.0,0.347164243,0.347164243],
    [-0.5,0.5,-0.5,0.0,0.0,-1.0,-1.0,0.0,0.0,-1.0,1.0,0.0,0.652835846,0.347164243],
    [0.5,-0.5,-0.5,0.0,-1.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,0.347164243,0.00405576639],
    [0.5,-0.5,0.5,0.0,-1.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.347164243,0.3097273],
    [-0.5,-0.5,0.5,0.0,-1.0,0.0,-1.0,0.0,0.0,-1.0,1.0,1.0,0.652835846,0.3097273],
    [-0.5,-0.5,-0.5,0.0,-1.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,0.652835846,0.00405576639],
    [-0.5,-0.5,0.5,-1.0,0.0,0.0,0.0,0.0,-1.0,-1.0,0.0,0.0,0.6902728,0.3097273],
    [-0.5,0.5,0.5,-1.0,0.0,0.0,0.0,0.0,-1.0,-1.0,0.0,1.0,0.995944262,0.3097273],
    [-0.5,0.5,-0.5,-1.0,0.0,0.0,0.0,0.0,-1.0,-1.0,1.0,1.0,0.995944262,0.00405576639],
    [-0.5,-0.5,-0.5,-1.0,0.0,0.0,0.0,0.0,-1.0,-1.0,1.0,0.0,0.6902728,0.00405576639],
    [0.5,-0.5,-0.5,1.0,0.0,0.0,0.0,0.0,1.0,-1.0,0.0,0.0,0.3097273,0.00405576639],
    [0.5,0.5,-0.5,1.0,0.0,0.0,0.0,0.0,1.0,-1.0,0.0,1.0,0.00405576639,0.00405576639],
    [0.5,0.5,0.5,1.0,0.0,0.0,0.0,0.0,1.0,-1.0,1.0,1.0,0.00405576639,0.3097273],
    [0.5,-0.5,0.5,1.0,0.0,0.0,0.0,0.0,1.0,-1.0,1.0,0.0,0.3097273,0.3097273]
  ],
  "Faces": [
    [
      [0,2,3],
      [0,3,1],
      [8,4,5],
      [8,5,9],
      [10,6,7],
      [10,7,11],
      [12,13,14],
      [12,14,15],
      [16,17,18],
      [16,18,19],
      [20,21,22],
      [20,22,23]
    ]
  ]
}

There are a few things I'm unsure about:

  • I'm not confident that the weird array construct in the nrgVertex and nrgFace classes is a good idea. However I prefer the data to be in an array in the file as the JSON will otherwise include a label for each member of every vertex. Way to verbose I think.

  • I have read about mutable and immutable structs and classes, I decided that vertexes and faces should be immutable in my mesh since it is intended to define the structure of the file only, not for editing the data. Are they though? Also, should I try to make nrgMesh itself immutable as well?

  • Most of the code that interacts with JSON.net is directly from the docs or from other examples google turned up. Especially the implementation of the JsonConverter classes. They seem to work, but are they any good?

  • I probably should do something with a namespace. I'm not sure if this would be appropriate in this case.

My goal is to get better at using c# and JSON.net, feedback would be very helpful.

\$\endgroup\$
1
\$\begingroup\$

I'll start by quickly pointing that you should follow the conventions for the language you are working with. This means that your classes should be named in PascalCase without any underscores.

You also break encapsulation on nrgMesh by exposing the fields Vertices and Faces. Turn them into properties.

public class NrgMesh
{
    public NrgVertex[] Vertices{get; private set;}
    public NrgFace[][] Faces{get; private set;}
    //...
}

From now on my review will only focus on refactoring NrgMesh and I'll leave the serialization and NrgFace to other user, or to you.

You already have a class perfectly capable of representing your NrgVertex, that class is Mesh. All you wanted to do is to "convert" a Mesh into an array of Meshes, with each index being a Mesh instance, I clarify:

private Mesh CreateMesh()
{
    Mesh mesh = new Mesh()
    {
        vertices = new Vector3[Vertices.Length],
        normals = new Vector3[Vertices.Length],
        tangents = new Vector4[Vertices.Length],
        uv = new Vector2[Vertices.Length],
        uv2 = new Vector2[Vertices.Length],
        uv3 = new Vector2[Vertices.Length],
        uv4 = new Vector2[Vertices.Length],
        colors = new Color[Vertices.Length],
    };
    int i = 0;
    foreach (var vertex in Vertices) //Vertices is now Mesh[]
    {
        mesh.vertices[i] = vertex.vertices[i];
        mesh.normals[i] = vertex.normals[i];
        mesh.tangents[i] = vertex.tangents[i];
        mesh.uv[i] = vertex.uv[i];
        mesh.uv2[i] = vertex.uv2[i];
        mesh.uv3[i] = vertex.uv3[i];
        mesh.uv4[i] = vertex.uv4[i];
        mesh.colors[i] = vertex.colors[i];
        ++i;
    }
    return mesh;
}

I am sorry to say but this pretty much renders your NrgVertex class useless.

Your ConvertUnityToNrgMesh should be converted into a constructor parametrized by a Mesh. This same method has two responsibilities, one is to create the Vertices and other is to create the Faces, break them into methods

public class NrgMesh
{
    public Mesh[] Vertices { get; private set; }
    public NrgFace[][] Faces { get; private set; }
    public NrgMesh(Mesh source)
    {
        CreateVertices(source);
        CreateFaces(source);
    }
    //...
} 

By following up with this idea the conversion becomes much more simple:

public class NrgMesh
{
    public Mesh[] Vertices { get; private set; }
    public NrgFace[][] Faces { get; private set; }
    public NrgMesh(Mesh source)
    {
        CreateVertices(source);
        CreateFaces(source);
    }

    private void CreateFaces(Mesh mesh)
    {
        Faces = new NrgFace[mesh.subMeshCount][];
        for (int i = 0; i < mesh.subMeshCount; ++i)
        {
            int[] submesh = mesh.GetTriangles(i);

            int submeshLength = submesh.Length / 3;
            Faces[i] = new NrgFace[submeshLength];

            for (int j = 0; j < submeshLength; ++j)
            {
                Faces[i][j] = new NrgFace(new int[3] { submesh[(j * 3) + 0], submesh[(j * 3) + 1], submesh[(j * 3) + 2] });
            }
        }
    }

    private Vector4[] CreateVector4(Vector4[] vector, int idx)
    {
        bool hasValue = vector != null && idx < vector.Length;
        return new Vector4[]
        {
            new Vector4(
                hasValue ? vector[idx].x : 0,
                hasValue ? vector[idx].y : 0,
                hasValue ? vector[idx].z : 0,
                hasValue ? vector[idx].w : 0
            )
        };
    }

    private Vector3[] CreateVector3(Vector3[] vector, int idx)
    {
        bool hasValue = vector != null && idx < vector.Length;
        return new Vector3[]
        {
            new Vector3(
                hasValue ? vector[idx].x : 0, 
                hasValue ? vector[idx].y : 0, 
                hasValue ? vector[idx].z : 0
            )
        };
    }

    private Vector2[] CreateVector2(Vector2[] vector, int idx)
    {
        bool hasValue = vector != null && idx < vector.Length;
        return new Vector2[]
        {
            new Vector2(
                hasValue ? vector[idx].x : 0,
                hasValue ? vector[idx].y : 0
            )
        };
    }

    private Color[] CreateColor(Color[] colors, int idx)
    {
        bool hasValue = colors != null && idx < colors.Length;
        return new Color[]
        {
            new Color(
                hasValue ? colors[idx].r : 0,
                hasValue ? colors[idx].g : 0,
                hasValue ? colors[idx].b : 0,
                hasValue ? colors[idx].a : 0
            )
        };
    }

    private void CreateVertices(Mesh mesh)
    {
        Vertices = new Mesh[mesh.vertexCount];
        for (int i = 0; i < mesh.vertexCount; ++i)
        {
            Vertices[i] = new Mesh()
            {
                vertices = CreateVector3(mesh.vertices, i),
                normals = CreateVector3(mesh.normals, i),
                tangents = CreateVector4(mesh.tangents, i),
                uv = CreateVector2(mesh.uv, i),
                uv2 = CreateVector2(mesh.uv2, i),
                uv3 = CreateVector2(mesh.uv3, i),
                uv4 = CreateVector2(mesh.uv4, i),
                colors = CreateColor(mesh.colors, i)
            };
        }
    }

    private Mesh CreateMesh()
    {
        Mesh mesh = new Mesh()
        {
            vertices = new Vector3[Vertices.Length],
            normals = new Vector3[Vertices.Length],
            tangents = new Vector4[Vertices.Length],
            uv = new Vector2[Vertices.Length],
            uv2 = new Vector2[Vertices.Length],
            uv3 = new Vector2[Vertices.Length],
            uv4 = new Vector2[Vertices.Length],
            colors = new Color[Vertices.Length],
        };
        int i = 0;
        foreach (var vertex in Vertices)
        {
            mesh.vertices[i] = vertex.vertices[i];
            mesh.normals[i] = vertex.normals[i];
            mesh.tangents[i] = vertex.tangents[i];
            mesh.uv[i] = vertex.uv[i];
            mesh.uv2[i] = vertex.uv2[i];
            mesh.uv3[i] = vertex.uv3[i];
            mesh.uv4[i] = vertex.uv4[i];
            mesh.colors[i] = vertex.colors[i];
        }
        return mesh;
    }

    public Mesh ConvertNrgToUnityMesh()
    {
        var mesh = CreateMesh();
        for (int i = 0; i < Faces.Length; ++i)
        {
            int[] faces = new int[Faces[i].Length * 3];

            for (int j = 0; j < Faces[i].Length; ++j)
            {
                var f = Faces[i][j].data;
                for (int k = 0; k < 3; ++k) faces[(j * 3) + k] = f[k];
            }
            mesh.SetTriangles(faces, i);
        }

        return mesh;
    }

    public void WriteToFile(string filename)
    {
        using (StreamWriter file = File.CreateText(filename))
        {
            JsonSerializer serializer = new JsonSerializer();
            serializer.Formatting = Formatting.Indented;
            serializer.Serialize(file, this);
        }
    }

    public static NrgMesh ReadFromFile(string filename)
    {
        using (StreamReader file = File.OpenText(filename))
        {
            JsonSerializer serializer = new JsonSerializer();
            return (NrgMesh)serializer.Deserialize(file, typeof(NrgMesh));
        }
    }
}

Now what you probably want to do is to have a JsonConverter for NrgMesh to serialize both the Vertices and Faces the way you want.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.