5
votes

If you subdivide a cylinder into an 8-sided prism, calculating vertex normals based on their position ("smooth shading"), it looks pretty good.

If you subdivide a cone into an 8-sided pyramid, calculating normals based on their position, you get stuck on the tip of the cone (technically the vertex of the cone, but let's call it the tip to avoid confusion with the mesh vertices).

8-sided cylinder vs cone

For each triangular face, you want to match the normals along both edges. But because you can only specify one normal at each vertex of a triangle, you can match one edge or the other, but not both. You can compromise by choosing a tip normal that is the average of the two edges, but now none of your edges look good. Here is a detail of what choosing the average normal for each tip vertex looks like.

face detail with tip averaging

In a perfect world, the GPU could rasterize a true quad, not just triangles. Then we could specify each face with a degenerate quad, allowing us to specify a different normal for the two adjoining edges of each triangle. But all we have to work with are triangles... We can cut the cone into multiple "stacks", so that the edge discontinuities are only visible at the tip of the cone rather than along the whole thing, but there will still be a tip!

Anybody have any tricks for smooth-shaded low-poly cones?

2
You need to set the vertex normals of each face, you should be able to do this even if you have to create extra vertices. This will allow an approximation of smooth shading to be performed. It won't be perfect but it will be better.ChrisF♦
In the images above, the tip vertex normal is set to the face normal. The two lower vertices have their normals set to match their neighbors. As a result, the shading is smooth at the very base, but is quickly contaminated by interpolation with the tip vertex. If I set all the vertex normals to be the face normal, I would get flat shading.Ned Twigg
you should duplicate the vertex on the tip for each triangle on the side, and set a different normal for each.nbonneel
Your suggestion works if I could pass a quad to the GPU, but I can't. If I duplicate the tip vertex, then there will be two overlapping triangles, and they will show through each other.Ned Twigg

2 Answers

10
votes

I was struggling with cones in modern OpenGL (i.e. shaders) made up from triangles a bit but then I found a surprisingly simple solution! I would say it is much better and simpler than what is suggested in the currently accepted answer.

I have an array of triangles (obviously each has 3 vertices) which form the cone surface. I did not care about the bottom face (circular base) as this is really straightforward. In all my work I use the following simple vertex structure:

  • position: vec3 (was automatically converted to vec4 in the shader by adding 1.0f as the last element)

  • normal_vector: vec3 (was kept as vec3 in the shaders as it was used for calculation dot product with the light direction)

  • color: vec3 (I did not use transparency)

In my vertex shader I was only transforming the vertex positions (multiplying by projection and model-view matrix) and also transforming the normal vectors (multiplying by transformed inverse of model-view matrix). Then the transformed positions, normal vectors and untransformed colors were passed to fragment shader where I calculated the dot product of light direction and normal vector and multiplied this number with the color.

Let me start with what I did and found unsatisfactory:

Attempt#1: Each cone face (triangle) was using a constant normal vector, i.e. all vertices of one triangle had the same normal vector. This was simple but did not achieve smooth lighting, each face had a constant color because all fragments of the triangle had the same normal vector. Wrong.

Attempt#2: I calculated the normal vector for each vertex separately. This was easy for the vertices on the circular base of the cone but what should be used for the tip of the cone? I used the normal vector of the whole triangle (i.e. the same value as in attempt#). Well this was better because I had smooth lighting in the part closer to the base of the cone but not smooth near the tip. Wrong.

But then I found the solution:

Attempt#3: I did everything as in attempt#2 except I assigned the normal vector in the cone-tip vertices equal to zero vector vec3(0.0f, 0.0f, 0.0f). This is the key to the trick! Then this zero normal vector is passed to the fragment shader, (i.e. between vertex and fragment shaders it is automatically interpolated with the normal vectors of the other two vertices). Of course then you need to normalize the vector in the fragment (!) shader because it does not have constant size of 1 (which I need for the dot product). So I normalize it - of course this is not possible for the very tip of the cone where the normal vector has the size of zero. But it works for all other points. And that's it.

There is one important thing to remember, either you can only normalize the normal vector in the fragment shader. Sure you will get error if you try to normalize vector of zero size in C++. So If you need normalization before entering into fragment shader for some reason make sure you exclude the normal vectors of size of zero (i.e. the tip of the cone or you will get error).

This produces smooth shading of the cone in all points except the very point of the cone-tip. But that point is just not important (who cares about one pixel...) or you can handle it in a special way. Another advantage is that you can use even very simple shader. The only change is to normalize the normal vectors in the fragment shader rather than in vertex shader or even before.

example of smooth cone here

4
votes

Yes, it certainly is a limitation of triangles. I think showing the issue as you approach a cone from a cylinder makes the problem quite clear:

enter image description here

Here's some things you could try...

  1. Use quads (as @WhitAngl says). To hell with new OpenGL, there is a use for quads after all.

  2. Tessellate a bit more evenly. Setting the normal at the tip to a common up vector removes any harsh edges, though looks a bit strange against the unlit side. Unfortunately this goes against your question title, low polygon cone.

    enter image description here

  3. Making sure your cone is centred around the object space origin (or procedurally generating it in the vertex shader), use the fragment position to generate the normal...

    in vec2 coneSlope; //normal x/z magnitude and y
    in vec3 objectSpaceFragPos;
    
    uniform mat3 normalMatrix;
    
    void main()
    {
        vec3 osNormal = vec3(normalize(objectSpaceFragPos.xz) * coneSlope.x, coneSlope.y);
        vec3 esNormal = normalMatrix * osNormal;
        ...
    }
    

    Maybe there's some fancy tricks you can do to reduce fragment shader ops too. Then there's the whole balance of tessellating more vs more expensive shaders.

A cone is a fairly simple object and, while I like the challenge, in practice I can't see this being an issue unless you want lots of cones. In which case you might get into geometry shaders or instancing. Better yet you could draw the cones using quads and raycast implicit cones in the fragment shader. If the cones are all on a plane you could try normal mapping or even parallax mapping.