Manifold Geometry // Многообразная Геометрия

Архивы

Построение трубки по двум сечениям

"It is a geometric developer’s responsibility to not only implement sophisticated mathematical algorithms but to hide that complexity from the calling application and the user. Too often we see programmatic interfaces to geometric modeling operations that require the caller be versed in all the intricacies of the mathematics." Ken Versprill (http://isicad.net/articles.php?article_num=16045).

Рассмотрим классическую задачу геометрического моделирования о протягивании трубки через пару сечений вдоль заданной траектории.

Исходные сечения и траектория.

Эта операция настолько стандартна, что речь, казалось бы, должна идти об одной функции геометрического ядра, и углубляться здесь особо некуда. Однако оказывается, что кое-какие трудности есть, и о них следует поговорить. Начнем с примитивного теста, представленного нашим читателем Дмитрием:

// Construct path.
TColgp_Array1OfPnt pathPoles(1, 3);
pathPoles(1) = gp_Pnt(0,   0,   0);
pathPoles(2) = gp_Pnt(100, 0,   0);
pathPoles(3) = gp_Pnt(100, 100, 0);
//
Handle(Geom_BezierCurve) path = new Geom_BezierCurve(pathPoles);
  
// Construct sections.
Handle(Geom_Curve) c1 = new Geom_Circle(gp_Ax2( gp_Pnt(0,   0,   0), gp::DX() ), 10.0);
Handle(Geom_Curve) c2 = new Geom_Circle(gp_Ax2( gp_Pnt(100, 100, 0), gp::DY() ), 20.0);
  
// Make pipe.
GeomFill_Pipe Pipe(path, c1, c2);
Pipe.Perform();
  
// Get the result.
const Handle(Geom_Surface)& result = Pipe.Surface();

Данный код создает опорные кривые, показанные на рисунке выше: траекторию и две окружности, представляющие первое и последнее сечения. Однако выполнение кода неожиданно приводит к ошибке. OpenCascade выбрасывает исключение на конструировании поверхности в методе GeomFill_NSections::ComputeSurface().

Неожиданность.

Победа науки над здравым смыслом

Исключение Standard_ConstructionError говорит о том, что была сделана попытка создать геометрический примитив с некорректными параметрами. Вспомните фундаментальное отношение для B-сплайнов, связывающее количество узлов с числом контрольных точек и степенью функции (m = n + p + 1) [Piegl L. On NURBS: A Survey. 1991]. Если это отношение нарушено, возникает исключение, которое мы и наблюдаем. Копнув немного глубже, легко убедиться, что метод GeomFill_NSections::ComputeSurface() не учитывает периодичность сечений и создает B-поверхность так, будто опорные кривые никогда не являются периодическими (sic!). Понятно, что это ошибка ядра. Мы вернемся к этой проблеме в конце заметки, а пока копаем дальше.

Раз оператор GeomFill_Pipe лажает на периодических сечениях, давайте сделаем их непериодическими насильно. Для этого используем следующий трюк:

Handle(Geom_Curve)
  c1 = new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp_Pnt(0, 0, 0), gp::DX() ), 10.0), 0, 2*M_PI);
//
Handle(Geom_Curve)
  c2 = new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp_Pnt(100, 100, 0), gp::DY() ), 20.0), 0, 2*M_PI);

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

Неожиданность номер 2.

Не нужен микроскоп, чтобы разглядеть абсурдность полученной формы. Поверхность мало того, что не протянута вдоль траектории, но имеет самопересечение, сплющиваясь где-то на середине пути. Оказывается, второе сечение оператор GeomFill_Pipe воспринимает иначе, чем этого мог ожидать наивный пользователь. Метод GeomFill_Pipe::Init() ИЗМЕНЯЕТ исходные сечения, размещая их на траектории при помощи инструмента GeomFill_SectionPlacement. Чтобы правильно зарядить оператор GeomFill_Pipe, сечения должны задаваться в ЛОКАЛЬНОЙ системе координат, связанной с началом траектории, то есть вот так:

Открытие номер 1.

Таким образом, наш код следует в очередной раз поправить (заметьте, как изменились параметры в gp_Ax2):

Handle(Geom_Curve)
  c1 = new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 10.0), 0, 2*M_PI);
//
Handle(Geom_Curve)
  c2 = new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 20.0), 0, 2*M_PI);

Повторный тест дает результат лучше, чем раньше, но и к нему есть вопросы. Нетрудно видеть, что результирующая поверхность не проходит через опорные сечения!

Неожиданность номер 3.

Здесь надо лирически отступить. Построение трубки — это в сущности оператор скиннинга, требующий наличия согласованных сечений в виде B-кривых (сплайнов). Неудивительно поэтому, что GeomFill_Pipe преобразует сечения-окружности в сплайны, притом рациональные. В то же время, известно, что скиннинг желательно осуществлять на нерациональных сечениях во избежание разнообразных аномалий [Piegl L., Tiller W. Surface skinning revisited // The Visual Computer. 2002. N 4 (18). p. 273–283]. Чтобы запретить оператору GeomFill_Pipe преобразовывать сечения сомнительным образом, сделаем это сами, указав, что нас интересует полиномиальная (НЕ рациональная) аппроксимация:

Handle(Geom_Curve)
  c1 = GeomConvert::CurveToBSplineCurve(new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 10.0), 0, 2*M_PI), Convert_Polynomial);
//
Handle(Geom_Curve)
  c2 = GeomConvert::CurveToBSplineCurve(new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 20.0), 0, 2*M_PI), Convert_Polynomial);

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

Открытие номер 2.

Мораль

Библиотека OpenCascade — это мощный геометрический инструмент, содержащий тонну интересных математических букв. Но «история одной трубки», изложенная выше — это тревожный симптом. Не таких упражнений ждет от библиотеки геометрического моделирования пользователь. Ему нужен простой и понятный прикладной интерфейс, в котором операция кинематического моделирования — это вызов одной функции, а не полдня занимательной герменевтики.

Отметим здесь, что GeomFill_Pipe — это наследие, устаревший код. Мы видим, что данный оператор требует переосмысления. Настоящая заметка помогла вскрыть очередное проблемное место библиотеки, над которым теперь предстоит работать. По этому поводу в официальном багтрекере была заведена проблема #30003.

Приложение

Финальный код выглядит следующим образом:

// Construct path.
TColgp_Array1OfPnt pathPoles(1, 3);
pathPoles(1) = gp_Pnt(0,   0,   0);
pathPoles(2) = gp_Pnt(100, 0,   0);
pathPoles(3) = gp_Pnt(100, 100, 0);
//
Handle(Geom_BezierCurve) path = new Geom_BezierCurve(pathPoles);
  
// Construct sections.
Handle(Geom_Curve)
  c1 = GeomConvert::CurveToBSplineCurve(new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 10.0), 0, 2*M_PI), Convert_Polynomial);
//
Handle(Geom_Curve)
  c2 = GeomConvert::CurveToBSplineCurve(new Geom_TrimmedCurve(new Geom_Circle(gp_Ax2( gp::Origin(), gp::DX() ), 20.0), 0, 2*M_PI), Convert_Polynomial);
  
// Make pipe.
GeomFill_Pipe Pipe(path, c1, c2);
Pipe.Perform();
  
// Get the result.
const Handle(Geom_Surface)& result = Pipe.Surface();