Всем привет, продолжаем софт-марафон! В принципе, четкого плана на сегодня нет - буду последовательно добавлять функционал, доверюсь интуиции и ощущениям, чтобы определить момент, когда на сегодня будет достаточно 🙂
Здесь я решил поместить небольшую врезку-спойлер с финальным результатом. Пока здесь будет пусто (update: результат добавлен), но по окончанию проекта я размещу результаты работы. И в некоторых из следующих статей "марафона" продублирую аналогичным образом. Итак:
Отправной точкой для создания-рисования 3D-объектов будет как ни крути события мыши на созданной плоскости basePlaneEntity
. Поэтому первично следует добавить механизм захвата данных событий. Вряд ли обойдется без обработки нажатия, перемещения курсора и "отпускания" кнопки, так что события onPressed
, onMoved
, onReleased
- наши железобетонные клиенты.
И для вышеобозначенных задач QML предоставляет нам тип ObjectPicker
, объект данного типа добавляется по старой схеме - в качестве компонента к Entity
:
Entity { id: basePlaneEntity objectName: "basePlaneEntity" CuboidMesh { id: baseCuboid xExtent: 35 yExtent: 25 zExtent: basePlaneZExtent } Transform { id: baseCuboidTransform } DiffuseSpecularMaterial { id: baseCuboidMaterial ambient: "#ffffff" shininess: 100 } ObjectPicker { id: baseCuboidPicker hoverEnabled: true dragEnabled: true enabled: true onPressed: { } onMoved: { } onReleased: { } } components: [ baseCuboid, baseCuboidMaterial, baseCuboidTransform, baseCuboidPicker ] }
Добавленный baseCuboidPicker
сразу отправлен в components
:
components: [ baseCuboid, baseCuboidMaterial, baseCuboidTransform, baseCuboidPicker ]
Если сейчас запустить проект, то объявленные обработчики onPressed
, onMoved
и onReleased
будут вызываться при возникновении соответствующих событий. В качестве аргумента все эти обработчики имеют объект типа PickEvent
, который, в свою очередь, имеет целых два свойства с информацией о координате точки срабатывания события в пространстве. Эти свойства:
localIntersection
- в локальной системе координатworldIntersection
- соответственно в глобальной системе координат
Вспоминаем систему координат из первой части, и идея заключается в том, что по нажатию кнопки мыши в двумерной плоскости экрана в некой точке необходимо определить соответствующую координату базовой плоскости:
При всем при этом нам известно, что z = basePlaneZExtent / 2, так что проверим работоспособность. В обработчик нажатия забрасываем:
onPressed: { if (pick.buttons === Qt.LeftButton) { console.log("onPressed", pick.worldIntersection); } }
Запускаем, осуществляем нажатие и проверяем вывод:
Четко и безошибочно 👍 Собственно, на этом задача QML как интерфейсной части приложения исчерпывается, действие пользователя обработано, необходимо передать данные о нем дальше, то есть в PaintEntity
, где и будет осуществляться дальнейшее. Единственный момент, добавим свойство isAddingPaintObject
к baseCuboidPicker
для того, чтобы гарантировать обработку onMoved
события в PaintEntity
только в том случае(!), если этому предшествовало нажатие нужной кнопки мыши. То есть onMoved
передаст событие в PaintEntity
только после обработки события onPressed
. Итог будет таким:
ObjectPicker { id: baseCuboidPicker hoverEnabled: true dragEnabled: true enabled: true property bool isAddingPaintObject: false function computeBasePlaneCoordinate(pickEvent) { var pickVector = Qt.vector3d(camera.position.x - pickEvent.worldIntersection.x, camera.position.y - pickEvent.worldIntersection.y, camera.position.z - pickEvent.worldIntersection.z); var lineParam = (baseCuboid.zExtent / 2 - pickEvent.worldIntersection.z) / pickVector.z; var planeCoordinate = Qt.vector3d(lineParam * pickVector.x + pickEvent.worldIntersection.x, lineParam * pickVector.y + pickEvent.worldIntersection.y, baseCuboid.zExtent / 2); return planeCoordinate; } onPressed: { if (pick.buttons === Qt.LeftButton) { isAddingPaintObject = true; paintEntity.onBasePlaneMousePressed(pick.worldIntersection); } } onMoved: { if (isAddingPaintObject === true) { paintEntity.onBasePlaneMouseMoved(pick.worldIntersection); } } onReleased: { isAddingPaintObject = false; } }
У меня в голове сформировалась примерная последовательность действий для добавления 3D-объектов на сцену, и для этой последовательности проверка данного свойства будет важна. Если впоследствии окажется, что это лишнее, то спокойно удалим и все.
Теперь нужны пока не существующие методы onBasePlaneMousePressed
и onBasePlaneMouseMoved
в классе PaintEntity
. И для доступности данных методов из QML их нужно объявить с Q_INVOKABLE
.
paintentity.h:
public: PaintEntity(QNode *parent = nullptr); Q_INVOKABLE void onBasePlaneMousePressed(const QVector3D &coordinate); Q_INVOKABLE void onBasePlaneMouseMoved(const QVector3D &coordinate);
paintentity.cpp:
void PaintEntity::onBasePlaneMousePressed(const QVector3D &coordinate) { } void PaintEntity::onBasePlaneMouseMoved(const QVector3D &coordinate) { }
Что ж, пожалуй самое время реализовать создание и отрисовку какого-нибудь 3D-объекта. Пусть будет кубоид, в процессе работы оценим удобство придуманной архитектуры, подправим в случае необходимости, затем по образу и подобию данного кубоида набросаем других фигур, которые потребуются.
Итак, структура и последовательность действий будут такими:
- добавляем класс базового 3D-объекта -
PaintObject
. - конкретный объекты будут производными от
PaintObject
, наследуя общие данные и методы и переопределяя свои собственные, уникальные. PaintEntity
будет содержать контейнер для всех добавленных на сцену объектов.
Создаем PaintObject
, paintobject.h:
#ifndef PAINTOBJECT_H #define PAINTOBJECT_H #include <QObject> #include <Qt3DCore> #include <Qt3DExtras> #include <QSharedPointer> #include "constants.h" class PaintObject : public QObject { Q_OBJECT public: enum class Type { None = 0, Cuboid, Sphere, Cone, Cylinder, Torus, ExtrudedText, Curve }; Q_ENUM(Type) PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId); Q_PROPERTY(Type type READ getType WRITE setType); Type getType(); void setType(Type objectType); quint32 getId(); void setId(quint32 objectId); virtual void onMouseMoved(const QVector3D &coordinate) = 0; virtual void addMaterial(); signals: private slots: protected: Type type; QSharedPointer<Qt3DCore::QEntity> entity; QVector3D initialCoordinate; quint32 id; private: }; #endif // PAINTOBJECT_H
paintobject.cpp:
#include <QDebug> #include "paintobject.h" PaintObject::PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId) : type(PaintObject::Type::None), id(objectId) { entity = QSharedPointer<Qt3DCore::QEntity>(new Qt3DCore::QEntity(parentEntity)); } void PaintObject::addMaterial() { Qt3DExtras::QDiffuseSpecularMaterial *defaultMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); defaultMaterial->setAmbient(QColor(Qt::black)); defaultMaterial->setSpecular(QColor(Qt::white)); defaultMaterial->setShininess(4); entity->addComponent(defaultMaterial); } quint32 PaintObject::getId() { return id; } void PaintObject::setId(quint32 objectId) { id = objectId; } PaintObject::Type PaintObject::getType() { return type; } void PaintObject::setType(Type objectType) { type = objectType; }
Я сразу добавил enum
со всеми будущими графическими примитивами, поскольку уже понятно, какие объекты будут нужны:
enum class Type { None = 0, Cuboid, Sphere, Cone, Cylinder, Torus, ExtrudedText, Curve }; Q_ENUM(Type)
Q_ENUM
даст нам доступ из QML. Сразу, чтобы потом не забыть, регистрируем в main.cpp:
qmlRegisterUncreatableType<PaintObject>("microtechnics.paintObject", 1, 0, "PaintObject", "PaintObject type failed");
У каждого PaintObject
будет свой id для идентификации и, соответственно, тип объекта. onMouseMoved
делаем чисто виртуальным, так как любой из создаваемых объектов будет строиться по-разному. addMaterial()
напротив будет общим для конкретных объектов. Но... Я плюс-минус предполагаю, как будут строиться кривые Безье, и данное предположение намекает, что этот метод (как и многие другие) именно для кривой должен будет работать иначе. Поэтому сразу addMaterial()
делаем виртуальным.
Наследуемся от PaintObject
и создаем класс Cuboid
:
#ifndef CUBOID_H #define CUBOID_H #include <QObject> #include "paintobject.h" class Cuboid : public PaintObject { Q_OBJECT public: Cuboid(Qt3DCore::QEntity *parentEntity, quint32 objectId, const QVector3D &coordinate = QVector3D(0, 0, 0)); void onMouseMoved(const QVector3D &coordinate); }; #endif // CUBOID_H
#include "cuboid.h" constexpr qreal defaultZExtent = 1; Cuboid::Cuboid(Qt3DCore::QEntity *parentEntity, quint32 objectId, const QVector3D &coordinate) : PaintObject(parentEntity, objectId) { type = PaintObject::Type::Cuboid; Qt3DExtras::QCuboidMesh *mesh = new Qt3DExtras::QCuboidMesh(); Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); mesh->setXExtent(0); mesh->setYExtent(0); mesh->setZExtent(0); transform->setTranslation(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2)); initialCoordinate = coordinate; entity->addComponent(mesh); entity->addComponent(transform); addMaterial(); } void Cuboid::onMouseMoved(const QVector3D &coordinate) { Qt3DExtras::QCuboidMesh *currentMesh = entity->componentsOfType<Qt3DExtras::QCuboidMesh>().at(0); currentMesh->setXExtent(qAbs(coordinate.x() - initialCoordinate.x())); currentMesh->setYExtent(qAbs(coordinate.y() - initialCoordinate.y())); currentMesh->setZExtent(defaultZExtent); Qt3DCore::QTransform *currentTransform = entity->componentsOfType<Qt3DCore::QTransform>().at(0); currentTransform->setTranslation(QVector3D((coordinate.x() + initialCoordinate.x()) / 2, (coordinate.y() + initialCoordinate.y()) / 2, constants::basePlaneZExtent + defaultZExtent / 2)); }
Логика создания чего-либо, что будет отображаться на сцене, остается неизменной. Создается QEntity
, к которому осуществляется добавление компонентов, определяющих его поведение. В данном случае это:
- Qt3DExtras::QCuboidMesh
- Qt3DCore::QTransform
- Qt3DExtras::QDiffuseSpecularMaterial - дефолтный материал, который добавляется по умолчанию. В дальнейшем материал будет задаваться из интерфейса. Скорее всего.
Геометрические аспекты кубоида задаются свойствами - xExtent
, yExtent
, zExtent
- по сути длина, ширина, высота. В конструкторе делаем их нулевыми, а при перемещении мыши меняем в зависимости от этого самого перемещения. Высоту держим постоянной, по умолчанию:
constexpr qreal defaultZExtent = 1;
Посмотрим визуально на результат данных действий. В paintentity.h добавляем:
QVector< QSharedPointer<PaintObject> > paintObjects;
Для каждого из создаваемых PaintObject
в качестве id будет выступать индекс объекта в paintObjects
. Теперь добавляем создание кубоида при возникновении события onPressed
и изменение созданного объекта при событии onMoved
:
void PaintEntity::onBasePlaneMousePressed(const QVector3D &coordinate) { paintObjects.append(QSharedPointer<PaintObject>(new Cuboid(this, paintObjects.size(), coordinate))); } void PaintEntity::onBasePlaneMouseMoved(const QVector3D &coordinate) { paintObjects.at(paintObjects.size() - 1)->onMouseMoved(coordinate); }
Запускаем:
И, на всякий случай, пояснительно-графическое творение:
Работает четко 👌
Текста получается несколько больше, чем я планировал... В следующих частях буду исправляться 🙂
Исходный код и проект: mtPaint3D_day_2