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 QUrl 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 QUrl 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 scaling 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 | |