Thursday, February 16, 2017

opengl - How do I implement flat shading in GLSL?


I'm working with GLSL and trying to implement flat shading on a 3D model (rather than smooth shading). To illustrate what I mean, here are two screenshots of cubes in Blender. Here's one with flat shading.


enter image description here


And here's the same cube with smooth shading.



enter image description here


I understand the theory behind this kind of shading. Each face on the cube (six total) has a normal facing away from the surface. Each vertex (eight total) has a normal computed by summing together face normals, then normalizing to unit length. This results in each vertex normal pointing directly away from the center of the cube.


Smooth shading can be implemented in basically two ways. In the first, color is computed per-vertex (using light direction and normal), then fragment color is interpolated among all vertices. In the second, normals themselves are interpolated, then color is computed per-fragment (using the same lighting calculations).


Here are my current GLSL shaders to implement the first option (there's no specular lighting yet, but it gets the idea across with ambient and diffuse). Vertex shader first.


in vec3 vPosition;
in vec3 vNormal;

out vec4 fColor;

uniform mat4 mvp;

uniform vec3 aColor;
uniform vec3 lDirection;
uniform vec3 lColor;

void main()
{
gl_Position = mvp * vec4(vPosition, 1);

vec4 ambient = vec4(aColor, 1);
vec4 diffuse = vec4(max(dot(lDirection, -vNormal), 0) * lColor, 1);


fColor = ambient + diffuse;
}

Then fragment.


in vec4 fColor;

void main()
{
gl_FragColor = fColor;

}

So that works fine for smoothed shading, with fragment values interpolated among vertices. I'll also point out that I'm using buffers and index arrays for rendering.


For flat shading, each fragment on a face should instead use the same normal (such that every pixel on the surface has the same final color after lighting calculations). The problem is that I can't pass data to shaders per-face, but only per-vertex. Given this, I can think of three solutions.




  1. Pass four vertices per face, with each vertex storing the face normal. This would still technically be smooth shading, but done in such a way that every interpolated pixel will use the same color (making if effectively flat). This approach seems wrong because it would basically ruin my vertex buffer, since I'd have to pass 24 vertices (four per face) despite the cube only containing eight unique vertices.




  2. Use GLSL's flat mode (there's a flat keyword in GLSL). Using this approach, each fragment would only pull from a single "provoking vertex" rather than interpolating from all vertices on the face. This feels wrong because that I wouldn't actually be using the correct face normal. I also haven't been able to figure out the proper syntax for this style anyway. For the record, I'm aware of glShadeModel, but it's apparently deprecated.





  3. Average vertex normals per fragment rather than interpolating them. To me, this feels like exactly the correct solution, since every pixel on a face would use the same normal, with that normal computed by summing and normalizing vertex normals (similar to how vertex normals are computed from face normals to begin with).




From those options, #3 clearly feels like the correct solution, but I haven't had any luck in figuring out how. So that's my question.


How can I tell the fragment shader to use a normal averaged among all vertices, rather than interpolated?



Answer



You may have heard 3D modelers talk about "hard edges" or "sharp edges", which are roughly equivalent to what you're looking for here. When modelers create a hard edge, their software will internally utilize your first method: Pass four vertices per face, with each vertex storing the face normal.


This allows a modeler to determine which edges of the object should be "smooth" (with light and other values interpolated between faces) and which should be "sharp" (with light calculated separately for each face), without needing to reconfigure the render pipeline.



That's especially useful because the "sharpness" of an edge is usually a property of that model. If this is something you wanted to manage scene-wide, changes to shaders and such might be more appropriate.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...