Three.js: Bug in default shader with normal map

Created on 23 Sep 2019  路  29Comments  路  Source: mrdoob/three.js

Description of the problem

There are rending issues due to the default shader (checked with MeshStandardMaterial and MeshPhongMaterial) when using the normal map if all the texture coordinates for the face are exactly the same.

I had exactly the same issue with other shaders and this was related to the dFdx(uv) and dFdy(uv) shader functions returning 0 and leading to a division by 0.

Live example:

Three.js version
  • [x] Dev
  • [ ] r108
  • [x] r104
Browser
  • [ ] All of them
  • [x] Chrome
  • [x] Firefox
  • [ ] Internet Explorer
OS
  • [ ] All of them
  • [ ] Windows
  • [ ] macOS
  • [x] Linux
  • [ ] Android
  • [ ] iOS
Hardware Requirements (graphics card, VR Device, ...)
OpenGL vendor: NVIDIA Corporation (0x10de)
OpenGL renderer: GeForce RTX 2060/PCIe/SSE2 (0x1f08)
OpenGL version: 4.6.0 NVIDIA 430.40

Most helpful comment

This is what I believe is happening...

Even if the uv's at each vertex are identical, interpolation of uv's across the face of the primitive can result in different values due to numerical roundoff.

Consequently,

vec2 st0 = dFdx( vUv.st );

may not be the zero vector we would hope it would be.

The next line

float scale = sign( st1.t * st0.s - st0.t * st1.s );

will return - 1, 0, or 1.

And the following line is normalizing something that is very close to zero

vec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );

which just amplifies the numerical errors since it returns a unit-length vector.

All 29 comments

TBH, I do not understand the bug. Especially since uv_grid_opengl.jpg is not a normal map. It's a map for debugging texture coordinates. Why do you assign it to .normalMap?

Just for simplicity because it doesn't matter which texture you use.
Even if you use a normal map, you get exactly the same rendering bug, i.e. the black dots in the appearance.

Here is the modified jsfiddle where the floor diffuse and normal maps are used:
https://jsfiddle.net/b3tva4em/~~ _fixed version:_ https://jsfiddle.net/q4rwtvjy/

And the rendering bug is exactly the same.

Note that this is a simplified example to show the issue.
It doesn't make sense to create a simple triangle like this, but this situation is very likely with default texture mapping.

the black dots in the appearance.

Can you please share a screenshot that shows how the fiddle is rendered on your computer? I'm not sure I can see them. The triangle looks like so on my iMac:

image

Oops, sorry.
I already noticed that this is not reproducible on all the systems.

rendering_bug

I could reproduce it on two different Linux machines with NVIDIA graphics card.
But not on the mac where I tested.

Does it make any differences if upgrade to the latest NVIDIA driver (from 430.40 to 430.50)?

https://www.nvidia.com/Download/driverResults.aspx/151568/en-us

I just upgraded to 430.50 and tested but it doesn't make any difference.

Unfortunately seems to be a bug in the driver and/or device. You may want to report the issue to Nvidia instead.

Ok, thank you.

I thought you might be interested in making your shaders more robust (even if the problem comes from the NVIDIA driver) by adding some checks to avoid division by 0 or other undefined operations.

Fixing the driver issue is definitely more expedient than building something around it.

Fixing the driver issue is definitely more expedient than building something around it.

I completely agree.
I'm not an expert but to me the result I get with the Nvidia driver make sense.

In any case thank you for looking at my issue!

I thought you might be interested in making your shaders more robust (even if the problem comes from the NVIDIA driver) by adding some checks to avoid division by 0 or other undefined operations.

You can tweak these shaders directly by modifying THREE.ShaderChunk.normalmap_pars_fragment and/or THREE.ShaderChunk.normal_fragment_begin before creating a material.

Let us know if you find a workaround that solves the bug in your system and depending on the performance implications we can consider it.

@stefaniapedrazzi You fiddles throw CORS errors. It would be nice if you would be careful and provide an example that uses a proper normal map that does not throw errors.

