Получение согласованных нормалей из трехмерного кубического пути Безье

Я пишу класс BezierPath, который содержит список BezierPoints. Каждая точка Безье имеет позицию, касательную и не касающуюся:

введите здесь описание изображения

BezierPath содержит функции для получения линейных положений и касательных из пути. Мой следующий шаг — предоставить функциональность для получения нормалей из пути.

Я знаю, что любая заданная линия в 3D будет иметь неограниченное количество перпендикулярных линий, поэтому однозначного ответа не будет.

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

Моя первая попытка получить начальные касательные — использовать метод Unity3D Quaternion.LookRotation:

Quaternion lookAt = Quaternion.LookRotation(tangent);
Vector3 normal = lookAt * Vector3.up;
Handles.DrawLine(position, position + normal * 10.0f);

В результате получается следующее (зеленые линии — касательные, синие — нормали):

введите здесь описание изображения

По большей части это кажется хорошим базовым результатом, но похоже, что в некоторых направлениях есть внезапные изменения:

введите здесь описание изображения

Итак, мой вопрос: есть ли хороший способ получить согласованные нормали по умолчанию для линий в 3D?

Спасибо, Вес


person Vesuvian    schedule 22.08.2014    source источник
comment
Я ничего не знаю о единстве, совсем не знаком с ним, но я знаю математику Безье как свои пять пальцев и могу дать вам функции, которые вам нужны, чтобы получить нормальную единицу измерения в любой координате, если хотите. Это не будет ответ единства, но он сработает. Если все в порядке, я напишу ответ   -  person Mike 'Pomax' Kamermans    schedule 23.08.2014
comment
Это действительно не проблема Unity. Модераторы здесь, кажется, просто помечают большинство моих вопросов как связанные с единством, поэтому я подумал, что избавлю их от проблем. Я был бы очень рад услышать ваше решение для получения нормалей!   -  person Vesuvian    schedule 23.08.2014


Ответы (1)


Получение нормали для точки на кривой Безье на самом деле довольно прямолинейно, поскольку нормали просто перпендикулярны касательной функции (ориентированы в плоскости направления движения кривой), а касательная функция кривой Безье на самом деле просто еще одна кривая Безье, на 1 порядок ниже. Найдем нормаль кубической кривой Безье. Обычная функция, где (a,b,c,d) — координаты кривой в одном измерении:

function computeBezier (t, a, b, c, d) {
  return a * (1-t)³ + 3 * b * (1-t)² * t + 3 * c * (1-t) * t² + d * t³
}

Обратите внимание, что кривые Безье симметричны, единственная разница между t и 1-t заключается в том, какой конец кривой представляет «начало». Использование a * (1-t)³ означает, что кривая начинается с a. Использование a * t³ вместо этого заставит его начинаться с d.

Итак, давайте определим быструю кривую со следующими координатами:

a = (-100,100,0)
b = (200,-100,100)
c = (0,100,-500)
d = (-100,-100,100)

просто трехмерная кривая

Чтобы получить нормаль для этой функции, нам сначала понадобится производная:

function computeBezierDerivative (t,a,b,c,d) {
  a = 3*(b−a)
  b = 3*(c-b)
  c = 3*(d-c)
  return a * (1-t)² + 2 * b * (1-t) * t + c * t²
}

Сделанный. Вычисление производной до глупости просто (фантастическое свойство кривых Безье).

Теперь, чтобы получить нормаль, нам нужно взять нормализованный вектор касательной с некоторым значением t и повернуть его на четверть оборота. Мы можем повернуть его в нескольких направлениях, поэтому еще одно ограничение состоит в том, что мы хотим повернуть его только в плоскости, которая определяется касательным вектором, а касательный вектор «прямо рядом с ним», на бесконечно малом расстоянии друг от друга.

Касательный вектор для любой кривой Безье формируется просто путем взятия любых имеющихся у вас измерений и их оценки по отдельности, поэтому для трехмерной кривой:

             | computeBezierDerivative(t, x values) |    |x'|
