steps3D - Построение касательного (TBN) базиса в пространстве экрана (screen-space)

Построение касательного (TBN) базиса в пространстве экрана (screen-space)

Обычно мы для каждой вершины помимо ее исходных трехмерных координат и ее текстурных координат также задаем и TBN-базис в ней. На самом деле не обязательно задавать все три вектора \(t\), \(b\) и \(n\) - достаточно задать \(n\) и \(t\), а бинормаль \(b\) можно найти через их векторное произведение прямо в вершинном шейдере.

Оказывается задавать для каждой вершины вектор \(t\) (как и вектор \(b\)) не обязательно - его можно найти в фрагментном шейдере через простую математику. Именно это мы сейчас и рассмотрим.

Пусть у нас в фрагментном шейдере есть трехмерные координаты точки \(p\), вектор нормали \(n\) и текстурные координаты \((u,v)\). Тогда касательный вектор \(t\) можно определить как нормированную проекцию производной \( \frac{\partial p}{\partial u} \) на касательную плоскость (проходящую через саму точку \(p\) и задаваемую вектором нормали \(n\)).

\[ t = normalize( proj ( \frac{\partial p}{\partial u} ) ) \]

Здесь через proj обозначено проектирование на касательную плоскость задаваемое следующим уравнением:

\[ proj ( a ) = a - (a,n) \cdot n \]

Понятно что находить частную производную \( \frac{\partial p}{\partial u} \) в фрагментом шейдере было бы довольно затруднительно. Но мы можем использовать стандартную технику замены переменных при дифференцировании:

\[ \frac{\partial p}{\partial u} = \frac{\partial p}{\partial x} \cdot \frac{\partial x}{\partial u} + \frac{\partial p}{\partial y} \cdot \frac{\partial y}{\partial u} \]

Здесь через \(x\) и \(y\) обозначены экранные координаты. Их использование удобно тем, что производные по ним легко могут быть найдены в фрагментном шейдере через встроенные функции dFdx dFdy:

\[ \frac{\partial p}{\partial x} = dFdx ( p ) \]

Но тогда возникает следующий вопрос - а как нам найти производные \( \frac{\partial x}{\partial u} \) и \( \frac{\partial y}{\partial u} \). Обычный прием для случая одной переменной \( y'_x = \frac{1}{x'_y} \) уже не работает непосредственно. Но есть его аналог для случая нескольких переменных, который мы сейчас и рассмотрим. Аналогом приведенной выше формулы для случая одной переменной в случае двух переменных будет следующая матричная формула:

\[ \begin{pmatrix} \frac{\partial x}{\partial u} & \frac{\partial y}{\partial u} \\ \frac{\partial x}{\partial v} & \frac{\partial y}{\partial v} \end{pmatrix} = \begin{pmatrix} \frac{\partial u}{\partial x} & \frac{\partial u}{\partial y} \\ \frac{\partial v}{\partial x} & \frac{\partial v}{\partial y} \end{pmatrix} ^{-1} = J^{-1} \]

Для матриц 2x2 есть готовая формула обратной матрицы, которая приводится ниже

\[ J^{-1} = \begin{pmatrix} \frac{\partial v}{\partial y} & -\frac{\partial u}{\partial y} \\ \frac{\partial v}{\partial x} & \frac{\partial u}{\partial x} \end{pmatrix} \]

Отсюда мы легко получаем нужные нам частные производные. С их учетом мы можем записать следующее уравнение (для ненормализованного) касательного вектора \(t\):

\[ t = proj \left( \frac{\partial p}{\partial x} \cdot \frac{\partial x}{\partial u} + \frac{\partial p}{\partial y} \cdot \frac{\partial y}{\partial u} \right) = \frac{\partial x}{\partial u} \cdot proj (\frac{\partial p}{\partial x}) + \frac{\partial y}{\partial u} \cdot proj (\frac{\partial p}{\partial y}) = \frac{1}{\det J} \left ( \frac{\partial v}{\partial y} \cdot proj (\frac{\partial p}{\partial x}) - \frac{\partial u}{\partial y} \cdot proj (\frac{\partial p}{\partial y}) \right) \]

Поскольку мы все равно будем нормировать вектор \(t\), то множитель \( \frac{1}{\det j} \) можно отбросить. Но при этом есть один нюанс - знак \( \det J \). Если \( \det J \lt 0 \), то нам нужно "перевернуть" вектор \(t\).

После вычисления \(t\) мы можем при помощи векторного произведения найти вектор бинормали \(b\), но нужно обеспечить согласованность найденного базиса и по отношению к экранной системе координат. Ниже приводится исходный код на GLSL функции для расчета базиса касательного пространства по предложенному выше алгоритму.

Обратите внимание, что идущие на вход трехмерные координаты фрагмента \(p\) и нормаль \(n\) должны быть в одной и той же системе координат, можно использовать как мировую систему, так и систему координат камеры. При этом найденный касательный базис будет задан именно в этом самом пространстве.

struct TangentFrame
{
  vec3 t;
  vec3 b;
  vec3 n;
};

