WPF типы 3D координат

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

Трехмерные системы координат

Для цифрового трехмерного мира сформировалась определенная терминология систем координат. Это не стандарт, но эти понятия очень помогают программистам при разработках 3D приложений и игр.

Трехмерных координатных систем для преобразования объектов может быть множество, перечислю список основных, которые связаны с визуализацией 3D мира:
  • Мировые координаты
  • Локальные координаты
  • Координаты камеры
  • Координаты группы 3D объектов
В WPF 3D API принята правосторонняя система трехмерных координат: ось X направлена вправо, ось Y вверх, ось Z направлена на наблюдателя. Величины трехмерных координат выражаются в относительных значениях. Например размеры 3D мира:
- ширина 2 единицы, -1 ≤ x ≤ 1,
- высота 4 единицы, -1 ≤ y ≤ 3, 
- глубина 10 единиц, 1 ≤ z ≤ -9.

Локальные координаты

Локальные 3D координаты

Каждый 3D объект имеет собственную координатную систему, называемую локальной. Относительно этой локальной системы он может перемещаться, вращаться и масштабироваться. Изменение координат предмета в локальной системе не влияет на положение других объектов в общем 3D пространстве.

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

Центр локальной системы не обязательно должен находиться в середине объекта. Например, если мы создаём футбольный мяч, то ноль системы координат должен находиться в центре мяча. Если мы создаем 3D модель стрелы подъемного крана, то центр локальной системы будет находиться далеко от середины: в точке шарнира крепления стрелы.

Простейший трехмерный объект - это треугольник. Из комбинаций треугольников строятся все 3D объекты, называемые сеткой - mesh. В нашем случае квадраты состоят из двух треугольников, имеющих общие вершины. Локальные координаты квадратов:
- вершина 0: (-0.25, -0.25, 0),
- вершина 1: (0.25, -0.25, 0),
- вершина 2: (0.25, 0.25, 0),
- вершина 3: (-0.25, 0.25, 0).

Реальный 3D mesh может состоять из многих тысяч треугольников. Координаты всех вершин треугольников располагаются в локальной трехмерной системе координат объекта. В дальнейшем, при размещении объекта в виртуальном пространстве, локальные координаты преобразуются в мировые.

XAML код вращения 3D объектов в локальной системе координат

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

<!-- Позиция прямоугольника №1  в мировых координатах -->
<f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Aqua" Position="-0.7, 0, 0" Back="Aqua">
    <f:Rectangle3D.Transform>
        <!-- Вращение прямоугольника №1 в локальной системе координат -->
        <RotateTransform3D CenterX="-0.7" CenterY="0" CenterZ="0">
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D    Axis="1,0,0" Angle="0"  x:Name="rotateLocal1"/>
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
    </f:Rectangle3D.Transform>
</f:Rectangle3D>

<!-- Позиция прямоугольника №2  в мировых координатах -->
<f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Yellow" Position="0.7, 0, 0" Back="Yellow">
    <f:Rectangle3D.Transform>
         <!-- Вращение прямоугольника №2 в локальной системе координат -->
        <RotateTransform3D CenterX="0.7" CenterY="0" CenterZ="0">
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D    Axis="0,1,0" Angle="0"  x:Name="rotateLocal2"/>
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
    </f:Rectangle3D.Transform>
</f:Rectangle3D>

Мировые координаты

Мировые 3D координаты

Виртуальное 3D пространство также имеет собственную координатную систему. Позиции всех предметов, расположенных в пространстве, исчисляются в данной системе координат. Это реальные координаты виртуального трехмерного мира. Изменение положения, размера пространства влияет на расположение и размеры всех его объектов. Координаты 3D пространства называются мировыми координатами.

На картинке вращаются в одной координатной системе все объекты 3D сюжета.

Чтобы разместить наши квадраты в определённой точке мирового пространства необходимо преобразовать локальные координаты в мировые. Теперь координаты квадратов будут такими:
квадрат 1
- вершина 0: (-0.95, -0.25, 0),
- вершина 1: (-0.45, -0.25, 0),
- вершина 2: (-0.45, 0.25, 0),
- вершина 3: (-0.95, 0.25, 0).
квадрат 2
- вершина 0: (0.45, -0.25, 0),
- вершина 1: (0.95, -0.25, 0),
- вершина 2: (0.95, 0.25, 0),
- вершина 3: (0.45, 0.25, 0).