Tangent(t) = | computeBezierDerivative(t, y values) | => |y'|
             | computeBezierDerivative(t, z values) |    |z'|

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

                   |x'|
NormalTangent(t) = |y'| divided by sqrt(x'² + y'² + z'²)
                   |z'|

Итак, давайте нарисуем их зеленым цветом:

наша кривая с касательными, вычисленными во многих точках

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

Учитывая исходную точку f(t1)=p, мы берем точку f(t2)=q с t2=t1+e, где e — небольшое значение, например 0,001, — эта точка q имеет производную q' = pointDerivative(t2), и, чтобы упростить нам задачу, мы перемещаем этот касательный вектор чуть-чуть на p-q, так что оба вектора "начинаются" с p. Довольно просто.

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

Теперь у нас есть два вектора, исходящие из одной и той же координаты: наша настоящая касательная и касательная «следующей» точки, которая настолько близка, что вполне может быть одной и той же точкой. К счастью, из-за того, как работают кривые Безье, эта вторая касательная никогда не совпадает, но немного отличается, и «слегка отличается» — это все, что нам нужно: если у нас есть два нормализованных вектора, начинающихся в одной и той же точке но указывая в разные стороны, мы можем найти ось, вокруг которой нам нужно повернуть одну, чтобы получить другую, просто взяв перекрестное произведение между ними, и, таким образом, мы можем найти плоскость, через которую проходят они оба.

Порядок имеет значение: мы вычисляем c = тангенс₂ × тангенс₁, потому что, если мы вычислим c = тангенс₁ × тангенс₂, мы будем вычислять ось вращения и результирующие нормали в «неправильном» направление. Исправить это буквально просто «взять вектор, умножить на -1» в конце, но зачем исправлять постфактум, когда мы можем сделать это правильно, здесь. Давайте посмотрим на эти оси вращения синим цветом:

наша кривая с добавленной осью перекрестного произведения

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

Для произвольного вращения вокруг оси в 3D эта работа, возможно, кропотлива, но несложна, а четверть оборота вообще особенные тем, что они сильно упрощают математику: чтобы повернуть точку вокруг нашей оси вращения c, получается матрица вращения:

    |     c₁²     c₁*c₂ - c₃  c₁*c₃ + c₂ |
R = | c₁*c₂ + c₃      c₂²     c₂*c₃ - c₁ |
    | c₁*c₃ - c₂  c₂*c₃ + c₁      c₃²    |

Где индексы 1, 2 и 3 на самом деле являются просто компонентами x, y и z нашего вектора. Так что это все еще легко, и все, что осталось, это матрично повернуть нашу нормализованную касательную:

n = R * Tangent "T"

Который:

    | T₁ * R₁₁ + T₂ * R₁₂ + T₃ * R₁₃ |    |nx|
n = | T₁ * R₂₁ + T₂ * R₂₂ + T₃ * R₂₃ | => |ny|
    | T₁ * R₃₁ + T₂ * R₃₂ + T₃ * R₃₃ |    |nz|

И у нас есть вектор(ы) нормалей, которые нам нужны. Идеально!

За исключением того, что мы можем сделать лучше: поскольку мы работаем не с произвольными углами, а с прямыми углами, мы можем использовать существенное сокращение. Точно так же, как вектор c был перпендикулярен обеим касательным, наша нормаль n перпендикулярна как c, так и регулярному касательному, поэтому мы можно использовать векторное произведение во второй раз, чтобы найти нормаль:

                    |nx|
n = c × tangent₁ => |ny|
                    |nz|

Это даст нам точно такой же вектор с меньшими затратами.

Наша кривая с нормалями!

И если нам нужны внутренние нормали, это тот же вектор, просто умножьте на -1:

Наша кривая с внутренними нормалями

Довольно легко, если вы знаете трюки! И, наконец, поскольку код всегда полезен, его суть заключается в Программа, которую я использовал, чтобы убедиться, что говорю правду.

Что, если нормальные люди ведут себя очень странно?

