Having Geometric Specular Antialiasing would be incredible

Hello! How’s going?

I’m working on a offsceen canvas model visualizer for fine art prints that we make in our creative studio. It’s a PBR workflow and so far, it looks really good. However, at certain angles, the aluminum material from the back of the object will cause a lot of specular aliasing on it.

It was a lot worse before because the front face of the aluminum object (just behind the artwork plane) would be lit by the environment map, so I had to set a black 1.0 roughness material to it. Good workaround! Still, what’s left is the specular aliasing from the sides of the aluminum back.

Things that I’ve tried so far:

  • FXAA. It only made everything a bit blurrier. Specular aliasing kept being the same.
  • MSAA. It does improve things a bit, but not that much. Going above x4 not only did not improve specular aliasing at all, but it also became a resource hog. My implementation runs a small benchmark to determine if MSAA should be enabled, and also if resolution should be lowered. MSAA would be disabled in a lot of mobile devices, resulting in worse aliasing. Interestingly enough, geometry aliasing is not that bad compared to the specular ones, which are really distracting and prominent.
  • SMAA. Compared to FXAA, targets the edges a bit better, leaving the rest of the image looking sharper. Still, like FXAA, it didn’t help at all.
  • Setting the HDRI map to work with PMREM. AFAIK, it should work out of the box without really calling PMREM manually. It did nothing, it looked the same.

Things that will make it worse:

  • Beveling and curving the edges of the glass and aluminum in Blender. While it does make it more visually realistic at certain angles (really, it is an upgrade in realism) and I would love to have it, it caused soooo much specular aliasing, fireflies and sparkling at the silhouette that I can’t justify to have. I also made sure the normal map of the aluminum didn’t map in the bevels, but still the problem persisted. It even happened with the glass. The current demo in the website is the model without bevels because honestly, it was that bad. Not even MSAA x8 could save it.

Here is where Geometric Specular Antialiasing comes in

I do believe this is a problem that should be mitigated from the shader pipeline. Valve explored this concept years ago when they began exploring VR. Highly recommended to watch their talk. They had to tackle aliasing as best as possible in both performance and visual terms, because having aliasing + low framerate in VR it just degrades the experience so much.

They came up with a cool math which, to my understanding, will increase the roughness values of the pixels that will be prone to have high specularity values when viewed at specific camera + geometric angles. Correct me if I’m wrong about it.

Here is their code:

// Dense meshes without normal maps also alias, and roughness mips can’t help you!
// We use partial derivatives of interpolated vertex normals to generate a geometric
roughness term that approximates curvature. Here is the hacky math:

float3 vNormalWsDdx = ddx( vGeometricNormalWs.xyz );
float3 vNormalWsDdy = ddy( vGeometricNormalWs.xyz );
float flGeometricRoughnessFactor = pow( saturate( max( dot( vNormalWsDdx.xyz, vNormalWsDdx.xyz ), dot( vNormalWsDdy.xyz, vNormalWsDdy.xyz ) ) ), 0.333 );
vRoughness.xy = max( vRoughness.xy, flGeometricRoughnessFactor.xx ); // Ensure we don’t double-count roughness if normal map encodes geometric roughness
// MSAA center vs centroid interpolation: It’s not perfect
// Normal interpolation can cause specular sparkling at silhouettes due to over-interpolated vertex normals
// Here’s a trick we are using:

float3 vNormalWs : TEXCOORD0;
centroid float3 vCentroidNormalWs : TEXCOORD1; 

// In the pixel shader, choose the centroid normal if normal length squared is greater than 1.01

if ( dot( i.vNormalWs.xyz, i.vNormalWs.xyz ) >= 1.01 ) {
  i.vNormalWs.xyz = i.vCentroidNormalWs.xyz;
}

The talk slides where they mention this are here and the specific timing in the video presentation is here.

My (failed?) attempt to implement it

I’m just getting started in the 3D world. I know pretty much nothing about shader code. All I could do is dump the compiled PBR shader code in three.js and tried to inject Valve’s code where material.roughness is calculated. Although I could load properly load the shader, it didn’t seem to antialias anything.

Valve’s code to Three.js looked like this:

vec3 normalW = inverseTransformDirection( normal, viewMatrix );
vec3 nDfdx = dFdx(normalW);
vec3 nDfdy = dFdy(normalW);
float slopeSquare = max(dot(nDfdx, nDfdx), dot(nDfdy, nDfdy));

float geometricRoughnessFactor = pow(saturate(max(dot(nDfdx, nDfdx), dot(nDfdy, nDfdy))), 0.333);

