WPF 3D Кубик Рубика

3D кубик Рубика

Перемешивание и сборка кубика Рубика

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

Следующий шаг после тестирования класса единичного трёхмерного кубика Cube3D - это применение его на практике для разработки компьютерной игрушки Кубик Рубика. Предлагаемое приложение служит отличным стартом создания реалистичной трёхмерной игры. В исходнике действующая модель кубика Рубика создаётся двумя классами: маленькие кубики Cube3D и полная сборка RubiksCube.

Кубики для кубика Рубика

Сегменты трехмерного кубика Рубика созданы на основе объектов класса Cube3D описанном на странице с исходником WPF вращение 3D кубиков. Класс Cube3D сводит к минимуму программный код визуализации правильного гексаэдра. Минимально достаточно только создание объекта Cube3D, без каких-либо настроек свойств, и "запчасть" для кубика Рубика готова. Окрашивание и управление сегментами реализуется классом RubiksCube.

Класс создания кубика Рубика

Создание трехмерной фигуры из маленьких кубиков происходит в одном классе RubiksCube, наследующего от ModelVisual3D. Принадлежность к классам Visual3D разрешает RubiksCube быть дочерним элементом области просмотра трехмерной пространства, поскольку ViewPort3D может содержать только потомков Visual3D.

Класс RubiksCube создаёт из сегментов кубик Рубика, закрашивает стороны в установленные цвета и управляет анимацией поворотов групп кубиков игрушки. RubiksCube не имеет открытых свойств настроек, но при необходимости их легко реализовать на основе данного исходника. Визуально кубик Рубика создаётся в файле разметки XAML, а повороты инициируются программным кодом.

Принцип создания кубика Рубика

Класс RubiksCube создаёт игровой гексаэдр по сегментам, сверху вниз, вдоль оси Y. Каждый сегмент - это 9 маленьких кубиков: первый сегмент находится в области положительных значений, второй - в центре оси координат, третий - со смещением в отрицательную область оси Y. Центральный гексаэдр скрыт другими кубиками, но оставлен для простоты кода.

Сегментация условная, каждый маленький кубик добавляется в коллекцию RubiksCube.Children отдельно и является самостоятельной единицей большого кубика Рубика. Цель сегментации - упростить создание кубика Рубика путём уменьшения количества программного кода: три сегмента создаются одним методом.

Маленькие гексаэдры в сборке большого куба располагаются симметрично относительно трёх осей координат. Размещаются вплотную к друг друг на расстоянии небольшого зазора, переменная ответственная за зазор - double _gap. Зазоры между кубиками создают впечатление реальной механической игрушки.

Программный код метода создания кубика Рубика из сегментов:
// Создание кубика сегментами из 9 кубиков сверху вниз по оси Y.
// size - размер кубиков и он же задаёт 
// смещение кубиков относительно соответствующих осей.
// gap - зазор между кубиками.
private void Create(double size, double gap)
{
    // Верхний сегмент
    Segment(size, size + gap, gap);
    // Сегмент в середине
    Segment(size, 0, gap);
    // Нижний сегмент
    Segment(size, -(size + gap), gap);

    // Раскраска кубика Рубика
    ColorFill();
}

private void Segment(double size, double y, double gap)
{
    // Каждый маленький кубик - отдельный дочерний элемент.
    // После поворотов сегментов кубики изменяют
    // своё положение в пределах конструкции кубика Рубика.
    Children.Add(new Cube3D()
    { 
        // Первичное смещение по осям.
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = size + gap
        },
        // Размер кубика
        Size = size
    });


    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });


    //
    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = 0
        },
        Size= size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = 0
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = 0
        },
        Size = size
    });


    //
    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });
}

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

Первоначальная позиция позиция кубиков определяется свойством Cube3D.Position = (0,0,0) и располагает маленькие кубики в центре своих осей координат. В дальнейшем позиция каждого маленького гексаэдра остаётся неизменной относительно своей координатной системы, доказательством этого служит равенство Cube3D.Position = (0,0,0) после любой трансформации.

Перемещения и вращения составляющих кубиков в исходнике приложения происходит преобразованием их координатных систем. Для позиционирования маленьких кубиков при создании сегментов и вращения сегментов во время игры в методах RubiksCube.Segment(...) и RubiksCube.Rotate(...) определён программный код трансформации системы координат каждого гексаэдра кубика Рубика.

Программный код первичной расстановки маленьких гексаэдров:
// size - смещение относительно осей координат.
// gap - зазор между кубиками.
private void Segment(double size, double y, double gap)
{
    Children.Add(new Cube3D()
    {
        // Перенос координатной системы текущего кубика на необходимое смещение. 
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y, 
            OffsetZ = size + gap
        },
        Size = size
    });
}

Каждый кубик хранит матрицу своей трансформации в свойстве Cube3D.Transform. Хранение координат положения начинается во время первичной расстановки по местам маленьких гексаэдров и сохраняется до закрытия приложения.

Во время игры, вращение соответствующих сегментов происходит путём комбинирования трансформации переноса кубиков (TranslateTransform3D) с трансформацией вращения (RotateTransform3D). Если применить трансформацию вращения к кубикам без учёта их положения - смещение аннулируется и каждый кубик будет вращаться вокруг своей первоначальной позиции, т.е. (x = 0; y = 0; z = 0).