Например, что, если мы используем трехмерную кривую, но она плоская (например, все координаты z равны 0)? Вещи внезапно делают ужасные вещи. Например, давайте посмотрим на кривую с координатами (0,0,0), (-38,260,0), (-25,541,0) и (-15,821,0):

введите здесь описание изображения

Точно так же особенно извилистые кривые могут давать довольно закрученные нормали. Глядя на кривую с координатами (0,0,0), (-38,260,200), (-25,541,-200) и (-15,821,600):

введите здесь описание изображения

В этом случае нам нужны нормали, которые вращаются и скручиваются как можно меньше, что можно найти с помощью алгоритма кадра, минимизирующего вращение, например, описанного в разделе 4 или "Вычисление кадров, минимизирующих вращение" (Wenping Wang, Bert Jüttler, Dayue Zheng, и Ян Лю, 2008 г.).

Реализация их 9-строчного алгоритма требует немного больше работы на обычном языке программирования, таком как Java/Processing:

ArrayList<VectorFrame> getRMF(int steps) {
  ArrayList<VectorFrame> frames = new ArrayList<VectorFrame>();
  double c1, c2, step = 1.0/steps, t0, t1;
  PointVector v1, v2, riL, tiL, riN, siN;
  VectorFrame x0, x1;

  // Start off with the standard tangent/axis/normal frame
  // associated with the curve just prior the Bezier interval.
  t0 = -step;
  frames.add(getFrenetFrame(t0));

  // start constructing RM frames
  for (; t0 < 1.0; t0 += step) {
    // start with the previous, known frame
    x0 = frames.get(frames.size() - 1);

    // get the next frame: we're going to throw away its axis and normal
    t1 = t0 + step;
    x1 = getFrenetFrame(t1);

    // First we reflect x0's tangent and axis onto x1, through
    // the plane of reflection at the point midway x0--x1
    v1 = x1.o.minus(x0.o);
    c1 = v1.dot(v1);
    riL = x0.r.minus(v1.scale( 2/c1 * v1.dot(x0.r) ));
    tiL = x0.t.minus(v1.scale( 2/c1 * v1.dot(x0.t) ));

    // Then we reflection a second time, over a plane at x1
    // so that the frame tangent is aligned with the curve tangent:
    v2 = x1.t.minus(tiL);
    c2 = v2.dot(v2);
    riN = riL.minus(v2.scale( 2/c2 * v2.dot(riL) ));
    siN = x1.t.cross(riN);
    x1.n = siN;
    x1.r = riN;

    // we record that frame, and move on
    frames.add(x1);
  }

  // and before we return, we throw away the very first frame,
  // because it lies outside the Bezier interval.
  frames.remove(0);

  return frames;
}

Тем не менее, это работает очень хорошо. С примечанием, что рамка Френе является «стандартной» касательной/осью/нормальной рамкой:

VectorFrame getFrenetFrame(double t) {
  PointVector origin = get(t);
  PointVector tangent = derivative.get(t).normalise();
  PointVector normal = getNormal(t).normalise();
  return new VectorFrame(origin, tangent, normal);
}

Для нашей плоской кривой теперь мы видим идеально работающие нормали:

введите здесь описание изображения

А в неплоской кривой вращение минимально:

введите здесь описание изображения

И, наконец, эти нормали можно равномерно переориентировать, вращая все векторы вокруг связанных с ними касательных векторов.

