Как рассчитать тангенс и бинормаль?

Говоря о картировании рельефа, зеркальном освещении и подобных вещах в OpenGL Shading Language (GLSL)

У меня есть:

  • Массив вершин (например, {0.2,0.5,0.1, 0.2,0.4,0.5, ...})
  • Массив нормалей (например, {0.0,0.0,1.0, 0.0,1.0,0.0, ...})
  • Положение точечного источника света в мировом пространстве (например, {0.0,1.0, -5.0})
  • Положение зрителя в мировом пространстве (например, {0.0,0.0,0.0}) (предположим, что зритель находится в центре мира)

Теперь, как я могу вычислить бинормаль и касательную для каждой вершины? Я имею в виду, какова формула для вычисления бинормалей, что я должен использовать на основе этой информации? А по поводу касательной?

Я все равно построю матрицу TBN, поэтому, если вы знаете формулу для построения матрицы непосредственно на основе этой информации, будет неплохо!

О, да, у меня тоже есть координаты текстуры, если нужно. И поскольку я говорю о GLSL, было бы неплохо решение для каждой вершины, я имею в виду такое, которое не требует доступа к более чем одной информации о вершине за раз.

---- Обновлять -----

Я нашел такое решение:

vec3 tangent;
vec3 binormal;

vec3 c1 = cross(a_normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(a_normal, vec3(0.0, 1.0, 0.0));

if (length(c1)>length(c2))
{
    tangent = c1;
}
else
{
    tangent = c2;
}

tangent = normalize(tangent);

binormal = cross(v_nglNormal, tangent);
binormal = normalize(binormal);

Но я не знаю, правильно ли это на 100%.


person user464230    schedule 10.03.2011    source источник
comment
Может быть, это вопрос к gamedev.stackexchange.com?   -  person nkint    schedule 10.03.2011
comment
@ user464230: Нет, здесь тоже нормально. 3D-графика не ограничивается играми.   -  person datenwolf    schedule 10.03.2011
comment
Это решение предполагает, что вы примените текстуру карты нормалей к плоской грани с normal = x. Это не сработает, если применить его к произвольной модели. Что вам действительно нужно сделать, так это решить систему уравнений, которую я дал вам для каждой грани, и использовать средние значения для вершин. Если вы хотите серьезно заниматься 3D-программированием, вам нужно научиться переводить линейную алгебру - как я показал ниже - в исходный код.   -  person datenwolf    schedule 10.03.2011
comment
Если вы хотите серьезно заниматься 3D-программированием, вам придется научиться переводить линейную алгебру - как я показал ниже - в исходный код ... Чувак, правда, вы меня не встречаете !!! Не говори мне, что ты будешь учиться ... Ты даже не представляешь, что я сделал ... Береги свои слова!   -  person user464230    schedule 10.03.2011
comment
@ user464230: Нет, я ничего не знаю о том, что вы на самом деле сделали. Но вы спросили, как вычислить тангенс и бинормаль. Итак, что вы можете ожидать, так это математическое описание процесса. Если вам просто нужен какой-то исходный код, который вы можете скопировать и вставить, ну, его там много. Но чтобы понять, как его эффективно использовать, вы должны понимать, что он делает на математическом уровне. Для этого вам необходимо научиться читать и понимать линейную алгебру. То, как вы задаете свой вопрос (ы) и комментируете мой пост, ясно показывает мне, что вы еще не научились правильно читать математику.   -  person datenwolf    schedule 10.03.2011
comment
Требуется ли условие в примере кода? А что v_nglNormal по сравнению с a_normal?   -  person Mike Weir    schedule 13.09.2014
comment
Из кода кажется, что он реализован в glsl. Ради любви к богу рассчитай это.   -  person RecursiveExceptionException    schedule 25.07.2016


Ответы (4)


Соответствующие входные данные для вашей проблемы - это координаты текстуры. Касательная и бинормаль - это векторы, локально параллельные поверхности объекта. А в случае отображения нормалей они описывают локальную ориентацию текстуры нормалей.

Таким образом, вам нужно рассчитать направление (в пространстве модели), в котором указывают векторы текстурирования. Допустим, у вас есть треугольник ABC с координатами текстуры HKL. Это дает нам векторы:

D = B-A
E = C-A

F = K-H
G = L-H

Теперь мы хотим выразить D и E через касательное пространство T, U, т. Е.

D = F.s * T + F.t * U
E = G.s * T + G.t * U

Это система линейных уравнений с 6 неизвестными и 6 уравнениями, ее можно записать как

| D.x D.y D.z |   | F.s F.t | | T.x T.y T.z |
|             | = |         | |             |
| E.x E.y E.z |   | G.s G.t | | U.x U.y U.z |

Обращение матрицы FG дает

| T.x T.y T.z |           1         |  G.t  -F.t | | D.x D.y D.z |
|             | = ----------------- |            | |             |
| U.x U.y U.z |   F.s G.t - F.t G.s | -G.s   F.s | | E.x E.y E.z |

Вместе с нормалью вершины T и U образуют базис локального пространства, называемого касательным пространством, описываемым матрицей

| T.x U.x N.x |
| T.y U.y N.y |
| T.z U.z N.z |

Преобразование из касательного пространства в пространство объекта. Для расчета освещения требуется обратное. Немного упражняясь, можно обнаружить:

T' = T - (N·T) N
U' = U - (N·U) N - (T'·U) T'

