| 1 | // Copyright (C) 2022 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "heightfieldgeometry_p.h" |
| 5 | |
| 6 | /*! |
| 7 | \qmltype HeightFieldGeometry |
| 8 | \inqmlmodule QtQuick3D.Helpers |
| 9 | \inherits Geometry |
| 10 | \since 6.4 |
| 11 | \brief A height field geometry. |
| 12 | |
| 13 | This helper implements a height-field geometry. It defines a surface built from a grayscale image. |
| 14 | The y-coordinate of the surface at a given point in the horizontal plane is determined by the |
| 15 | pixel value at the corresponding point in the image. The image's x-axis and y-axis will go along |
| 16 | the geometry's x-axis and z-axis respectively. |
| 17 | */ |
| 18 | |
| 19 | /*! |
| 20 | \qmlproperty vector3d HeightFieldGeometry::extents |
| 21 | This property defines the extents of the height-field, that is |
| 22 | the dimensions of a box large enough to always contain the geometry. |
| 23 | The default value is (100, 100, 100) when the image is square. |
| 24 | */ |
| 25 | |
| 26 | /*! |
| 27 | \qmlproperty url HeightFieldGeometry::heightMap |
| 28 | \obsolete |
| 29 | |
| 30 | This property defines the URL of the height map image. |
| 31 | |
| 32 | Use \l HeightFieldGeometry::source instead. |
| 33 | */ |
| 34 | |
| 35 | /*! |
| 36 | \qmlproperty url HeightFieldGeometry::source |
| 37 | This property defines the URL of the height map image. |
| 38 | */ |
| 39 | |
| 40 | /*! |
| 41 | \qmlproperty bool HeightFieldGeometry::smoothShading |
| 42 | This property defines whether the height map is shown with smooth shading |
| 43 | or with hard angles between the squares of the map. |
| 44 | |
| 45 | The default value is \c true, meaning smooth shading is turned on. |
| 46 | */ |
| 47 | |
| 48 | |
| 49 | HeightFieldGeometry::HeightFieldGeometry() |
| 50 | { |
| 51 | updateData(); |
| 52 | } |
| 53 | |
| 54 | const QUrl &HeightFieldGeometry::source() const |
| 55 | { |
| 56 | return m_heightMapSource; |
| 57 | } |
| 58 | |
| 59 | void HeightFieldGeometry::setSource(const QUrl &newSource) |
| 60 | { |
| 61 | if (m_heightMapSource == newSource) |
| 62 | return; |
| 63 | m_heightMapSource = newSource; |
| 64 | |
| 65 | updateData(); |
| 66 | update(); |
| 67 | |
| 68 | emit sourceChanged(); |
| 69 | } |
| 70 | |
| 71 | bool HeightFieldGeometry::smoothShading() const |
| 72 | { |
| 73 | return m_smoothShading; |
| 74 | } |
| 75 | |
| 76 | void HeightFieldGeometry::setSmoothShading(bool smooth) |
| 77 | { |
| 78 | if (m_smoothShading == smooth) |
| 79 | return; |
| 80 | m_smoothShading = smooth; |
| 81 | |
| 82 | updateData(); |
| 83 | update(); |
| 84 | |
| 85 | emit smoothShadingChanged(); |
| 86 | } |
| 87 | |
| 88 | const QVector3D &HeightFieldGeometry::extents() const |
| 89 | { |
| 90 | return m_extents; |
| 91 | } |
| 92 | |
| 93 | void HeightFieldGeometry::setExtents(const QVector3D &newExtents) |
| 94 | { |
| 95 | m_extentsSetExplicitly = true; |
| 96 | if (m_extents == newExtents) |
| 97 | return; |
| 98 | m_extents = newExtents; |
| 99 | |
| 100 | updateData(); |
| 101 | update(); |
| 102 | emit extentsChanged(); |
| 103 | } |
| 104 | |
| 105 | struct HeightFieldVertex |
| 106 | { |
| 107 | QVector3D position; |
| 108 | QVector3D normal; |
| 109 | QVector2D uv; |
| 110 | }; |
| 111 | |
| 112 | void HeightFieldGeometry::updateData() |
| 113 | { |
| 114 | const QQmlContext *context = qmlContext(this); |
| 115 | |
| 116 | const auto resolvedUrl = context ? context->resolvedUrl(m_heightMapSource) : m_heightMapSource; |
| 117 | if (!resolvedUrl.isValid()) |
| 118 | return; |
| 119 | |
| 120 | clear(); |
| 121 | const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl); |
| 122 | |
| 123 | QImage heightMap(qmlSource); |
| 124 | int numRows = heightMap.height(); |
| 125 | int numCols = heightMap.width(); |
| 126 | |
| 127 | if (numRows < 2 || numCols < 2) |
| 128 | return; |
| 129 | |
| 130 | const int numVertices = numRows * numCols; |
| 131 | |
| 132 | if (!m_extentsSetExplicitly) { |
| 133 | auto prevExt = m_extents; |
| 134 | if (numRows == numCols) { |
| 135 | m_extents = {100, 100, 100}; |
| 136 | } else if (numRows < numCols) { |
| 137 | float f = float(numRows) / float(numCols); |
| 138 | m_extents = {100.f, 100.f, 100.f * f}; |
| 139 | } else { |
| 140 | float f = float(numCols) / float(numRows); |
| 141 | m_extents = {100.f * f, 100.f, 100.f}; |
| 142 | } |
| 143 | if (m_extents != prevExt) { |
| 144 | emit extentsChanged(); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | QVector<HeightFieldVertex> vertices; |
| 149 | vertices.reserve(asize: numVertices); |
| 150 | |
| 151 | const float rowF = m_extents.z() / (numRows - 1); |
| 152 | const float rowOffs = -m_extents.z() / 2; |
| 153 | const float colF = m_extents.x() / (numCols - 1); |
| 154 | const float colOffs = -m_extents.x() / 2; |
| 155 | for (int x = 0; x < numCols; x++) { |
| 156 | for (int y = 0; y < numRows; y++) { |
| 157 | float f = heightMap.pixelColor(x, y).valueF() - 0.5; |
| 158 | HeightFieldVertex vertex; |
| 159 | vertex.position = QVector3D(x * colF + colOffs, f * m_extents.y(), y * rowF + rowOffs); |
| 160 | vertex.normal = QVector3D(0, 0, 0); |
| 161 | vertex.uv = QVector2D(float(x) / (numCols - 1), 1.f - float(y) / (numRows - 1)); |
| 162 | vertices.push_back(t: vertex); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | QVector<quint32> indices; |
| 167 | for (int ix = 0; ix < numCols - 1; ++ix) { |
| 168 | for (int iy = 0; iy < numRows - 1; ++iy) { |
| 169 | const int idx = iy + ix * numRows; |
| 170 | |
| 171 | const auto tri0 = std::array<int, 3> { idx + numRows + 1, idx + numRows, idx }; |
| 172 | const auto tri1 = std::array<int, 3> { idx + 1, idx + numRows + 1, idx }; |
| 173 | |
| 174 | for (const auto [i0, i1, i2] : { tri0, tri1 }) { |
| 175 | indices.push_back(t: i0); |
| 176 | indices.push_back(t: i1); |
| 177 | indices.push_back(t: i2); |
| 178 | |
| 179 | if (m_smoothShading) { |
| 180 | // Calculate face normal |
| 181 | const QVector3D e0 = vertices[i1].position - vertices[i0].position; |
| 182 | const QVector3D e1 = vertices[i2].position - vertices[i0].position; |
| 183 | QVector3D normal = QVector3D::crossProduct(v1: e0, v2: e1).normalized(); |
| 184 | |
| 185 | // Add normal to vertex, will normalize later |
| 186 | vertices[i0].normal += normal; |
| 187 | vertices[i1].normal += normal; |
| 188 | vertices[i2].normal += normal; |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | if (m_smoothShading) { |
| 195 | // Normalize |
| 196 | for (auto &vertex : vertices) |
| 197 | vertex.normal.normalize(); |
| 198 | } |
| 199 | |
| 200 | // Calculate bounds |
| 201 | QVector3D boundsMin = vertices[0].position; |
| 202 | QVector3D boundsMax = vertices[0].position; |
| 203 | |
| 204 | for (const auto &vertex : vertices) { |
| 205 | const auto &p = vertex.position; |
| 206 | boundsMin = QVector3D(qMin(a: boundsMin.x(), b: p.x()), qMin(a: boundsMin.y(), b: p.y()), qMin(a: boundsMin.z(), b: p.z())); |
| 207 | boundsMax = QVector3D(qMax(a: boundsMax.x(), b: p.x()), qMax(a: boundsMax.y(), b: p.y()), qMax(a: boundsMax.z(), b: p.z())); |
| 208 | } |
| 209 | |
| 210 | addAttribute(semantic: QQuick3DGeometry::Attribute::PositionSemantic, offset: 0, componentType: QQuick3DGeometry::Attribute::F32Type); |
| 211 | addAttribute(semantic: QQuick3DGeometry::Attribute::TexCoord0Semantic, offset: sizeof(QVector3D) * 2, componentType: QQuick3DGeometry::Attribute::F32Type); |
| 212 | |
| 213 | if (m_smoothShading) |
| 214 | addAttribute(semantic: QQuick3DGeometry::Attribute::NormalSemantic, offset: sizeof(QVector3D), componentType: QQuick3DGeometry::Attribute::F32Type); |
| 215 | |
| 216 | addAttribute(semantic: QQuick3DGeometry::Attribute::IndexSemantic, offset: 0, componentType: QQuick3DGeometry::Attribute::ComponentType::U32Type); |
| 217 | |
| 218 | setStride(sizeof(HeightFieldVertex)); |
| 219 | QByteArray vertexBuffer(reinterpret_cast<char *>(vertices.data()), vertices.size() * sizeof(HeightFieldVertex)); |
| 220 | setVertexData(vertexBuffer); |
| 221 | setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); |
| 222 | setBounds(min: boundsMin, max: boundsMax); |
| 223 | |
| 224 | QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()), indices.size() * sizeof(quint32)); |
| 225 | setIndexData(indexBuffer); |
| 226 | } |
| 227 | |