Normal buffer not correct in "large" mesh

Hello,
I’m trying to optimize the number of objects in my program to avoid performance problems. In the videos you see a box with black corners.
In the first video, the normals are ok. I made a new Geometry/Mesh for very planar face of the corner.
In the second video the normals are not ok. Here I put all planar faces into one Geometry/Mesh. In both cases, the normals should be correct. But it looks like they are not and I don’t know why. Does anyone have an Idea? Password for the videos is “JME-Forum”
Video one: https://howatherm.hartechcloud.de/index.php/s/eccJ2SoXRk4o7Sd
Video two: https://howatherm.hartechcloud.de/index.php/s/mkgDizgcxPbce5C

Source video one

public static void erzeugeGeometrieFlaechenkoerper (JMBauteil jmBauteil, LinkedList<JMFlaeche> flaechen){
        
        Integer numberOfVertices = 0;   // Summe der Eckpunkte
        Integer numberOfIndexes = 0;    // 3 Felder mit Punkten bilden ein Dreieck
                                        // Die Anzahl der Normalen entspricht der Anzahl der Indexe
                                        // Für jeden Punkt im Index gibt es eine Normale.
                                        // Alle Normalen eines Dreiecks sind identisch (bei gekrümmten Flächen wäre das vermutlich anders)
        Material mat;
        Integer anzahlFlaechen = 0;                                
        
        for (JMFlaeche flaeche : flaechen){                 // Liste der Flächen durchlaufen

            anzahlFlaechen++;
            numberOfVertices = numberOfVertices + flaeche.getAnzahlPunkte();                // Anzahl der Punkte addieren
            numberOfIndexes = numberOfIndexes + (flaeche.getAnzahlPunkte()-2)*3;            // je 3 Punkte bilden ein Dreieck. Wenn die Fläche 5 Ecken hat
                                                                                            // dann werden dafür 3 Dreiecke benötigt (daher -2)
        }
        Vector3f[] vertices = new Vector3f[numberOfVertices];           // Feld für die Eckpunkte in der passenden Größe erzeugen
        short indexes[] = new short[numberOfIndexes];                   // Feld für die Indexe in der passenden Größe erzeugen
        Vector3f nb[] = new Vector3f[numberOfIndexes];                  // sFeld für die Normalen in der passenden Größe erzeugen
        
        Vector2f [] texCoord = new Vector2f[4];
        texCoord[0] = new Vector2f(0,0);
        texCoord[1] = new Vector2f(1,0);
        texCoord[2] = new Vector2f(0,1);
        texCoord[3] = new Vector2f(1,1);
       
        short points=0; // durchläuft alle Punkte
        short index=0;  // durchläuft die Liste aller Indizes und ihrer Normalen
        anzahlFlaechen = 0;
        
        for (JMFlaeche flaeche : flaechen){     // Liste der Flächen durchlaufen
            anzahlFlaechen++;
            
            short indexStart = points;                      // Der Index wird über die Punkte der aktuellen Fläche erstellt
            short indexStop =(short) (indexStart +          // Deshalb muss die Position des ersten Punktes des ersten Dreiecks und die 
                              flaeche.getAnzahlPunkte()-3); // Position des ersten Punktes des letzten Dreiecks in der Liste 
                                                            // aller Punkte (vertices[]) gespeichert werden.
        
                for (Punkt3D p : flaeche.getPunkte()){                                      // Eckpunkte in der Liste der Eckpunkte speichern
                vertices[points] = new Vector3f((float)p.x,(float)p.y,(float)p.z);
                points++;
            }
            
            Vector3f normal = new Triangle(vertices[indexStart],vertices[indexStart+1],vertices[indexStart+2]).getNormal();
            
            for (int i=indexStart; i <= indexStop; i++){    // Punkte durchlaufen (außer die letzten 2)
                
                indexes[index] = (short)indexStart;                 // Punkt1 Alle Dreiecke gehen vom ersten Punkt der Fläche aus
                nb[index] = normal;
                index++;
                
                indexes[index] =(short) (i+1);                      // Punkt 2 
                nb[index] = normal;
                index++;
                
                indexes[index] =(short) (i+2);                      // Punkt 3
                nb[i+2] = normal;
                nb[index] = normal;
                index++;
            }
        }                                       // Liste der Flächen durchlaufen
         
        Mesh  m = new Mesh();                  // enthält die fertig konvertierte Mesh
        
        // Setting buffers
        m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
        m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
        m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(indexes));
        m.setBuffer(VertexBuffer.Type.Normal,3, BufferUtils.createFloatBuffer(nb));
        m.updateBound();
        m.setStatic();
        
        String iD = jmBauteil.getID();     // Damit weiter unten auf die Daten der Geometrie zugegriffen werden kann
        Geometry geom = new Geometry(iD, m);    // wenn sie an der Node hängt
        
        if (jmBauteil.isMarked()){              // Beuteil ist markiert
            mat = MaterialListe.getMaterial(EnumMaterial.OBJEKT_MARKIERT);
        } else {                                // Bauteil ist NICHT markiert
            mat = MaterialListe.getMaterial(jmBauteil.getMaterial());
        }
     
        geom.setMaterial(mat);
        geom.setShadowMode(RenderQueue.ShadowMode.Off);
        jmBauteil.getNode().attachChild(geom);                      // Geometrie an die Node des Bauteils hängen
        if (jmBauteil.isUnsichtbar()){                              // Objekt unsichtbar
            jmBauteil.getNode().setCullHint(Spatial.CullHint.Always);             // Node nicht rendern
        }
        
        jmBauteil.getNode().setName(jmBauteil.getID());             // ID in der Node speichern
    }

