Accessing un-interpolated face normals?

I started out wanting to create flat shading instead of smooth shading and went down a bit of a rabbit hole.

  1. I’m creating a low-poly game, and I’ve got assets exported from blender with flat shading, those look good. But I also want to generate some low poly terrain. It comes out looking smooth because the normals get interpolated.
  2. Ideally the flat interpolation qualifier is what I’m looking for, to send an un-interpolated normal from the vertex shader to the fragment shader, (but that doesn’t seem to be supported yet in WebGL?)
  3. Failing that, I decide to try and calculate the normal myself at the fragment shader. I know I can do this with the method described here, which uses the derivative functions in GLSL.
  4. However, I keep getting the error:
'dFdx' : no matching overloaded function found 

When using WebGL 2. And when switching to WebGL 1:

'GL_OES_standard_derivatives' : extension is disabled

Which is odd because running:

var available_extensions = context.getSupportedExtensions();
console.log(available_extensions);

Shows that the standard_derivatives extensions is supported (but only when using WebGL 1).

I even tried manually injecting #extension GL_OES_standard_derivatives : enable into the shader chunks, but I might not be doing this right (since it’s not documented, I just perused the source and made some guesses) and it seems to have no effect.

So what gives? And finally, is there an easier way to get flat shading on a mesh that I dynamically generated?

I think the issue you’re having with standard derivatives is that it is only available as an extension in WebGL 1. In GL2 it is always there (so the extension doesn’t exist). That means you have to do do the enabling a little differently:

#ifdef GL_OES_standard_derivatives
    #extension GL_OES_standard_derivatives : enable
    #define standard_derivatives true
#endif

#ifdef GL2
    #define standard_derivatives true
#endif

Thanks for the reply dave!

That makes a lot of sense. I just tried that, but I’m still getting the error:

ERROR: 0:9: 'dFdx' : no matching overloaded function found 

This is the project if you’d like to take a look. I’m adding that snippet you provided to the shader chunks in CustomShader.js and the actual shader trying to use dFdx is in FragShader.

Hmm

So it looks like you need to use: new pc.BasicMaterial() not new pc.Material() for it to work.

I’m not sure exactly why, @will or @mr_f can probably help.

Sorry to bother you again @dave, but I’m still having trouble with this.

When I change it to new pc.BasicMaterial() it seems like the shader has no effect at all (like shaders aren’t being applied to the material). Do I need to do something different when applying it to a BasicMaterial ? Do you have a working example of this I could look at?

Alternative Way of Doing Flat Shading?

On a slightly tangential note, I’ve seen discussions of a different technique, instead of calculating the normal at each pixel like that, to just change the normals that are passed to the vertex shader.

I believe this is how Three.js does it. Just compute the face normals, and then set that face normal on each vertex so even after intepolation, everything still looks flat. (That way you can achieve flat shading without any custom shaders). Although I haven’t been able to correctly compute this on a generated mesh in PlayCanvas.

So if you know of any example of that, that would also help!

I don’t think is any issue doing this in PlayCanvas. It just depends on how you generate your vertex and index buffers. For each vertex you will need a separate normal and then update the index buffer to match.

For future reference, in case anyone else got stuck on the same thing too, I finally managed to get it right. I forked the terrain generation tutorial demo.

Before:

After:

Here’s the project. I essentially just had to do exactly what was described above, duplicate the vertices so that instead of say, drawing 2 triangles out of 4 vertices by reusing them, I’d have 3 vertices for each triangle for a total of 6.

And here’s just a function that takes in vertex data and restructures it to get a flat look:

Terrain.prototype.makeGeometryFlat = function(vertexData){
    // Takes vertex data containing {indices,positions,normals,uvs} 
    // and duplicates the verticies so that the normals don't get interpolated 
    // returns new vertex data 
    
    var indices = vertexData.indices; 
    var positions = vertexData.positions; 
    var uvs = vertexData.uvs; 
    
    var new_indices = [];
    var new_positions = [];
    var new_uvs = [];
    
    for(var i = 0;i<indices.length;i++){
        // Loop over all the verticies 
        // Create a new positions for each 
        var index = indices[i];
        var x = positions[index*3];
        var y = positions[index*3+1];
        var z = positions[index*3+2];
        new_positions.push(x,y,z);
        
        // Do the same for UV's 
        var u = uvs[index*2];
        var v = uvs[index*2+1];
        new_uvs.push(u,v);
        
        // Use this new position in our index array 
        new_indices.push(i);
    }
    
    var normals = pc.calculateNormals(new_positions, new_indices);
    
    return {
        indices: new_indices,
        positions: new_positions,
        normals: normals,
        uvs: new_uvs
    };
};
1 Like