@mrdoob @Mugen87 Are you sure this a device issue? Have you confirmed that the shader does not divide by zero when the uv's are the same?

@mrdoob @Mugen87 Are you sure this a device issue? Have you confirmed that the shader does not divide by zero when the uv's are the same?

We just can't reproduce. But seems like @stefaniapedrazzi may be able to help out with this?

I fixed the last fiddle to fix the CORS error: https://jsfiddle.net/q4rwtvjy/

We just can't reproduce. But seems like @stefaniapedrazzi may be able to help out with this?

If I have some time I will try to find out where does the issue come from and detect if there are invalid operation (at least on my system).

I fixed the last fiddle to fix the CORS error: https://jsfiddle.net/q4rwtvjy/

For what it's worth: With a current Ubuntu and an integrated Intel HD4000(4500 maybe) I'm getting a completely black screen with material.normalMap = normalMap; active and the same gray triangle that @Mugen87 posted when I comment it out. No weird effects beyond that.

I found out that the problematic operation is the normalize function with zero-vector here (both S and T):
https://github.com/mrdoob/three.js/blob/84f9c5145548f67ffebf6e82478df97118c7bfd3/src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js#L26-L32

if I change line 31 and 32 to the following the weird effect is solved:

vec3 S = ( q0 * st1.t - q1 * st0.t ) * scale;
vec3 T = ( - q0 * st1.s + q1 * st0.s ) * scale;
if (S != vec3(0.0))
  S = normalize(S);
if (T != vec3(0.0))
  T = normalize(T);

It seems like the current implementation of the normalize function of the Nvidia driver I use doesn't handle zero-vectors.

@stefaniapedrazzi Could you run the WebGL conformance test in your system?

https://www.khronos.org/registry/webgl/sdk/tests/webgl-conformance-tests.html

If there is no test for this issue would be great to add it.

This is what I see on my iPhone.

IMG_0323

if (S != vec3(0.0))

Just wanna say that additional if statements in shader code _can_ degrade performance. It would be better to have a solution without conditional branching.

Just wanna say that additional if statements in shader code can degrade performance.

@Mugen87 The OP is only trying to help locate the issue at this point.

This is what I believe is happening...

Even if the uv's at each vertex are identical, interpolation of uv's across the face of the primitive can result in different values due to numerical roundoff.

Consequently,

vec2 st0 = dFdx( vUv.st );

may not be the zero vector we would hope it would be.

The next line

float scale = sign( st1.t * st0.s - st0.t * st1.s );

will return - 1, 0, or 1.

And the following line is normalizing something that is very close to zero

vec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );

which just amplifies the numerical errors since it returns a unit-length vector.

The OP is only trying to help locate the issue at this point.

@WestLangley Not sure why you mention this...

@Mugen87 I think @WestLangley means that it's better to not worry about performance issues while we try to pinpoint where the artifacts are coming from.

@stefaniapedrazzi Could you run the WebGL conformance test in your system?

https://www.khronos.org/registry/webgl/sdk/tests/webgl-conformance-tests.html

If there is no test for this issue would be great to add it.

I ran the WebGL conformance test and all the tests about normalize function and all/conformance/glsl/functions are passing.
The normalization of a zero vector is not tested, but the khronos documentation doesn't specify what should normalize do for a zero-vector.
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/normalize.xhtml

FYI @kenrussell @kainino0x

The example by @stefaniapedrazzi is too trivial to draw conclusions from. The artifacts are not "solved" by his proposed changes -- the artifacts may not be visible for one use case, but the normals are not correct. Add OrbitControls and use MeshNormalMaterial to see more clearly what is happening.

I do not think this is a device issue. This is an obvious limitation of the algorithm; there is no reason it should work if the triangle uvs are co-equal.

I think https://github.com/mrdoob/three.js/issues/17559#issuecomment-534555812 explains what is happening.

I would remove the "device issue" label and consider this an edge case the algorithm does not handle.

Was this page helpful?
0 / 5 - 0 ratings