![]() |
Главная
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Обычно мы для каждой вершины помимо ее исходных трехмерных координат и ее текстурных координат также задаем и 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