How to implement flat shading in OpenGL without duplicate vertices?
Asked Answered
C

2

10

I am trying to render 3D prisms in LWJGL OpenGL with flat shading. For example, I have a cube indexed as following:

enter image description here

I only have 8 vertices in the vertex buffer, which I have indexed as above. Is there any way to implement flat normal shading on the cube such as below? I don't want to rewrite my vertex and index buffers to include duplicate vertices if possible.

Crescantia answered 2/2, 2020 at 1:42 Comment(7)
When you say "flat shading", what exactly do you mean by that? Do you mean that you want to compute a face normal which is constant across the triangle's surface, or do you actually want to compute lighting in the VS? Or something else. "I don't want to rewrite my vertex and index buffers to include duplicate vertices if possible." Why not? It'd make things a whole lot easier.Tyrus
I've already implemented vertices and indices for a generalized nth dimensional 3D prism, so I'd prefer not to go back and rewrite all of that. When I say flat shading, I just mean creating something like the cube pictured above. I can already calculate the face normal for each triangle, but I don't know how to format them in the normals array.Crescantia
So you know how to write a diffuse shading shader, is your question, how you can send data to the GPU so that normals correspond to the right triangle, when you don't have an IBO?Preconcert
@Rabbid76 I am afraid you're slightly wrong in this case as for n-dimensional objects the math is not implemented in shaders natively and if any kind of projection into 3D occurs you can no longer compute normals from the projected faces without thge loss of information ... Of coarse in case the whole ND mesh is passed to shaders it might be possible but it would most likely not be passed in GLSL native way so geometry shader would be also useless.Carlycarlye
@Crescantia take a look at these: 4D rendering techniques and simple ND rendering engine for some ideas. However as I mentioned in the previous comment if you want correct Flat shading you need to compute and pass ND normals instead of 3D ones and adjust the lighting dot product to ND !!! The examples use just simple 3D normals so they lack the dimensions above 3D ... Its the same as you would project 3D object on a plane and shade it whole with single color so you would not see the edges ...Carlycarlye
@Crescantia would be interesting to see how you store your mesh and what rendering style you use ...Carlycarlye
If you are drawing basic solids like this and want to reduce the number of vertices being buffered, consider using a geometry shader.Greyson
G
11

If you don't need any other attributes (e.g. texture coordinates), then there is an option to create a cube mesh with face normal vectors, by 8 vertices only. Use the flat Interpolation qualifier for the normal vector.

Vertex shader:

flat out vec3 surfaceNormal;

Fragment sahder:

flat out vec3 surfaceNormal;

When the flat qualifier is used, then the output of the vertex shader will not be interpolated. The value given to the fragment shader is one of the attributes associated to one vertex of the primitive, the Provoking vertex.
For a GL_TRINANGLE primitive this is either the last or the first vertex. That can be chosen by glProvokingVertex.

Choose the first vertex:

glProvokingVertex(GL_FIRST_VERTEX_CONVENTION);

For the order of the points of your cube mesh (image in the question)

  front      back

  1     3    7     5
   +---+      +---+
   |   |      |   |
   +---+      +---+
  0     2    6     4

you have to setup the following vertex coordinates and normal vectors:

//  x   y   z   nx, ny, nz
    -1, -1, -1,   0, -1,  0,  // 0, nv front
    -1, -1,  1,   0,  0,  1,  // 1, nv top
     1, -1, -1,   0,  0,  0,  // 2
     1, -1,  1,   1,  0,  0,  // 3, nv right
     1,  1, -1,   0,  1,  0,  // 4, nv back
     1,  1,  1,   0,  0,  0,  // 5
    -1,  1, -1,   0,  0, -1,  // 6, nv bottom
    -1,  1,  1,  -1,  0,  0,  // 7, nv left 

Define the indices in that way, that the vertices 7, 3, 0, 4, 6, 1 are the first vertex for both triangles of the left, right, front, back, bottom and top of the cube:

0, 2, 3,   0, 3, 1, // front
4, 6, 7,   4, 7, 5, // back
3, 2, 4,   3, 4, 5, // right
7, 6, 0,   7, 0, 1, // left
6, 4, 2,   6, 2, 0, // bottom 
1, 3, 5,   1, 5, 7  // top

Draw 12 triangle primitives. e.g:

glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