material.roughness = max( roughnessFactor, 0.0525 );
material.roughness = min( max(material.roughness, geometricRoughnessFactor ), 1.0 );

Note that I might be completely off in my assumption of what vec3 normalW and the rest of the values are because as mentioned, my knowledge about shaders is almost non existant. I also skipped the centroid part because I have no idea how and where to place that code.

Here’s geometricRoughnessFactor dumped directly using gl_FragColor = vec4(geometricRoughnessFactor * 6.0, 0.0, 0.0, 1.0);

Keep in mind I’ve amplified the value x6 to make the lines at the edge look better in the screenshot.

Moving the camera results in smooth edges reveal with little to no aliasing. But for some reason this doesn’t really translate into further specular calculations at all. Seems like the values are not sufficient, although I’m not sure.

Thoughts? Can this be implemented in three.js?

I believe specular aliasing is one of those problems people would quickly point out TAA as the solution to it. I don’t think it is. If the problem lies in the shader pipeline, TAA would be a rough patch on top of it. It won’t solve the problem completely. Furthermore, proper TAA with motion vectors (if I understand correctly) would be a complement to proper geometric specular antialiasing implementation, creating smooth pleasing edges when objects and cameras are in motion.

I do think Valve’s approach is pretty clever when it comes to good performance + lovely visuals. It is a low hanging fruit! Tackling this from the shader pipeline would be a good idea, or at least to have it as a toggle.

And please if someone knows how to properly modify the shader code to make this to work, it would be incredible <3. Thank you!

2 Likes

That aliasing looks it might be related to the Bloom applied?

There’s are a bunch of different antialising techniques available in various forms for threejs… (as a post process)

MSAA, SMAA, TAA, FXAA, to name a few…

all of these are really sensitive to having the renderer set up correctly… renderer dimensions being set up correctly. Post procesing pipeline being set up correctly… passes appllied in the right order…

sample count settings on the rendertargets…

There are a lot of little parts to get right.

So there may still be some hope that you can get an existing solution…

r.e. “modifying the shader code to make it work”… there is
https://ycw.github.io/three-shaderlib-skim/dist/#/latest/standard/fragment
site (and other similar sites) that let you browse the unrolled shader code for the built in materials. This can help a lot when trying to modify the built in shaders.

You can perform global modifications to all shaders by modifying THREE.ShaderChunks (iirc… forget what its called) to edit the chunks globally…

Or you can use material.onBeforeCompile = (shader)=>{

to intercept an instance of a materials shader before it gets compiled, and perform surgery on its vertex and fragment shaders.

1 Like

Thanks for your answer. However, I managed to set up all the steps correctly, including the order of the passes and their respective resolution, with the appropiate pixel ratio and all that. Bloom is unrelated, it is basically the last pass and disabling it won’t change the specular aliasing. Even more, just rendering directly without any effects will still have the aliasing problem.

Also, as mentioned too, I didn’t have problems injecting vertex or fragment code into the materials. My method dumps the exact compiled shader of three.js (this is the fragment one) so I can modify it anywhere I want without having to manually monkey-patch. Nonetheless, this is not really the point I wanted to discuss. I can edit the shader but unfortunately I know pretty much nothing about shader programming, that I don’t know how to implement Valve’s solution to three.js PBR shader pipeline.

The purpose of this post is to share have a discussion about specular aliasing, and to explore ways to properly address it. Antialiasing methods are not efficient in tackling it. MSAA is the best when it comes to it, and yet there’s work to be done. Supersampling the image is very expensive and also won’t guarantee to fix it completely.

If someone knows or have an idea on what to do to at least improve things in a notable way, it would be great. I do believe a good solution lies in the shader pipeline when it comes to roughness and specular calculations

1 Like

If you’d like to see this in three, your best bet would be to create a minimal example showing the specular aliasing and making a three.js issue linking the paper as well as a compelling before / after image from the paper or other reference implementation showing the improvement. Ideally your minimal example would use the same or similar model to your comparison screenshots but I know that may not be possible. At least someone should be able to point you to the right area where your fix code can be added or provide some more insight into whether there would be interested in adding this to the project.

I’d also recommend keeping your issue in the repo short and to the point. Detail can be nice but longer issues become more difficult to follow and I’ve noticed can less engagement. I might also take a look at what else is out there since it looks like there are some more recent papers on the topic.

Remove sub-pixel artifact (no background):

  • render 2x and downscale… stencil ROI?
  • bitwise not some shallow 45° edges mask
  • depth test