Cogs.Core
OGC3DTilesUtils.cpp
1#include "OGC3DTilesUtils.h"
2
3#include "OGC3DTilesTileset.h"
4
5#include "Foundation/Logging/Logger.h"
6#include "Foundation/Geometry/GeodeticUtils.h"
7
8#include <cmath>
9
10namespace {
11 Cogs::Logging::Log logger = Cogs::Logging::getLogger("OGC3DTilesUtils");
12}
13
14using namespace Cogs::Core;
15
16bool
17OGC3DTilesUtils::isInsideBox(const glm::dvec3& point, const Cogs::Geometry::DBoundingBox& box)
18{
19 return point.x > box.min.x && point.x < box.max.x &&
20 point.y > box.min.y && point.y < box.max.y &&
21 point.z > box.min.z && point.z < box.max.z;
22};
23
24double
25OGC3DTilesUtils::distanceFromBBox(const glm::dvec3 point, const Cogs::Geometry::DBoundingBox& bbox)
26{
27 if (OGC3DTilesUtils::isInsideBox(point, bbox)) {
28 return 0.0;
29 }
30
31 // Ref: https://stackoverflow.com/a/18157551
32 const double dx = std::max({ bbox.min.x - point.x, 0.0, point.x - bbox.max.x });
33 const double dy = std::max({ bbox.min.y - point.y, 0.0, point.y - bbox.max.y });
34 const double dz = std::max({ bbox.min.z - point.z, 0.0, point.z - bbox.max.z });
35 return glm::length(glm::dvec3(dx, dy, dz));
36};
37
38void
39OGC3DTilesUtils::printBBox(const Cogs::Geometry::BoundingBox& bbox, const std::string& prefix)
40{
41 printf("%s: bbox=<%.2f, %.2f, %.2f> | <%.2f, %.2f, %.2f> (w=%.2f, h=%.2f, d=%.2f)\n",
42 prefix.c_str(),
43 bbox.min.x, bbox.min.y, bbox.min.z,
44 bbox.max.x, bbox.max.y, bbox.max.z,
45 float(bbox.max.x - bbox.min.x), float(bbox.max.y - bbox.min.y), float(bbox.max.z - bbox.min.z));
46}
47
48void
49OGC3DTilesUtils::printBBox(const Cogs::Geometry::DBoundingBox& bbox, const std::string& prefix)
50{
51 printf("%s: bbox=<%.2f, %.2f, %.2f> | <%.2f, %.2f, %.2f> (w=%.2f, h=%.2f, d=%.2f)\n",
52 prefix.c_str(),
53 bbox.min.x, bbox.min.y, bbox.min.z,
54 bbox.max.x, bbox.max.y, bbox.max.z,
55 float(bbox.max.x - bbox.min.x), float(bbox.max.y - bbox.min.y), float(bbox.max.z - bbox.min.z));
56}
57
58void
59OGC3DTilesUtils::transformBBox(Cogs::Geometry::DBoundingBox& bbox, const glm::mat4& mat)
60{
61 glm::dvec3 points[2] = { bbox.min, bbox.max };
62
63 static constexpr double mx = std::numeric_limits<double>::max();
64 static constexpr double mn = -mx;
65 bbox.min = glm::dvec3(mx, mx, mx);
66 bbox.max = glm::dvec3(mn, mn, mn);
67
68 for (int i = 0; i < 8; i++) {
69 glm::dvec4 corner = glm::dvec4(points[(i & 4) >> 2].x, points[(i & 2) >> 1].y, points[i & 1].z, 1.0);
70 glm::dvec4 xfmd = mat * corner;
71 bbox += glm::dvec3(xfmd.x, xfmd.y, xfmd.z);
72 }
73};
74
75void
76OGC3DTilesUtils::translateBBox(Cogs::Geometry::DBoundingBox& bbox, const glm::dvec3& offset)
77{
78 bbox.min += offset;
79 bbox.max += offset;
80}
81
82int
83OGC3DTilesUtils::bboxInsideViewFrustum(const Cogs::Geometry::DBoundingBox& bbox, const glm::mat4& viewProjectionMatrix)
84{
85 // NOTE: Parts of this code is lifted from "ManageLodLevels.cpp::cellInsideFrustum()"
86 glm::dvec3 corners[8] = {
87 bbox.min,
88 glm::dvec3(bbox.min.x, bbox.min.y, bbox.max.z),
89 glm::dvec3(bbox.min.x, bbox.max.y, bbox.min.z),
90 glm::dvec3(bbox.min.x, bbox.max.y, bbox.max.z),
91 glm::dvec3(bbox.max.x, bbox.min.y, bbox.min.z),
92 glm::dvec3(bbox.max.x, bbox.min.y, bbox.max.z),
93 glm::dvec3(bbox.max.x, bbox.max.y, bbox.min.z),
94 bbox.max
95 };
96
97 uint32_t allInsideMask = 0b111111;
98 {
99 uint32_t anyInsideMask = 0u;
100 for (size_t i = 0; i < 8; i++) {
101 glm::dvec4 p = viewProjectionMatrix * glm::dvec4(corners[i], 1.f);
102 const uint32_t cornerMask =
103 (p.x <= p.w ? 1 : 0) |
104 (p.y <= p.w ? 2 : 0) |
105 (p.z <= p.w ? 4 : 0) |
106 (-p.w <= p.x ? 8 : 0) |
107 (-p.w <= p.y ? 16 : 0) |
108 (-p.w <= p.z ? 32 : 0);
109 anyInsideMask = anyInsideMask | cornerMask;
110 allInsideMask = allInsideMask & cornerMask;
111 }
112
113 if (anyInsideMask != 0b111111) {
114 // At least one plane where all corners are outside => outside.
115 return 0;
116 }
117 }
118
119 // FIXME: No need to check if completely or partially inside for now. Implement later.
120 return 1;
121}
122
123// NOTE: This is a version of the LLtoECEF() in Foundation::GeodeticUtils.cpp. The difference is that
124// this method takes radians as input instead of degrees. This function is called a bazillion times
125// whenever a dataset uses LatLong-region based bbox'es and the 3DTiles spec uses radians for
126// all LatLongs values.
127glm::dvec3
128latLngAltToVec3(double latitudeRad, double longitudeRad, double altitudeMeter)
129{
130 constexpr double semimajorAxis = 6378137.0;
131 constexpr double semiminorAxis = 6356752.314245;
132 const double sinLat = std::sin(latitudeRad);
133 const double cosLat = std::cos(latitudeRad);
134 const double sinLong = std::sin(longitudeRad);
135 const double cosLong = std::cos(longitudeRad);
136 constexpr double ellipsoidFlatness = ((semimajorAxis - semiminorAxis) / semimajorAxis);
137 constexpr double firstEccentricitySquared = (ellipsoidFlatness * (2.0 - ellipsoidFlatness));
138 const double n = semimajorAxis / std::sqrt(1.0 - firstEccentricitySquared * sinLat * sinLat);
139
140 return glm::dvec3((altitudeMeter + n) * cosLat * cosLong,
141 (altitudeMeter + n) * cosLat * sinLong,
142 (altitudeMeter + (1.0 - firstEccentricitySquared) * n) * sinLat);
143}
144
145// See "https://github.com/CesiumGS/3d-tiles/tree/main/specification#core-bounding-volumes" for definition
146Cogs::Geometry::DBoundingBox
147OGC3DTilesUtils::boundingVolumeConverter(const Cogs::Core::OGC3DTiles::BoundingVolume& bv, const glm::mat4& transform)
148{
149 Cogs::Geometry::DBoundingBox bbox = Cogs::Geometry::makeEmptyBoundingBox<Cogs::Geometry::DBoundingBox>();
150
151 if (bv.type == OGC3DTiles::BoundingVolumeType::BOX) { // Oriented bounding box
152 // FIXME: Converting this type of bbox directly to a Cogs::Geometry::BoundingBox is
153 // not 100% correct as the volume is not properly preserved. The only way around this is to make
154 // our own BBox class which preserves the orientation.
155 glm::vec3 center = glm::vec3(bv.values[0], bv.values[1], bv.values[2]);
156
157 glm::vec3 dx = glm::abs(glm::vec3(bv.values[3], bv.values[4], bv.values[5]));
158 glm::vec3 dy = glm::abs(glm::vec3(bv.values[6], bv.values[7], bv.values[8]));
159 glm::vec3 dz = glm::abs(glm::vec3(bv.values[9], bv.values[10], bv.values[11]));
160 bbox.min = center;
161 bbox.max = center;
162 bbox.min -= dx;
163 bbox.min -= dy;
164 bbox.min -= dz;
165 bbox.max += dx;
166 bbox.max += dy;
167 bbox.max += dz;
168 }
169 else if (bv.type == OGC3DTiles::BoundingVolumeType::REGION) { // [west, south, east, north, minimum height, maximum height]
170 // NOTE: west, south, east & north are defined in radians NOT degrees (as usual).
171 // Min/max height is defined in meters. Latitude is South/North, Longitude is West/East.
172 const double west = bv.values[0];
173 const double south = bv.values[1];
174 const double east = bv.values[2];
175 const double north = bv.values[3];
176 const double minHeight = bv.values[4];
177 const double maxHeight = bv.values[5];
178
179 // FIXME: I think this process can be improved. We only need to do multiple "samples" whenever the
180 // east/west or north/south spans between +90 degrees (bbox gets too small) or 180-degrees (a 2-sample
181 // version could then result in a flat box.).
182
183 const double latRange = std::abs(east - west);
184 const double lngRange = std::abs(north - south);
185 int latSteps = 1;
186 if (latRange > glm::pi<double>()) {
187 latSteps = int(latRange / (glm::pi<double>() / 3.0)); // Divide by something which is not divisible by 2*PI.
188 assert(latSteps >= 1);
189 }
190
191 int lngSteps = 1;
192 if (lngRange > glm::pi<double>()) {
193 lngSteps = int(lngRange / (glm::pi<double>() / 3.0));
194 assert(lngSteps >= 1);
195 }
196
197 bbox += latLngAltToVec3(north, west, minHeight);
198
199 for (int i = 1; i <= latSteps; ++i) {
200 for (int j = 1; j <= lngSteps; ++j) {
201 bbox += latLngAltToVec3((south + j * (lngRange / lngSteps)), (west + i * (latRange / latSteps)), minHeight);
202 bbox += latLngAltToVec3((south + j * (lngRange / lngSteps)), (west + i * (latRange / latSteps)), maxHeight);
203 }
204 }
205
206 bbox += latLngAltToVec3(south, east, maxHeight);
207
208#if 1 // Sanity test
209 assert(std::abs(bbox.min.x - bbox.max.x) > 0.1 &&
210 std::abs(bbox.min.y - bbox.max.y) > 0.1 &&
211 std::abs(bbox.min.z - bbox.max.z) > 0.1 && "BBox is flat!");
212#endif
213
214 // NOTE: Region boxes shall never be transformed according to the 3DTiles spec
215 return bbox;
216 }
217 else if (bv.type == OGC3DTiles::BoundingVolumeType::SPHERE) { // Sphere
218 glm::vec3 center = glm::vec3(bv.values[0], bv.values[1], bv.values[2]);
219
220 bbox.min = center;
221 bbox.max = center;
222 glm::vec3 dx = center + glm::vec3(bv.values[3], 0, 0);
223 glm::vec3 dy = center + glm::vec3(0, bv.values[3], 0);
224 glm::vec3 dz = center + glm::vec3(0, 0, bv.values[3]);
225 bbox += dx;
226 bbox += dy;
227 bbox += dz;
228 bbox += -dx;
229 bbox += -dy;
230 bbox += -dz;
231 }
232 else {
233 LOG_ERROR(logger, "Invalid BoundingVolume: No valid candidates.");
234 assert(false && "Invalid BoundingVolume: No valid candidates.");
235 }
236
237 OGC3DTilesUtils::transformBBox(bbox, transform);
238 return bbox;
239}
240
241/*
242* NOTE: If global a tile-coord is provided then the tileset's root-boundingbox needs to be provided as 'parentBBox'.
243* For local subtree tile-coords the subtree's bbox is needed as 'parentBBox'.
244*/
245Cogs::Geometry::DBoundingBox
246Cogs::Core::OGC3DTilesUtils::getBBoxFromCoord(const OGC3DTiles::Coord& tileCoord, const Cogs::Geometry::DBoundingBox& parentBBox)
247{
248 const double width = parentBBox.max.x > parentBBox.min.x ? parentBBox.max.x - parentBBox.min.x : parentBBox.min.x - parentBBox.max.x;
249 const double height = parentBBox.max.y > parentBBox.min.y ? parentBBox.max.y - parentBBox.min.y : parentBBox.min.y - parentBBox.max.y;
250 const double depth = parentBBox.max.z > parentBBox.min.z ? parentBBox.max.z - parentBBox.min.z : parentBBox.min.z - parentBBox.max.z;
251
252 assert(width >= 0 && "Invalid bbox width: Cannot be negative.");
253 assert(height >= 0 && "Invalid bbox height: Cannot be negative.");
254 assert(depth >= 0 && "Invalid bbox depth: Cannot be negative.");
255
256 const bool isOctTree = tileCoord.z >= 0;
257 const double range = static_cast<double>(std::pow(2, tileCoord.level));
258
259 const double dx = width / range;
260 const double dy = height / range;
261 const double dz = depth / range;
262
263 glm::dvec3 min = glm::dvec3(parentBBox.min.x + tileCoord.x * dx,
264 parentBBox.min.y + tileCoord.y * dy,
265 isOctTree ? (parentBBox.min.z + tileCoord.z * dz) : parentBBox.min.z);
266
267 glm::dvec3 max = glm::dvec3(parentBBox.min.x + (tileCoord.x + 1) * dx,
268 parentBBox.min.y + (tileCoord.y + 1) * dy,
269 isOctTree ? (parentBBox.min.z + (tileCoord.z + 1) * dz) : parentBBox.max.z);
270
271 Cogs::Geometry::DBoundingBox bbox = Cogs::Geometry::makeEmptyBoundingBox<Cogs::Geometry::DBoundingBox>();
272 bbox += min;
273 bbox += max;
274
275 return bbox;
276}
277
279Cogs::Core::OGC3DTilesUtils::getBoundingVolumeFromCoord(const OGC3DTiles::Coord& tileCoord, const OGC3DTiles::BoundingVolume& globalBoundingVolume)
280{
281 OGC3DTiles::BoundingVolume bv = globalBoundingVolume;
282
283 const double range = static_cast<float>(std::pow(2, tileCoord.level));
284 const bool isOctTree = tileCoord.z >= 0;
285
286 if (globalBoundingVolume.type == OGC3DTiles::BoundingVolumeType::BOX) { // Oriented box [x, y, x, xaxis.x, xaxis.y, xaxis.z, yaxis.x, yaxis.y, yaxis.z, zaxis.x, zaxis.y, zaxis.z] (axis are half-lengths)
287 glm::dvec3 center = glm::dvec3(globalBoundingVolume.values[0], globalBoundingVolume.values[1], globalBoundingVolume.values[2]);
288 glm::dvec3 xa = glm::abs(glm::dvec3(globalBoundingVolume.values[3], globalBoundingVolume.values[4], globalBoundingVolume.values[5]));
289 glm::dvec3 ya = glm::abs(glm::dvec3(globalBoundingVolume.values[6], globalBoundingVolume.values[7], globalBoundingVolume.values[8]));
290 glm::dvec3 za = glm::abs(glm::dvec3(globalBoundingVolume.values[9], globalBoundingVolume.values[10], globalBoundingVolume.values[11]));
291
292 // FIXME: Optimize this. I am sure we can reduce the amount of operations/multiplications.
293
294 glm::dvec3 xan = glm::normalize(xa);
295 glm::dvec3 yan = glm::normalize(ya);
296 glm::dvec3 zan = glm::normalize(za);
297
298 const double dx = glm::length(xa) / range;
299 const double dy = glm::length(ya) / range;
300 const double dz = glm::length(za) / range;
301
302 const glm::dvec3 corner = center - xa - ya - za;
303 const glm::dvec3 newcenter = corner + xan * (dx * (tileCoord.x * 2 + 1)) + yan * (dy * (tileCoord.y * 2 + 1)) + zan * (dz * (tileCoord.z * 2 + 1));
304
305 bv.values[0] = newcenter.x;
306 bv.values[1] = newcenter.y;
307
308 const glm::dvec3 newxa = xa / range;
309 const glm::dvec3 newya = ya / range;
310
311 bv.values[3] = newxa.x;
312 bv.values[4] = newxa.y;
313 bv.values[5] = newxa.z;
314
315 bv.values[6] = newya.x;
316 bv.values[7] = newya.y;
317 bv.values[8] = newya.z;
318
319 if (isOctTree) {
320 bv.values[2] = newcenter.z;
321 const glm::dvec3 newza = za / range;
322 bv.values[9] = newza.x;
323 bv.values[10] = newza.y;
324 bv.values[11] = newza.z;
325 }
326 else {
327 bv.values[2] = globalBoundingVolume.values[2];
328 bv.values[9] = globalBoundingVolume.values[9];
329 bv.values[10] = globalBoundingVolume.values[10];
330 bv.values[11] = globalBoundingVolume.values[11];
331 }
332 }
333 else if (globalBoundingVolume.type == OGC3DTiles::BoundingVolumeType::REGION) { // Region [west, south, east, north, minimum height, maximum height]
334 // East/West
335 const double width = globalBoundingVolume.values[2] > globalBoundingVolume.values[0] ? globalBoundingVolume.values[2] - globalBoundingVolume.values[0] : globalBoundingVolume.values[0] - globalBoundingVolume.values[2];
336 // North/South
337 const double height = globalBoundingVolume.values[3] > globalBoundingVolume.values[1] ? globalBoundingVolume.values[3] - globalBoundingVolume.values[1] : globalBoundingVolume.values[1] - globalBoundingVolume.values[3];
338 // Top/Bottom
339 const double depth = globalBoundingVolume.values[5] > globalBoundingVolume.values[4] ? globalBoundingVolume.values[5] - globalBoundingVolume.values[4] : globalBoundingVolume.values[4] - globalBoundingVolume.values[5];
340
341 const double dx = width / range;
342 const double dy = height / range;
343 const double dz = depth / range;
344
345 bv.values[0] = globalBoundingVolume.values[0] + tileCoord.x * dx; // west
346 bv.values[1] = globalBoundingVolume.values[1] + tileCoord.y * dy; // south
347 bv.values[2] = globalBoundingVolume.values[0] + (tileCoord.x + 1) * dx; // east
348 bv.values[3] = globalBoundingVolume.values[1] + (tileCoord.y + 1) * dy; // north
349
350 if (isOctTree) {
351 bv.values[4] = globalBoundingVolume.values[4] + tileCoord.z * dz; // Min height
352 bv.values[5] = globalBoundingVolume.values[4] + (tileCoord.z + 1) * dz; // Max height
353 }
354 else {
355 bv.values[4] = globalBoundingVolume.values[4];
356 bv.values[5] = globalBoundingVolume.values[5];
357 }
358
359#if 0
360 printf("%s: %s (range=%f, w=%f, h=%f, d=%f, dx=%f, dy=%f, dz=%f)\n (global: %s)\n", tileCoord.toString().c_str(), bv.toString().c_str(),
361 range, width, height, depth, dx, dy, dz, globalBoundingVolume.toString().c_str());
362#endif
363 }
364 else if (globalBoundingVolume.type == OGC3DTiles::BoundingVolumeType::SPHERE) { // Sphere [x, y, z, radius]
365 const double delta = globalBoundingVolume.values[3] / range;
366 const glm::dvec3 newcenter = glm::dvec3(delta * tileCoord.x, delta * tileCoord.y, delta * tileCoord.z);
367 bv.values[0] = newcenter.x;
368 bv.values[1] = newcenter.y;
369 bv.values[2] = newcenter.z;
370 bv.values[3] = delta;
371 // FIXME: We need a dataset with sphere-bbox'es to be able to implement and test this.
372 // Remove the warning if everything is fine.
373 LOG_WARNING(logger, "getBoundingVolumeFromCoord(): Division of sphere-based boundingvolumes not tested yet.");
374 }
375 else {
376 LOG_ERROR(logger, "Invalid BoundingVolume: No valid candidates.");
377 assert(false && "Invalid BoundingVolume: No valid candidates.");
378 }
379
380 return bv;
381}
Log implementation class.
Definition: LogManager.h:139
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180