Нормализуя векторы T 'и U', называя их касательными и бинормальными, мы получаем матрицу, трансформирующуюся из объекта в касательное пространство, в котором мы делаем освещение:

| T'.x T'.y T'.z |
| U'.x U'.y U'.z |
| N.x  N.y  N.z  |

Мы сохраняем их T 'и U' вместе с нормалью вершины как часть геометрии модели (как атрибуты вершины), чтобы мы могли использовать их в шейдере для вычислений освещения. Я повторяю: вы не определяете касательную и бинормаль в шейдере, вы предварительно вычисляете их и сохраняете как часть геометрии модели (как и нормали).

(Обозначения между вертикальными полосами выше - это все матрицы, а не детерминанты, которые обычно используют вертикальные полосы вместо скобок в своих обозначениях.)

person datenwolf    schedule 10.03.2011
comment
Ваша инверсия матрицы неверна. Обратная матрица - это не скаляр, а матрица. Так должно быть (1 / fs gt-ft gs) ((gt, -ft) (- gs, gs)) - person Gottfried; 25.11.2012
comment
@Gottfried: Где вы видите там скаляр? Вы видите детерминантный метод инвертирования матрицы. Знаменатель коэффициента масштабирования - это определитель матрицы [F G] (F, G - векторы-столбцы). Вы также можете использовать исключение Гаусса-Джордана, дающее тот же результат. - person datenwolf; 25.11.2012
comment
Кстати, вы можете найти тот же вывод, что и я, в большинстве учебников по 3D-программированию. - person datenwolf; 25.11.2012
comment
Я знаю, что вы делаете инверсию через определитель, но если вы посмотрите свой пост, вы увидите, что вы забыли матричную часть. Вы делите только на определитель, который является скаляром. Сравните, например, с помощью ссылки, я надеюсь, что смогу прояснить это, и я уверен, что это было просто надзор. - person Gottfried; 28.11.2012
comment
@Gottfried: Ах, я смотрел не на ту сторону уравнения. Да, я это совершенно упустил. Спасибо - person datenwolf; 28.11.2012
comment
@datenwolf: ваш ответ начинается с предположения, что вы знаете три точки на треугольнике. Я согласен с вашей точкой зрения в отношении статических моделей. Но при моделировании воды касательные и побитовые векторы меняются со временем и, следовательно, должны вычисляться в шейдере. Итак, как можно вычислить касательную и битангенс, если у вас нет доступа к другим данным положения вершины (например, в пиксельном шейдере)? - person fishfood; 25.08.2013
comment
@fishfood: шейдерные операции изначально локализованы. Если вы модифицируете сетку на ЦП (скажем, для воды), то наиболее эффективно также выполнить расчет касательного пространства на этом этапе. Теоретически вы могли бы сделать это во фрагментном шейдере, но с определенными потерями производительности. И, конечно, есть смысл взглянуть на настоящую проблему с большего расстояния. Например, для некоторой волнистой поверхности вы должны использовать подход Фурье для ее анимации. Но имея под рукой коэффициенты Фурье, вы можете задать касательное пространство для точки напрямую, не оценивая вспомогательные точки. - person datenwolf; 25.08.2013
comment
@fishfood: В любом случае, если вы анимируете сетку на CPU, вы тут же выполняете расчет касательного пространства. Для данной вершины у вас уже есть данные в кеше, поэтому у вас отличная локальность. И если вы хотите использовать графический процессор для анимации сетки, вы должны сделать это в вычислительном шейдере, где вы также можете сделать расчет касательного пространства проще и эффективнее, чем при рендеринге. - person datenwolf; 25.08.2013
comment
изменение меша (особенно при тесселяции, происходит на графическом процессоре, а не на процессоре!) - person fishfood; 26.08.2013
comment
@fishfood: Я не хочу здесь теряться в технофилософских дискуссиях. Но дело в том, что вы можете изменить сетку так же хорошо на CPU, а затем загрузить обновленные данные в GPU. И в зависимости от конкретного приложения это может (а может и не дать) дать лучшую производительность. Если ваша программа в значительной степени привязана к графическому процессору, например, из-за того, что вы используете большую скорость заполнения, но у ЦП остается много вычислительного времени (очень вероятно, что это происходит с современными многоядерными системами ЦП), то выполните модификацию сетки на ЦП даст лучшую производительность. - person datenwolf; 26.08.2013
comment
@fishfood: Если вы думаете о вершинном шейдере как о модификации меша, ну, это не так, или практически говоря, любое преобразование, которое выполняется на нормалях вершин, также применяется к остальным базовым векторам касательного пространства (из которых нормаль одна ). Этап программируемой тесселяции также не изменяет сетку, а уточняет ее. Фактически, используя обратную связь преобразования, вы можете выполнять модификацию сетки. Но даже тогда вы бы предварительно вычислили измененное касательное пространство во время модификации меша, вместо того, чтобы делать это позже. - person datenwolf; 26.08.2013
comment
@datenwolf извините за возобновление этой ветки, но я хотел спросить, как бы вы предварительно вычислили касательные, не дублируя в основном все вершины меша? Я имею в виду, что обычно каждая вершина меша используется для генерации N треугольников, и касательный вектор, ограниченный этой вершиной, различается в зависимости от треугольника, который визуализируется ... Так что я не вижу другого способа пред- вычисление вектора в автономном режиме без обеспечения того, чтобы каждая вершина строила только один треугольник - person felipeek; 14.02.2021
comment
@felipeek в случае, когда триангулированная сетка представляет собой аппроксимацию гладкой поверхности с плавно вложенным касательным пространством, вы можете предположить, что базис касательного пространства представляет собой (взвешенное) среднее значение оснований смежных треугольников. - По сути, это то же приближение / предположение, которое мы делаем, когда вычисляем нормали вершин путем усреднения нормалей соседних треугольников; на самом деле нормаль - это один базовый вектор базиса касательного пространства, поэтому его распространение на другие базовые векторы хорошо обосновано. - person datenwolf; 14.02.2021