Source video two

public static void erzeugeGeometrieFlaechenkoerper (JMBauteil jmBauteil, LinkedList<JMFlaeche> flaechen){
        
        Integer numberOfVertices = 0;   // Summe der Eckpunkte
        Integer numberOfIndexes = 0;    // 3 Felder mit Punkten bilden ein Dreieck
                                        // Die Anzahl der Normalen entspricht der Anzahl der Indexe
                                        // Für jeden Punkt im Index gibt es eine Normale.
                                        // Alle Normalen eines Dreiecks sind identisch (bei gekrümmten Flächen wäre das vermutlich anders)
        Material mat;
        Integer anzahlFlaechen = 0;                                
        
        for (JMFlaeche flaeche : flaechen){                 // Liste der Flächen durchlaufen

            anzahlFlaechen++;
            numberOfVertices = numberOfVertices + flaeche.getAnzahlPunkte();                // Anzahl der Punkte addieren
            numberOfIndexes = numberOfIndexes + (flaeche.getAnzahlPunkte()-2)*3;            // je 3 Punkte bilden ein Dreieck. Wenn die Fläche 5 Ecken hat
                                                                                            // dann werden dafür 3 Dreiecke benötigt (daher -2)
        }
        Vector3f[] vertices = new Vector3f[numberOfVertices];           // Feld für die Eckpunkte in der passenden Größe erzeugen
        short indexes[] = new short[numberOfIndexes];                   // Feld für die Indexe in der passenden Größe erzeugen
        Vector3f nb[] = new Vector3f[numberOfIndexes];                  // sFeld für die Normalen in der passenden Größe erzeugen
        
        Vector2f [] texCoord = new Vector2f[4];
        texCoord[0] = new Vector2f(0,0);
        texCoord[1] = new Vector2f(1,0);
        texCoord[2] = new Vector2f(0,1);
        texCoord[3] = new Vector2f(1,1);
       
        short points=0; // durchläuft alle Punkte
        short index=0;  // durchläuft die Liste aller Indizes und ihrer Normalen
        anzahlFlaechen = 0;
        
        for (JMFlaeche flaeche : flaechen){     // Liste der Flächen durchlaufen
            anzahlFlaechen++;
            
            short indexStart = points;                      // Der Index wird über die Punkte der aktuellen Fläche erstellt
            short indexStop =(short) (indexStart +          // Deshalb muss die Position des ersten Punktes des ersten Dreiecks und die 
                              flaeche.getAnzahlPunkte()-3); // Position des ersten Punktes des letzten Dreiecks in der Liste 
                                                            // aller Punkte (vertices[]) gespeichert werden.
        
                for (Punkt3D p : flaeche.getPunkte()){                                      // Eckpunkte in der Liste der Eckpunkte speichern
                vertices[points] = new Vector3f((float)p.x,(float)p.y,(float)p.z);
                points++;
            }
            
            Vector3f normal = new Triangle(vertices[indexStart],vertices[indexStart+1],vertices[indexStart+2]).getNormal();
            
            for (int i=indexStart; i <= indexStop; i++){    // Punkte durchlaufen (außer die letzten 2)
                
                indexes[index] = (short)indexStart;                 // Punkt1 Alle Dreiecke gehen vom ersten Punkt der Fläche aus
                nb[index] = normal;
                index++;
                
                indexes[index] =(short) (i+1);                      // Punkt 2 
                nb[index] = normal;
                index++;
                
                indexes[index] =(short) (i+2);                      // Punkt 3
                nb[i+2] = normal;
                nb[index] = normal;
                index++;
            }
        }                                       // Liste der Flächen durchlaufen
         
        Mesh  m = new Mesh();                  // enthält die fertig konvertierte Mesh
        
        // Setting buffers
        m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
        m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
        m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(indexes));
        m.setBuffer(VertexBuffer.Type.Normal,3, BufferUtils.createFloatBuffer(nb));
        m.updateBound();
        m.setStatic();
        
        String iD = jmBauteil.getID();     // Damit weiter unten auf die Daten der Geometrie zugegriffen werden kann
        Geometry geom = new Geometry(iD, m);    // wenn sie an der Node hängt
        
        if (jmBauteil.isMarked()){              // Beuteil ist markiert
            mat = MaterialListe.getMaterial(EnumMaterial.OBJEKT_MARKIERT);
        } else {                                // Bauteil ist NICHT markiert
            mat = MaterialListe.getMaterial(jmBauteil.getMaterial());
        }
     
        geom.setMaterial(mat);
        geom.setShadowMode(RenderQueue.ShadowMode.Off);
        jmBauteil.getNode().attachChild(geom);                      // Geometrie an die Node des Bauteils hängen
        if (jmBauteil.isUnsichtbar()){                              // Objekt unsichtbar
            jmBauteil.getNode().setCullHint(Spatial.CullHint.Always);             // Node nicht rendern
        }
        
        jmBauteil.getNode().setName(jmBauteil.getID());             // ID in der Node speichern
    }

