Search code examples
3dmeshgodotgdscript

How to make a parser of PLY in Godot: Triangle Fan


I've been creating a tool for shading 3D models in Godot but I need to be able to translate the PLY format to SurfaceTool in Godot. PLY format has a header that explains what is the structure and then gives the data. Here's an example of a cube:

ply
format ascii 1.0
comment made by Greg Turk
comment this file is a cube
element vertex 8
property float x
property float y
property float z
element face 6
property list uchar int vertex_index
end_header
0 0 0
0 0 1
0 1 1
0 1 0
1 0 0
1 0 1
1 1 1
1 1 0
4 0 1 2 3
4 7 6 5 4
4 0 4 5 1
4 1 5 6 2
4 2 6 7 3
4 3 7 4 0

I have put in diferrent arrays the vertex and the faces info in header. I've created the mesh like this:

var meshBuilder = SurfaceTool.new()
meshBuilder.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)

I have added the vertex like this:

for i in range(vertex_count):
                    meshBuilder.add_normal(Vector3.UP)
                    meshBuilder.add_vertex(Vector3(float(data[0]),
                                        float(data[1]),
                                        float(data[2])))
                    line = loadedFile.get_line()
                    if line == "":
                        break
                    data = line.split(" ")

But I do not know how to add the faces. It is a triangle fan, where the first face 4 0 1 2 3 is 4 vertex, composed of the triangles 0,1,2 and 0,2,3 and not 0,1,2 and 1,2,3. That's the order of the used vertex.

Maybe I am supposed to add the vertex in that exact order that the face says but I do not know how to use add_uv because I have been told to use it.


Solution

  • First of all, do not add the vertex to the SurfaceTool directly. You are going to store the vertex in an array, and then add them according to the faces you read from the file.

    Second, in the PLY format each face is a Triangle Fan. The whole is not a Triangle Fan. Furthermore, SurfaceTool will not let you create multiple triangle fans at once (a second call to begin internally calls clear). Thus, we need to commit each Triangle Fan as we create them.

    And, by the way, there is no UV (texture coordinates) in your example.


    Let us start by storing the vertex into an array (assuming you just read "end_header"):

    var vertex_array:Array = []
    for vertex_index in vertex_count:
        line = loadedFile.get_line()
        data = line.split(" ")
        var vertex := Vector3(float(data[0]), float(data[1]), float(data[2]))
        vertex_array.append(vertex)
    

    Then we need to read the faces:

    var mesh := ArrayMesh.new()
    var mesh_builder := SurfaceTool.new()
    
    for face_index in faces_count:
        line = loadedFile.get_line()
        data = line.split(" ")
    
        mesh_builder.begin(Mesh.PRIMITIVE_TRIANGLE_FAN)
    
        for face_vertex_index in range(1, data.size()):
            mesh_builder.add_vertex(vertex_array[int(data[face_vertex_index])])
    
        mesh_builder.commit(mesh)
    

    Note that I did forgo processing the number of vertex in the face (data[0]). Instead I skip it by using range(1, data.size()) (the range would start at 0 instead of 1 if we included it). The other values in data are the indexes of the array where we stored the vertex.

    So 4 0 1 2 3 means I need to go take the vertex from that array which are at position 0, 1, 2, and 3 (As I said, I did forgo processing the 4).


    Setting normals requires extra work. We need to process each triangle separately (not each triangle fan), and compute the normal.

    The normal is vector perpendicular to the triangle, we can compute by doing a cross product of the differences of the vertex. Which means we need the set of vertex that make each triangle, to compute their normals.

    If we are going to process each triangle separately, we might as well use PRIMITIVE_TRIANGLES and use generate_normals (which only works when we are using PRIMITIVE_TRIANGLES):

    var mesh := ArrayMesh.new()
    var mesh_builder := SurfaceTool.new()
    
    for face_index in faces_count:
        line = loadedFile.get_line()
        data = line.split(" ")
    
        mesh_builder.begin(Mesh.PRIMITIVE_TRIANGLES)
    
        for triangle_index in range(2, data.size() - 1):
            mesh_builder.add_vertex(vertex_array[int(data[1])])
            mesh_builder.add_vertex(vertex_array[int(data[triangle_index])])
            mesh_builder.add_vertex(vertex_array[int(data[triangle_index + 1])])
    
        surface_tool.generate_normals()
        mesh_builder.commit(mesh)
    

    Remember they are stored as triangle fans. They all share the first vertex (data[1]). So, in 4 0 1 2 3 the triangles share the vertex stored in the array at position 0.

    We can iterate over data to get the second vertex. Which means this time we need to skip two elements (the 4 and 0), hence the 2 in range(2, data.size() - 1).

    Finally the third vertex is always the next one after the second. Which also means we cannot iterate until the end, hence the - 1 in range(2, data.size() - 1).


    Afterwards, you can then set the mesh to a MeshIntance.