Как правило, у вас есть 2 способа создания матрицы TBN: автономный и интерактивный.

  • Он-лайн = прямо во фрагментном шейдере с использованием производных инструкций. Эти производные дают вам плоскую основу TBN для каждой точки многоугольника. Чтобы получить гладкую, мы должны повторно ортогонализировать ее на основе заданной (гладкой) вершинной нормали. Эта процедура еще более тяжелая для графического процессора, чем первоначальное извлечение TBN.

    // compute derivations of the world position
    vec3 p_dx = dFdx(pw_i);
    vec3 p_dy = dFdy(pw_i);
    // compute derivations of the texture coordinate
    vec2 tc_dx = dFdx(tc_i);
    vec2 tc_dy = dFdy(tc_i);
    // compute initial tangent and bi-tangent
    vec3 t = normalize( tc_dy.y * p_dx - tc_dx.y * p_dy );
    vec3 b = normalize( tc_dy.x * p_dx - tc_dx.x * p_dy ); // sign inversion
    // get new tangent from a given mesh normal
    vec3 n = normalize(n_obj_i);
    vec3 x = cross(n, t);
    t = cross(x, n);
    t = normalize(t);
    // get updated bi-tangent
    x = cross(b, n);
    b = cross(n, x);
    b = normalize(b);
    mat3 tbn = mat3(t, b, n);
    
  • Off-line = подготовить касательную как атрибут вершины. Это труднее получить, потому что это не только добавит еще один атрибут вершины, но также потребует перекомпоновки всех остальных атрибутов. Более того, это не на 100% даст вам лучшую производительность, так как вы получите дополнительную стоимость хранения / передачи / анимации (!) Атрибута вершины vector3.

