Flat Shading using legacy GLSL on OSX

Flat shading gives geometry an edgy and geometric look and keeps the faces of meshes sharp and clean. Sometimes that’s all you want. With commonly used meshes the normals are usually interpolated between vertices which can make flat-shading wasteful. This post discusses how to optimise geometry for flat-shading. It also shows how to get OpenGL to do the right thing when faced with provoking vertices.

By default, vertex normals are interpolated across triangles. A naive approach to flat shading would be to break up a conventional mesh into individual triangles, and duplicate the per-triangle normal across all vertices. But this makes vertex re-use impossible. For example, to render a simple flat shaded box using 6 * 6 vertices, meaning you have lots of duplicate vertex positions and a mesh that is a mess.

We want to keep the vertex count low, and vertices unique. We achieve this by telling the shader that we don’t want to use interpolated normals, but want to use the normal of one special vertex as the normal for the whole triangle. This vertex is called a Provoking Vertex.

Note that future versions of GLSL ( > version 140 ) will have the ‘flat’ attribute available by default, and that this tutorial has been written as a lament to OS X’s currently wilful OpenGL implementation.

This tutorial is best read if you download the example, look at its source and then compare the following walk-through.

This is how the example app should look like.

Files 

This tutorial comes with a working example project (see screenshot above). Place this into your openFrameworks apps/dev directory:

Here’s how to do it:

Build a mesh 

Start numbering at the bottom left, front face. Then go counter-clockwise. When done with the front face, start bottom left, back face; go counter-clockwise, too. This numbering scheme will help you, should you happen to want to extrude along the z-axis sometime in the future.

image
Make a drawing on paper and number your vertices. It helps. (N.b. normals in blue; The origin of the coordinate system is at the centre of the cube.)

E.g. a cube with 8 vertices:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ofVec3f vertices[] = {
	ofVec3f(-1, -1,  1),		// front square vertices
	ofVec3f( 1, -1,  1),
	ofVec3f( 1,  1,  1),
	ofVec3f(-1,  1,  1),
	
	ofVec3f(-1, -1, -1),		// back square vertices
	ofVec3f( 1, -1, -1),
	ofVec3f( 1,  1, -1),
	ofVec3f(-1,  1, -1),
};

Triangulate the Mesh 

by convention begin with the lowest index. Start with that index to draw both triangles that form one quad of the box. To do this, draw a diagonal from your current starting index to the opposing vertex to get two triangles. List the 3 indices for each triangle. Winding is anti-clockwise when listing a triangle facing the camera, and clockwise when listing a triangle which is hidden. When done, move to the next face. Start drawing your next two triangles with the next index.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
ofIndexType indices[] = {
	// -- winding is counter-clockwise (facing camera)
	0,1,2,		// pos z
	0,2,3,
	
	1,5,6,		// pos x
	1,6,2,
	
	2,6,7,		// pos y
	2,7,3,
	
	// -- winding is clockwise (facing away from camera)
	3,4,0,		// neg x
	3,7,4,
	
	4,5,1,		// neg y
	4,1,0,
	
	5,7,6,		// neg z
	5,4,7,
};

Note that pairs of triangles use the same vertex as the starting vertex. This will be our provoking vertex, the vertex whose normal is the normal for the whole triangle. Provoking vertices work by telling GLSL that either the first or the last vertex of a triangle holds the normal for the whole triangle. Since both triangles sit on the same face, they can share a normal and a provoking vertex by starting with the same index.

Add Normals 

For every vertex, you’d need one normal. The normal should be the perpendicular to the triangle that begins with your vertex index. Look at the indices to find the triangle in your drawing. When you have found the triangle, you should see from the drawing which face it is part of, and thus get the required normal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ofVec3f normals[] = {
	ofVec3f( 0,  0,  1),
	ofVec3f( 1,  0,  0),
	ofVec3f( 0,  1,  0),
	ofVec3f(-1,  0,  0),
	ofVec3f( 0, -1,  0),
	ofVec3f( 0,  0, -1),
	ofVec3f( 1,  0,  0), // can be anything, will not be used
	ofVec3f( 1,  0,  0), //  -- " --
};

Choose a Convention 

When flat shading, GLSL/OpenGL uses one vertex normal as the normal for the whole triangle this is called the provoking vertex. By default, OpenGL uses the last vertex, as GL_LAST_VERTEX_CONVENTION is set. We don’t want this, but want to use the first vertex.

1
2
3
4
5
6
7
8
myShader.begin();

glShadeModel(GL_FLAT);
glProvokingVertex(GL_FIRST_VERTEX_CONVENTION);
myMesh.draw();
glShadeModel(GL_SMOOTH);

myShader.end();

Use Extensions 

GLSL will allow flat attributes from version 140 on, but OSX’s default OpenGl version 2.0 coming with GLSL 120 doesn’t support flat varyings in shaders out of the box.

We thus need to activate an extension to get some modern niceties from our shader processor, and then we can use the “flat” specifier for our varying.

1
2
3
4
5
6
7
8
#extension GL_EXT_gpu_shader4 : require

flat varying vec3  normal;	// note 'flat varying'

void main(){
	normal = gl_NormalMatrix * gl_Normal;
	gl_Position = ftransform();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#extension GL_EXT_gpu_shader4 : require

flat varying vec3  normal;	// note 'flat varying'

void main(){
	vec3 N = normalize(normal); 

	// voila, we can use our flat normal! 
	// This will colour our fragment with the current normal for debug purposes.
	gl_FragColor = vec4((N + vec3(1.0, 1.0, 1.0)) * 0.5,1.0);
}

You can find more information on this extension at the opengl registry.


Tagged:

tutorialcode


RSS:

Find out first about new posts by subscribing to the RSS Feed


Further Posts:

Vulkan Video Decode: First Frames h.264 video island rendergraph synchronisation vulkan code
C++20 Coroutines Driving a Job System code coroutines c++ job-system
Vulkan Render-Queues and how they Sync island rendergraph synchronisation vulkan code
Rendergraphs and how to implement one island rendergraph vulkan code
Implementing Bitonic Merge Sort in Vulkan Compute code algorithm compute glsl island
Callbacks and Hot-Reloading Reloaded: Bring your own PLT code hot-reloading c assembly island
Callbacks and Hot-Reloading: Must JMP through extra hoops code hot-reloading c assembly island
How far back should the screen go? math tutorial
Earth Normal Maps from NASA Elevation Data tutorial code
Using ofxPlaylist tutorial code