При необходимости, можно привязать относительные координаты к абсолютным величинам. Если приложение будет визуализировать чертеж детали, за одну единицу принимается величина, например, 100мм. Если приложение демонстрирует 3D график доходов предприятия за одну единицу принять, например, 50 пикселей.

XAML код трансформации мировых координат.

Для соблюдения истинности мировых координат, все трехмерные элементы должны принадлежать коллекции детей элемента класса ModelVisual3D с названием "groupWorld".

<!-- Все 3D объекты должны размещаться в мировой координатной системе -->
<ModelVisual3D x:Name="groupWorld">
    <ModelVisual3D x:Name="groupRectangles">
        <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Aqua" Position="-0.7, 0, 0" Back="Aqua">
            <!-- Локальная система прямоугольника №1 -->
        </f:Rectangle3D>
        <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Yellow" Position="0.7, 0, 0" Back="Yellow">
            <!-- Локальная система прямоугольника №2 -->
        </f:Rectangle3D>
        <ModelVisual3D.Transform>
            <!-- Координатная система группы прямоугольников -->
        </ModelVisual3D.Transform>
    </ModelVisual3D>
    <!-- Земля 3D сцены. Позиция в мировых координатах -->
    <f:Rectangle3D x:Name="rectangleEarth" SizeX="2" SizeY="5" Front="#FFECA206" 
        Position="0, 0, -0.25">
        <!-- Локальная система Земли -->
    </f:Rectangle3D>

    <ModelVisual3D.Transform>
        <!-- Мировая система 3D сцены -->
    </ModelVisual3D.Transform>
</ModelVisual3D>

Координаты камеры

Координаты камеры 3D пространства

Камера - это то окно через которое наблюдатель смотрит на виртуальный мир. На это плоское окно проецируется трехмерное изображение.

Координаты камеры определяют eё положение относительно 3D пространства. Координаты камеры не изменяют локальные и мировые координаты, но позволяют смотреть трехмерное изображение с разных сторон. Центр координатной системы камеры совпадает с центром мировых координат.

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

Камера в WPF 3D API определяется установкой свойства ViewPort3D.Camera. Чтобы увидеть созданный 3D мир, для камеры необходимо определить 3 важных параметра:
- Position, позиция в координатной системе, например: "0,1,7";
- LookDirection, направление взгляда, например: "0,-20,-100";
- UpDirection, положение верха камеры, например: "0,1,0".

Кроме этого, для камеры перспективы устанавливается ширина поля зрения в градусах, например, FieldOfView="30". Для ортографической - ширина объектива камеры, например, Width="5".

XAML разметка определения камеры 3D сцены

<Viewport3D>
    <Viewport3D.Camera>
        <!-- Позиция камеры в мировых координатах -->
        <PerspectiveCamera x:Name="perCamera" Position="0,1,7" LookDirection="0,-20,-100" 
                FieldOfView="30" UpDirection="0,1,0" >
             <!-- Координатная система камеры -->
            <PerspectiveCamera.Transform>
                <RotateTransform3D>
                    <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D Axis="0,1,0" x:Name="rotateCamera"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
            </PerspectiveCamera.Transform>
        </PerspectiveCamera>
    </Viewport3D.Camera>

    <ModelVisual3D>
    <!-- Источник света -->
    </ModelVisual3D>
    <ModelVisual3D x:Name="groupWorld">
        <ModelVisual3D x:Name="groupRectangles">
            <!-- Группа 3D j,]trnjd -->
        </ModelVisual3D>
        <f:Rectangle3D x:Name="rectangleEarth" SizeX="2" SizeY="5" Front="#FFECA206" 
                Position="0, 0, -0.25">
            <!-- Земля 3D сцены -->
        </f:Rectangle3D>
        <ModelVisual3D.Transform>
            <!-- Мировая координатная система -->
        </ModelVisual3D.Transform>
    </ModelVisual3D>
</Viewport3D>

Координаты группы 3D объектов

Координаты группы 3D объектов

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

На картинке две фигуры вращаются вокруг общего центра координат.

Для того, чтобы изменить положение в пространстве нескольких объектов одновременно необходимо объединить их в одну группу. В результате объединения у группы образуется общая система координат. Далее определить желаемый центр координатной системы этой группы. Затем можно трансформировать: повернуть, переместить или масштабировать группу объектов.

