WPF вращение 3D кубиков

Все исходники /  Язык программирования C# /  OS Windows /  Desktop /  WPF программирование / WPF вращение 3D кубиков

Приложение WPF вращения 3D кубиков

Вращение 3D кубиков WPF

WPF 3D API позволяет создавать реалистичные трехмерные сцены со сложностью незначительно превышающей построение 2D интерфейса. Трехмерную графику можно легко встроить в любой двухмерный элемент приложения, будь то контейнер или кнопка. Из 3D объектов WPF можно создавать интерфейсы приложений с красивыми объемными эффектами и игры не требующие высокой производительности в реальном времени.

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

Данный исходник предлагается в продолжение тем WPF 3D на страницах 3D графика и WPF 3D координаты.

Viewport3D - окно в 3D мир

Вывод 3D объектов в окно приложения WPF происходит через контейнер Viewport3D. Дочерними элементами Viewport3D могут быть только объекты классов унаследованных от Visual3D. Viewport3D является потомком FrameworkElement, благодаря этому трехмерное изображение можно вывести практически в любом 2D элементе, например: Grid, Canvas, Button, Label, TextBlock и т.д. При размещении Viewport3D в двухмерных элементах в некоторых случаях необходимо определять размеры Viewport3D.Width и Viewport3D.Height, иначе трехмерное изображение может быть невидимо.

Координатные оси и LookDirection

Чтобы увидеть 3D содержимое Viewport3D необходимо создать камеру с помощью свойства Viewport3D.Camera. В нашем примере определена камера проекции перспективы PerspectiveCamera. Минимально необходимо определить свойства камеры: Position - позиция камеры, LookDirection - направление взгляда камеры, UpDirection - угловая ориентация камеры в пространстве.

Position и LookDirection взаимосвязанные между собой величины посредством точки в пространстве на которую должна смотреть камера. Изменяя позицию камеры, необходимо корректировать направление взгляда и наоборот. Методом вычитания векторов точки в пространстве и позиции камеры определяется вектор направления взгляда камеры LookDirection. Вектор LookDirection начинается от центра координат и указывает только направление взгляда камеры.

Пример вычисления вектора направления LookDirection (см. рисунок выше):
LookDirection = LookPoint - Camera
LookDirection = (-4; 2) - (-1; -3) =>
LookDirection = (-3; 5)
Проверка направления взгляда переносом 
вектора LookDirection в позицию камеры:
| x1 = -3 + (-1) => x1 = -4
| y1 = 5 + (-3)  => y1 = 2
LookPoint = (x1; y1) =>
Направление вектора LookDirection вычислено верно.

UpDirection - по умолчанию направление вектора вдоль оси Y (0, 1, 0). Вектор поворота камеры вправо на 90 градусов имеет значения (1, 0, 0), влево - (-1, 0, 0). Аналогичные повороты на 45 градусов соответственно: (1, 1, 0) и (-1, 1, 0).

Viewport3D.Children - объекты трехмерного пространства, коллекция объектов типа Visual3D, к ним же относятся и источники освещения. Для того чтобы определить собственный класс создания трехмерных объектов, необходимо чтобы он был наследником классов Visual3D. В прикрепленном исходнике 3D куб визуализирует класс Cube3D наследующий от ModelVisual3D.

<Window x:Class="Wpf3DCube.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:mycode="clr-namespace:Wpf3DCube.PashaCode"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Background="#000044">
        <Viewport3D>
            <Viewport3D.Camera>
                <PerspectiveCamera 
                    x:Name="perCamera" 
                    Position="0,0,5"
                    LookDirection="0,0,-5" 
                    UpDirection="0,1,0" />
            </Viewport3D.Camera>

<!-- === объекты Viewport3D.Children ==== -->

            <!-- Определение источников света -->
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <AmbientLight Color="#FF444444"></AmbientLight>
                        <PointLight Color="White" Position="-2,8,5"></PointLight>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            
            <!-- Коллекция 3D геометрии -->
             
            <mycode:Cube3D>
                ...
            </mycode:Cube3D>
            
            <mycode:Cube3D>
                ...
            </mycode:Cube3D>
            
            <mycode:Cube3D>
                ...
            </mycode:Cube3D>

<!-- === /объекты Viewport3D.Children ==== -->
        </Viewport3D>
    ...
        
    </Grid>
