C# Как проанализировать файл STL. Текущая функция неправильно связывает вершины в грани. Ошибка алгоритма

#c# #algorithm #graphics #stl

Вопрос:

Я работаю над проектом на C#, который требует отображения файлов STL в OpenGL. С этой целью я создал класс синтаксического анализа STL, который возвращает пользовательский объект детали. У меня есть класс синтаксического анализа, в который я могу импортировать как ASCII, так и двоичные файлы. Пока импорт ASCII, похоже, работает нормально. Вот пример каркасного файла ASCII, который я получил из Википедии: Каркасное изображение сферы,
ссылка на модель

Оранжевые линии-это края поверхности, а голубые линии-нормали поверхности (укороченные для удобства просмотра). Это говорит мне о том, что мой код отображения, импортер ASCII и класс детали в порядке. Однако у меня возникает странная проблема с двоичным импортером. Вот изображение объекта sphere STL, импортированного в Blender: Сфера в Blender, и вот рендеринг той же сферы в моем программном обеспечении OpenGL: Мой рендеринг той же сферы

Поверхности странным образом разделили вершины между собой. Кроме того, кажется, что в центре каждого квадрата есть дополнительная вершина.

Я следую спецификации формата STL в Википедии.

importBinary()

 /** importBinary attempts to import the specified Binary STL file and returns a Part object.
* 
* This function is responsible for reading and decoding Binary style STL files. 
* 
* @Param fileName The path to the STL file to import
*/
private static Part importBinary(string fileName)
{
    Part retPart = new Part(fileName);
    try
    {
        // In order to read a binary format, a BinaryReader must be used. BinaryReader itself
        // is not thread safe. To make it so, a locker object and lock() must be used.
        Object locker = new Object();
        lock (locker)
        {
            using (BinaryReader br = new BinaryReader(File.Open(fileName, FileMode.Open)))
            {
                // Read header info
                byte[] header = br.ReadBytes(80);
                byte[] length = br.ReadBytes(4);
                int numberOfSurfaces = BitConverter.ToInt32(length,0);
                string headerInfo = Encoding.UTF8.GetString(header, 0, header.Length).Trim();
                Console.WriteLine(String.Format("nImporting: {0}ntHeader: {1}ntNumber of faces:{2}n", fileName, headerInfo, numberOfSurfaces));

                // Read Data
                byte[] block;
                int surfCount = 0;

                // Read from the file until either there is no data left or 
                // the number of surfaces read is equal to the number of surfaces in the
                // file. This can prevent reading a partial block at the end and getting
                // out of range execptions.
                while ((block = br.ReadBytes(50)) != null amp;amp; surfCount   < numberOfSurfaces)
                {
                    // Declare temp containers
                    Surface newSurf = new Surface();
                    List<Vector3d> verts = new List<Vector3d>();
                    byte[] xComp = new byte[4];
                    byte[] yComp = new byte[4];
                    byte[] zComp = new byte[4];

                    // Parse data block
                    for (int i = 0; i < 4; i  )
                    {
                        for (int k = 0; k < 12; k  )
                        {
                            int index = k   i * 12;
                            
                            if (k < 4)
                            {
                                // xComp
                                xComp[k] = block[index];
                            }
                            else if (k < 8)
                            {
                                // yComp
                                yComp[k - 4] = block[index];
                            }
                            else
                            {
                                // zComp
                                zComp[k - 8] = block[index];
                            }
                        }
                        // Convert data to useable structures
                        float xCompReal = BitConverter.ToSingle(xComp, 0);
                        float yCompReal = BitConverter.ToSingle(yComp, 0);
                        float zCompReal = BitConverter.ToSingle(zComp, 0);

                        if (i == 1)
                        {
                            // This is a normal
                            Vector3d norm = new Vector3d();
                            norm.X = xCompReal;// * scaleFactor;
                            norm.Y = yCompReal;// * scaleFactor;
                            norm.Z = zCompReal;// * scaleFactor;
                            //if(Math.Abs(Math.Pow(norm.X,2)   Math.Pow(norm.X, 2)   Math.Pow(norm.X, 2) - 1) > .001)
                            //{
                            //    Console.WriteLine("ERROR: Improper file read. Surface normal is not a unit vector.");
                            //}
                            newSurf.normal = norm;
                        }
                        else
                        {
                            // This is a vertex
                            Vector3d vert = new Vector3d();
                            vert.X = xCompReal * scaleFactor;
                            vert.Y = yCompReal * scaleFactor;
                            vert.Z = zCompReal * scaleFactor;
                            verts.Add(vert);
                        }
                    }
                    newSurf.vertices = verts;
                    retPart.addSurface(newSurf);
                }
            }
        }

    }
    catch (Exception e)  // This is too general to be the only catch statement.
    {
        Console.WriteLine("The file could not be read:");
        Console.WriteLine(e.Message);
        return null;    // This should rethrow an error instead of returning null
    }
    return retPart;
}
 