XAML код группы 3D объектов

Добавим прямоугольники класса Rectangle3D в одну группу элемента класса ModelVisual3D, после этого можно применять трансформацию ко всем элементам группы одновременно.

<ModelVisual3D x:Name="groupRectangles">
    <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Aqua" Position="-0.7, 0, 0" Back="Aqua">
       <!-- Первый прямоугольник -->
    </f:Rectangle3D>
    <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Yellow" Position="0.7, 0, 0" Back="Yellow">
       <!-- Второй прямоугольник -->
    </f:Rectangle3D>

    <!-- Трансформация всех 3D объектов группы -->
    <ModelVisual3D.Transform>
        <Transform3DGroup>
            <!-- Смещаем центр координат группы для оси вращения -->
            <TranslateTransform3D OffsetX="0.4"/>
            <RotateTransform3D>
                <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D Axis="0,1,0" Angle="0"  x:Name="rotateGroup" />
                </RotateTransform3D.Rotation>
            </RotateTransform3D>
            <!-- Восстанавливаем позицию центра координат -->
            <TranslateTransform3D OffsetX="-0.4"/>
        </Transform3DGroup>
    </ModelVisual3D.Transform>
</ModelVisual3D>

Исследование координатных систем

Для исследования систем координат WPF 3D API создадим три трехмерные фигуры. Два маленьких прямоугольника будут играть роль локальных фигур, один большой прямоугольник - "Землю" 3D сцены. Очень удобно работать с разметкой XAML: все изменения кода тут же визуально отражаются в дизайнере.

Но есть у XAML разметки и недостаток: очень тяжело работать с большим объемом кода. Чтобы использовать визуальность дизайнера XAML разметки, но минимизировать количество строк, призовём на помощь программный код. Создадим программный симбиоз в виде класса генерирования прямоугольников и разметки XAML для визуализации редактирования параметров фигур.

Класс трехмерных фигур Rectangle3D

Viewport3D разрешает в качестве своего содержимого только классы наследуемые от Visual3D. Поэтому наш класс, назовём его Rectangle3D, должен быть потомком VIsual3D или его производных классов. Самое простое решение - сделать класс создания прямоугольников наследником ModelVisual3D.

Листинг класса Rectangle3D:
public class Rectangle3D : ModelVisual3D
{
    private double _sizeX;
    public double SizeX
    {
        get => _sizeX;
        set
        {
            _sizeX = value;
            UpdateDate();
        }
    }

    private double _sizeY;
    public double SizeY
    {
        get => _sizeY;
        set
        {
            _sizeY = value;
            UpdateDate();
        }
    }


    private Point3D _pos;
    public Point3D Position
    {
        get => _pos;
        set
        {
            _pos = value;
            UpdateDate();
        }
    }


    // Материалы граней
    private Brush _front;
    public Brush Front
    {
        get => _front;
        set
        {
            _front = value;
            UpdateDate();
        }
    }

    private Brush _back;
    public Brush Back
    {
        get => _back;
        set
        {
            _back = value;
            UpdateDate();
        }
    }

    private void UpdateDate()
    {
        CreateMeshes3D(_sizeX, _sizeY, _pos, _front, _back);
    }

    private void CreateMeshes3D(double sizex, double sizey, Point3D pos, Brush front, Brush back)
    {
        Children.Clear();

        var left_bottom = new Point3D(-sizex / 2 + pos.X, -sizey / 2 + pos.Y, pos.Z);
        var right_bottom = new Point3D(sizex / 2 + pos.X, -sizey / 2 + pos.Y, pos.Z);
        var right_top = new Point3D(sizex / 2 + pos.X, sizey / 2 + pos.Y, pos.Z);
        var left_top = new Point3D(-sizex / 2 + pos.X, sizey / 2 + pos.Y, pos.Z);

        var meshGeometry3D = new MeshGeometry3D
        {
            // Координат вершин 4-е, но у 2-х треугольников вершин 6.
            // Для разрешения этого несоответствия служат индексы треугольников.
            // Можно указать 6 координат вершин, тогда индексы не понадобятся.
            Positions = new Point3DCollection
            {
                left_bottom,
                right_bottom,
                right_top,
                left_top,
            },
            // Индексы определяют последовательность обхода вершин 
            // треугольников. Из 4-х вершин получаем два треугольника.
            TriangleIndices = new Int32Collection
            {
                0, 1, 2,
                0, 2, 3
            }
        };

        var model3D = new GeometryModel3D
        {
            Geometry = meshGeometry3D,
            Material = new DiffuseMaterial(front),
            BackMaterial = new DiffuseMaterial(back)
        };

        var visual3DMesh = new ModelVisual3D
        {
            Content = model3D
        };

        Children.Add(visual3DMesh);
    }
}

