Текстурирование и нормали

37

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

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

Несложно заметить, что стороны куба, имеют мягкие, сглаженные границы. К сожалению, если визуализация осуществляется в среде Windows ХР, качества такого уровня не будет. Из-за упрощенной поддержки в ХР видеодрайверов WPF не пытается выполнить сглаживание граней трехмерных фигур, оставляя их зубчатыми.

Первая задача в построении куба — это определение способа разбиения его на треугольники, которые распознает объект MeshGeometry. Каждый треугольник подобен простой двухмерной фигуре.

Куб состоит из шести квадратных сторон. Каждая квадратная сторона требует для своего отображения двух треугольников. Ниже показано, как можно разбить куб на треугольники:

<MeshGeometry3D Positions="0,0,0 10,0,0 0,10,0 10,10,0 0,0,10 10,0,10 0,10,10 10,10,10" 
                TriangleIndices="0,2,1 1,2,3 0,4,2 2,4,6 0,1,4 1,5,4 1,7,5 1,3,7 4,5,6 7,6,5 2,6,3 3,6,7"></MeshGeometry3D>

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

Трехмерный куб

Коллекция Positions определяет углы куба. Она начинается с четырех точек задней стороны (с z-координатой, равной 0), а затем добавляет четыре точки передней стороны (где z = 10). Свойство TriangleIndices отображает эти точки на треугольники.

Например, первый элемент в этой коллекции — 0, 2, 1. Он описывает треугольник от первой точки (0,0,0) до второй (0,0,10) и третьей (0,10,0). Это — один из двух треугольников, формирующих заднюю грань куба (индекс 1, 2, 3 описывает второй треугольник задней грани).

Вспомните, что при определении треугольники должны определяться в направлении против часовой стрелки, чтобы их лицевая сторона смотрела вперед. Однако в кубе это правило нарушено. Квадраты передней стороны определяются в порядке против часовой стрелки (см. индексы 4, 5, 6 и 7, 6, 5), но поверхность задней стороны описана по часовой стрелке, включая индексы 0, 2, 1 и 1, 2. 3. Это объясняется тем, что обратная сторона куба должна обращать свою лицевую сторону назад. Чтобы лучше представить это, предположим, что куб будет вращаться вокруг оси Y, так что обратная сторона переместится вперед. Теперь те треугольники, которые смотрели назад, будут повернуты вперед, что сделает их полностью видимыми, и получается именно то поведение, которое нужно.

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

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

Чтобы понять, почему так происходит, необходимо немного узнать о нормалях. Каждая нормаль определяет, как вершина ориентирована относительно источника света. В большинстве случаев нормаль должна быть перпендикулярна поверхности треугольника.

Нормали передней стороны куба

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

Нормали можно представлять себе по-другому. Когда вектор нормали направлен противоположно вектору освещения, то поверхность будет освещена полностью. В данном примере это значит, что прямой свет, направленный в (0,0,-1), полностью осветит переднюю грань куба, т.е. получится то, что ожидалось.

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

Нормали на видимых сторонах куба

Так почему же созданный куб ведет себя подобным образом в плане освещения? Виной тому общие точки в коллекции Positions. Хотя нормали определяют текстурирование треугольников, они определены только в вершинах треугольника. Каждая точка в коллекции Positions имеет только одну нормаль, определенную для нее. Это означает, что разделение одних и тех же точек между разными треугольниками также приводит к разделению их общих нормалей.

Выбор правильных нормалей может быть непрост. Чтобы получить нужный результат, помните о следующих двух принципах:

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

private Vector3D CalculateNormal(Point3D pO, Point3D p1, Point3D p2)
{
   Vector3D vO = new Vector3D (p1.X - pO.X, p1.Y - pO.Y, p1.Z - pO.Z);
   Vector3D v1 = new Vector3D (p2.X - pl.X, p2.Y - p1.Y, p2.Z - p1.Z);
   return Vector3D.CrossProduct(vO, vl);
}

Затем понадобится вручную установить свойство Normals, заполнив его векторами. Помните, что к каждой позиции должна быть добавлена одна нормаль.

Пройди тесты
Лучший чат для C# программистов