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. Исходник упакован вместе с файлом приложения.