Метод вращения выбранного сегмента:
private void Rotate(bool clockwise, Vector3D axisRotation, Func selectSegment)
{
    // Управление последовательностью анимации.
    if (_isCanAnimation == false) return;
    _isCanAnimation = false;

    // Выбор оси вращения.
    RotateTransform3D rotate = new(new AxisAngleRotation3D(axisRotation, 0));

    foreach (Cube3D item in Children)
    {
        // Функция критерия отбора кубиков для сегмента поворота.
        if (selectSegment(item) == true)
        {
            // Комбинирование предыдущей трансформации с трансформацией вращения.
            item.Transform = new Transform3DGroup()
            {
                Children = { item.Transform, rotate }
            };
        }
    }
    // Программный код анимации поворота.
    DoubleAnimation rotateAnimation = new()
    {
        Duration = TimeSpan.FromSeconds(_animationDuration)
    };

    rotateAnimation.By = clockwise == false ? 90 : -90;

    rotateAnimation.Completed += RotateAnimation_Completed;
    rotate.Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotateAnimation);

    void RotateAnimation_Completed(object? sender, EventArgs e)
    {
        _isCanAnimation = true;
    }
}

Выбор сегмента для поворота

Координатная система кубика рубика

Маленькие кубики располагаются со смещением относительно осей координат, группы по 9 кубиков имеют равное смещение только по одной координатной оси. После поворотов во время игры, кубики изменяют своё положение, состав групп меняется, но в абсолютно всегда создаются вновь сегменты из 9 кубиков имеющих равные координаты только по одной координатной оси. В исходном коде приложения это свойство конструкции кубика Рубика определяет принцип выбора сегмента для вращения.

При определении сегмента поворота используется равенство смещения кубиков по оси вращения. Например: центральный вертикальный сегмент - это кубики со смещением по оси Х равным 0. Для поворота этого сегмента необходимо выбрать все кубики с координатой item.Transform.Value.OffsetX = 0.

После поворота, из за особенностей вычисления чисел с плавающей запятой, координата Х может иметь значение не равное нулю, например вот такое: item.Transform.Value.OffsetX = 2.353672812205332Е-16. Поэтому положение сегмента вычисляется с поправкой на неточность:
item.Transform.Value.OffsetX > -0.1 && item.Transform.Value.OffsetX < 0.1.
Соответственно, крайние сегменты выбираются аналогично, по равному смещению вдоль оси поворота.

Методы класса RubiksCube для выбора и вращения сегментов относительно оси Х:
#region  === Группы вращения вокруг оси Х ===
// Поворот левого вертикального сегмента
public void RotateLeftX(bool clockwise = false)
{
    // Критерий выбора кубиков для поворота.
    static bool select(Cube3D item) => item.Transform.Value.OffsetX < -0.1;
    RotateX(clockwise, select);
}
// Поворот центрального вертикального сегмента
public void RotateMiddleX(bool clockwise = false)
{
    static bool select(Cube3D item) => item.Transform.Value.OffsetX > -0.1 && item.Transform.Value.OffsetX < 0.1;
    RotateX(clockwise, select);
}
// Поворот правого вертикального сегмента
public void RotateRightX(bool clockwise = false)
{
    static bool select(Cube3D item) => item.Transform.Value.OffsetX > 0.1;
    RotateX(clockwise, select);
}
// Унифицированный метод выбора оси поворота.
private void RotateX(bool clockwise, Func select)
{
    Rotate(clockwise, new Vector3D(1, 0, 0), select);
}

#endregion

Закраска сторон кубика Рубика

Выбор стороны для закраски определённым цветом осуществляется аналогично механике выбора сегмента для поворота. Окрашиваются наружные стороны кубиков расположенных в одинаковой позиции. Например, верхняя сторона - это все кубики с координатой оси Y > 0.1 пространственной единицы, нижние кубики отбираются координатой Y < -0.1 единицы.

Закраска других сторон происходит аналогично - выбором крайних кубиков с равным смещением относительно выбранной оси координат. В зависимости от расположения в сборке у кубиков окрашиваются от одной до трех граней.

Программный код метода раскраски кубика Рубика в приложении:
private void ColorFill()
{
    foreach (Cube3D item in Children)
    {
        // Закраска передней стороны
        if (item.Transform.Value.OffsetZ > 0.1) item.Front = Brushes.Red;

        // Закраска задней стороны
        if (item.Transform.Value.OffsetZ < -0.1) item.Back = Brushes.Orange;

        // Левой
        if (item.Transform.Value.OffsetX < -0.1) item.Left = Brushes.Green;

        // Правой
        if (item.Transform.Value.OffsetX > 0.1) item.Right = Brushes.Blue;

        // Верхней
        if (item.Transform.Value.OffsetY > 0.1) item.Top = Brushes.White;

        // Нижней
        if (item.Transform.Value.OffsetY < -0.1) item.Bottom = Brushes.Yellow;
    }
}

Управление поворотами

Повороты осуществляются при нажатии клавиши Пробел. Управление простейшее: 9 поворотов для перемешивания сегментов и 9 поворотов в обратную сторону для сборки кубика Рубика. На основе этого управления можно легко создать код произвольного выбора сегмента для поворота, например с помощью кнопок.

Исходник приложения Кубик Рубика

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