</Window>

Класс создания объемного куба Cube3D

Цель класса Cube3D - создать простой и удобный интерфейс пользователя для визуализации объемного куба. Внутри себя класс инкапсулирует методы построения сложного трехмерного объекта из группы примитивных треугольников. Как было описано выше, содержимым Viewport3D могут быть только потомки абстрактного класса Visual3D. Для возможности добавления объектов класса к коллекции Viewport3D.Children Cube3D наследует от ModelVisual3D. Открытые свойства класса используются в исходнике для настройки трехмерного куба в файле разметки XAML.

Интерфейс пользователя Cube3D

Интерфейс построения объемного куба в классе Cube3D сводится к нескольким открытым свойствам:
  • Position типа Point3D - определяет размещение объекта в трехмерном пространстве.
  • Size типа double - определяет размер грани куба
  • 6 свойств типа Brush определяют материал (цвет) отдельно для каждой грани.
После изменения любого из свойств происходит перерисовка куба. Программный код открытых свойств класса Cube3D:
private double _size = 0.5;
public double Size
{
    get => _size;
    set
    {
        _size = value;
        // Перерисовка куба после изменения свойства.
        DrawCube(_size, _pos, _front, _top, _left, _right, _bottom, _back);
    }
}

private Point3D _pos;
public Point3D Position
{
    get => _pos;
    set
    {
        _pos = value;
        DrawCube(_size, _pos, _front, _top, _left, _right, _bottom, _back);
    }
}

// Материалы граней
private Brush _front = _defaultColor;
public Brush Front
{
    get => _front;
    set
    {
        _front = value;
        DrawCube(_size, _pos, _front, _top, _left, _right, _bottom, _back);
    }
}

private Brush _top = _defaultColor;
public Brush Top
{
    get => _top;
    set
    {
        _top = value;
        DrawCube(_size, _pos, _front, _top, _left, _right, _bottom, _back);
    }
}

...

private Brush _bottom = _defaultColor;
public Brush Bottom
{
    get => _bottom;
    set
    {
        _bottom = value;
        DrawCube(_size, _pos, _front, _top, _left, _right, _bottom, _back);
    }
}

Метод создания прямоугольной грани

Закрытый метод AddFace(...) формирует прямоугольную грань из двух треугольников. Метод принимает четыре координаты вершин и материал грани. При создании треугольников вершины упорядочиваются, поэтому индексы вершин определять нет необходимости. Метод возвращает готовую грань куба с материалом в виде объекта типа GeometryModel3D. Использование в приложении метода AddFace(...) избавляет от излишнего повторяющегося кода: на шесть граней куба программный код только одного метода.

Создание грани из треугольников происходит в два этапа:
  • Создание координатной сетки из вершин треугольников в объекте класса MeshGeometry3D.
  • Создание трехмерного предмета по координатам объекта MeshGeometry3D из указанного материала в объекте класса GeometryModel3D.
Программный код метода:
private static GeometryModel3D AddFace(
    Point3D point1,
    Point3D point2,
    Point3D point3,
    Point3D point4,
    Material material)
{
    // Строит трехмерный объект из сетчатой геометрии
    // и указанного материала. 
    GeometryModel3D geometryModel3D = new()
    {
        // Координаты построения геометрии объекта.
        Geometry = new MeshGeometry3D()
        {
            Positions = new()
            {
                // Координаты вершин граней.
                point1,
                point2,
                point3,
                point3,
                point4,
                point1
            }
        },
        Material = material
    };

    return geometryModel3D;
}

Метод рисования 3D куба

Последовательность по номерам вершин 3D куба

Закрытый метод DrawCube(...) формирует из граней готовый куб, определяет положение куба в пространстве и цвет каждой грани в отдельности. Внутри метода создаются вершины для всех углов трехмерного куба. Затем, упорядочивая вершины против часовой стрелки, грани объединяются в группу представляющую 3D куб как единое целое.

Чтобы визуализировать объединённую 3D геометрию, группа присваивается свойству ModelVisual3D.Content класса Cube3D. И только после этого трехмерные кубики становятся готовыми к рендерингу в области просмотра Viewport3D.