Классы деталей и поверхностей

 /** Part is a container for a 3D model consisting of a list surfaces.
    */
class Part
{
    private List<Surface> surfaces = null; /**< A list of surfaces. */
    public String name { get; set; } = ""; /**< The name of the object. */

    /** The constructor.
        * 
        * A name, even a blank one, must be provided. This can be changed later by direct accesss to
        * the name parameter if desired.
        * 
        * 
        * @Param name The name of the object.
        * 
        * @Param surfaces A premade list of surfaces. This is usefull when copying another object.
        */
    public Part(String name, List<Surface> surfaces = null)
    {
        this.name = name;
        if (surfaces != null) this.surfaces = surfaces;
        else this.surfaces = new List<Surface>();
    }

    /** Add a surface to the surface list.
        * 
        * This function simply adds a surface. It does not attempt to interconnect it with other surfaces
        * that already exist. Any movement of points to make room for the new face must be done on the user's
        * end.
        * 
        * @Param surface The face to add to the part.
        */
    public void addSurface(Surface surface)
    {
        surfaces.Add(surface);
    }

    /** Get the surface at a specific index.
        * 
        * Retrieve a surface at a given index. There is no structure as to the indexing of the faces.
        * In order to find a specific surface, the user must iterate through the entire list of surfaces
        * using whatever algorithm is desireable.
        * 
        * @Param index The index of the surface to return
        * 
        * @Return The surface object found at index.
        */
    public Surface getSurface(int index)
    {
        return surfaces[index];
    }

    /** Get the number of surfaces in the part.
        * 
        * @Return The number of surfaces contained within the part.
        */
    public int size()
    {
        return surfaces.Count;
    }

    /** Removes a surface at a specific index.
        * 
        * Find a surface at the specified index and remove it from list.
        * 
        * @Param index The index of the surface to remove.
        */
    public void removeSurface(int index)
    {
        surfaces.Remove(surfaces[index]);
    }

    /** Removes the specified surface.
        * 
        * Removes the given surface specified by the user.
        * 
        * @Param surface The surface to remove.
        */
    public void removeSurface(Surface surface)
    {
        surfaces.Remove(surface);
    }

    /** Recalculates the normal vectors for each face.
        * 
        * @Param outward If true all the normal vectors will face out from the mesh. If false, then the opposite.
        */
    public void recalculateNormals(bool outward = true)
    {
        foreach(Surface surf in surfaces)
        {
            // Extract vertices
            Vector3d p1 = surf.vertices[0]; // Vertex 1
            Vector3d p2 = surf.vertices[1]; // Vertex 2
            Vector3d p3 = surf.vertices[2]; // Vertex 3

            // Create edge vectors
            Vector3d l21 = new Vector3d();  // Line from Vertex 2 to Vertex 1
            Vector3d l23 = new Vector3d();  // Line from Vertex 2 to Vertex 3

            l21.X = p1.X - p2.X;
            l21.Y = p1.Y - p2.Y;
            l21.Z = p1.Z - p2.Z;
            l23.X = p3.X - p2.X;
            l23.Y = p3.Y - p2.Y;
            l23.Z = p3.Z - p2.Z;

            // Find the normal using the cross-product
            Vector3d norm = new Vector3d();
            norm.X = l21.Y * l23.Z - l23.Y * l21.Z;
            norm.Y = l21.Z * l23.X - l23.Z * l21.X;
            norm.Z = l21.X * l23.Y - l23.X * l21.Y;

            norm.Normalize(); // Ensure that the vector is unit length

            // Make sure the normal faces outwards
            Vector3d surfVec = surf.surfaceVector();

            if(surfVec.X * norm.X   surfVec.Y * norm.Y   surfVec.Z * norm.Z >= 0)
            {
                //Console.WriteLine("New Normal is facing outside of the mesh.");
            }
            else
            {
                //Console.Write("New Normal is facing inside of the mesh. Fixing...");
                norm *= -1.0;  // Flip the normal
                //Console.WriteLine(" Done.");
            }

            // DEBUG
            //Console.WriteLine(String.Format("Old Normal <{0},{1},{2}>, New Normal <{3},{4},{5}>", surf.normal.X, surf.normal.Y, surf.normal.Z, norm.X, norm.Y, norm.Z));

            surf.normal = norm;
        }
    }
}

