GLSL Barycentric Coordinates with hitAttributeEXT

In Vulkan ray tracing shaders, hardware intersection pipelines report ray-triangle hit locations using a two-component vector instead of a three-component coordinate. This article explains how to reconstruct the full three-dimensional barycentric coordinates from the GLSL hitAttributeEXT vec2 variable within closest-hit and any-hit shaders, along with how to use those coordinates to interpolate custom per-vertex attributes across a triangle.

The Barycentric Coordinate Principle

Any point \(P\) inside a triangle formed by vertices \(V_0\), \(V_1\), and \(V_2\) can be expressed as a linear combination of its vertices:

\[P = w V_0 + u V_1 + v V_2\]

Because the weights must sum to 1 (\(u + v + w = 1.0\)), only two scalar values are required to define any point on the triangle's surface. Hardware intersection units take advantage of this constraint to minimize memory bandwidth and register usage by returning only a two-component vector.

Declaring hitAttributeEXT

In your closest-hit (.rchit) or any-hit (.rahit) shader, declare the built-in attribute using the hitAttributeEXT storage qualifier:

hitAttributeEXT vec2 attribs;

When using built-in triangle intersection, the hardware automatically populates attribs with the parametric coordinates \((u, v)\) of the hit point.

Reconstructing the Three Components

Under the standard Vulkan and SPIR-V ray tracing conventions:

  • attribs.x corresponds to \(u\), the weight of Vertex 1 (\(V_1\)).
  • attribs.y corresponds to \(v\), the weight of Vertex 2 (\(V_2\)).
  • The remaining component \(w\), representing the weight of Vertex 0 (\(V_0\)), is calculated as \(1.0 - u - v\).

To reconstruct the full vec3 barycentric weights in GLSL:

vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);

In this reconstructed vector:

  • barycentrics.x maps directly to \(V_0\).
  • barycentrics.y maps directly to \(V_1\).
  • barycentrics.z maps directly to \(V_2\).

Interpolating Vertex Attributes

Once reconstructed, the weights can be used to interpolate any vertex attribute fetched from your vertex or index buffers.

// Fetch indices for the hit triangle
uint primitiveID = gl_PrimitiveID;
uvec3 indices = indicesBuffer.indices[primitiveID];

// Fetch vertex data
Vertex v0 = verticesBuffer.vertices[indices.x];
Vertex v1 = verticesBuffer.vertices[indices.y];
Vertex v2 = verticesBuffer.vertices[indices.z];

// Interpolate normals
vec3 interpolatedNormal = v0.normal * barycentrics.x +
                          v1.normal * barycentrics.y +
                          v2.normal * barycentrics.z;

// Interpolate UV texture coordinates
vec2 interpolatedUV = v0.uv * barycentrics.x +
                      v1.uv * barycentrics.y +
                      v2.uv * barycentrics.z;

Normalize interpolated normal vectors afterward to correct for non-linear scale drift across the primitive.