Теперь элементы класса Rectangle3D можно добавлять в XAML разметку трехмерного контейнера Viewport3D.

Листинг XAML трехмерного кода приложения

Для реалистичности добавим два источника света. DirectionalLight играет роль солнечного света, AmbientLight добавляет небольшую подсветку для освещения "теневых" сторон.

<Viewport3D>
    <Viewport3D.Camera>
        <PerspectiveCamera x:Name="perCamera" Position="0,1,7" LookDirection="0,-20,-100" 
            FieldOfView="30" UpDirection="0,1,0" >
            <PerspectiveCamera.Transform>
                <RotateTransform3D>
                    <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D Axis="0,1,0" x:Name="rotateCamera"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
            </PerspectiveCamera.Transform>
        </PerspectiveCamera>
        <!--<OrthographicCamera Position="0,1,7" LookDirection="0,-20,-100" 
            UpDirection="0,1,0" Width="3">
            <OrthographicCamera.Transform>
                <RotateTransform3D>
                    <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D Axis="0,1,0" x:Name="rotateCamera"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
            </OrthographicCamera.Transform>
        </OrthographicCamera>-->
    </Viewport3D.Camera>

    <ModelVisual3D>
        <ModelVisual3D.Content>
            <Model3DGroup>
                <AmbientLight Color="#FF444444"/>
                <DirectionalLight Color="White" Direction="1,0,-1"/>
            </Model3DGroup>
        </ModelVisual3D.Content>
    </ModelVisual3D>

    <ModelVisual3D x:Name="groupWorld">
        <ModelVisual3D x:Name="groupRectangles">
            <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Aqua" Position="-0.7, 0, 0" 
                    Back="Aqua">
                <f:Rectangle3D.Transform>
                    <RotateTransform3D CenterX="-0.7" CenterY="0" CenterZ="0">
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D    Axis="1,0,0" Angle="0"  x:Name="rotateLocal1"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </f:Rectangle3D.Transform>
            </f:Rectangle3D>
            <f:Rectangle3D SizeX="0.5" SizeY="0.5" Front="Yellow" Position="0.7, 0, 0" 
                    Back="Yellow">
                <f:Rectangle3D.Transform>
                    <RotateTransform3D CenterX="0.7" CenterY="0" CenterZ="0">
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D    Axis="0,1,0" Angle="0"  x:Name="rotateLocal2"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </f:Rectangle3D.Transform>
            </f:Rectangle3D>
            <ModelVisual3D.Transform>
                <Transform3DGroup>
                    <TranslateTransform3D OffsetX="0.4"/>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D Axis="0,1,0" Angle="0"  x:Name="rotateGroup" />
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                    <TranslateTransform3D OffsetX="-0.4"/>
                </Transform3DGroup>
            </ModelVisual3D.Transform>
        </ModelVisual3D>

        <f:Rectangle3D x:Name="rectangleEarth" SizeX="2" SizeY="5" Front="#FFECA206" 
            Position="0, 0, -0.25">
            <f:Rectangle3D.Transform>
                <RotateTransform3D CenterX="-0.7" CenterY="0" CenterZ="0">
                    <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D    Axis="1,0,0" Angle="-90"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
            </f:Rectangle3D.Transform>
        </f:Rectangle3D>

        <ModelVisual3D.Transform>
            <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="0" CenterZ="0" >
                    <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D Axis="0,1,0" Angle="0" x:Name="rotateWorld"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
            </Transform3DGroup>
        </ModelVisual3D.Transform>
    </ModelVisual3D>
</Viewport3D>

Исходник трехмерного приложения WPF

К статье прилагается полный исходный код демонстрации систем трехмерных координат для приложений WPF. Исходник написан в среде MS Visual Studio 2019, .NET 5.0. Перед разбором исходника рекомендуется прочитать данную статью.

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