NodeMaterial renders incorrectly when using mesh tangents

NodeMaterial PerturbNormalBlock seems to be ignoring invertX and invertY options when it uses mesh generated tangents rather than autogenerated ones.

You can see it with the GLTF “NormalTangentMirrorTest” sample model: glTF-Sample-Models/2.0/NormalTangentMirrorTest at master · KhronosGroup/glTF-Sample-Models · GitHub

A playground example here: Babylon.js Playground

As you can see, the reflections are all messed up. Changing the invert options doesn’t seem to do anything.

The issue doesn’t happen when using NodeMaterial where tangent isn’t connected, to enforce auto generated tangents even when the mesh has them:

I do not think the invert parameters are supported if the full tangent space as been provided as they should be fitting together.

@Deltakosh and @PatrickRyan could correct me if I am wrong.

So you are saying that it is impossible to render that GLTF test model correctly in NodeMaterial? That doesn’t sound right.

Nope, this part should work and there is a bug but I am not sure if it is the exact root cause. I digging into it.

1 Like

Issue has been found and not related to the texture flag, I will try to push the fix tomorrow which will probably be a new input on the perturbNormal block as well as a new block to generate the correct TBN matrix.

Nice. Hopefully the new system still seamlessly triggers the auto generated normals if the attribute is missing. Can’t have different node trees for different mesh attribute combinations. :grimacing:

Fix will be available here: Create a TBN NME node to handle tangent space orientation by sebavan · Pull Request #12461 · BabylonJS/Babylon.js · GitHub

Quick testing on the snapshot playground indicates that the fix is working. :slight_smile:

However, one minor issue is that if PerturbNormalBlock has both worldTangent and TBN connected at the same time there is a shader error. To replicate, just comment out line 39 in this playground:

This happens cause the tangents block as been changed to a vector4 (w is the local tangent space orientation).

You need to delete the tangent node, add it back and wire to both tangents and tbn, but by default it keeps back compat.

Looks like there is at least one more related issue in the latest babylonjs 5.5.5 version:


MorphTargetsBlock tangent output is Vector3 so it isn’t possible to connect it to the new tangent input in TBNBlock, and probably wouldn’t work correctly even if I could connect it. :s

NME link here as reference:

EDIT: And looks like if I connect the tangents to the input directly just to temporarily bypass the morph target block, I seem to get a shader error again. Not sure what causes it:

I now confirmed with a local version of MorphTargetsBlock that it starts working after changing tangentOutput to Vec4 and updating the related generated shader lines:

However, even after that there seems to also be yet another an issue that causes backface reflections to render incorrectly, that didn’t happen previously. I’ll try to setup a playground example for that.

Ok, here is also a playground example for the backface reflection issue. (Without MorphTargetsBlock to avoid the other bug)

First without TBN the backfaces render as expected (except the original bug about orientation of course):

With TBN block the backface reflections are almost black:

This seems to happen in both left handed and right handed coordinate modes.

EDIT: Actually. I just realized that the TBN normal input was connected to the non flipped normal in the above example, so that likely is the problem. Unfortunately connecting a flipped normal doesn’t work either:
You get FrontFacingBlock must only be used in a fragment shader error and I don’t see a way to get rid of it. :face_with_spiral_eyes:

I am looking into it ASAP. The last one is not supported in PBR either. I will at least fix the morph tangents and try to think about a trick for the last one.

Not sure what you mean about the last one not supported in PBR? The model does look fine even on back side if you throw the NormalTangentMirrorTest.glb into the sandbox.

I was actually just now playing around trying to figure out the last issue. I locally was able to make the TBNBlock work also in fragment only mode by:

Changing target getter setter to:

  public get target() {
    return this._target;

  public set target(value: NodeMaterialBlockTargets) {
    if (
      value === NodeMaterialBlockTargets.VertexAndFragment ||
      value === NodeMaterialBlockTargets.Fragment
    ) {
      this._target = value;

So that Fragment target is also allowed.

Adding TBN as excluded variable name in initialize():


And also changing _buildBlock() code to:

    const normal = this.normal;
    const tangent = this.tangent;
    const world =;
    const TBN = this.TBN;

    const tbnTarget = === NodeMaterialBlockTargets.Fragment
        ? NodeMaterialBlockTargets.Fragment
        : NodeMaterialBlockTargets.Vertex;

    // Vertex
    if ( === NodeMaterialBlockTargets.Vertex) {
      state._emitVaryingFromString(TBN.associatedVariableName, 'mat3');

    if ( === tbnTarget) {
      // Declare output type for fragment only block as no varying is used in that case
      const tbnType =
        tbnTarget === NodeMaterialBlockTargets.Fragment ? 'mat3 ' : '';
      state.compilationString += `
          // ${}
          vec3 tbnNormal = normalize(${normal.associatedVariableName}).xyz;
          vec3 tbnTangent = normalize(${tangent.associatedVariableName}.xyz);
          vec3 tbnBitangent = cross(tbnNormal, tbnTangent) * ${tangent.associatedVariableName}.w;
          ${tbnType}${TBN.associatedVariableName} = mat3(${world.associatedVariableName}) * mat3(tbnTangent, tbnBitangent, tbnNormal);

    // Fragment
    if ( === NodeMaterialBlockTargets.Fragment) {

With that in place I was able to pass a FrontFacingBlock determined normal to it and the backside is no longer black and front side was correct too.

  • However the reflections on the back were still messed up like in the original issue.
  • I also tried to flip the tangent w component for backfaces to see what would happen then. That caused backside reflections to appear otherwise normal, except they were right side up, even though they should be upside down on the concave backside. So I guess flipping the w coponent isn’t the solution there. :grimacing:

Yup double sided lighting is not supported with tangents as we can not interpolate the tangents and their orientation easily so they have to be coming from the vertex shader.

Unfortunately double sided requires as well the face orientation to flip the space for the triangle winding order which can only be done in fragment. in your case it might be easier to try and flip the TBN alltogether ?

Actually just flipping the whole tangent, not just the w component seemed to work. Now both front and backside look correct.

Although this still requires that NodeMaterialBlockTargets.Fragment compatible TBNBlock hack from above, as flipping either normal or tangent needs to indeed happen in the fragment shader. :slight_smile:

While waiting for the MorphTargetBlock fix to go live, I did some additional chages to my attempt to make the TBNBlock fragment target compatible so that it can be used with flipped normals and tangents.

I updated my earlier comment above with code that should work for both VertexAndFragment and Fragment type TBNBlocks.

I’m not sure if that is the best way to do it, as now it doesn’t appear to be possible to change the TBNBlock type in NME, only in javascript. But maybe it is possible to automatically change the target to fragment if either tangent or normal inputs come from a Fragment block? Or maybe just make it Fragment block by default (since the output can only be used in fragment shader anyway), with optional VertexAndFragment mode if someone really wants to optimze handful of instructions to vertex shader.

Regardless… Hopefully someone can take a look at implementing something like my code to the official Babylon.js. Because while a custom TBNBlock works, it is going to require me to manually backport all future changes to it whenever Babylonjs version is upgraded. :grimacing:

Do you want to try to make a PR for it ?