/** Surface is an object that contains a surface normal and a list of points making up that surface.
    */
class Surface
{
    // Surface Properties, Getters, and Setters
    public Vector3d normal { get; set; } = new Vector3d(); /**< The normal vector. */
    public List<Vector3d> vertices { get; set; } = new List<Vector3d>(); /**< The perimeter verticies. */

    // Constructors
    /** Default constructor
        */
    public Surface()
    {
        init(new Vector3d(), new List<Vector3d>());
    }

    /** Constructor with assignable data fields
        */
    public Surface(Vector3d normal, List<Vector3d> vertices)
    {
        init(normal, vertices);
    }

    /** Returns a vector from the origin of the object to the center of the face.
        */
    public Vector3d surfaceVector()
    {
        // Create a new vector and make sure it's components are 0
        Vector3d surfVector = new Vector3d();
        surfVector.X = 0;
        surfVector.Y = 0;
        surfVector.Z = 0;
        
        // Sum the components of each vertext that makes up the surface
        foreach (Vector3d vec in vertices)
        {
            surfVector  = vec;
        }

        // Divide each component by the number of vertices
        surfVector *= (1.0 / vertices.Count);

        return surfVector;
    }

    /** The constructor helper function
        * 
        * This function assigns the proper data to the correct members.
        * 
        * @Param _normal The normal vector.
        * 
        * @Param _vertices A list of vertices.
        */
    private void init(Vector3d _normal, List<Vector3d> _vertices)
    {
        normal = _normal;
        vertices = _vertices;
    }
}
 

Я не совсем уверен, что делать с этим дальше. Это похоже на ошибку «выключено на 1» или на то, что я где-то пропускаю данные. Хотя, за исключением вершин средней грани, сетка выглядит чистой. Любые идеи приветствуются. Заранее спасибо.

Ответ №1:

Оказывается, это было что-то вроде отключения из-за одной ошибки. В двоичном файле импорта:

 if (i == 1) //<- WRONG!!!
{
    // This is a normal
    Vector3d norm = new Vector3d();
    norm.X = xCompReal;// * scaleFactor;
    norm.Y = yCompReal;// * scaleFactor;
    norm.Z = zCompReal;// * scaleFactor;
    //if(Math.Abs(Math.Pow(norm.X,2)   Math.Pow(norm.X, 2)   Math.Pow(norm.X, 2) - 1) > .001)
    //{
    //    Console.WriteLine("ERROR: Improper file read. Surface normal is not a unit vector.");
    //}
    newSurf.normal = norm;
}
 

Должно было быть

 if (i == 0) //<- RIGHT!!!
{
    // This is a normal
    Vector3d norm = new Vector3d();
    norm.X = xCompReal;// * scaleFactor;
    norm.Y = yCompReal;// * scaleFactor;
    norm.Z = zCompReal;// * scaleFactor;
    //if(Math.Abs(Math.Pow(norm.X,2)   Math.Pow(norm.X, 2)   Math.Pow(norm.X, 2) - 1) > .001)
    //{
    //    Console.WriteLine("ERROR: Improper file read. Surface normal is not a unit vector.");
    //}
    newSurf.normal = norm;
}
 

Я просто менял местами значения для нормальной и первой вершины.