Calculate Tangents for BufferGeometry in Three.js

This article explains how to manually calculate and assign tangent vectors to a Three.js BufferGeometry to enable proper normal mapping. You will learn how to use the built-in Three.js utility library to compute tangents automatically using the industry-standard MikkTSpace algorithm, as well as how to write a custom mathematical loop to compute and assign 4D tangent attributes manually.

Why Tangents are Necessary for Normal Mapping

Normal maps store perturbation vectors in “tangent space” rather than world or object space. To transform these normal map vectors into world space during rendering, the fragment shader needs three vectors per vertex to establish a coordinate frame: the Normal, the Tangent, and the Bitangent (often calculated dynamically in the shader using the cross product of the normal and tangent).

In Three.js, tangent vectors are represented as 4-component vectors (x, y, z, w). The w component (either 1 or -1) defines the “handedness” of the coordinate system, which determines how the bitangent is oriented.


The most reliable way to manually trigger tangent generation in Three.js is using BufferGeometryUtils. This utility uses the MikkTSpace algorithm, which ensures your normal maps align perfectly with baking software like Substance Painter or Blender.

First, import the utility and the MikkTSpace library (packaged with Three.js):

import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { createLWJSInterface } from 'three/examples/jsm/libs/meshopt_decoder.module.js'; // or a standard MikkTSpace loader

To compute and assign the tangents, call computeMikkTSpaceTangents:

// Ensure your geometry has 'position', 'normal', and 'uv' attributes
BufferGeometryUtils.computeMikkTSpaceTangents(geometry, MikkTSpace);

This automatically generates and assigns a 4-component tangent attribute to your BufferGeometry.


Method 2: Pure Mathematical Calculation (Custom Implementation)

If you cannot use external utilities, you can calculate the tangents manually by looping through the triangles of your geometry. The formula uses the UV coordinates and vertex positions to find the direction of the U texture coordinate relative to the 3D triangle.

Here is the complete JavaScript function to manually calculate and assign tangents:

function calculateTangents(geometry) {
  const positionAttribute = geometry.attributes.position;
  const normalAttribute = geometry.attributes.normal;
  const uvAttribute = geometry.attributes.uv;
  const indexAttribute = geometry.index;

  const vertexCount = positionAttribute.count;
  const tangents = new Float32Array(vertexCount * 4);

  // Temporary arrays to accumulate tangents and bitangents
  const tan1 = new Array(vertexCount).fill(null).map(() => new THREE.Vector3());
  const tan2 = new Array(vertexCount).fill(null).map(() => new THREE.Vector3());

  const p1 = new THREE.Vector3();
  const p2 = new THREE.Vector3();
  const p3 = new THREE.Vector3();

  const uv1 = new THREE.Vector2();
  const uv2 = new THREE.Vector2();
  const uv3 = new THREE.Vector2();

  const sdir = new THREE.Vector3();
  const tdir = new THREE.Vector3();

  const triangleCount = indexAttribute ? indexAttribute.count / 3 : vertexCount / 3;

  for (let i = 0; i < triangleCount; i++) {
    let i1, i2, i3;

    if (indexAttribute) {
      i1 = indexAttribute.getX(i * 3);
      i2 = indexAttribute.getX(i * 3 + 1);
      i3 = indexAttribute.getX(i * 3 + 2);
    } else {
      i1 = i * 3;
      i2 = i * 3 + 1;
      i3 = i * 3 + 2;
    }

    p1.fromBufferAttribute(positionAttribute, i1);
    p2.fromBufferAttribute(positionAttribute, i2);
    p3.fromBufferAttribute(positionAttribute, i3);

    uv1.fromBufferAttribute(uvAttribute, i1);
    uv2.fromBufferAttribute(uvAttribute, i2);
    uv3.fromBufferAttribute(uvAttribute, i3);

    const x1 = p2.x - p1.x;
    const x2 = p3.x - p1.x;
    const y1 = p2.y - p1.y;
    const y2 = p3.y - p1.y;
    const z1 = p2.z - p1.z;
    const z2 = p3.z - p1.z;

    const s1 = uv2.x - uv1.x;
    const s2 = uv3.x - uv1.x;
    const t1 = uv2.y - uv1.y;
    const t2 = uv3.y - uv1.y;

    const r = 1.0 / (s1 * t2 - s2 * t1);

    sdir.set(
      (t2 * x1 - t1 * x2) * r,
      (t2 * y1 - t1 * y2) * r,
      (t2 * z1 - t1 * z2) * r
    );

    tdir.set(
      (s1 * x2 - s2 * x1) * r,
      (s1 * y2 - s2 * y1) * r,
      (s1 * z2 - s2 * z1) * r
    );

    tan1[i1].add(sdir);
    tan1[i2].add(sdir);
    tan1[i3].add(sdir);

    tan2[i1].add(tdir);
    tan2[i2].add(tdir);
    tan2[i3].add(tdir);
  }

  const n = new THREE.Vector3();
  const t = new THREE.Vector3();
  const temp = new THREE.Vector3();

  for (let i = 0; i < vertexCount; i++) {
    n.fromBufferAttribute(normalAttribute, i);
    t.copy(tan1[i]);

    // Gram-Schmidt orthogonalization
    temp.copy(n).multiplyScalar(n.dot(t));
    t.sub(temp).normalize();

    // Calculate handedness (w component)
    temp.crossVectors(n, t);
    const w = (temp.dot(tan2[i]) < 0.0) ? -1.0 : 1.0;

    tangents[i * 4] = t.x;
    tangents[i * 4 + 1] = t.y;
    tangents[i * 4 + 2] = t.z;
    tangents[i * 4 + 3] = w;
  }

  // Assign the attribute to your geometry
  geometry.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4));
}

Activating Tangent Vectors in Materials

Once you have calculated and assigned your tangents using either method, you must instruct your MeshStandardMaterial or MeshPhysicalMaterial to read them.

Three.js automatically detects the tangent attribute if it exists on the geometry. However, you should ensure that your texture mapping parameters are configured correctly:

const normalMap = new THREE.TextureLoader().load('path/to/normalMap.png');

const material = new THREE.MeshStandardMaterial({
  map: colorMap,
  normalMap: normalMap,
  normalScale: new THREE.Vector2(1, 1) // Adjust intensity if needed
});

With the tangent attribute added to your BufferGeometry, Three.js will bypass its default screen-space derivative approximation for normal mapping and use your precise, calculated vertex tangents.