From 928884a8f2dcf2a0d8df01bfdb8e290bfca57d29 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Wed, 19 Jun 2024 19:37:35 +0100 Subject: [PATCH] Terrain-relative height proof of concept --- src/apps/rdemo/Demo_Mesh.h | 82 +++++++++++++++++++ src/apps/rdemo/rdemo.cpp | 1 + src/rocky/EntityPosition.cpp | 83 +++++++++++++++++++ src/rocky/EntityPosition.h | 72 +++++++++++++++++ src/rocky/vsg/ECS.h | 60 +++++++++++++- src/rocky/vsg/EntityTransform.cpp | 127 ++++++++++++++++++++++++++++++ src/rocky/vsg/EntityTransform.h | 86 ++++++++++++++++++++ src/rocky/vsg/MapNode.cpp | 2 + 8 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 src/rocky/EntityPosition.cpp create mode 100644 src/rocky/EntityPosition.h create mode 100644 src/rocky/vsg/EntityTransform.cpp create mode 100644 src/rocky/vsg/EntityTransform.h diff --git a/src/apps/rdemo/Demo_Mesh.h b/src/apps/rdemo/Demo_Mesh.h index 1d1cf0eb..1f753d0f 100644 --- a/src/apps/rdemo/Demo_Mesh.h +++ b/src/apps/rdemo/Demo_Mesh.h @@ -155,6 +155,88 @@ auto Demo_Mesh_Relative = [](Application& app) } }; +auto Demo_Mesh_TerrainRelative = [](Application& app) + { + static entt::entity entity = entt::null; + + if (entity == entt::null) + { + // Create a new entity to host our mesh + entity = app.entities.create(); + + // Attach the new mesh: + Mesh& mesh = app.entities.emplace(entity); + mesh.name = "Terrain-Relative Mesh"; + + // Make some geometry that will be relative to a geolocation: + const float s = 250.0; + vsg::vec3 verts[8] = { + { -s, -s, -s }, + { s, -s, -s }, + { s, s, -s }, + { -s, s, -s }, + { -s, -s, s }, + { s, -s, s }, + { s, s, s }, + { -s, s, s } + }; + unsigned indices[48] = { + 0,3,2, 0,2,1, 4,5,6, 4,6,7, + 1,2,6, 1,6,5, 3,0,4, 3,4,7, + 0,1,5, 0,5,4, 2,3,7, 2,7,6 + }; + + vsg::vec4 color{ 1, 0, 1, 0.85 }; + + for (unsigned i = 0; i < 48; ) + { + mesh.add({ + {verts[indices[i++]], verts[indices[i++]], verts[indices[i++]]}, + {color, color, color} }); + + if ((i % 6) == 0) + color.r *= 0.8, color.b *= 0.8; + } + + // Add a transform component so we can position our mesh relative + // to some geospatial coordinates. We then set the bound on the node + // to control horizon culling a little better + auto& xform = app.entities.emplace(entity); + xform.setPosition(EntityPosition(GeoPoint(SRS::WGS84, -0.270710, 39.708408, 0), s)); + xform.node->bound.radius = s * sqrt(2); + } + + if (ImGuiLTable::Begin("Mesh")) + { + auto& mesh = app.entities.get(entity); + + ImGuiLTable::Checkbox("Visible", &mesh.active); + + auto* style = app.entities.try_get(entity); + if (style) + { + if (ImGuiLTable::ColorEdit4("Color", (float*)&style->color)) + mesh.dirty(); + + if (ImGuiLTable::SliderFloat("Wireframe", &style->wireframe, 0.0f, 32.0f, "%.0f")) + mesh.dirty(); + } + + auto& xform = app.entities.get(entity).node; + + if (ImGuiLTable::SliderDouble("Latitude", &xform->position.basePosition.y, 38.813321, 39.736707, "%.3lf")) + xform->dirty(); + + if (ImGuiLTable::SliderDouble("Longitude", &xform->position.basePosition.x, -1.271772, -0.083614, "%.3lf")) + xform->dirty(); + + if (ImGuiLTable::SliderDouble("Relative Altitude", &xform->position.altitude, 0.0, 2500.0, "%.1lf")) + xform->dirty(); + + ImGuiLTable::End(); + } + }; + auto Demo_Mesh_Multi = [](Application& app) { diff --git a/src/apps/rdemo/rdemo.cpp b/src/apps/rdemo/rdemo.cpp index ef7f478e..c25a35b8 100644 --- a/src/apps/rdemo/rdemo.cpp +++ b/src/apps/rdemo/rdemo.cpp @@ -77,6 +77,7 @@ std::vector demos = Demo{ "Line - relative", Demo_Line_Relative }, Demo{ "Mesh - absolute", Demo_Mesh_Absolute }, Demo{ "Mesh - relative", Demo_Mesh_Relative }, + Demo{ "Mesh - terrain-relative", Demo_Mesh_TerrainRelative }, Demo{ "Icon", Demo_Icon }, Demo{ "User Model", Demo_Model } } }, diff --git a/src/rocky/EntityPosition.cpp b/src/rocky/EntityPosition.cpp new file mode 100644 index 00000000..448f1f4a --- /dev/null +++ b/src/rocky/EntityPosition.cpp @@ -0,0 +1,83 @@ +#include "EntityPosition.h" +#include "Math.h" +#include "Utils.h" + +using namespace ROCKY_NAMESPACE; +using namespace ROCKY_NAMESPACE::util; + +#undef LC +#define LC "[EntityPosition] " + +EntityPosition EntityPosition::INVALID; + +EntityPosition::EntityPosition() +{ + //nop +} + +EntityPosition& +EntityPosition::operator=(EntityPosition&& rhs) +{ + basePosition = rhs.basePosition; + altitude = rhs.altitude; + rhs.basePosition = { }; // invalidate rhs + return *this; +} + +EntityPosition::EntityPosition(const GeoPoint& in_basePosition, double in_altitude) : + basePosition(in_basePosition), + altitude(in_altitude) +{ + //nop +} + +bool +EntityPosition::transform(const SRS& outSRS, EntityPosition& output) const +{ + if (valid() && outSRS.valid()) + { + GeoPoint outBasePosition; + if (basePosition.transform(outSRS, outBasePosition)) + { + output = EntityPosition(outBasePosition, altitude); + return true; + } + } + return false; +} + +bool +EntityPosition::transformInPlace(const SRS& to_srs) +{ + if (valid() && to_srs.valid()) + { + if (basePosition.transformInPlace(to_srs)) + { + return true; + } + } + return false; +} + +#include "json.h" + +namespace ROCKY_NAMESPACE +{ + void to_json(json& j, const EntityPosition& obj) { + if (obj.valid()) { + j = json::object(); + set(j, "basePosition", obj.basePosition); + set(j, "altitude", obj.altitude); + } + } + + void from_json(const json& j, EntityPosition& obj) { + GeoPoint basePosition; + double altitude = 0; + + get_to(j, "basePosition", basePosition); + get_to(j, "altitude", altitude); + + obj = EntityPosition(basePosition, altitude); + } +} diff --git a/src/rocky/EntityPosition.h b/src/rocky/EntityPosition.h new file mode 100644 index 00000000..489288d0 --- /dev/null +++ b/src/rocky/EntityPosition.h @@ -0,0 +1,72 @@ +/** + * rocky c++ + * Copyright 2024 Pelican Mapping + * MIT License + */ +#pragma once + +#include + +namespace ROCKY_NAMESPACE +{ + /** + * A georeferenced 3D point with terrain-relative altitude + */ + class ROCKY_EXPORT EntityPosition + { + public: + GeoPoint basePosition; + double altitude; + + //! Constructs an empty (and invalid) EntityPosition. + EntityPosition(); + + //! Constructs a EntityPosition + EntityPosition(const GeoPoint& srs, double altitude); + + //! Destruct + ~EntityPosition() { } + + //! Transforms this EntityPosition into another SRS and puts the + //! output in the "output" + //! @return true upon success, false upon failure + bool transform(const SRS& outSRS, EntityPosition& output) const; + + //! Transforms this point in place to another SRS + bool transformInPlace(const SRS& srs); + + bool operator == (const EntityPosition& rhs) const { + return basePosition == rhs.basePosition && altitude == rhs.altitude; + } + + bool operator != (const EntityPosition& rhs) const { + return !operator==(rhs); + } + + //! Does this object contain a valid geo point? + bool valid() const { + return basePosition.valid(); + } + + public: + static EntityPosition INVALID; + + // copy/move ops + EntityPosition(const EntityPosition& rhs) = default; + EntityPosition& operator=(const EntityPosition& rhs) = default; + EntityPosition(EntityPosition&& rhs) { *this = rhs; } + EntityPosition& operator=(EntityPosition&& rhs); + }; + + + /** + * Base class for any object that has a position on a Map. + */ + class TerrainRelativePositionedObject + { + public: + //! Center position of the object + virtual const EntityPosition& objectPosition() const = 0; + }; + +} diff --git a/src/rocky/vsg/ECS.h b/src/rocky/vsg/ECS.h index 760d12e2..875048b0 100644 --- a/src/rocky/vsg/ECS.h +++ b/src/rocky/vsg/ECS.h @@ -5,6 +5,7 @@ */ #pragma once #include +#include #include #include #include @@ -321,6 +322,51 @@ namespace ROCKY_NAMESPACE } }; + /** + * ECS Component that provides an entity with a geotransform. + */ + struct ROCKY_EXPORT TerrainRelativeTransform : public ECS::Component + { + vsg::ref_ptr node; + vsg::dmat4 local_matrix = vsg::dmat4(1.0); + TerrainRelativeTransform* parent = nullptr; + + //! Sets the transform's geoposition, creating the node on demand + void setPosition(const EntityPosition& p) + { + if (!node) + node = EntityTransform::create(); + + node->setPosition(p); + } + + //! Returns true if the push succeeded (and a pop will be required) + inline bool push(vsg::RecordTraversal& rt, const vsg::dmat4& m) + { + if (node) + { + return node->push(rt, m * local_matrix); + } + else if (parent) + { + return parent->push(rt, m * local_matrix); + } + else return false; + } + + inline void pop(vsg::RecordTraversal& rt) + { + if (node) + { + node->pop(rt); + } + else if (parent) + { + parent->pop(rt); + } + } + }; + /** * ECS Component representing a moving entity */ @@ -477,7 +523,19 @@ namespace ROCKY_NAMESPACE } else { - e.component.node->accept(rt); + auto* trxform = registry.try_get(e.entity); + if (trxform) + { + if (trxform->push(rt, identity_matrix)) + { + e.component.node->accept(rt); + trxform->pop(rt); + } + } + else + { + e.component.node->accept(rt); + } } } } diff --git a/src/rocky/vsg/EntityTransform.cpp b/src/rocky/vsg/EntityTransform.cpp new file mode 100644 index 00000000..aaf778b1 --- /dev/null +++ b/src/rocky/vsg/EntityTransform.cpp @@ -0,0 +1,127 @@ +/** + * rocky c++ + * Copyright 2024 Pelican Mapping + * MIT License + */ +#include "EntityTransform.h" +#include "engine/TerrainEngine.h" +#include "engine/Utils.h" +#include + +using namespace ROCKY_NAMESPACE; + +EntityTransform::EntityTransform() +{ + // force creation of the first viewlocal data structure + _viewlocal[0].dirty = true; +} + +void +EntityTransform::setPosition(const EntityPosition& position_) +{ + if (position != position_) + { + position = position_; + dirty(); + } +} + +void +EntityTransform::dirty() +{ + for (auto& view : _viewlocal) + view.dirty = true; +} + +void +EntityTransform::accept(vsg::RecordTraversal& record) const +{ + // traverse the transform + if (push(record, vsg::dmat4(1.0))) + { + vsg::Group::accept(record); + pop(record); + } +} + +bool +EntityTransform::push(vsg::RecordTraversal& record, const vsg::dmat4& local_matrix) const +{ + auto state = record.getState(); + + // update the view-local data if necessary: + auto& view = _viewlocal[record.getState()->_commandBuffer->viewID]; + if (view.dirty || local_matrix != view.local_matrix) + { + GeoPoint wgs84Point; + position.basePosition.transform(SRS::WGS84, wgs84Point); + wgs84Point.z = position.altitude; + TerrainEngine* terrainEngine; + if (record.getValue("terrainengine", terrainEngine)) + { + const auto& tilePager = terrainEngine->tiles; + unsigned bestLod = 0; + vsg::ref_ptr tileNode; + for (const auto& [key, entry] : tilePager._tiles) + { + if (key.extent().contains(position.basePosition) && bestLod < key.levelOfDetail() && entry._tile->dataLoader.available()) + { + bestLod = key.levelOfDetail(); + tileNode = entry._tile; + } + } + if (tileNode) + { + GeoPoint heightfieldPoint; + // pre-transform position in case Z matters e.g. ECEF + if (position.basePosition.transform(tileNode->dataLoader.value().elevation.heightfield.srs(), heightfieldPoint)) + { + auto height = tileNode->dataLoader.value().elevation.heightfield.heightAtLocation(heightfieldPoint.x, heightfieldPoint.y); + Log()->info("Read height {:f}", height); + // todo: apply heighfield range + wgs84Point.z += height; + } + } + } + SRS worldSRS; + if (record.getValue("worldsrs", worldSRS)) + { + if (wgs84Point.transform(worldSRS, view.worldPos)) + { + view.matrix = + to_vsg(worldSRS.localToWorldMatrix(glm::dvec3(view.worldPos.x, view.worldPos.y, view.worldPos.z))) * + local_matrix; + } + } + + view.local_matrix = local_matrix; + view.dirty = false; + } + + // horizon cull, if active: + if (horizonCulling) + { + std::shared_ptr horizon; + if (state->getValue("horizon", horizon)) + { + if (!horizon->isVisible(view.matrix[3][0], view.matrix[3][1], view.matrix[3][2], bound.radius)) + return false; + } + } + + // replicates RecordTraversal::accept(MatrixTransform&): + state->modelviewMatrixStack.push(state->modelviewMatrixStack.top() * view.matrix); + state->dirty = true; + state->pushFrustum(); + + return true; +} + +void +EntityTransform::pop(vsg::RecordTraversal& record) const +{ + auto state = record.getState(); + state->popFrustum(); + state->modelviewMatrixStack.pop(); + state->dirty = true; +} diff --git a/src/rocky/vsg/EntityTransform.h b/src/rocky/vsg/EntityTransform.h new file mode 100644 index 00000000..e38bfb66 --- /dev/null +++ b/src/rocky/vsg/EntityTransform.h @@ -0,0 +1,86 @@ +/** + * rocky c++ + * Copyright 2024 Pelican Mapping + * MIT License + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace ROCKY_NAMESPACE +{ + template + struct TerrainRelativePositionedObjectAdapter : public TerrainRelativePositionedObject + { + vsg::ref_ptr object; + virtual const EntityPosition& objectPosition() const { + return object->position; + } + static std::shared_ptr> create(vsg::ref_ptr object_) { + auto r = std::make_shared< TerrainRelativePositionedObjectAdapter>(); + r->object = object_; + return r; + } + }; + /** + * Transform node that accepts geospatial coordinates and creates + * a local ENU (X=east, Y=north, Z=up) coordinate frame for its children + * that is tangent to the earth at the transform's geo position on the + * terrain surface. + */ + class ROCKY_EXPORT EntityTransform : + public vsg::Inherit, + TerrainRelativePositionedObject + { + public: + EntityPosition position; + + //! Sphere for horizon culling + vsg::dsphere bound = { }; + + //! whether horizon culling is active + bool horizonCulling = true; + + public: + //! Construct an invalid EntityTransform + EntityTransform(); + + //! Call this is you change position directly. + void dirty(); + + //! Same as changing position and calling dirty(). + void setPosition(const EntityPosition& p); + + public: // TerrainRelativePositionedObject interface + + const EntityPosition& objectPosition() const override { + return position; + } + + public: + + EntityTransform(const EntityTransform& rhs) = delete; + + void accept(vsg::RecordTraversal&) const override; + + bool push(vsg::RecordTraversal&, const vsg::dmat4& m) const; + + void pop(vsg::RecordTraversal&) const; + + protected: + + + struct Data { + bool dirty = true; + GeoPoint worldPos; + vsg::dmat4 matrix; + vsg::dmat4 local_matrix; + }; + util::ViewLocal _viewlocal; + + }; +} // namespace diff --git a/src/rocky/vsg/MapNode.cpp b/src/rocky/vsg/MapNode.cpp index 3e1f07d5..4652ad2a 100644 --- a/src/rocky/vsg/MapNode.cpp +++ b/src/rocky/vsg/MapNode.cpp @@ -180,5 +180,7 @@ MapNode::accept(vsg::RecordTraversal& rv) const rv.setValue("worldsrs", worldSRS()); + rv.setValue("terrainengine", terrain->engine.get()); + Inherit::accept(rv); }