Top.Mail.Ru

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

Всем привет, продолжаем софт-марафон! В принципе, четкого плана на сегодня нет - буду последовательно добавлять функционал, доверюсь интуиции и ощущениям, чтобы определить момент, когда на сегодня будет достаточно 🙂

Здесь я решил поместить небольшую врезку-спойлер с финальным результатом. Пока здесь будет пусто (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 - соответственно в глобальной системе координат

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

ObjectPicker в QML.

При всем при этом нам известно, что 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);
}

Запускаем:

И, на всякий случай, пояснительно-графическое творение:

Qt и 3D. QML Cuboid.

Работает четко 👌

Текста получается несколько больше, чем я планировал... В следующих частях буду исправляться 🙂

Исходный код и проект: mtPaint3D_day_2

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