В предыдущем примере мы создали треугольник, который, конечно, нельзя считать подлинно трехмерной фигурой. Поэтому сейчас создадим куб.
XAML код у нас будет выглядеть следующим образом:
<Window x:Class="_3DApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:_3DApp" mc:Ignorable="d" Title="MainWindow" Height="200" Width="300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <Slider Height="25" Minimum="0" Maximum="360" Value="{Binding ElementName=rotate, Path= Angle}" /> <Viewport3D Grid.Row="1"> <Viewport3D.Camera> <PerspectiveCamera Position="0.5,0.5,3.5" LookDirection="0,0,-3.5" /> </Viewport3D.Camera> <Viewport3D.Children> <ModelVisual3D> <ModelVisual3D.Content> <DirectionalLight Color="White" Direction="-1,-1,-2" /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="0,0,0 1,0,0 0,1,0 1,1,0 0,0,1 1,0,1 0,1,1 1,1,1" 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"/> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial Brush="Blue" /> </GeometryModel3D.Material> </GeometryModel3D> </ModelVisual3D.Content> <ModelVisual3D.Transform> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D x:Name="rotate" Axis="0 1 0" /> </RotateTransform3D.Rotation> </RotateTransform3D> </ModelVisual3D.Transform> </ModelVisual3D> </Viewport3D.Children> </Viewport3D> </Grid> </Window>
В отличие от ранее рассмотренного нами примера с треугольником мы сделали несколько изменений - во-первых, увеличилось количество вершин. Чтобы создать куб, нам потребовалось определить восемь вершин, которые составили 6 сторон куба. Каждая такая сторона состоит из двух треугольников, которые мы определили в свойстве TriangleIndices, то есть нам потребовалось определить 12 треугольников.
Второе отличие состоит в том, что мы определили трехмерную трансформацию - поворот вокруг оси Y. О трехмерных трансформациях мы погорим дальше, а пока мы создали элемент Slider поверх окна ViewPort3D, привязав его свойство Value к свойству Angle элемента AxisAngleRotation3D. И благодаря этому мы можем повращать куб вперед и назад.
Говоря об освещении я сказал, что освещение вычисляется для каждой вершины, а затем цвет интерполируется на весь треугольник. Для треугольника это нормально. Но при создании куба вы можете увидеть, что на стороне куба в том месте, где находится совмещение треугольников (диагональ квадрата, образуемого двумя треугольниками), подобное освещение работает некорректно. И в этом месте мы можем увидеть полосу. Для сглаживания цветов в объекте MeshGeometry3D предназначено свойство Normals.
Это свойство содержит нормали для каждой вершины. Нормаль можно представить как вектор, перпендикулярный поверхности треугольника и определяющий, как вершина ориентирована относительно источника света.
В математическом плане вектор нормали представляет векторное произведение векторов, составляющих две стороны треугольника. Для вычисления вектора нормалей можно воспользоваться средствами самого C#. Так, мы можем использовать следующий код:
private Vector3D CreateNormal(Point3D p0, Point3D p1, Point3D p2) { Vector3D v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z); Vector3D v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z); return Vector3D.CrossProduct(v0, v1); }
Затем полученные значения надо подставить в свойство Normals. Так, в нашем случае объект MeshGeometry3D будет выглядеть следующим образом:
<MeshGeometry3D Positions="0,0,0 1,0,0 0,1,0 1,1,0 0,0,1 1,0,1 0,1,1 1,1,1" 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" Normals="0,1,0 0,1,0 1,0,0 1,0,0 0,1,0 0,1,0 1,0,0 1,0,0" />
В итоге мы получим более сглаженное изображение куба:
Процесс текстурирования трехмерных объектов предусматривает наложение на их поверхность изображений. Сначала мы в качестве кисти материала указываем ImageBrush, который принимает изображение. Затем сопоставляем координаты текстуры с вершинами объекта. Допустим, в файле bronz.jpg у нас находится изображение, тогда весь измененный код объекта GeometryModel3D будет выглядеть так:
<GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="0,0,0 1,0,0 0,1,0 1,1,0 0,0,1 1,0,1 0,1,1 1,1,1" 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" TextureCoordinates="0,1 1,1 0,0 1,0 0,1 1,1 0,0 1,0"/> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <ImageBrush ImageSource="bronz.jpg" /> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D>
В данном случае у нас двухмерные координаты текстуры сопоставляются в координатами вершины. Для лицевой стороны куба наложение происходит так:
Правда, если вы запустите приложение, то увидите, что текстурированию подверглись только передняя и задняя стороны куба. Остальные стороны демонстрируют смазанный эффект.
Чтобы избежать подобного эффекта, нам надо определить по четыре разных точки для каждой стороны куба. То есть всего у нас получится 24 вершины, а не 8, как сейчас. После этого мы можем совместить с каждой точкой текстуры вершину, и изображение будет отображаться на каждой стороне куба, хотя объем кода при этом немного возрастет.
С помощью одной Visual Studio очень не просто создавать сложные трехмерные объекты. Поэтому в данном случае лучше воспользоваться специальными программами. Одной из таких программ является Blender. Для экспорта трехмерных моделей из Blender в XAML можно использовать проект XAML Exporter for Blender.