My understanding is that the buffer size for vertices, normals and texture coordinates should always be the same. But, you are initializing these arrays with different sizes. Without understanding every line of your code, I think that this is the root cause of your problem.

I think you need to understand that the index buffer references to the item (or number) stored in the respective vertex position, normals and texcoords array. You cannot have different sizes here, otherwise your index reference is ill-fated.

As a side note, you may come into a situation where you want to have “flat” shading of your objects. In this case, you would have different normals for the same vertex position. But the index buffer is referencing to both arrays simultaneously, which means that you would also have to duplicate the vertex coordinates and match them with the according normal and index. I hope that I explained it well.

When I have a simple quad mesh like shown in Custom Mesh Shapes :: jMonkeyEngine Docs then I have 4 vertices that build 2 triangles. So in the index I have a list of 6 vertices that build the 2 triangles.
I thought that the normalbuffer consists of the normals for each of these 6 vertices, because in more compex meshes the normals of triangles that share vertices may have different normals. Is that wrong? And if it is, how do i calculate the correct normal for a vertice shared by 2 or even more triangles?

What you are saying is correct. I think a better explanation can be given by considering the following examples:

A cube generally consists of 8 corner points. In the first example, if you initialize

vertices=new Vector3f[8];
normals=new Vector3f[8];

you will get a smooth shaded, sometimes called as soft shaded, cube, as shown in the image below.

To get a flat shaded cube, you need to consider 4 individual vertices for each of the 6 sides of a cube. In that case, you also need to setup your index buffer differently because you need to refer to the appropriate normal vector.

vertices=new Vector3f[4*6];
normals=new Vector3f[4*6];

This will result in the flat shaded cube, if all your calculations and indices are correct.

To answer the last question, you basically need to figure out the 3 vertices of each triangle and then calculate the normal vector with a cross product of the two direction vectors, like so:

normal=c.subtract(a).cross(b.subtract(a)).normalizeLocal();

For flat shading, this normal vector should repeat for all three vertices of the same triangle.