diff options
Diffstat (limited to 'src/extras/3dtext/qtext3dgeometry.cpp')
-rw-r--r-- | src/extras/3dtext/qtext3dgeometry.cpp | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/src/extras/3dtext/qtext3dgeometry.cpp b/src/extras/3dtext/qtext3dgeometry.cpp new file mode 100644 index 000000000..529c93b79 --- /dev/null +++ b/src/extras/3dtext/qtext3dgeometry.cpp @@ -0,0 +1,525 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB). +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtext3dgeometry.h" +#include "qtext3dgeometry_p.h" +#include <Qt3DRender/qbuffer.h> +#include <Qt3DRender/qbufferdatagenerator.h> +#include <Qt3DRender/qattribute.h> +#include <private/qtriangulator_p.h> +#include <qmath.h> +#include <QVector3D> +#include <QTextLayout> +#include <QTime> + +QT_BEGIN_NAMESPACE + +namespace Qt3DExtras { + +namespace { + +using IndexType = unsigned short; + +struct TriangulationData { + struct Outline { + int begin; + int end; + }; + + QVector<QVector3D> vertices; + QVector<IndexType> indices; + QVector<Outline> outlines; + QVector<IndexType> outlineIndices; + bool inverted; +}; + +TriangulationData triangulate(const QString &text, const QFont &font) +{ + TriangulationData result; + int beginOutline = 0; + + // Initialize path with text and extract polygons + QPainterPath path; + path.setFillRule(Qt::WindingFill); + path.addText(0, 0, font, text); + QList<QPolygonF> polygons = path.toSubpathPolygons(QTransform().scale(1.f, -1.f)); + + // maybe glyph has no geometry + if (polygons.size() == 0) + return result; + + const int prevNumIndices = result.indices.size(); + + // Reset path and add previously extracted polygons (which where spatially transformed) + path = QPainterPath(); + path.setFillRule(Qt::WindingFill); + for (QPolygonF &p : polygons) + path.addPolygon(p); + + // Extract polylines out of the path, this allows us to retrive indicies for each glyph outline + QPolylineSet polylines = qPolyline(path); + QVector<IndexType> tmpIndices; + tmpIndices.resize(polylines.indices.size()); + memcpy(tmpIndices.data(), polylines.indices.data(), polylines.indices.size() * sizeof(IndexType)); + + int lastIndex = 0; + for (const IndexType idx : tmpIndices) { + if (idx == std::numeric_limits<IndexType>::max()) { + const int endOutline = lastIndex; + result.outlines.push_back({beginOutline, endOutline}); + beginOutline = endOutline; + } else { + result.outlineIndices.push_back(idx); + ++lastIndex; + } + } + + // Triangulate path + const QTriangleSet triangles = qTriangulate(path); + + // Append new indices to result.indices buffer + result.indices.resize(result.indices.size() + triangles.indices.size()); + memcpy(&result.indices[prevNumIndices], triangles.indices.data(), triangles.indices.size() * sizeof(IndexType)); + for (int i = prevNumIndices, m = result.indices.size(); i < m; ++i) + result.indices[i] += result.vertices.size(); + + // Append new triangles to result.vertices + result.vertices.reserve(triangles.vertices.size() / 2); + for (int i = 0, m = triangles.vertices.size(); i < m; i += 2) + result.vertices.push_back(QVector3D(triangles.vertices[i] / font.pointSizeF(), triangles.vertices[i + 1] / font.pointSizeF(), 0.0f)); + + return result; +} + +inline QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio) +{ + return a + (b - a) * ratio; +} + +} // anonymous namespace + +QText3DGeometryPrivate::QText3DGeometryPrivate() + : QGeometryPrivate() + , m_font(QFont(QStringLiteral("Arial"))) + , m_depth(1.f) + , m_edgeSplitAngle(90.f * 0.1f) + , m_positionAttribute(nullptr) + , m_normalAttribute(nullptr) + , m_indexAttribute(nullptr) + , m_vertexBuffer(nullptr) + , m_indexBuffer(nullptr) +{ + m_font.setPointSize(4); +} + +void QText3DGeometryPrivate::init() +{ + Q_Q(QText3DGeometry); + m_positionAttribute = new Qt3DRender::QAttribute(q); + m_normalAttribute = new Qt3DRender::QAttribute(q); + m_indexAttribute = new Qt3DRender::QAttribute(q); + m_vertexBuffer = new Qt3DRender::QBuffer(Qt3DRender::QBuffer::VertexBuffer, q); + m_indexBuffer = new Qt3DRender::QBuffer(Qt3DRender::QBuffer::IndexBuffer, q); + + const quint32 elementSize = 3 + 3; + const quint32 stride = elementSize * sizeof(float); + + m_positionAttribute->setName(Qt3DRender::QAttribute::defaultPositionAttributeName()); + m_positionAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float); + m_positionAttribute->setVertexSize(3); + m_positionAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + m_positionAttribute->setBuffer(m_vertexBuffer); + m_positionAttribute->setByteStride(stride); + m_positionAttribute->setByteOffset(0); + m_positionAttribute->setCount(0); + + m_normalAttribute->setName(Qt3DRender::QAttribute::defaultNormalAttributeName()); + m_normalAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float); + m_normalAttribute->setVertexSize(3); + m_normalAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + m_normalAttribute->setBuffer(m_vertexBuffer); + m_normalAttribute->setByteStride(stride); + m_normalAttribute->setByteOffset(3 * sizeof(float)); + m_normalAttribute->setCount(0); + + m_indexAttribute->setAttributeType(Qt3DRender::QAttribute::IndexAttribute); + m_indexAttribute->setVertexBaseType(Qt3DRender::QAttribute::UnsignedShort); + m_indexAttribute->setBuffer(m_indexBuffer); + m_indexAttribute->setCount(0); + + q->addAttribute(m_positionAttribute); + q->addAttribute(m_normalAttribute); + q->addAttribute(m_indexAttribute); + + update(); +} + +/*! + * \qmltype Text3DGeometry + * \instantiates Qt3DExtras::QText3DGeometry + * \inqmlmodule Qt3D.Extras + * \brief Text3DGeometry allows creation of a 3D text in 3D space. + * + * The Text3DGeometry type is most commonly used internally by the Text3DMesh type + * but can also be used in custom GeometryRenderer types. + */ + +/*! + * \qmlproperty QString Text3DGeometry::text + * + * Holds the text used for the mesh. + */ + +/*! + * \qmlproperty QFont Text3DGeometry::font + * + * Holds the font of the text. + */ + +/*! + * \qmlproperty float Text3DGeometry::depth + * + * Holds the extrusion depth of the text. + */ + +/*! + * \qmlproperty float Text3DGeometry::edgeSplitAngle + * + * Holds the threshold angle for smooth normals. + */ + +/*! + * \qmlproperty Attribute Text3DGeometry::positionAttribute + * + * Holds the geometry position attribute. + */ + +/*! + * \qmlproperty Attribute Text3DGeometry::normalAttribute + * + * Holds the geometry normal attribute. + */ + +/*! + * \qmlproperty Attribute Text3DGeometry::indexAttribute + * + * Holds the geometry index attribute. + */ + +/*! + * \class Qt3DExtras::QText3DGeometry + * \inheaderfile Qt3DExtras/QText3DGeometry + * \inmodule Qt3DExtras + * \brief The QText3DGeometry class allows creation of a 3D text in 3D space. + * \since 5.8 + * \ingroup geometries + * \inherits Qt3DRender::QGeometry + * + * The QText3DGeometry class is most commonly used internally by the QText3DMesh + * but can also be used in custom Qt3DRender::QGeometryRenderer subclasses. + */ + +/*! + * Constructs a new QText3DGeometry with \a parent. + */ +QText3DGeometry::QText3DGeometry(Qt3DCore::QNode *parent) + : QGeometry(*new QText3DGeometryPrivate(), parent) +{ + Q_D(QText3DGeometry); + d->init(); +} + +/*! + * \internal + */ +QText3DGeometry::QText3DGeometry(QText3DGeometryPrivate &dd, Qt3DCore::QNode *parent) + : QGeometry(dd, parent) +{ + Q_D(QText3DGeometry); + d->init(); +} + +/*! + * \internal + */ +QText3DGeometry::~QText3DGeometry() +{} + +/*! + * \internal + * Updates vertices based on text, font, depth and smoothAngle properties. + */ +void QText3DGeometryPrivate::update() +{ + if (m_text.trimmed().isEmpty()) // save enough? + return; + + TriangulationData data = triangulate(m_text, m_font); + + const int numVertices = data.vertices.size(); + const int numIndices = data.indices.size(); + + struct Vertex { + QVector3D position; + QVector3D normal; + }; + + QVector<IndexType> indices; + QVector<Vertex> vertices; + + // TODO: keep 'vertices.size()' small when extruding + vertices.reserve(data.vertices.size() * 2); + for (QVector3D &v : data.vertices) // front face + vertices.push_back({ v, // vertex + QVector3D(0.0f, 0.0f, -1.0f) }); // normal + for (QVector3D &v : data.vertices) // front face + vertices.push_back({ QVector3D(v.x(), v.y(), m_depth), // vertex + QVector3D(0.0f, 0.0f, 1.0f) }); // normal + + for (int i = 0, verticesIndex = vertices.size(); i < data.outlines.size(); ++i) { + const int begin = data.outlines[i].begin; + const int end = data.outlines[i].end; + const int verticesIndexBegin = verticesIndex; + + QVector3D prevNormal = QVector3D::crossProduct( + vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position, + vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized(); + + for (int j = begin; j < end; ++j) { + const bool isLastIndex = (j == end - 1); + const IndexType cur = data.outlineIndices[j]; + const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust + const QVector3D normal = QVector3D::crossProduct(vertices[cur + numVertices].position - vertices[cur].position, vertices[next].position - vertices[cur].position).normalized(); + + // use smooth normals in case of a short angle + const bool smooth = QVector3D::dotProduct(prevNormal, normal) > (90.0f - m_edgeSplitAngle) / 90.0f; + const QVector3D resultNormal = smooth ? mix(prevNormal, normal, 0.5f) : normal; + if (!smooth) { + vertices.push_back({vertices[cur].position, prevNormal}); + vertices.push_back({vertices[cur + numVertices].position, prevNormal}); + verticesIndex += 2; + } + + vertices.push_back({vertices[cur].position, resultNormal}); + vertices.push_back({vertices[cur + numVertices].position, resultNormal}); + + const int v0 = verticesIndex; + const int v1 = verticesIndex + 1; + const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2; + const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3; + + indices.push_back(v0); + indices.push_back(v1); + indices.push_back(v2); + indices.push_back(v2); + indices.push_back(v1); + indices.push_back(v3); + + verticesIndex += 2; + prevNormal = normal; + } + } + + { // upload vertices + QByteArray data; + data.resize(vertices.size() * sizeof(Vertex)); + memcpy(data.data(), vertices.data(), vertices.size() * sizeof(Vertex)); + + m_vertexBuffer->setData(data); + m_positionAttribute->setCount(vertices.size()); + m_normalAttribute->setCount(vertices.size()); + } + + // resize for following insertions + const int indicesOffset = indices.size(); + indices.resize(indices.size() + numIndices * 2); + + // copy values for back faces + IndexType *indicesFaces = indices.data() + indicesOffset; + memcpy(indicesFaces, data.indices.data(), numIndices * sizeof(IndexType)); + + // insert values for front face and flip triangles + for (int j = 0; j < numIndices; j += 3) + { + indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices; + indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices; + indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices; + } + + { // upload indices + QByteArray data; + data.resize(indices.size() * sizeof(IndexType)); + memcpy(data.data(), indices.data(), indices.size() * sizeof(IndexType)); + + m_indexBuffer->setData(data); + m_indexAttribute->setCount(indices.size()); + } +} + +void QText3DGeometry::setText(QString text) +{ + Q_D(QText3DGeometry); + if (d->m_text != text) { + d->m_text = text; + d->update(); + emit textChanged(text); + } +} + +void QText3DGeometry::setFont(QFont font) +{ + Q_D(QText3DGeometry); + if (d->m_font != font) { + d->m_font = font; + d->update(); + emit fontChanged(font); + } +} + +void QText3DGeometry::setDepth(float depth) +{ + Q_D(QText3DGeometry); + if (d->m_depth != depth) { + d->m_depth = depth; + d->update(); + emit depthChanged(depth); + } +} + +void QText3DGeometry::setEdgeSplitAngle(float smoothAngle) +{ + Q_D(QText3DGeometry); + if (d->m_edgeSplitAngle != smoothAngle) { + d->m_edgeSplitAngle = smoothAngle; + d->update(); + emit edgeSplitAngleChanged(smoothAngle); + } +} + +/*! + * \property QString Text3DGeometry::text + * + * Holds the text used for the mesh. + */ +QString QText3DGeometry::text() const +{ + Q_D(const QText3DGeometry); + return d->m_text; +} + +/*! + * \property QFont Text3DGeometry::font + * + * Holds the font of the text. + */ +QFont QText3DGeometry::font() const +{ + Q_D(const QText3DGeometry); + return d->m_font; +} + +/*! + * \property float Text3DGeometry::depth + * + * Holds the extrusion depth of the text. + */ +float QText3DGeometry::depth() const +{ + Q_D(const QText3DGeometry); + return d->m_depth; +} + +/*! + * \property float Text3DGeometry::edgeSplitAngle + * + * Holds the threshold angle for smooth normals. + */ +float QText3DGeometry::edgeSplitAngle() const +{ + Q_D(const QText3DGeometry); + return d->m_edgeSplitAngle; +} + +/*! + * \property Text3DGeometry::positionAttribute + * + * Holds the geometry position attribute. + */ +Qt3DRender::QAttribute *QText3DGeometry::positionAttribute() const +{ + Q_D(const QText3DGeometry); + return d->m_positionAttribute; +} + +/*! + * \property Text3DGeometry::normalAttribute + * + * Holds the geometry normal attribute. + */ +Qt3DRender::QAttribute *QText3DGeometry::normalAttribute() const +{ + Q_D(const QText3DGeometry); + return d->m_normalAttribute; +} + +/*! + * \property Text3DGeometry::indexAttribute + * + * Holds the geometry index attribute. + */ +Qt3DRender::QAttribute *QText3DGeometry::indexAttribute() const +{ + Q_D(const QText3DGeometry); + return d->m_indexAttribute; +} + +} // Qt3DExtras + +QT_END_NAMESPACE |