Изображение полиэдра
Метод drawEdge фиксирует обрабатываемое ребро, записывая его начало и конец в компоненты begin и end, и инициализирует список просветов list, помещая в него единственный элемент — ребро целиком. Затем с помощью вызова метода addShadow производится учет теней от всех граней полиэдра и осуществляется изображение всего списка оставшихся просветов. Для изображения просвета необходимо вычисление трехмерных координат точек ребра по известным их одномерным координатам. Это действие выполняется с помощью метода R3 по известной из аналитической геометрии формуле.
Учет тени от конкретной грани производится путем определения одномерной тени на обрабатываемом ребре с помощью вызова метода shadow и индуктивного перевычисления списка просветов по алгоритму, описанному выше. Отметим дополнительно, что вырожденные отрезки в список просветов никогда не включаются.
Заключительным этапом решения задачи является реализация метода shadow. Этот метод должен определить отрезок на обрабатываемом ребре, который представляет собой тень от фиксированной грани полиэдра.
Прежде всего следует разобраться с тем, что же такое тень от грани. Сначала заметим, что вертикальные грани дают пустую тень. Для остальных граней множество затеняемых ими точек пространства образует полубесконечную призму, у которой основанием является грань полиэдра, а боковая поверхность параллельна вектору проектирования.
Тень от грани на ребро при этом оказывается просто пересечением ребра с этой призмой. Заметим, что призму следует считать открытой, т.е. считать, что граница призмы тени не принадлежит, так как в противном случае каждая грань будет затенять сама себя и изображение окажется пустым.
Естественно попытаться свести нахождение пересечения ребра с призмой к каким-нибудь более элементарным операциям. Мы сделаем это, представив призму в виде пересечения открытых полупространств. Например, треугольную призму можно представить в виде пересечения четырех полупространств (см. рис. 13.6).
Первое полупространство ограничено плоскостью, проходящей через грань полиэдра, и расположено со стороны, противоположной направлению вектора проектирования. Это полупространство мы назовем горизонтальным. Остальные полупространства (их число совпадает с количеством ребер грани) ограничены вертикальными плоскостями, проходящими через ребра грани, и расположены так, что сама грань (или, что тоже самое — ее центр) содержится в соответствующем полупространстве. Эти полупространства мы будем называть вертикальными.
Искомое пересечение ребра и призмы, представляющей собой тень, сводится теперь к последовательному нахождению пересечения ребра с полупространствами (сначала горизонтальным, а затем — всеми вертикальными). Приведем реализацию всех необходимых для этого методов класса ShadowDrawer.
private static final double EPSILON = 1.e-12; private Segment shadow(Facet f) { if (f.vertical(pr)) return new Segment(t1, t0); int n = f.getVertexesQuantity(); Vertex a = f.getVertex(n-1); Vertex b = f.getVertex(0); Segment result = hCross(f, a); if (result.degenerate()) return result; result.intersection(vCross(a, b, f.getCenter())); if (result.degenerate()) return result; for (int i=1; i<n; i++) { a = b; b = f.getVertex(i); result.intersection(vCross(a, b, f.getCenter())); if (result.degenerate()) return result; } return result; } private Segment hCross(Facet f, R3Vector a) { R3Vector n = f.getNormal(); if (R3Vector.scalMul(n, pr) < 0.0) n.mul(-1); return crossWith(a, n); } private Segment vCross(R3Vector a, R3Vector b, R3Vector c) { R3Vector n = R3Vector.vectMul(R3Vector.minus(b,a), pr); if (R3Vector.scalMul(n, R3Vector.minus(a,c)) < 0.0) n.mul(-1); return crossWith(a, n); } private Segment crossWith(R3Vector a, R3Vector n) { double f0 = R3Vector.scalMul(n, R3Vector.minus(begin, a)); double f1 = R3Vector.scalMul(n, R3Vector.minus(end, a)); if(Math.abs(f0) < EPSILON) f0 = 0.; if(Math.abs(f1) < EPSILON) f1 = 0.; if(f0 >= 0. && f1 >= 0.) return new Segment(t1, t0); if(f0 < 0. && f1 < 0. ) return new Segment(t0, t1); double t = - f0 / (f1 - f0); if (f0 < 0.) return new Segment(t0, t); return new Segment(t, t1); }
Договоримся задавать полупространство, пересечения ребра с которым необходимо уметь вычислять, точкой на ограничивающей его плоскости и вектором внешней нормали к этой плоскости. Не разбираясь пока в деталях реализации метода crossWith с указанными аргументами (в тексте программы это точка a и вектор нормали n ), обсудим задачу нахождения одномерной тени, решаемую методами shadow, hCross и vCross.
Для реализации метода hCross нахождения пересечения с горизонтальным полупространством нужно найти нормаль к грани (выбрав правильное направление) и вызвать метод crossWith. Ясно, что из соображений эффективности операцию вычисления нормали к грани лучше выполнить только один раз, что и делается в конструкторе класса Facet. Направление нормали будет правильным при условии положительности скалярного произведения ее с вектором проектирования. Если это условие нарушено, то вектор следует заменить на противоположный, умножив его на минус единицу.
Вычисление пересечения с вертикальным полупространством с помощью метода vCross удастся реализовать, зная соответствующее ребро грани (точки и ) и центр грани . Точку при этом можно использовать в качестве аргумента для вызова метода crossWith непосредственно, а нормаль к вертикальной плоскости, проходящей через ребро , находится следующим образом. Сначала вычисляется векторное произведение вектора и вектора проектирования. Результат этой операции заведомо является нормалью, но может быть направлен не в ту сторону — не наружу, а внутрь.
Так как все грани полиэдра по самому его определению являются выпуклыми многоугольниками, центр каждой из граней всегда находится внутри ее. По этой причине вектор всегда направлен внутрь вертикального полупространства, пересечение с которым мы ищем. Если точки и являются вершинами грани, следующими друг за другом в порядке против часовой стрелки, то положительность скалярного произведения векторов и вычисленного по указанному выше способу вектора нормали гарантирует нужное направление последнего. В противном случае необходимо его умножение на минус единицу. Вычисление центра грани также целесообразно производить единственный раз — в конструкторе класса Facet.
Вернемся теперь к методу shadow. Если грань, одномерную тень от которой мы ищем, вертикальна, то ее тень пуста. В этом случае возвращаемый отрезок тени должен быть вырожден, что и реализуется в первых двух строках тела метода.
Для невертикальной грани сначала вычисляется отрезок result, являющийся результатом пересечения ребра с горизонтальным полупространством. Затем последовательно для всех ребер грани индуктивно находится пересечение текущего значения величины result с вертикальными полупространствами, соответствующими ребрам. При этом вычисления немедленно прекращаются, если будет получен вырожденный отрезок.
Теперь нам осталось разобраться с последним из методов класса ShadowDrawer — методом crossWith нахождения пересечения ребра с полупространством, заданным точкой и вектором внешней нормали . В одномерных координатах точкам и соответствует ноль и единица, а результатом работы метода должны быть одномерные координаты отрезка пересечения.
Рассмотрим функцию , где — точка на ребре , — ее одномерная координата, а угловые скобки означают скалярное произведение двух векторов. Эта функция линейна и является знакопостоянной на отрезке в том случае, если ребро целиком лежит вне или внутри полупространства. В случае пересечения ребра с плоскостью, ограничивающей полупространство, одномерная координата точки пересечения находится из уравнения .
Из вышесказанного следует, что задача нахождения пересечения ребра с полупространством может быть решена таким образом. Вычислим сначала значения функции в концах отрезка — и (в программной реализации им соответствуют величины f0 и f1 ). Если обе эти величины неотрицательны, то пересечение пусто, если, наоборот, они обе отрицательны, то весь отрезок лежит в полупространстве.
Для определения одномерной координаты точки пересечения решим уравнение . В силу линейности функции она имеет вид . Так как , а , то коэффициенты и легко определяются. После этого легко вычислить и корень указанного уравнения: , , .
Теперь остается выбрать только нужную часть отрезка, что и реализовано в двух последних строках тела метода crossWith. В заключение обсудим назначение третьей и четвертой строк, в которых используется малая по абсолютной величине константа EPSILON. Их роль — попытка исправить те ошибки в работе метода, которые обусловлены приближенными вычислениями с числами типа double. В результате ошибок округления некоторые видимые части ребер не изображаются и наоборот.
Добавление указанных операторов несколько улучшает ситуацию, не исправляя ее полностью. Одна из задач, предназначенных для самостоятельного решения, предлагает справиться с этой проблемой.