Программный код метода:
private void DrawCube(
    double size, 
    Point3D pos,
    Brush front, 
    Brush top,
    Brush left, 
    Brush right, 
    Brush bottom, 
    Brush back)
{
    // Отсчёт точек от левого нижнего угла грани.

    // Размерности граней симметричны в обе стороны в абсолютных величинах.
    double absX = size / 2;
    double absY = size / 2;
    double absZ = size / 2;

    // Вершины всех граней куба.
    Point3D front_left_bottom = new(-absX + pos.X, -absY + pos.Y, absZ + pos.Z);
    Point3D front_right_bottom = new(absX + pos.X, -absY + pos.Y, absZ + pos.Z);
    Point3D front_right_top = new(absX + pos.X, absY + pos.Y, absZ + pos.Z);
    Point3D front_left_top = new(-absX + pos.X, absY + pos.Y, absZ + pos.Z);
    Point3D backside_right_top = new(absX + pos.X, absY + pos.Y, -absZ + pos.Z);
    Point3D backside_left_top = new(-absX + pos.X, absY + pos.Y, -absZ + pos.Z);
    Point3D backside_left_bottom = new(-absX + pos.X, -absY + pos.Y, -absZ + pos.Z);
    Point3D backside_right_bottom = new(absX + pos.X, -absY + pos.Y, -absZ + pos.Z);


    Model3DGroup m3dg = new();

    // 1 Передняя
    DiffuseMaterial material = new(front);
    GeometryModel3D faceFront = AddFace(
            front_left_bottom,
            front_right_bottom,
            front_right_top,
            front_left_top,
            material);

    m3dg.Children.Add(faceFront);

    // 2 Верхняя
    material = new(top);
    GeometryModel3D faceTop =
        AddFace(
            front_left_top,
            front_right_top,
            backside_right_top,
            backside_left_top,
            material);
    m3dg.Children.Add(faceTop);

    // 3 Левая
    material = new(left);
    GeometryModel3D faceLeft =
        AddFace(
            backside_left_bottom,
            front_left_bottom,
            front_left_top,
            backside_left_top,
            material);
    m3dg.Children.Add(faceLeft);

    // 4 Правая
    material = new(right);
    GeometryModel3D faceRight =
        AddFace(
            front_right_bottom,
            backside_right_bottom,
            backside_right_top,
            front_right_top,
            material);
    m3dg.Children.Add(faceRight);

    // 5 Нижняя
    material = new(bottom);
    GeometryModel3D faceBottom =
        AddFace(
            backside_left_bottom,
            backside_right_bottom,
            front_right_bottom,
            front_left_bottom,
            material);
    m3dg.Children.Add(faceBottom);

    // 6 Задняя
    material = new(back);
    GeometryModel3D faceBack =
        AddFace(
            backside_right_bottom,
            backside_left_bottom,
            backside_left_top,
            backside_right_top,
            material);
    m3dg.Children.Add(faceBack);

    Content = m3dg;
}

XAML

Прорисовка и трансформация трехмерных кубиков определена в файле XAML приложения. Запуск вращения происходит по нажатию кнопки мыши на области окна приложения.

Для использования программного кода собственного класса Cube3D, в разметке XAML приложения необходимо сопоставить для него пространство имён текущей сборки с префиксом для имени класса в разметке XAML: xmlns:mycode="clr-namespace:Wpf3DCube.PashaCode". После этой процедуры можно создавать трехмерные кубы настраивая внешний вид с помощью интерфейса пользователя класса Cube3D.

Кубики освещаются точечным источником, по свойствам напоминающим свет от электрической лампочки. При вращении кубиков интенсивность освещения граней меняется, тем самым усиливая иллюзию трехмерного пространства. Если освещать кубики только одним точечным источником, неосвещаемые грани будут полностью черного цвета. Для устранения этого недостатка в трехмерный мир добавлен ещё источник рассеянного света <AmbientLight Color="#FF444444"></AmbientLight>. Чтобы был виден цвет неосвещенных граней и в то же время не терялась реалистичность, интенсивность рассеянного света понижена до серого цвета. Если же сделать рассеянный свет белым иллюзия трехмерного света исчезнет.

