Всем доброго времени суток! С этой статьи стартанет новый формат контента на нашем сайте, который отчасти продиктован обстоятельствами, отчасти интересом к результату. Суть заключается в следующем... В данный момент мне нужен некий программный продукт (о деталях чуть ниже) за относительно короткий срок. И, собственно, возникла мысль запечатлеть весь процесс разработки от начала и до конца в виде некоего статейного марафона.
Одно из ключевых отличий от моих "стандартных" статей будет в том, что я не буду расписывать все детали от и до в подробностях. Скорее это будет больше повествовательный материал/дневник реализации конкретной задачи, решение которой мне нужно на текущий момент для дальнейшего использования в одном проекте.
Но при этом, конечно, на любые возникшие вопросы я буду рад ответить в комментариях, либо на форуме, либо любым другим путем. В данном контексте среди возможных способов связи форум приветствуется более других, поскольку сейчас одним из приоритетных моментов является ускорение его роста )
Итак, вкратце о цели. Вкратце - по той причине, что цель абсолютно ясна и понятна. Нужно ПО, представляющее из себя 3D-рисовалку объектов вроде майкрософтовского Paint 3D:
То есть требуется полный функционал для создания произвольной 3D-сцены с поддержкой отрисовки базовых фигур, перемещения их в пространстве, задания размеров/свойств, вращения фигур, изменения цветов, возможности вращения камеры и изменения углов обзора, зума, сохранения/загрузки текущей сцены и т. д. В общем, нужны все очевидно необходимые функции для подобной утилиты 👍 Если что-то забыл перечислить, то в процессе уже будем смотреть, что еще потребуется.
Здесь я решил поместить небольшую врезку-спойлер с финальным результатом. Пока здесь будет пусто (update: результат добавлен), но по окончанию проекта я размещу результаты работы. И в некоторых из следующих статей "марафона" продублирую аналогичным образом. Итак:
Из фигур точно нужны:
- кубоид
- конус
- сфера
- тор
- объемный текст
- кривая Безье 2-го порядка
В качестве инструментов беру то, что мне нравится - Qt / C++ / QML. На QML будет интерфейсная часть, на C++, соответственно, внутренняя логика. Отвести на этой я решил 9 дней, что трансформируется в 9 же статей, в конце каждой из которых будем смотреть на текущий прогресс. Заниматься этой утилитой буду по мере наличия возможностей, с тем лишь ограничением, чтобы каждый день какое-никакое но продвижение было ) Поэтому не исключено, что в какой-то из дней будет сделано больше, в какой-то меньше, это все станет понятно лишь в процессе...
Ну и еще дополнение к тому, что сверхподробных пояснений особо не будет - в конце каждой статьи код будет представлен в полном объеме. Так что в случае, если после прочтения останутся вопросы, ответы на них можно будет найти в том числе и там.
На этом вводную часть завершаю, переходим к началу активных действий. И первым делом необходимо прикинуть примерную разметку окна приложения, чтобы начать добавлять основные элементы управления. Видение будущего интерфейса сформировалось в такую схему:
Дальнейший путь пролегает через Qt Creator. Создаем пустое Qt Quick приложение. В нашем распоряжении файлы:
- main.cpp
- main.qml
Сразу же соорудим разметку окна в соответствии с упомянутой схемой. Элементы разместим в GridLayout
размером 12 рядов * 6 колонок так, что наши области займут пространство:
Накидаем код в main.qml, начав непосредственно с главного окна:
ApplicationWindow { id: window width: Screen.width * 3 / 4 height: Screen.height * 3 / 4 visible: true title: qsTr("mtPaint3D") color: "#505050" }
В общем-то задали размеры, заголовок окна и цвет. Да, кстати, дизайну пока будем уделять ровно 0% времени, оставим оформление на самый последний этап. Логика тут простая - какой смысл создавать, к примеру, иконки для кнопок, если велика вероятность, что в процессе разработки логики часть элементов управления будет просто выкинута за ненадобностью. Все в классических традициях геймдизайна, когда на начальных этапах объекты/персонажи могут выглядеть в виде серых ящиков/неких невнятных субстанций. Попросту из-за того же самого - нет смысла дизайнерам N месяцев заниматься прорисовкой персонажа, если есть все шансы, что через (N + 1) месяцев он будет выброшен из игры в связи с изменением механики/сюжета и т. п.
Поэтому цвет я задал первый попавшийся, исключительно для визуального разграничения областей окна. Так, добавляем mainLayout
и заодно компонент GridRectangle
, дабы объединить общие свойства в одном месте и не дублировать их впоследствии:
ApplicationWindow { id: window width: Screen.width * 3 / 4 height: Screen.height * 3 / 4 visible: true title: qsTr("mtPaint3D") color: "#505050" GridLayout { id: mainLayout rows: 12 columns: 6 flow: GridLayout.TopToBottom anchors.fill: parent component GridRectangle : Rectangle { Layout.fillHeight: true Layout.fillWidth: true Layout.preferredWidth: Layout.columnSpan Layout.preferredHeight: Layout.rowSpan color: "#808080" border.color: Qt.darker(color) border.width: 1 } GridRectangle { id: objectBlock Layout.row: 0 Layout.column: 0 Layout.rowSpan: 12 Layout.columnSpan: 1 } GridRectangle { id: viewBlock Layout.row: 0 Layout.column: 1 Layout.rowSpan: 1 Layout.columnSpan: 5 } GridRectangle { id: paintBlock Layout.row: 1 Layout.column: 1 Layout.rowSpan: 11 Layout.columnSpan: 5 } } }
В итоге получили:
Надо бы за сегодня набросать основу для дальнейшего развития области рисования объектов. Идея такая...
Все объекты, отображаемые на сцене будут представлять из себя экземпляры Qt3DCore::QEntity
(в QML классу соответствует Entity
). Класс представляет из себя контейнер для хранения одного или нескольких Qt3DCore::QComponent
, которые и определяют вид и свойства данного QEntity
. Рассмотрим небольшой псевдо-пример, что представляет из себя сфера:
- Qt3DExtras::QSphereMesh - отвечает за геометрические точки, в данном случае, сферы. Здесь же хранятся параметры, в частности, радиус сферы.
- Qt3DCore::QTransform - из названия уже понятно, что данный класс определяет возможности трансформирования объекта. Через
QTransform
, я так предполагаю, мы и осуществим как минимум перемещение и вращение созданных 3D-фигур. - Qt3DExtras::QDiffuseSpecularMaterial - ответственен за материал и его свойства для данного графического объекта.
Впоследствии мы все это используем на практике, пока же важна только сама суть:
- Создаем
QEntity
. - Добавляем к нему компоненты.
- Добавляем
QEntity
на сцену. - Получаем графическое изображение, определяемое добавленными компонентами.
Логика изящна и прекрасна в своей простоте и четкости ✅
При этом стоит оговориться, что QEntity
- это своего рода "глобальный" контейнер - не только для создания фигур и т. д. Освещение, к примеру, также будем создавать аналогичным образом. Собственно, как раз сейчас и осуществим на практике. Наша локальная цель - создать плоскость, которая будет своего рода холстом для наших будущих рисовательных действий. Для реализации можно использовать класс Qt3DExtras::QPlaneMesh
, который и предназначен для добавления прямоугольной плоскости, но мне визуально хочется чуть поддать объему, поэтому я использую Qt3DExtras::QCuboidMesh
. Резюмируя: вместо плоскости будет кубоид, высота которого мала относительно его длины и ширины.
В main.qml добавляем в специально для этого и созданный paintBlock
:
GridRectangle { id: paintBlock Layout.row: 1 Layout.column: 1 Layout.rowSpan: 11 Layout.columnSpan: 5 Scene3D { id: scene3D anchors.fill: parent focus: true aspects: ["input", "logic"] Entity { id: rootEntity Entity { id: lightEntity components: [ PointLight { color: "#ffffa7" intensity: 0.5 enabled: true }, Transform { translation: Qt.vector3d(10, 0, 10) } ] } Camera { id: camera projectionType: CameraLens.PerspectiveProjection fieldOfView: 45 nearPlane : 0.1 farPlane : 100 aspectRatio: 1 position: Qt.vector3d(0.0, -25.0, 25.0) upVector: Qt.vector3d(0.0, 0.0, 1.0) viewCenter: Qt.vector3d(0.0, 0.0, 0.0) } RenderSettings { id: renderSettings activeFrameGraph: ForwardRenderer { camera: camera clearColor: "transparent" } pickingSettings.pickMethod: PickingSettings.TrianglePicking } InputSettings { id: inputSettings } Entity { id: baseCuboidEntity CuboidMesh { id: baseCuboid xExtent: 35 yExtent: 25 zExtent: basePlaneZExtent } Transform { id: baseCuboidTransform } DiffuseSpecularMaterial { id: baseCuboidMaterial ambient: "#ffffff" shininess: 100 } components: [ baseCuboid, baseCuboidMaterial, baseCuboidTransform ] } } } }
Я скопом добавил все, что требуется для начала, поэтому пробежимся чуть подробнее:
scene3D - QML объект, внутри которого будет все осуществлено. Далее следует корневой Entity
- rootEntity
.
lightEntity - освещение сцены. У меня - PointLight, альтернативные варианты в QML - AreaLight, DirectionalLight и SpotLight. Везде я прикрепил ссылки на подробную документацию. В данном случае через компонент PointLight
задаем цвет и интенсивность. Можно задать изначально чисто белый цвет (#ffffff), далее от него плясать, пробуя менять. Но мне изначально хочется более теплового лампового света, поэтому ставлю #ffffa7, после запуска увидим, что ровно такой эффект и получится. lightEntity
содержит второй компонент - Transform
, который позволяет нам определить позицию источника света в пространстве. Чуть ниже добавлю схему с пространственными координатами получившихся объектов. И в итоге все четко в соответствии с идеей, которую мы обсудили ранее - создается Entity
, компоненты которого определяют его поведение.
Идем дальше, и дальше у нас - camera. В двух словах, таким образом задается положение наблюдателя, в соответствии с которым трехмерное пространство рендерится в двумерное изображение на экране. В связи с упомянутой концепцией данного формата статей, не буду глубоко погружаться. Если будут вопросы - обязательно пишите 👍
И в завершение - baseCuboidEntity. Это та самая плоскость, которая на самом деле не плоскость, а кубоид. Полностью аналогично добавили Entity
и дали ему в распоряжение нужные компоненты:
- CuboidMesh
- Transform
- DiffuseSpecularMaterial
Высота кубоида здесь задана переменной basePlaneZExtent
, которую я заблаговременно вынес в отдельный файл с константами, так как уже на данный момент очевидно, что она будет также востребована и в C++ коде:
#ifndef CONSTANTS_H #define CONSTANTS_H namespace constants { inline constexpr double basePlaneZExtent = 0.1; } #endif // CONSTANTS_H
И для доступа к данному значению из QML в main.cpp:
engine.rootContext()->setContextProperty("basePlaneZExtent", constants::basePlaneZExtent);
В конце статьи выложу вдогонку к коду готовый проект, так что при желании можно будет собрать и поэкспериментировать с изменением тех или иных свойств для визуализации их назначения. Здесь же у нас область размером 35 * 25, цвет материала я задал белым, поэтому получаем:
И с конкретными значениями (0 по оси z соответствует центру кубоида, соответственно, верхняя грань кубоида: z = 0.05, нижняя грань z = -0.05):
Именно то, что и планировали, можно двигаться дальше. Я думаю, сейчас еще добавим по-быстрому кнопки для изменения текущего вида сцены. Будет их две - "Top View" для вида условно сверху и "Front View" для возврата к такому виду, как на скриншоте. Физически мы будем менять положение наблюдателя (Camera
), что и даст нужный эффект. Снова в main.qml, теперь уже в viewBlock
, добавляем кнопки в количестве 2 штуки:
GridRectangle { id: viewBlock Layout.row: 0 Layout.column: 1 Layout.rowSpan: 1 Layout.columnSpan: 5 Row { id: viewButtonsLayout spacing: 2 anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter Button { id: topViewButton text: qsTr("Top View") onClicked: ParallelAnimation { PropertyAnimation { target: camera; property: "position"; to: Qt.vector3d(0.0, 0.0, 35.0); duration: 675; } PropertyAnimation { target: camera; property: "upVector"; to: Qt.vector3d(0.0, 1.0, 0.0); duration: 675; } } } Button { id: frontViewButton text: qsTr("Front View") onClicked: ParallelAnimation { PropertyAnimation { target: camera; property: "position"; to: Qt.vector3d(0.0, -25.0, 25.0); duration: 675; } PropertyAnimation { target: camera; property: "upVector"; to: Qt.vector3d(0.0, 0.0, 1.0); duration: 675; } } } } }
Дизайном вообще не заморачиваемся, базовый вид вполне устроит:
Благодаря ParallelAnimation
при нажатии кнопок мы получим плавное изменения указанных свойств. Здесь у нас в качестве объекта изменений - camera
, в качестве свойств - position
и upVector
. Немного схематичного творчества для визуализации:
Для Front View: position (0.0, -25.0, 25.0), upVector - (0.0, 0.0, 1.0). То есть мы смещены по оси y на -25 и по оси z на 25. В итоге мы видим сцену условно спереди и под наклоном. При Top View мы перемещаемся четко на позицию над сценой (0.0, 0.0, 35.0), поэтому и видим ее строго сверху.
PropertyAnimation
дает следующий эффект плавности:
Так, и завершим мы на сегодня добавлением класса, который будет в себе инкапсулировать всю работу с добавленными пользователем объектами, да и сами эти объекты. В rootEntity
добавляем пока пустой PaintEntity
:
PaintEntity { id: paintEntity }
По итогу на данный момент получаем:
И здесь PaintEntity
- это не существующий класс QML, а добавленный нами в проект:
#ifndef PAINTENTITY_H #define PAINTENTITY_H #include <Qt3DCore> #include <Qt3DExtras> class PaintEntity : public Qt3DCore::QEntity { Q_OBJECT public: PaintEntity(QNode *parent = nullptr); private slots: signals: private: }; #endif // PAINTENTITY_H
#include <QDebug> #include "paintentity.h" PaintEntity::PaintEntity(QNode *parent) : Qt3DCore::QEntity(parent) { }
Остается сообщить QML о наших действиях по добавлению PaintEntity
. В main.cpp:
qmlRegisterType<PaintEntity>("microtechnics.paintEntity", 1, 0, "PaintEntity");
В main.qml:
import microtechnics.paintEntity 1.0
На этом завершаем первый день и остаемся в предвкушении дня последующего )
Исходный код и проект: mtPaint3D_day_1