TangentFrame computeScreenspaceTBN ( in vec3 p, in vec3 n, in vec2 uv )
{
  vec3 dpdx = dFdx ( p );
  vec3 dpdy = dFdy ( p );

    // Project the position gradients onto the tangent plane
  vec3 dpdx_s = dpdx - dot ( dpdx, n ) * n;
  vec3 dpdy_s = dpdy - dot ( dpdy, n ) * n;

    // Compute the jacobian matrix
  vec2  duvdx = dFdx ( uv );
  vec2  duvdy = dFdy ( uv );
  float jSign = sign ( duvdx.x * duvdy.y - duvdx.y * duvdy.x );

  TangentFrame frame;

  frame.t = jSign * (duvdy.y * dpdx_s - duvdx.y * dpdy_s);

    // The sign intrinsic returns 0 if the argument is 0
  if ( jSign != 0.0 )
    frame.t = normalize ( frame.t );

    // The second factor here ensures a consistent handedness between
    // the tangent frame and surface basis w.r.t. screenspace.
  frame.b = jSign * sign ( dot ( dpdy, cross ( n, dpdx ) ) ) * cross ( n, frame.t );
  frame.n = n;

  return frame;
}

Мы заменяем фактически 4 байта на вершину на некоторое количество довольно простой арифметики. Стоит ли этого - скорее всего нужно для конкретной задачи попробовать и сравнить быстродействие.

На следующем рисунке приводится рассчитанный описанным выше образом касательный вектор \(t\), переведенный в цвет так же как обычно делается с картами нормалей. Если присмотреться к рисунку, то можно заметить, что полученный касательный вектор не является непрерывным вдоль поверхности выводимого объекта (в отличии от нормали, для которой применяется линейная интерполяция, дающее непрерывное поле нормалей).

Это связано с тем, что на самом деле мы выводим плоские грани - треугольники. И вдоль каждой грани соответствующие производные постоянны с разрывом на границе грани. Проекция на касательное пространство слегка сглаживает \(t\), но разрывы все равно можно разглядеть.

Рис 1. Рассчитанный касательный вектор \(t\).

Однако если по рассчитанному таким образом TBN-базису посчитать освещение (с использованием карты нормалей), то никаких визуальных артефактов на нем не видно.

Рис 2. Освещение, рассчитанное по вычисленному касательному вектору

void main ()
{
    vec3            nt  = 2.0 * texture ( bumpMap, uv ).rgb - vec3 ( 1.0 );
    TangentFrame    tbn = computeScreenspaceTBN ( p, normalize ( n ), uv );

    vec3 v2  = normalize ( eye - p.xyz );       // vector to the eye
    vec3 l2  = normalize ( light );
    vec3 h2  = normalize ( v2 + l2 );

    vec4  clr  = vec4 ( 0.1, 0.7, 0.3, 1.0 );
    float ka   = 0.2;
    float kd   = 0.5;
    float ks   = 0.3;
    
    vec3    n2   = nt.x * tbn.t + nt.y * tbn.b + nt.z * tbn.n;
    float   diff = max ( 0.0, dot ( n2, l2 ) );
    float   spec = pow ( max ( dot ( n2, h2 ), 0.0 ), 120 );

    color = (ka + kd*diff)*clr + ks*vec4(spec);
}

На самом деле этот же подход можно успешно применять и в случае visibility buffer. Только там у нас сразу есть все вершины треугольника, но мы не можем применять функции dFdx и dFdy (так как обработка идет как screen-space эффект).

Давайте рассмотрим как мы можем в этом случае находить касательный вектор \(t\) в шейдере. Пусть у нас есть грань, заданная тремя вершинами \(p_0\), \(p_1\) и \(p_2\), в которых задана нормаль и текстурные координаты.

Тогда мы можем используя барицентрические координаты \(\lambda\) и \(\mu\) записать следующие уравнения:

\[ \begin{matrix} \begin{aligned} p = \lambda \cdot p_0 + \mu \cdot p_1 + (1 - \lambda - \mu) \cdot p_2 \\ \begin{pmatrix} u \\ v \end{pmatrix} = \lambda \cdot \begin{pmatrix} u_0 \\ v_0 \end{pmatrix} + \mu \cdot \begin{pmatrix} u_1 \\ v_1 \end{pmatrix} + (1 - \lambda - \mu) \cdot \begin{pmatrix} u_2 \\ v_2 \end{pmatrix} \end{aligned} \end{matrix} \]

Давайте выразим из последнего уравнения величины \(\lambda\) и \(\mu\):

\[ \begin{pmatrix} u \\ v \end{pmatrix} = \begin{pmatrix} u_2 \\ v_2 \end{pmatrix} + \begin{pmatrix} u_0 - u_2 & u_1 - u_2 \\ v_0 - v_2 & v_1 - v_2 \end{pmatrix} \cdot \begin{pmatrix} \lambda \\ \mu \end{pmatrix} \]

Отсюда получаем

\[ \begin{pmatrix} \lambda \\ \mu \end{pmatrix} = \begin{pmatrix} u_0 - u_2 & u_1 - u_2 \\ v_0 - v_2 & v_1 - v_2 \end{pmatrix} ^ {-1} \begin{pmatrix} u \\ v \end{pmatrix} \]

Их этого уравнения мы можем найти нужные нам частные производные

\[ \begin{pmatrix} \frac{ \partial \lambda}{\partial u} \\ \frac{ \partial \mu}{\partial u} \end{pmatrix} = \begin{pmatrix} u_0 - u_2 & u_1 - u_2 \\ v_0 - v_2 & v_1 - v_2 \end{pmatrix} ^ {-1} \begin{pmatrix} 1 \\ 0 \end{pmatrix} \]

И тогда мы можем выразить нужную нам производную \( \frac{\partial p}{\partial u} \)

\[ \frac{\partial p}{\partial u} = \frac{ \partial \lambda}{\partial u} \cdot (p_0 - p_2) + \frac{ \partial \mu}{\partial u} \cdot (p_1 - p_2) \]

Ссылки

Surface Gradient Bump Mapping Framework Overview

Followup: Normal Mapping Without Precomputed Tangents