Top.Mail.Ru

Qt и QML. mtPaint3D. Создаем утилиту для 3D-рисования. День 1.

Всем доброго времени суток! С этой статьи стартанет новый формат контента на нашем сайте, который отчасти продиктован обстоятельствами, отчасти интересом к результату. Суть заключается в следующем... В данный момент мне нужен некий программный продукт (о деталях чуть ниже) за относительно короткий срок. И, собственно, возникла мысль запечатлеть весь процесс разработки от начала и до конца в виде некоего статейного марафона.

Одно из ключевых отличий от моих "стандартных" статей будет в том, что я не буду расписывать все детали от и до в подробностях. Скорее это будет больше повествовательный материал/дневник реализации конкретной задачи, решение которой мне нужно на текущий момент для дальнейшего использования в одном проекте.

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

Итак, вкратце о цели. Вкратце - по той причине, что цель абсолютно ясна и понятна. Нужно ПО, представляющее из себя 3D-рисовалку объектов вроде майкрософтовского Paint 3D:

Paint 3D

То есть требуется полный функционал для создания произвольной 3D-сцены с поддержкой отрисовки базовых фигур, перемещения их в пространстве, задания размеров/свойств, вращения фигур, изменения цветов, возможности вращения камеры и изменения углов обзора, зума, сохранения/загрузки текущей сцены и т. д. В общем, нужны все очевидно необходимые функции для подобной утилиты 👍 Если что-то забыл перечислить, то в процессе уже будем смотреть, что еще потребуется.

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

Paint 3D Qt.

Из фигур точно нужны:

  • кубоид
  • конус
  • сфера
  • тор
  • объемный текст
  • кривая Безье 2-го порядка

В качестве инструментов беру то, что мне нравится - Qt / C++ / QML. На QML будет интерфейсная часть, на C++, соответственно, внутренняя логика. Отвести на этой я решил 9 дней, что трансформируется в 9 же статей, в конце каждой из которых будем смотреть на текущий прогресс. Заниматься этой утилитой буду по мере наличия возможностей, с тем лишь ограничением, чтобы каждый день какое-никакое но продвижение было ) Поэтому не исключено, что в какой-то из дней будет сделано больше, в какой-то меньше, это все станет понятно лишь в процессе...

Ну и еще дополнение к тому, что сверхподробных пояснений особо не будет - в конце каждой статьи код будет представлен в полном объеме. Так что в случае, если после прочтения останутся вопросы, ответы на них можно будет найти в том числе и там.

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

Layout

Дальнейший путь пролегает через Qt Creator. Создаем пустое Qt Quick приложение. В нашем распоряжении файлы:

  • main.cpp
  • main.qml

Сразу же соорудим разметку окна в соответствии с упомянутой схемой. Элементы разместим в GridLayout размером 12 рядов * 6 колонок так, что наши области займут пространство:

GridLayout

Накидаем код в 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
        }
    }
}

В итоге получили:

GridLayout в QML

Надо бы за сегодня набросать основу для дальнейшего развития области рисования объектов. Идея такая...

Все объекты, отображаемые на сцене будут представлять из себя экземпляры Qt3DCore::QEntity (в QML классу соответствует Entity). Класс представляет из себя контейнер для хранения одного или нескольких Qt3DCore::QComponent, которые и определяют вид и свойства данного QEntity. Рассмотрим небольшой псевдо-пример, что представляет из себя сфера:

Сфера в QML
  • 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, цвет материала я задал белым, поэтому получаем:

Создание QEntity

И с конкретными значениями (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; }
			}
		}
	}
}

Дизайном вообще не заморачиваемся, базовый вид вполне устроит:

QML buttons

Благодаря ParallelAnimation при нажатии кнопок мы получим плавное изменения указанных свойств. Здесь у нас в качестве объекта изменений - camera, в качестве свойств - position и upVector. Немного схематичного творчества для визуализации:

Camera upVector, position

Для 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
}

По итогу на данный момент получаем:

QEntity

И здесь 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

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x