person Mike 'Pomax' Kamermans    schedule 23.08.2014
comment
Большое спасибо, Майк, все это выглядит фантастически. Дайте мне несколько часов, чтобы он осознал и воспроизвел его, и я обязательно отмечу этот ответ как правильный. - person Vesuvian; 23.08.2014
comment
Майк, все работает прекрасно, но я заметил, что нормали для прямых линий равны 0,0,0. В обработке так же? - person Vesuvian; 24.08.2014
comment
если у вас есть прямая линия, у нас, строго говоря, есть только одна касательная на всем пути, поэтому нет никакого способа выяснить, через какую плоскость мы движемся. Здесь проще всего, если это часть полилинии, определить нормаль кривой до/после нее и использовать линейную интерполяцию для получения нормалей вдоль линии. Это будет ложью (потому что в трехмерных линейных сегментах нет нормального вектора, а есть только нормальная плоскость), но это будет ложь во благо, потому что они полностью выполнят свою работу. - person Mike 'Pomax' Kamermans; 24.08.2014
comment
с примечанием, что это линейная интерполяция с точки зрения нормального угла вокруг сегмента, а не точки вектора нормали, потому что тогда она стала бы прямой линией вместо красивой спирали =P - person Mike 'Pomax' Kamermans; 24.08.2014
comment
@Mike'Pomax'Kamermans Спасибо за очень подробный ответ - очень полезный и именно то, что я искал! Суть с фактическим кодом больше не находится по ссылке, которую вы предоставляете внизу, вы все равно можете загрузить ее снова? - person acrmuui; 10.04.2015
comment
к сожалению, я удалил его с тех пор (в предположении, что суть останется). - person Mike 'Pomax' Kamermans; 10.04.2015
comment
Ваш пост был чрезвычайно полезен для меня, за исключением того, что ваш метод вычисления нормали на самом деле не работает правильно, если у вас есть сегмент Безье, состоящий из разных сегментов. Как только он проходит разрыв от одного сегмента к другому, переход не является гладким. Вы когда-нибудь сталкивались с такой ситуацией раньше? - person rygo6; 26.11.2016
comment
да. Это означает, что ваш поли-Безье не является C1-непрерывным: конечная точка сегмента 1 и начальная точка сегмента 2 могут перекрываться, и направление касательных может выглядеть одинаково, но фактические касательные векторы, входящие и исходящие из точки, где сегменты соединяются, не равны, и поэтому все, что связано с производной, будет иметь разрыв от сегмента к сегменту. Если вам нужно плавное тангенциальное/нормальное поведение, вам придется исправить поли-Безье так, чтобы он демонстрировал непрерывность C1. - person Mike 'Pomax' Kamermans; 29.11.2016
comment
Привет. Действительно полезный пост. Мне потребовалось некоторое время, чтобы понять это, но теперь я понимаю это. Я использую это для создания 3D-цилиндров вокруг кривой, нормали можно использовать для освещения, а также для создания самой сетки. Однако при переключении знака нормалей на некоторых кривых в сетке возникает интерполированный поворот. Вы знаете, как я мог это решить? Сделать знаки последовательными? - person scippie; 01.07.2018
comment
зачем переключатели знаков для кривых, которые подходят к цилиндрам? Касательная плоскость всегда будет совмещена с поверхностью цилиндра? - person Mike 'Pomax' Kamermans; 01.07.2018
comment
Посмотрите на свой 4-й и 5-й скриншоты. (фиолетовый/красный) нормаль в какой-то момент переворачивается. Вот у меня то же самое. Все мои нормали постоянно имеют один и тот же знак, но в какой-то момент знак просто переворачивается и с этого момента остается прежним. Я распечатал их, и цифры показывают это. - person scippie; 02.07.2018
comment
Какой? Они определенно не переворачиваются, все нормали направлены наружу по отношению к касательной плоскости. Ни в коем случае нормаль не становится антинормалью или наоборот — вы можете видеть нормали, идущие вниз вокруг середины кривой, потому что это не кривая, точно описывающая цилиндр. Вы можете поместить один внутрь, но это совсем не одно и то же. Если мы выберем кривую, которая правильно подходит к цилиндру, сделав его хорошо симметричным, нормали кривой будут полностью вести себя по отношению к описанной поверхности цилиндра. - person Mike 'Pomax' Kamermans; 02.07.2018
comment
Да я тем временем понял, что был неправ, на ваших скринах кривая крутится, поэтому они выглядели как перелистывание, что у меня и получается. Вывод: у меня ошибка в расчетах :) - person scippie; 03.07.2018
comment
Хорошо, я действительно думаю, что воссоздал вашу программу здесь. Можете ли вы попробовать это со следующими координатами? Я получаю переворот нормали/бинормали примерно на 90% кривой: (0,0, 0,0, 0,0), (-0,38, 2,68, 0,00), (-0,25, 5,41, 0,00), (-0,15, 8,21). , 0.00) (они были сгенерированы случайным образом) и если это работает для вас, можете ли вы придумать какую-либо причину, почему это происходит на моем конце? - person scippie; 05.07.2018
comment
Прежде чем приступать к анализу: вы вычисляете 3D-нормали на 2D-кривой, так что это немного странно. Тем не менее, я посмотрю. - person Mike 'Pomax' Kamermans; 05.07.2018
comment
Итак, я заметил: кривая 2D случайно... (генератор случайных чисел был неисправен). Кажется, что это происходит только тогда, когда Z действительно постоянно. Но даже тогда я бы подумал, что этот алгоритм должен работать, не так ли? Кстати, спасибо за ваше время... - person scippie; 06.07.2018
comment
Да, я пытаюсь понять, почему нормали медленно меняются по мере приближения к конечной точке при использовании перекрестного/перекрестного подхода (imgur.com/a/2scASjb). Хотя, возможно, это займет немного больше времени из-за дневной работы =) - person Mike 'Pomax' Kamermans; 06.07.2018
comment
Это происходит потому, что следующая производная может стать ретроградной (т.е. они лежат в одной плоскости, но следующая производная, смещенная к текущей точке, будет лежать позади текущей, вращаясь или даже переворачивая нормаль). Мне придется подумать о другом способе получить истинную норму! Очень рад, что вы обратили на это мое внимание. - person Mike 'Pomax' Kamermans; 07.07.2018
comment
Я заметил такое поведение и в других местах, кроме конца, на других кривых. Спасибо, что вы изучите это! Я надеюсь, что вы можете найти легкое решение для этого. Боюсь, это немного выходит за рамки моего математического понимания. - person scippie; 08.07.2018
comment
Исправление было найдено в math.stackexchange.com/questions/2843307/, я добавлю редактирование к этому ответу, в котором объясняется, как использовать кадры RM, когда обычные нормали исчезают. - person Mike 'Pomax' Kamermans; 11.07.2018
comment
@Mike'Pomax'Kamermans очень полезно, большое спасибо за подробное описание. В производной есть ошибка, которая приводит к правильным нормалям, но неправильным касательным, в операторе возврата не должно быть 3 в последнем члене. - person Josh; 10.08.2019
comment
У меня не работает нормальная функция 3D. Зачем вам нужен массив данных, если мне просто нужна нормаль в заданной позиции t? Кроме того, почему вы начинаете с t0 = -steps, а не с t0 = 0? Вы в основном выходите за пределы диапазона кривой от 0 до 1, переходя от -step к 1.0? Это не имеет особого смысла. Наконец, если функция возвращает массив кадров, какой из них является правильным в любой заданной точке? - person WDUK; 03.11.2019
comment
Почти уверен, что ответ охватывает это: потому что нативная математическая нормаль не является красивой нормалью и поэтому практически бесполезна, если вы делаете такие вещи, как смещение для отслеживания камеры, планирование пути, выдавливание модели и миллион других задач, которые полагаются на достойные нормали. Кроме того, функция Безье — это сегмент бесконечной кривой, поэтому вы начинаете до нуля, чтобы двойное отражение давало нормаль в нуле, которая ведет себя достаточно хорошо. Что касается последнего вопроса: буквально любой вектор вне точки под прямым углом к ​​кривой является нормалью, поэтому нас не волнует правильность. - person Mike 'Pomax' Kamermans; 03.11.2019
comment
Как правило, мы стремимся к набору нормалей вдоль кривой, очерчивающих плавный путь. Нормали, которые вы получаете, вращаясь вокруг касательного вектора на четверть оборота, не дают этого, поэтому, хотя это хорошее первое исследование, это плохой набор нормалей реального мира. - person Mike 'Pomax' Kamermans; 03.11.2019