GLTFLoader: Normal-Tangent Test model result is incorrect

Description of the problem

I tried to display the Normal-Tangent Test model.
However, the displayed result seems to be different from Khronos’ sample.

Three.js + Normal-Tangent Test model result:
image

Khronos sample loader + Normal-Tangent Test model result:
image

I think that this sample model should have the same left and right results.

Related : https://emackey.github.io/testing-pbr/normal-tangent-readme.html

/cc @emackey

Three.js version
  • Dev
  • r85
Browser
  • All of them
  • Chrome
  • Firefox
  • Internet Explorer
OS
  • All of them
  • Windows
  • macOS
  • Linux
  • Android
  • iOS
Hardware Requirements (graphics card, VR Device, …)

ThinkPad X260 + Windows 10 + Intel HD Graphics 520

Author: Fantashit

2 thoughts on “GLTFLoader: Normal-Tangent Test model result is incorrect

  1. Thanks for writing this up @cx20.

    Just for more context, here are some related info & issues:

  2. I’d like to call out one sentence from the glTF spec on normal maps in particular:

    The normal vectors use OpenGL conventions where +X is right and +Y is up. +Z points toward the viewer.

    This sentence is what allows models to be shipped without tangent vectors, saving space.

    Let’s test it. Here I’ve made a lousy height map (bump map) out of a splotch in a paint program:

    TestHeightMap

    Let’s define this height field as an outward bump, where white pixels are closer to the viewer, and black pixels are further away.

    Using an online converter (of questionable quality, but we’ll examine the result in a moment), I’ve converted this from a height map to a normal map. Keep in mind there’s no 3D model here, no UV coordinates or mikktspace calculations or any geometry. Just a height map converted to a normal map. I had to manually configure the online converter per glTF’s instructions, such that X is right, Y is up, and Z faces the viewer. This is the result:

    TestNormalMap

    Let’s bring that back into a paint program and bust out the color channels to see where these vectors point. Below, each color channel has been separated into a grayscale image of its own. Remember that these will be interpreted such that black means -1.0, middle gray means 0.0, and white means +1.0.

    TestNormalMap-Decomposed

    So I think the online converter did what glTF asked, at least after configuring it correctly. In the Red (X) image, we can see the slope on the right has white pixels (+1.0), pointing the X vector at the right edge of the image. On the left side of the Red image, black pixels (-1.0) point the X vector at the left side of the image. In the Green (Y) image, white pixels along the top slope of the bump point the Y vector at the top of the image. The Z values are the least intuitive, but remember that the tip of the bump and the back plate itself both point at the viewer, and the slopes on all sides point away, so are all evenly darker.

    What if we load this into Blender Eevee, which (just like glTF) accepts OpenGL-style normal maps? What happens if the UV map is rotated, or even scaled to be inverted?

    NormalSpinTest

    Turns out, this works just fine. Indeed, the whole point of defining the tangent space this way is not to enable software to go crazy with the vectors, it’s to allow texture artists some sanity by ensuring that their normal maps will be right-side up regardless of the geometry.

    But, not all software uses the OpenGL convention. Some uses a different convention (sometimes called the DirectX convention), where the Y vectors point at the bottom of the image instead of the top. Here’s the decomposed Y channel of my image in this form. The lighter pixels are the ones facing the bottom of the image.

    TestNormalMap_DirectX-Green

    If I load one of these DirectX-style normal maps into Blender Eevee, can I still expect it to work?

    NormalSpinTest_DirectX_v3

    No. Blender was expecting +Y up. The math is wrong, and the reflected horizon line spins all around.
    The same thing happens if you load an OpenGL-style normal map into an engine that was expecting +Y down.

    This is what the NormalTangentTest model is attempting to test. Each row spins the UV coordinates into a different orientation, trying to make sure that the reflections remain right-side up in these different orientations.

Comments are closed.