Листинг разметки XAML приложения создания и вращения трехмерных кубиков:
<Window x:Class="Wpf3DCube.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:mycode="clr-namespace:Wpf3DCube.PashaCode"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Background="#000044">
        <Viewport3D>
            <Viewport3D.Camera>
                <PerspectiveCamera
                    x:Name="perCamera" 
                    Position="0,0,5" 
                    LookDirection="0,0,-5" 
                    UpDirection="0,1,0" />
            </Viewport3D.Camera>


            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <AmbientLight Color="#FF444444"></AmbientLight>
                        <PointLight Color="White" Position="-2,8,5"></PointLight>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>


             <!-- Коллекция из 3-х кубиков, объединяющая 3D вид и трансформацию.
            Рисовать можно  используя только класс Cube3D, 
            потому что он происходит от ModelVisual3D.
            -->

            <mycode:Cube3D x:Name="Cube1"
                Size="0.2" 
                Position="-0.6,0,0" 
                Front="#9acd32" 
                Top="#7b68ee" 
                Left="#696969" 
                Back="#7fffd4" 
                Bottom="#8a2be2" 
                Right="#e6e6fa" >
                <ModelVisual3D.Transform>
                    <Transform3DGroup>
                        <RotateTransform3D CenterX="-0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="1,0,0" 
                                    Angle="0" 
                                    x:Name="rotateX"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="-0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,1,0" 
                                    Angle="0" 
                                    x:Name="rotateY"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="-0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,0,1" 
                                    Angle="0" 
                                    x:Name="rotateZ"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                    </Transform3DGroup>
                </ModelVisual3D.Transform>
            </mycode:Cube3D>

            <mycode:Cube3D x:Name="Cube2"
                Size="0.5" 
                Position="0,0,0" 
                Front="Red" 
                Top="Yellow" 
                Left="Blue" 
                Back="DeepPink" 
                Bottom="DarkGreen" 
                Right="White" >
                <ModelVisual3D.Transform>
                    <Transform3DGroup>
                        <RotateTransform3D CenterX="0">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="1,0,0" 
                                    Angle="0" 
                                    x:Name="rotateX2"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="0">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,1,0" 
                                    Angle="0" 
                                    x:Name="rotateY2"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="0">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,0,1" 
                                    Angle="0" 
                                    x:Name="rotateZ2"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                    </Transform3DGroup>
                </ModelVisual3D.Transform>
            </mycode:Cube3D>

            <mycode:Cube3D x:Name="Cube3"
                Size="0.2" 
                Position="0.6,0,0" 
                Front="#483d8b" 
                Top="#800080" 
                Left="#db7093" 
                Back="#008080" 
                Bottom="#808000" 
                Right="#7cfc00" >
                <ModelVisual3D.Transform>
                    <Transform3DGroup>
                        <RotateTransform3D CenterX="0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="1,0,0" 
                                    Angle="0" 
                                    x:Name="rotateX3"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,1,0" 
                                    Angle="0" 
                                    x:Name="rotateY3"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <RotateTransform3D CenterX="0.6">
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D 
                                    Axis="0,0,1" 
                                    Angle="0" 
                                    x:Name="rotateZ3"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                    </Transform3DGroup>
                </ModelVisual3D.Transform>
            </mycode:Cube3D>

        </Viewport3D>

<!-- Код разметки для запуска вращения -->
        <Grid.Triggers>
            <EventTrigger RoutedEvent="Grid.MouseDown">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateX" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="720" 
                            Duration="0:0:5"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateY" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="720" 
                            Duration="0:0:6"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateZ" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="720" 
                            Duration="0:0:6"/>

                        <DoubleAnimation 
                            Storyboard.TargetName="rotateX2" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="360" 
                            Duration="0:0:6"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateY2" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="360" 
                            Duration="0:0:5"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateZ2" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="360" 
                            Duration="0:0:6"/>

                        <DoubleAnimation 
                            Storyboard.TargetName="rotateX3" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="1080" 
                            Duration="0:0:6"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateY3" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="360" 
                            Duration="0:0:6"/>
                        <DoubleAnimation 
                            Storyboard.TargetName="rotateZ3" 
                            Storyboard.TargetProperty="Angle" 
                            From="0" To="360" 
                            Duration="0:0:5"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Grid.Triggers>

    </Grid>
</Window>

Исходник приложения WPF 3D

Приложение вращения трехмерных кубиков создано в интегрированной среде программирования MS Visual Studio 2022, платформа .NET6.

Скачать исходник