Математика описана во многих местах (в Google), в том числе в сообщении @datenwolf.

Проблема здесь в том, что две вершины могут иметь одинаковые координаты нормали и текстуры, но разные касательные. Это означает, что вы не можете просто добавить атрибут вершины к вершине, вам нужно разделить вершину на 2 и указать разные касательные для клонов.

Лучший способ получить уникальную касательную (и другие атрибуты) для каждой вершины - сделать это как можно раньше = в экспортере. Там, на этапе сортировки чистых вершин по атрибутам, вам достаточно добавить касательный вектор к ключу сортировки.

В качестве радикального решения проблемы рассмотрите возможность использования кватернионов. Одиночный кватернион (vec4) может успешно представлять тангенциальное пространство заранее определенной степени удобства. Легко поддерживать ортонормированность (включая переход к фрагментному шейдеру), сохранять и при необходимости извлекать нормальные. Дополнительная информация на вики-странице KRI.

person kvark    schedule 10.03.2011
comment
@ user464230: Это не очень элегантно. Это метод грубой силы, использующий огромную вычислительную мощность современного графического процессора, вроде того, если я не разбираюсь в математике, я делаю это утомительным способом. @kvark уже сказал, что будет хуже. - person datenwolf; 10.03.2011
comment
@datenwolf. Я не сказал, что подход GLSL медленнее, даже если это так :). Мне нравится его универсальность: вам не нужно делать собственный экспортер (или повторно вычислять атрибуты сетки), вам не нужно заботиться о касательных во время скелетных анимаций. Просто работает. - person kvark; 10.03.2011
comment
@kvark: Вы пишите, что на GPU тяжелее, и да, это так. Извлечение частных производных - действительно тяжелая работа для графического процессора (это сводится к тому, что для каждого фрагмента должны эмулироваться вычисления соседей, либо он должен ждать их (не всегда с результатом, если фрагмент будет уничтожен или другой код ветка выполняется) .Да, это просто работает, но за счет снижения заполняемости. - person datenwolf; 10.03.2011
comment
@datenwolf. Ссылаясь на себя. Эта процедура еще более тяжелая для GPU, чем первоначальное извлечение TBN - я имел в виду не то, что вы читали :). Тем не менее я согласен с вашим предложением. - person kvark; 10.03.2011
comment
@kvark: Учитывая тот факт, что метод предварительного вычисления TBN может использоваться на оборудовании конвейера фиксированного объединения регистров (TNT, GeForce2), он действительно очень легкий. У меня все еще есть копия того документа о картировании нормалей на TNT с этим красивым предложением, выполняющим попиксельное вычисление касательного пространства, слишком сложное в вычислительном отношении. Как изменились времена :) - person datenwolf; 10.03.2011
comment
@kvark Привет, парень, я видел твой движок, и твой подход Quaternion кажется великолепным! Я не хочу выполнять все эти кватернионные операции непосредственно в графическом процессоре, но идея кажется очень хорошей. Вы можете мне больше рассказать? Пожалуйста? Как вы создаете эти кватернионы с помощью света и камеры? - person user464230; 11.03.2011
comment
@ user464230: Эти кватернио - просто более компактное представление матрицы TBN. Касательное пространство можно понимать как локальное преобразование, необходимое для выравнивания (поворота) локального касательного пространства поверхности с (глобальным) пространством объектов. Т.е. матрица TBN - это матрица вращения. Теперь вращение тоже можно выразить как кватернион. Таким образом, способ получить этот касательный кватернион (@kvark: хорошая идея, кстати!) Состоит в том, чтобы определить матрицу TBN и получить ее эквивалентный кватернион, что является проблемой собственных значений. В Википедии есть математика: en.wikipedia.org/wiki/Quaternions_and_spatial_rotation - person datenwolf; 11.03.2011
comment
Для пояснения: матрица TBN может формировать только полную базу, но не ортогонально, и в большинстве случаев текстурирования это так, и в этом случае кватернион предоставляет недостаточно информации. Неортогональность OTOH будет заметна только для сильно искаженных текстур, поэтому допущение, что ортогонормированное TBN будет нормальным. - person datenwolf; 11.03.2011
comment
@ user464230. Я рад, что тебе понравилось. Я с удовольствием расскажу вам об этом, но имейте в виду, что вики KRI, вероятно, содержит большинство ответов. Для света и камеры кватернионы создаются так же, как и для любого другого пространственного узла. Разница в том, что для этих объектов требуется выполнение проекции после пространственной трансформации. Я делаю прогнозы вручную (разумеется, с помощью простых библиотечных функций GLSL, таких как project / unproject). - person kvark; 11.03.2011