Gruelling answered 2/2, 2020 at 10:19 Comment(6)
Honest question but, wouldn't you still need 3 different normals per vertex? One for each face? The problem here is not so much the interpolation but the facte that a single vertex needs a different normal depending on the currently rendered faceMatins
@Matins No, If the normal vectors are normal to the face (sides of the cube), then you need just 1 per triangle. If the 2 triangles of a side share the 1st vertex, the one normal vector for 2 triangles is enough. You need 1 normal vector per provoking vertex. The provoking vertices are defined by the indices: 0, 4, 3, 7, 6, 1Gruelling
How do you tell the GPU which normal to read? I am used to the pattern where there is one normal and uv per each vertex, and so I guess I am a bit confused as to how to render when those don't match.Matins
@Matins "How do you tell the GPU [...]" - keyword flat and glProvokingVertex(GL_FIRST_VERTEX_CONVENTION). That means the normal vector which is associated to the 1st vertex of the triangle is used, the 2nd and 3rd are ignored. Note, there are 8 vertices and 8 normal vectors. Its a matter of the indices and the order of drawing the triangles. There are no uv coordiadntes. You can't use this solution with texture coordinates, because it makes no sense to falt shade the texture.Gruelling
Ah I see, you permute the indices of the face, such that for each of the 3 potential triangles there is a different normal. So if I understand this thing correctly, if you had a triangle fan with more than 3 triangles sharing the same vertex, you would need something more fancy. Or am I totally wrong?Matins
@No, you are right. If you investigate, the vertex array in the question, then you'll see, that 2 of the normal vectors are (0, 0, 0). That's because they are always ignored, because the corresponding vertex coordinate is never 1st in a triangle. The other 6 specify the 6 normal vectors of a cube. When the cube is draw, then it has to be ensured, that the triangles start at the correct point, that one with the proper normal vector. The order is defined by the indices.Gruelling
P
0

For flat shading, it is better to use a geometry shader to compute the normals for each of the primitives. Although you can use the provoking-vertex method when rendering a cube, you cannot use it for certain geometric objects where the number of faces is more than that of the vertices: e.g. consider the polyhedron obtained by gluing two tetrahedra at their base triangle. Such an object will have 6 triangles but only 5 vertices (note that Euler's formula still holds: v-e+f = 5-9+6 = 2), so there are not enough vertices to send the face-normals via the vertices. Even if you can do that, another reason not to use provokig-vertex method is that it is not convenient to do so, because you would have to find a way to enumare the vertices in a way such that each vertex uniquely 'represents' a single face, so that you can associate the face-normal with it.

In a nutshell, just use a geometry shader, it is much simpler and more importantly much more robust. Not to mention that the normal calculations are done on the fly inside the GPU, rather than you having to set them up on CPU, creating & binding the necessary buffers and defining attributes which increases both the set-up costs and eats up the memory bandwith between the CPU and the GPU.

Pungy answered 14/2, 2023 at 6:4 Comment(8)
"For flat shading, it is better to use a geometry shader to compute the normals for each of the primitives." -No, unfortunately geometry shaders are slow. Don't use a geometry shader if it can easily be solved without one.Gruelling
"No, unfortunately geometry shaders are slow" - My advice was not about geometry shaders' speed, it was about its usefulness and generality with regards to the question (to remind you, the question was "without duplicate vertices") and also to point out to the fact that for some geometries there is no solution without single vertex being duplicated. As for the speed, it all depends on the application requirements anyway. Also you need to test (profile) using both methods and compare the results, did you do so? I would appreciate it if you can share any code we can look at.Pungy
You said "... it is better to use a geometry shader.... ". My concern was about the term "it is better". I say "no, it is not better".Gruelling
and I explained to you the context in which my statement has been made, but you still refuse to see this. you just quoted a sentence by taking it out of its surrounding context (which was about the question above) and put it into your context (which was about speed). Also, you failed to provide any proof for your claim regarding the speed, which makes it all the more suspicious. I am asking you again: where is your code to back your claim? I don't need to be convinced, I have noted down your claim and will test this, but where is your code to support your claim? Show me your code.Pungy
I don't think you understand what I'm getting at. I just do not agree with the phrase "it is better". This phrase is the introduction of your answer and therefore of special importance, but it is wrong with this generalization. This is my opinion, which I wanted to state here.Gruelling
Of course, you do not have to agree with anyone on anything. But I guess we are seeing this issue from different view points. But I will check the speed issue, thank you for pointing out to this. I am really curious and want to see this. If I get any results for my system, I'll share them here. Thank you.Pungy
""I just do not agree with the phrase "it is better". This phrase is the introduction of your answer and therefore of special importance" - let me state again: "it is better" is meant in the context of the question above. So if it is perceived as a generalization it should be taken into consideration within the bounds of the surrounding context, which is again the topic of this page. If you take such a statement as a generalization outside its surrounding context, this is your doing not mine. I also want to make this point clear.Pungy
Yes is see, but even in the context of the question it is not "better".Gruelling

© 2022 - 2025 — McMap. All rights reserved.