Since flat shading doesn’t need lighting information for each vertex, there should only be one sample for every face. However, is this possible to do in a compact format that avoids any redundancy? Doing it the conventional way for something as perfect as a cube would yield four times as many vertices as is required in theory, which is far from ideal. Plato would be rolling in his grave.

There are a few ways in more modern versions of OpenGL that allow this kind of data reduction, including instancing, geometry shaders, and programmable vertex fetching.

However.

The amount of memory saved by reducing data in this way is rarely a significant performance factor. It’s easy to fall into the trap of measuring vertex memory and making assumptions about performance based on those measurements, but vertex submission is just the front-end of what is a very deep graphics pipeline with important operations happening all the way through it.

Well, I was thinking of extreme cases where you would have thousands of cubes on screen, a la Minecraft.

Minecraft doesn’t draw cubes, so how would instancing help?

If a vertex shader output has the [var]flat[/var] qualifier, then the value generated for one vertex (the provoking vertex) will be used for the entire primitive; the values generated for the other vertices will be ignored. So if you have three triangles sharing the same vertex, with a suitable ordering of the vertices you can ensure that only one of them uses the value associated with that vertex.

For a cube, you can split the faces so that four vertices are shared by three triangles and the other four by six triangles. Duplicating the latter four vertices gives you twelve vertices, each shared by three triangles. Thus you only need a 50% increase in the total number of vertices for flat shading (or any other per-face attribute).

In the general case, a triangle mesh approximating a smooth surface typically has an average of six triangles sharing each vertex, so storing per-face information would require doubling the number of vertices.

Also: you can perform flat shading with needing to store face normals per-vertex by calculating them in the fragment shader using cross(dFdx(p),dFdy(p)), where p is the interpolated vertex position (in either eye space or object space; the computed normal will be in the same space).