Исходя из ответа kvark, хотелось бы добавить еще мысли.

Если вам нужна ортонормированная матрица касательного пространства, вам все равно придется поработать. Даже если вы добавите атрибуты касательной и бинормали, они будут интерполироваться на этапах шейдера, и в конце они не нормализуются и не являются нормальными по отношению друг к другу.

Предположим, что у нас есть нормализованный нормальный вектор n, а также касательная t и бинормальb, или мы можем вычислить их из производных следующим образом:

// derivations of the fragment position
vec3 pos_dx = dFdx( fragPos );
vec3 pos_dy = dFdy( fragPos );
// derivations of the texture coordinate
vec2 texC_dx = dFdx( texCoord );
vec2 texC_dy = dFdy( texCoord );
// tangent vector and binormal vector
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
vec3 b = texC_dx.x * pos_dy - texC_dy.x * pos_dx;

Конечно, ортонормированная матрица касательного пространства может быть вычислена с помощью перекрестного произведения, но это будет работать только для правосторонних систем. Если матрица была зеркалирована (левая система), она превратится в правую систему:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( n, t );             // orthonormalization of the binormal vector 
                               //   may invert the binormal vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

В приведенном выше фрагменте кода бинормальный вектор перевернут, если касательное пространство является левосторонней системой. Чтобы этого избежать, нужно пройти тяжелый путь:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( b, cross( b, n ) ); // orthonormalization of the binormal vectors to the normal vector 
b = cross( cross( t, b ), t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

Обычный способ ортогонализации любой матрицы - это процесс Грама – Шмидта:

t = t - n * dot( t, n ); // orthonormalization ot the tangent vectors
b = b - n * dot( b, n ); // orthonormalization of the binormal vectors to the normal vector 
b = b - t * dot( b, t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

Другая возможность состоит в том, чтобы использовать определитель матрицы 2 * 2, который получается из выводов координат текстуры texC_dx, texC_dy, чтобы учесть направление вектора бинормали. Идея состоит в том, что определитель ортогональной матрицы равен 1, а определитель ортогональной зеркальной матрицы равен -1.

Определитель может быть вычислен с помощью функции GLSL determinant( mat2( texC_dx, texC_dy ) или по формуле texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y.

Для вычисления ортонормированной матрицы касательного пространства бинормальный вектор больше не требуется, и можно избежать вычисления единичного вектора (normalize) бинормального вектора.

float texDet = texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y;
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
t      = normalize( t - n * dot( t, n ) );
vec3 b = cross( n, t );                      // b is normlized because n and t are orthonormalized unit vectors
mat3 tbn = mat3( t, sign( texDet ) * b, n ); // take in account the direction of the binormal vector
person Rabbid76    schedule 04.07.2017
comment
@AlexGreen Большое спасибо, исправлено - person Rabbid76; 12.09.2017

Существует множество способов вычисления касательных, и если редактор карт нормалей не делает это так же, как средство визуализации, вы получите тонкие артефакты. Многие пекари используют алгоритм MikkTSpace, который отличается от трюка с производными фрагментов.

К счастью, если у вас есть индексированная сетка из программы, которая использует MikkTSpace (и никакие треугольники координат текстуры с противоположной ориентацией не имеют общего индекса), сложная часть алгоритма в основном выполняется за вас, и вы можете восстановить касательные следующим образом:

#include <cmath>
#include "glm/geometric.hpp"
#include "glm/vec2.hpp"
#include "glm/vec3.hpp"
#include "glm/vec4.hpp"

using glm::vec2;
using glm::vec3;
using glm::vec4;

void makeTangents(uint32_t nIndices, uint16_t* indices,
                  const vec3 *positions, const vec3 *normals,
                  const vec2 *texCoords, vec4 *tangents) {
  uint32_t inconsistentUvs = 0;
  for (uint32_t l = 0; l < nIndices; ++l) tangents[indices[l]] = vec4(0);
  for (uint32_t l = 0; l < nIndices; ++l) {
    uint32_t i = indices[l];
    uint32_t j = indices[(l + 1) % 3 + l / 3 * 3];
    uint32_t k = indices[(l + 2) % 3 + l / 3 * 3];
    vec3 n = normals[i];
    vec3 v1 = positions[j] - positions[i], v2 = positions[k] - positions[i];
    vec2 t1 = texCoords[j] - texCoords[i], t2 = texCoords[k] - texCoords[i];

    // Is the texture flipped?
    float uv2xArea = t1.x * t2.y - t1.y * t2.x;
    if (std::abs(uv2xArea) < 0x1p-20)
      continue;  // Smaller than 1/2 pixel at 1024x1024
    float flip = uv2xArea > 0 ? 1 : -1;
    // 'flip' or '-flip'; depends on the handedness of the space.
    if (tangents[i].w != 0 && tangents[i].w != -flip) ++inconsistentUvs;
    tangents[i].w = -flip;

    // Project triangle onto tangent plane
    v1 -= n * dot(v1, n);
    v2 -= n * dot(v2, n);
    // Tangent is object space direction of texture coordinates
    vec3 s = normalize((t2.y * v1 - t1.y * v2)*flip);
    
    // Use angle between projected v1 and v2 as weight
    float angle = std::acos(dot(v1, v2) / (length(v1) * length(v2)));
    tangents[i] += vec4(s * angle, 0);
  }
  for (uint32_t l = 0; l < nIndices; ++l) {
    vec4& t = tangents[indices[l]];
    t = vec4(normalize(vec3(t.x, t.y, t.z)), t.w);
  }
  // std::cerr << inconsistentUvs << " inconsistent UVs\n";
}

В вершинном шейдере они повернуты в мировое пространство:

  fragNormal = (model.model * vec4(inNormal, 0)).xyz;
  fragTangent = vec4((model.model * vec4(inTangent.xyz, 0)).xyz, inTangent.w);

Затем бинормаль и нормаль мирового пространства рассчитываются следующим образом (см. http://mikktspace.com/):

  vec3 binormal = fragTangent.w * cross(fragNormal, fragTangent.xyz);
  vec3 worldNormal = normalize(normal.x * fragTangent.xyz +
                               normal.y * binormal +
                               normal.z * fragNormal);

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

person Dan    schedule 02.04.2021