From 26d1238c8add0da318518d8fe586e67a2c16c113 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 17 Feb 2025 20:20:19 -0800 Subject: [PATCH] Added measuring tool --- app/android/jni/CMakeLists.txt | 30 +- app/android/jni/CameraMobile.cpp | 5 +- app/android/jni/CameraMobile.h | 3 +- app/android/jni/Measure.h | 65 ++ app/android/jni/RTABMapApp.cpp | 650 ++++++++++++++++-- app/android/jni/RTABMapApp.h | 30 +- app/android/jni/quad_color.cpp | 69 ++ app/android/jni/quad_color.h | 35 + app/android/jni/resources/text_atlas.png | Bin 0 -> 25668 bytes app/android/jni/scene.cpp | 222 +++++- app/android/jni/scene.h | 41 ++ app/android/jni/text_drawable.cpp | 366 ++++++++++ app/android/jni/text_drawable.h | 71 ++ app/ios/RTABMapApp.xcodeproj/project.pbxproj | 16 + app/ios/RTABMapApp/Base.lproj/Main.storyboard | 121 +++- app/ios/RTABMapApp/NativeWrapper.cpp | 43 ++ app/ios/RTABMapApp/NativeWrapper.hpp | 7 + app/ios/RTABMapApp/RTABMap.swift | 20 +- app/ios/RTABMapApp/ViewController.swift | 158 ++++- app/ios/RTABMapApp/text_atlas_png.h | 40 ++ 20 files changed, 1878 insertions(+), 114 deletions(-) create mode 100644 app/android/jni/Measure.h create mode 100644 app/android/jni/quad_color.cpp create mode 100644 app/android/jni/quad_color.h create mode 100644 app/android/jni/resources/text_atlas.png create mode 100644 app/android/jni/text_drawable.cpp create mode 100644 app/android/jni/text_drawable.h create mode 100644 app/ios/RTABMapApp/text_atlas_png.h diff --git a/app/android/jni/CMakeLists.txt b/app/android/jni/CMakeLists.txt index f00b604e..bcb5c710 100644 --- a/app/android/jni/CMakeLists.txt +++ b/app/android/jni/CMakeLists.txt @@ -114,7 +114,35 @@ add_definitions(${PCL_DEFINITIONS}) INCLUDE_DIRECTORIES(${INCLUDE_DIRS}) -add_library(NativeRTABMap SHARED ${sources}) +#################################### +# Generate resources files +#################################### +SET(RESOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/resources/text_atlas.png +) + +foreach(arg ${RESOURCES}) + get_filename_component(filename ${arg} NAME) + string(REPLACE "." "_" output ${filename}) + set(RESOURCES_HEADERS "${RESOURCES_HEADERS}" "${CMAKE_CURRENT_BINARY_DIR}/${output}.h") +endforeach(arg ${RESOURCES}) + +find_host_program(RTABMAP_RES_TOOL rtabmap-res_tool PATHS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +IF(NOT RTABMAP_RES_TOOL) + MESSAGE( FATAL_ERROR "RTABMAP_RES_TOOL is not defined (it is the path to \"rtabmap-res_tool\" application created by a non-Android build)." ) +ENDIF(NOT RTABMAP_RES_TOOL) + +ADD_CUSTOM_COMMAND( + OUTPUT ${RESOURCES_HEADERS} + COMMAND ${RTABMAP_RES_TOOL} -n rtabmap -p ${CMAKE_CURRENT_BINARY_DIR} ${RESOURCES} + COMMENT "[Creating resources]" + DEPENDS ${RESOURCES} +) +#################################### +# Generate resources files END +#################################### + +add_library(NativeRTABMap SHARED ${sources} ${RESOURCES_HEADERS}) target_link_libraries(NativeRTABMap ${LIBRARIES} android log diff --git a/app/android/jni/CameraMobile.cpp b/app/android/jni/CameraMobile.cpp index 316dd480..edb3e7f2 100644 --- a/app/android/jni/CameraMobile.cpp +++ b/app/android/jni/CameraMobile.cpp @@ -108,8 +108,9 @@ void CameraMobile::close() dataReady_.release(); } -void CameraMobile::resetOrigin() +void CameraMobile::resetOrigin(const rtabmap::Transform & offset) { + manualOriginOffset_ = offset; originUpdate_ = true; } @@ -193,7 +194,7 @@ void CameraMobile::poseReceived(const Transform & pose, double deviceStamp) previousAnchorPose_.setNull(); previousAnchorLinearVelocity_.clear(); previousAnchorStamp_ = 0.0; - originOffset_ = pose.translation().inverse(); + originOffset_ = manualOriginOffset_.isNull() ? pose.translation().inverse() : manualOriginOffset_; originUpdate_ = false; } diff --git a/app/android/jni/CameraMobile.h b/app/android/jni/CameraMobile.h index 6d1e2b6e..28bd85ab 100644 --- a/app/android/jni/CameraMobile.h +++ b/app/android/jni/CameraMobile.h @@ -99,7 +99,7 @@ public: void update(const SensorData & data, const Transform & pose, const glm::mat4 & viewMatrix, const glm::mat4 & projectionMatrix, const float * texCoord); void updateOnRender(); - void resetOrigin(); + void resetOrigin(const rtabmap::Transform & offset = rtabmap::Transform()); virtual bool isCalibrated() const; virtual bool odomProvided() const { return true; } @@ -150,6 +150,7 @@ private: EnvSensors lastEnvSensors_; Transform originOffset_; bool originUpdate_; + rtabmap::Transform manualOriginOffset_; float upstreamRelocalizationAccThr_; rtabmap::Transform previousAnchorPose_; std::vector previousAnchorLinearVelocity_; diff --git a/app/android/jni/Measure.h b/app/android/jni/Measure.h new file mode 100644 index 00000000..cd348710 --- /dev/null +++ b/app/android/jni/Measure.h @@ -0,0 +1,65 @@ +/* +Copyright (c) 2010-2025, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +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 Universite de Sherbrooke 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 HOLDER 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. +*/ + +#ifndef MEASURE_H_ +#define MEASURE_H_ + +#include + +class Measure +{ +public: + Measure( + const cv::Point3f & pt1, + const cv::Point3f & pt2, + const cv::Vec3f & n1, + const cv::Vec3f & n2) : + pt1_(pt1), + pt2_(pt2), + n1_(n1), + n2_(n2) + { + length_ = cv::norm(pt2_ - pt1_); + } + virtual ~Measure() {} + + float length() const {return length_;} + const cv::Point3f & pt1() const {return pt1_;} + const cv::Point3f & pt2() const {return pt2_;} + const cv::Vec3f & n1() const {return n1_;} + const cv::Vec3f & n2() const {return n2_;} + +private: + cv::Point3f pt1_; + cv::Point3f pt2_; + cv::Vec3f n1_; + cv::Vec3f n2_; + float length_; +}; + + +#endif /* MEASURE_H_ */ diff --git a/app/android/jni/RTABMapApp.cpp b/app/android/jni/RTABMapApp.cpp index 0e846fdb..29280963 100644 --- a/app/android/jni/RTABMapApp.cpp +++ b/app/android/jni/RTABMapApp.cpp @@ -132,6 +132,7 @@ rtabmap::ParametersMap RTABMapApp::getRtabmapParameters() parameters.insert(mappingParameters_.begin(), mappingParameters_.end()); + parameters.insert(rtabmap::ParametersPair(rtabmap::Parameters::kKpDetectorStrategy(), "5")); // GFTT/FREAK parameters.insert(rtabmap::ParametersPair(rtabmap::Parameters::kKpMaxFeatures(), std::string("200"))); parameters.insert(rtabmap::ParametersPair(rtabmap::Parameters::kGFTTQualityLevel(), std::string("0.0001"))); parameters.insert(rtabmap::ParametersPair(rtabmap::Parameters::kMemImagePreDecimation(), std::string(cameraColor_&&fullResolution_?"2":"1"))); @@ -264,13 +265,43 @@ RTABMapApp::RTABMapApp() : lastPoseEventTime_(0.0), visualizingMesh_(false), exportedMeshUpdated_(false), - optMesh_(new pcl::TextureMesh), + measuresUpdated_(false), + targetPoint_(new pcl::PointCloud), + quadSample_(new pcl::PointCloud), + quadSamplePolygons_(2), + metricSystem_(true), + snapAxisThr_(0.95), + markerDetection_(false), + measuringMode_(0), + addMeasureClicked_(false), + teleportClicked_(false), + removeMeasureClicked_(false), + optTextureMesh_(new pcl::TextureMesh), optRefId_(0), optRefPose_(0), mapToOdom_(rtabmap::Transform::getIdentity()) { - mappingParameters_.insert(rtabmap::ParametersPair(rtabmap::Parameters::kKpDetectorStrategy(), "5")); // GFTT/FREAK + pcl::PointXYZRGB ptWhite; + ptWhite.r = ptWhite.g = ptWhite.b = 255; + targetPoint_->push_back(ptWhite); + snapAxes_.push_back(cv::Vec3f(1,0,0)); + snapAxes_.push_back(cv::Vec3f(0,1,0)); + snapAxes_.push_back(cv::Vec3f(0,0,1)); + + float quadSize = 0.05f; + quadSample_->push_back(pcl::PointXYZ(-quadSize, -quadSize, 0.0f)); + quadSample_->push_back(pcl::PointXYZ(quadSize, -quadSize, 0.0f)); + quadSample_->push_back(pcl::PointXYZ(quadSize, quadSize, 0.0f)); + quadSample_->push_back(pcl::PointXYZ(-quadSize, quadSize, 0.0f)); + quadSamplePolygons_[0].vertices.resize(3); + quadSamplePolygons_[0].vertices[0] = 0; + quadSamplePolygons_[0].vertices[1] = 1; + quadSamplePolygons_[0].vertices[2] = 2; + quadSamplePolygons_[1].vertices.resize(3); + quadSamplePolygons_[1].vertices[0] = 0; + quadSamplePolygons_[1].vertices[1] = 2; + quadSamplePolygons_[1].vertices[2] = 3; #ifdef __ANDROID__ env->GetJavaVM(&jvm); @@ -278,19 +309,6 @@ RTABMapApp::RTABMapApp() : #endif LOGI("RTABMapApp::RTABMapApp()"); - createdMeshes_.clear(); - rawPoses_.clear(); - clearSceneOnNextRender_ = true; - openingDatabase_ = false; - exporting_ = false; - postProcessing_=false; - totalPoints_ = 0; - totalPolygons_ = 0; - lastDrawnCloudsCount_ = 0; - renderingTime_ = 0.0f; - lastPostRenderEventTime_ = 0.0; - lastPoseEventTime_ = 0.0; - bufferedStatsData_.clear(); #ifdef __ANDROID__ progressionStatus_.setJavaObjects(jvm, RTABMapActivity); #endif @@ -404,13 +422,16 @@ int RTABMapApp::openDatabase(const std::string & databasePath, bool databaseInMe lastPoseEventTime_ = 0.0; bufferedStatsData_.clear(); graphOptimization_ = true; + measuresUpdated_ = !measures_.empty(); + measures_.clear(); this->registerToEventsManager(); int status = 0; // Open visualization while we load (if there is an optimized mesh saved in database) - optMesh_.reset(new pcl::TextureMesh); + optTextureMesh_.reset(new pcl::TextureMesh); + optMesh_ = rtabmap::Mesh(); optTexture_ = cv::Mat(); optRefId_ = 0; if(optRefPose_) @@ -418,6 +439,7 @@ int RTABMapApp::openDatabase(const std::string & databasePath, bool databaseInMe delete optRefPose_; optRefPose_ = 0; } + visualizingMesh_ = false; cv::Mat cloudMat; std::vector > > polygons; #if PCL_VERSION_COMPARE(>=, 1, 8, 0) @@ -436,19 +458,20 @@ int RTABMapApp::openDatabase(const std::string & databasePath, bool databaseInMe if(!cloudMat.empty()) { LOGI("Open: Found optimized mesh! Visualizing it."); - optMesh_ = rtabmap::util3d::assembleTextureMesh(cloudMat, polygons, texCoords, textures, true); + optTextureMesh_ = rtabmap::util3d::assembleTextureMesh(cloudMat, polygons, texCoords, textures, true); + optMesh_ = rtabmap::Mesh(); optTexture_ = textures; - if(!optTexture_.empty()) + if(!optTexture_.empty()) { LOGI("Open: Texture mesh: %dx%d.", optTexture_.cols, optTexture_.rows); status=3; } - else if(optMesh_->tex_polygons.size()) + else if(optTextureMesh_->tex_polygons.size()) { LOGI("Open: Polygon mesh"); status=2; } - else if(!optMesh_->cloud.data.empty()) + else if(!optTextureMesh_->cloud.data.empty()) { LOGI("Open: Point cloud"); status=1; @@ -976,6 +999,11 @@ bool RTABMapApp::startCamera() LOGI("Cloud density level %d", cloudDensityLevel_); LOGI("Start camera thread"); + if(visualizingMesh_) + { + // We are measuring! + main_scene_.setMapRendering(false); + } cameraJustInitialized_ = true; if(useExternalLidar_) { @@ -1330,7 +1358,8 @@ int RTABMapApp::Render() #ifdef DEBUG_RENDERING_PERFORMANCE LOGD("Camera updateOnRender %fs", time.ticks()); #endif - if(main_scene_.background_renderer_ == 0 && camera_->getTextureId() != 0) + // We detect if we are in measuring mode if rtabmap is not running + if(main_scene_.background_renderer_ == 0 && camera_->getTextureId() != 0 && !(rtabmapThread_ == 0 || !rtabmapThread_->isRunning())) { main_scene_.background_renderer_ = new BackgroundRenderer(); main_scene_.background_renderer_->InitializeGlContent(((rtabmap::CameraMobile*)camera_)->getTextureId(), cameraDriver_ <= 2); @@ -1424,40 +1453,158 @@ int RTABMapApp::Render() { main_scene_.clear(); exportedMeshUpdated_ = false; + measuresUpdated_ = measures_.size()>0; } if(!main_scene_.hasCloud(g_optMeshId)) { LOGI("Adding optimized mesh to opengl (%d points, %d polygons, %d tex_coords, materials=%d texture=%dx%d)...", - optMesh_->cloud.point_step==0?0:(int)optMesh_->cloud.data.size()/optMesh_->cloud.point_step, - optMesh_->tex_polygons.size()!=1?0:(int)optMesh_->tex_polygons[0].size(), - optMesh_->tex_coordinates.size()!=1?0:(int)optMesh_->tex_coordinates[0].size(), - (int)optMesh_->tex_materials.size(), + optTextureMesh_->cloud.point_step==0?0:(int)optTextureMesh_->cloud.data.size()/optTextureMesh_->cloud.point_step, + optTextureMesh_->tex_polygons.size()!=1?0:(int)optTextureMesh_->tex_polygons[0].size(), + optTextureMesh_->tex_coordinates.size()!=1?0:(int)optTextureMesh_->tex_coordinates[0].size(), + (int)optTextureMesh_->tex_materials.size(), optTexture_.cols, optTexture_.rows); - if(optMesh_->tex_polygons.size() && optMesh_->tex_polygons[0].size()) + if(optTextureMesh_->tex_polygons.size() && optTextureMesh_->tex_polygons[0].size()) { - rtabmap::Mesh mesh; - mesh.gains[0] = mesh.gains[1] = mesh.gains[2] = 1.0; - mesh.cloud.reset(new pcl::PointCloud); - mesh.normals.reset(new pcl::PointCloud); - pcl::fromPCLPointCloud2(optMesh_->cloud, *mesh.cloud); - pcl::fromPCLPointCloud2(optMesh_->cloud, *mesh.normals); - mesh.polygons = optMesh_->tex_polygons[0]; - mesh.pose.setIdentity(); - if(optMesh_->tex_coordinates.size()) + optMesh_ = rtabmap::Mesh(); + optMesh_.gains[0] = optMesh_.gains[1] = optMesh_.gains[2] = 1.0; + optMesh_.cloud.reset(new pcl::PointCloud); + optMesh_.normals.reset(new pcl::PointCloud); + pcl::fromPCLPointCloud2(optTextureMesh_->cloud, *optMesh_.cloud); + pcl::fromPCLPointCloud2(optTextureMesh_->cloud, *optMesh_.normals); + optMesh_.polygons = optTextureMesh_->tex_polygons[0]; + if(optTextureMesh_->tex_coordinates.size()) { - mesh.texCoords = optMesh_->tex_coordinates[0]; - mesh.texture = optTexture_; + optMesh_.texCoords = optTextureMesh_->tex_coordinates[0]; + optMesh_.texture = optTexture_; } - main_scene_.addMesh(g_optMeshId, mesh, rtabmap::opengl_world_T_rtabmap_world, true); + main_scene_.addMesh(g_optMeshId, optMesh_, rtabmap::opengl_world_T_rtabmap_world, true); } else { pcl::IndicesPtr indices(new std::vector); // null pcl::PointCloud::Ptr cloud(new pcl::PointCloud); - pcl::fromPCLPointCloud2(optMesh_->cloud, *cloud); + pcl::fromPCLPointCloud2(optTextureMesh_->cloud, *cloud); main_scene_.addCloud(g_optMeshId, cloud, indices, rtabmap::opengl_world_T_rtabmap_world); } + + if(!measures_.empty()) + { + measuresUpdated_ = true; + } } + + if(camera_ != 0 && (rtabmapThread_ == 0 || !rtabmapThread_->isRunning())) + { + updateMeasuringState(); + } + else + { + main_scene_.removeLine(55555); + main_scene_.removeQuad(55555); + main_scene_.removeQuad(55556); + main_scene_.removeText(55555); + main_scene_.removeCloudOrMesh(-99999); + measuringTmpPts_.clear(); + measuringTmpNormals_.clear(); + } + + if(measuresUpdated_) + { + std::list measures = measures_; + measuresUpdated_ = false; + main_scene_.clearLines(); + main_scene_.clearTexts(); + main_scene_.clearQuads(); + int lineId = 0; + int textId = 0; + int quadId = 0; + float sphereRadius = 0.02f; + float textSize=0.05f; + float quadSize=0.05f; + float quadAlpha = 0.3f; + + tango_gl::Color color(1.0f, 0.0f, 1.0f); + tango_gl::Color xColor(1.0f, 0.0f, 0.0f); + tango_gl::Color yColor(0.0f, 1.0f, 0.0f); + tango_gl::Color zColor(0.0f, 0.0f, 1.0f); + + float restrictiveSnapThr = 0.9999; + for(std::list::iterator iter=measures.begin(); iter!=measures.end(); ++iter) + { + // Determinate color based on current snap axes + tango_gl::Color quadColor = color; + bool sameNormal = false; + if(iter->n1().dot(iter->n2()) > 0.99) // Same normal, plane to plane + { + sameNormal = true; + Eigen::Vector3f n(iter->n1()[0], iter->n1()[1], iter->n1()[2]); + float n1ProdX = n.dot(Eigen::Vector3f(snapAxes_[0][0], snapAxes_[0][1], snapAxes_[0][2])); + float n1ProdY = n.dot(Eigen::Vector3f(snapAxes_[1][0], snapAxes_[1][1], snapAxes_[1][2])); + float n1ProdZ = n.dot(Eigen::Vector3f(snapAxes_[2][0], snapAxes_[2][1], snapAxes_[2][2])); + if(fabs(n1ProdX) > restrictiveSnapThr) + { + quadColor = xColor; + } + else if(fabs(n1ProdY) > restrictiveSnapThr) + { + quadColor = yColor; + } + else if(fabs(n1ProdZ) > restrictiveSnapThr) + { + quadColor = zColor; + } + } + + const Measure & m = *iter; + LOGI("dist=%f, %f,%f,%f -> %f,%f,%f", m.length(), + m.pt1().x, m.pt1().y, m.pt1().z, + m.pt2().x, m.pt2().y, m.pt2().z); + cv::Point3f pt1 = rtabmap::util3d::transformPoint(m.pt1(), rtabmap::opengl_world_T_rtabmap_world); + cv::Point3f pt2 = rtabmap::util3d::transformPoint(m.pt2(), rtabmap::opengl_world_T_rtabmap_world); + main_scene_.addLine(++lineId, pt1, pt2, quadColor); + + if (fabs(iter->n1()[2]) < 0.00001 && sameNormal) + { + // Add a line so that in orthogonal view, we can see better where the lines are starting/finishing + cv::Point3f n = cv::Vec3f(0,0,1).cross(iter->n1()); + + n = rtabmap::util3d::transformPoint(n, rtabmap::opengl_world_T_rtabmap_world) * (quadSize/2); + cv::Point3f pa = pt1 + n; + cv::Point3f pb = pt1 - n; + main_scene_.addLine(++lineId, pa, pb, quadColor); + cv::Point3f pc = pt2 + n; + cv::Point3f pd = pt2 - n; + main_scene_.addLine(++lineId, pc, pd, quadColor); + } + + float diff = m.length(); + std::string text = uFormat("%0.2f m", diff); + if(!metricSystem_) + { + static const double METERS_PER_FOOT = 0.3048; + static double INCHES_PER_FOOT = 12.0; + double lengthInFeet = diff / METERS_PER_FOOT; + int feet = (int)lengthInFeet; + float inches = (lengthInFeet - feet) * INCHES_PER_FOOT; + if(feet > 0) + { + text = uFormat("%d' %0.1f\"", feet, inches); + } + else + { + text = uFormat("%0.1f\"", inches); + } + } + main_scene_.addText(++textId, text, rtabmap::Transform((pt1.x+pt2.x)/2.0f, (pt1.y+pt2.y)/2.0f, (pt1.z+pt2.z)/2.0f, 0, 0,0), textSize, quadColor); + cv::Vec3f n1 = rtabmap::util3d::transformPoint(m.n1(), rtabmap::opengl_world_T_rtabmap_world); + cv::Vec3f n2 = rtabmap::util3d::transformPoint(m.n2(), rtabmap::opengl_world_T_rtabmap_world); + Eigen::Quaternionf q1, q2; + q1.setFromTwoVectors(Eigen::Vector3f(0,0,1), Eigen::Vector3f(n1[0],n1[1],n1[2])); + q2.setFromTwoVectors(Eigen::Vector3f(0,0,1), Eigen::Vector3f(n2[0],n2[1],n2[2])); + main_scene_.addQuad(++quadId, quadSize, rtabmap::Transform(pt1.x, pt1.y, pt1.z, q1.x(), q1.y(), q1.z(), q1.w()), quadColor, quadAlpha); + main_scene_.addQuad(++quadId, quadSize, rtabmap::Transform(pt2.x, pt2.y, pt2.z, q2.x(), q2.y(), q2.z(), q2.w()), quadColor, quadAlpha); + } + } if(!openingDatabase_) { @@ -1576,7 +1723,8 @@ int RTABMapApp::Render() if(main_scene_.hasCloud(g_optMeshId)) { main_scene_.clear(); - optMesh_.reset(new pcl::TextureMesh); + optTextureMesh_.reset(new pcl::TextureMesh); + optMesh_ = rtabmap::Mesh(); optTexture_ = cv::Mat(); } @@ -1639,6 +1787,8 @@ int RTABMapApp::Render() lastPostRenderEventTime_ = 0.0; lastPoseEventTime_ = 0.0; bufferedStatsData_.clear(); + measuresUpdated_ = !measures_.empty(); + measures_.clear(); } // Did we lose OpenGL context? If so, recreate the context; @@ -2270,6 +2420,358 @@ int RTABMapApp::Render() } } +void RTABMapApp::updateMeasuringState() +{ + rtabmap::Transform openglCam = main_scene_.GetOpenGLCameraPose(); + rtabmap::Transform rtabmapCam = rtabmap::rtabmap_world_T_opengl_world * openglCam * rtabmap::opengl_world_T_rtabmap_world; + Eigen::Vector3f origin(openglCam.x(), openglCam.y(), openglCam.z()); + Eigen::Vector3f rtabmapOrigin(rtabmapCam.x(), rtabmapCam.y(), rtabmapCam.z()); + rtabmap::Transform v = openglCam.rotation() * rtabmap::Transform(0,0,-1,0,0,0); + Eigen::Vector3f dir(v.x(), v.y(), v.z()); + v = rtabmapCam.rotation() * rtabmap::Transform(1,0,0,0,0,0); + Eigen::Vector3f rtabmapDir(v.x(), v.y(), v.z()); + float textSize=0.05f; + tango_gl::Color color(1.0f, 0.0f, 1.0f); + tango_gl::Color xColor(1.0f, 0.0f, 0.0f); // in rtabmap world + tango_gl::Color yColor(0.0f, 1.0f, 0.0f); // in rtabmap world + tango_gl::Color zColor(0.0f, 0.0f, 1.0f); // in rtabmap world + float quadSize=0.05f; + float quadAlpha = 0.3f; + + if(removeMeasureClicked_) + { + if(!measuringTmpPts_.empty()) + { + main_scene_.removeLine(55555); + main_scene_.removeText(55555); + main_scene_.removeQuad(55555); + main_scene_.removeQuad(55556); + measuringTmpPts_.clear(); + measuringTmpNormals_.clear(); + } + else + { + bool removed = false; + for(std::list::iterator iter=measures_.begin(); iter!=measures_.end() && !removed; ++iter) + { + const Measure & m = *iter; + + for(int i=0; i<2;++i) + { + // intersecting the quad? + cv::Point3f pt = i==0?m.pt1():m.pt2(); + cv::Point3f n = i==0?m.n1():m.n2(); + pt = rtabmap::util3d::transformPoint(pt, rtabmap::opengl_world_T_rtabmap_world); + n = rtabmap::util3d::transformPoint(n, rtabmap::opengl_world_T_rtabmap_world); + Eigen::Quaternionf q; + q.setFromTwoVectors(Eigen::Vector3f(0,0,-1), Eigen::Vector3f(n.x,n.y,n.z)); + float dTmp; + Eigen::Vector3f nTmp; + int indexTmp; + if(rtabmap::util3d::intersectRayMesh( + origin, + dir, + *rtabmap::util3d::transformPointCloud(quadSample_, rtabmap::Transform(pt.x, pt.y, pt.z, q.x(), q.y(), q.z(), q.w())), + quadSamplePolygons_, + false, + dTmp, + nTmp, + indexTmp)) + { + measures_.erase(iter); + removed = true; + break; + } + } + } + + measuresUpdated_ |= removed; + } + } + + float distance =0.0f; + Eigen::Vector3f n; + int index; + if(rtabmap::util3d::intersectRayMesh( + rtabmapOrigin, + rtabmapDir, + *optMesh_.cloud, + optMesh_.polygons, + true, + distance, + n, + index)) + { + + Eigen::Vector3f intersectionPt = origin + dir*distance; + cv::Point3f pt(intersectionPt[0], intersectionPt[1], intersectionPt[2]); + cv::Point3f normal(n[0], n[1], n[2]); // rtabmap world + cv::Point3f normalGl = rtabmap::util3d::transformPoint(normal, rtabmap::opengl_world_T_rtabmap_world); // opengl world + tango_gl::Color quadColor = color; + float normalProdX = n.dot(Eigen::Vector3f(snapAxes_[0][0], snapAxes_[0][1], snapAxes_[0][2])); + float normalProdY = n.dot(Eigen::Vector3f(snapAxes_[1][0], snapAxes_[1][1], snapAxes_[1][2])); + float normalProdZ = n.dot(Eigen::Vector3f(snapAxes_[2][0], snapAxes_[2][1], snapAxes_[2][2])); + if(fabs(normal.x) > fabs(normal.z) && fabs(normal.y) > fabs(normal.z)) + { + if(fabs(normalProdX) > snapAxisThr_) + { + normal.x = snapAxes_[0][0] * (normalProdX>0?1:-1); + normal.y = snapAxes_[0][1] * (normalProdX>0?1:-1); + normal.z = snapAxes_[0][2] * (normalProdX>0?1:-1); + quadColor = xColor; + } + else if(fabs(normalProdY) > snapAxisThr_) + { + normal.x = snapAxes_[1][0] * (normalProdY>0?1:-1); + normal.y = snapAxes_[1][1] * (normalProdY>0?1:-1); + normal.z = snapAxes_[1][2] * (normalProdY>0?1:-1); + quadColor = yColor; + } + else if(measuringMode_ == 0) + { + // We force to be aligned with xy plane + normal.z = 0; + float n = cv::norm(normal); + normal.x/=n; + normal.y/=n; + normal.z/=n; + } + } + else if(fabs(normalProdZ) > snapAxisThr_) + { + normal.x = snapAxes_[2][0] * (normalProdZ>0?1:-1); + normal.y = snapAxes_[2][1] * (normalProdZ>0?1:-1); + normal.z = snapAxes_[2][2] * (normalProdZ>0?1:-1); + quadColor = zColor; + } + normalGl = rtabmap::util3d::transformPoint(normal, rtabmap::opengl_world_T_rtabmap_world); + + if(teleportClicked_) + { + camera_->resetOrigin(rtabmap::Transform(-pt.z, -pt.x, pt.y-Scene::kHeightOffset.y,0,0,0)); + } + else if(addMeasureClicked_) // Add measure + { + if((measuringMode_ == 1 || measuringTmpPts_.size()==1)) + { + if(measuringMode_ == 1) // Height single click + { + measuringTmpPts_.clear(); + measuringTmpNormals_.clear(); + + normal.x=0; + normal.y=0; + normal.z=1; + normalGl = rtabmap::util3d::transformPoint(normal, rtabmap::opengl_world_T_rtabmap_world); + + measuringTmpPts_.push_back(pt); + measuringTmpNormals_.push_back(normalGl); + pt.y = 0; + measuringTmpPts_.push_back(pt); + measuringTmpNormals_.push_back(normalGl); + } + else if(measuringMode_ == 0 || measuringMode_ == 2) // Plane to Plane or point to point + { + if(measuringMode_ == 0) + { + normalGl = measuringTmpNormals_.front(); + + // project point on line + float n = (pt-measuringTmpPts_.front()).dot(measuringTmpNormals_.front()); + pt = measuringTmpPts_.front() + (measuringTmpNormals_.front() * n); + } + measuringTmpPts_.push_back(pt); + measuringTmpNormals_.push_back(normalGl); + } + + // Add measure! + const cv::Point3f & pt1 = measuringTmpPts_.at(0); + const cv::Point3f & pt2 = measuringTmpPts_.at(1); + const cv::Vec3f & n1 = measuringTmpNormals_.at(0); + const cv::Vec3f & n2 = measuringTmpNormals_.at(1); + + Measure measure( + rtabmap::util3d::transformPoint(pt1, rtabmap::rtabmap_world_T_opengl_world), + rtabmap::util3d::transformPoint(pt2, rtabmap::rtabmap_world_T_opengl_world), + rtabmap::util3d::transformPoint(n1, rtabmap::rtabmap_world_T_opengl_world), + rtabmap::util3d::transformPoint(n2, rtabmap::rtabmap_world_T_opengl_world)); + + if(measure.length()>=0.01f) + { + measures_.push_back(measure); + + main_scene_.removeLine(55555); + main_scene_.removeText(55555); + main_scene_.removeQuad(55555); + main_scene_.removeQuad(55556); + measuringTmpPts_.clear(); + measuringTmpNormals_.clear(); + + measuresUpdated_ = true; + } + } + else + { + measuringTmpPts_.clear(); + measuringTmpNormals_.clear(); + + // init the first point + measuringTmpPts_.push_back(pt); + measuringTmpNormals_.push_back(normalGl); + } + } + else if(measuringMode_ >= 0) + { + // move action + if(measuringMode_ == 0 && measuringTmpNormals_.size()==1) + { + normalGl = measuringTmpNormals_.front(); + normal = rtabmap::util3d::transformPoint(normalGl, rtabmap::rtabmap_world_T_opengl_world); + n = Eigen::Vector3f(normal.x, normal.y, normal.z); + float normalProdX = n.dot(Eigen::Vector3f(snapAxes_[0][0], snapAxes_[0][1], snapAxes_[0][2])); + float normalProdY = n.dot(Eigen::Vector3f(snapAxes_[1][0], snapAxes_[1][1], snapAxes_[1][2])); + float normalProdZ = n.dot(Eigen::Vector3f(snapAxes_[2][0], snapAxes_[2][1], snapAxes_[2][2])); + if(fabs(normalProdX) > snapAxisThr_) + { + quadColor = xColor; + } + else if(fabs(normalProdY) > snapAxisThr_) + { + quadColor = yColor; + } + else if(fabs(normalProdZ) > snapAxisThr_) + { + quadColor = zColor; + } + else + { + quadColor = color; + } + } + + float quadWidthLeft = 0.05; + float quadWidthRight = quadWidthLeft; + float quadHeightBottom = quadWidthLeft; + float quadHeightTop = quadWidthLeft; + cv::Point3f pt2 = pt; + // project point on line + if(measuringMode_ == 0 && measuringTmpPts_.size() == 1) + { + cv::Point3f v = pt2-measuringTmpPts_.front(); + float n = v.dot(measuringTmpNormals_.front()); + pt2 = measuringTmpPts_.front() + (measuringTmpNormals_.front() * n); + v = rtabmap::util3d::transformPoint(v, rtabmap::rtabmap_world_T_opengl_world); + if(!(fabs(normal.z) > fabs(normal.x) && fabs(normal.z) > fabs(normal.y))) + { + quadHeightTop = v.z>quadSize?v.z:quadSize; + quadHeightBottom = -v.z>quadSize?-v.z:quadSize; + if(fabs(normal.x) > fabs(normal.y)) + { + cv::Point3f y = cv::Point3f(0,0,1).cross(normal); + float n = v.dot(y); + quadWidthRight = n>quadSize?n:quadSize; + quadWidthLeft = n<-quadSize?fabs(n):quadSize; + if(normal.x>0) + { + quadWidthLeft = n>quadSize?n:quadSize; + quadWidthRight = n<-quadSize?fabs(n):quadSize; + float tmp =quadHeightTop; + quadHeightTop= quadHeightBottom; + quadHeightBottom = tmp; + } + } + else + { + cv::Point3f x = normal.cross(cv::Point3f(0,0,1)); + float n = v.dot(x); + quadWidthRight = n<-quadSize?fabs(n):quadSize; + quadWidthLeft = n>quadSize?n:quadSize; + } + } + else + { + if(normal.z>0) + { + quadHeightBottom = v.x<-quadSize?fabs(v.x):quadSize; + quadHeightTop = v.x>quadSize?v.x:quadSize; + quadWidthRight = v.y<-quadSize?fabs(v.y):quadSize; + quadWidthLeft = v.y>quadSize?v.y:quadSize; + } + else + { + quadHeightTop = v.x<-quadSize?fabs(v.x):quadSize; + quadHeightBottom = v.x>quadSize?v.x:quadSize; + quadWidthRight = v.y<-quadSize?fabs(v.y):quadSize; + quadWidthLeft = v.y>quadSize?v.y:quadSize; + } + } + + /*std::string text = uFormat("%0.1f %0.1f %0.1f [%0.2f %0.2f %0.2f %0.2f]", v.x, v.y, v.z, + quadWidthLeft, + quadWidthRight, + quadHeightBottom, + quadHeightTop); + main_scene_.addText(55555, text, rtabmap::Transform(pt.x, pt.y, pt.z, 0,0,0), textSize, color);*/ + } + + Eigen::Quaternionf q; + q.setFromTwoVectors(Eigen::Vector3f(0,0,1), Eigen::Vector3f(normalGl.x,normalGl.y,normalGl.z)); + + if(measuringTmpPts_.size() == 1 && measuringMode_ != 1) + { + const cv::Point3f & pt1 = measuringTmpPts_.at(0); + main_scene_.addLine(55555, pt1, pt2, quadColor); + + main_scene_.addQuad(55555, + quadSize, + rtabmap::Transform( + measuringTmpPts_.front().x, + measuringTmpPts_.front().y, + measuringTmpPts_.front().z, + q.x(),q.y(), q.z(), q.w()), quadColor, quadAlpha); + + main_scene_.addQuad(55556, + quadWidthLeft, + quadWidthRight, + quadHeightBottom, + quadHeightTop, + rtabmap::Transform(pt2.x, pt2.y, pt2.z, q.x(),q.y(), q.z(), q.w()), quadColor, quadAlpha); + } + else + { + main_scene_.addQuad(55555, + quadWidthLeft, + quadWidthRight, + quadHeightBottom, + quadHeightTop, + rtabmap::Transform(pt2.x, pt2.y, pt2.z, q.x(),q.y(), q.z(), q.w()), quadColor, quadAlpha); + } + } + } + else + { + main_scene_.removeText(55555); + main_scene_.removeQuad(55555); + main_scene_.removeQuad(55556); + } + + Eigen::Vector3f target = origin+dir*(distance<100.0f?distance:0.5f); + rtabmap::Transform pose(target[0], target[1], target[2], 0,0,0); + if(main_scene_.hasCloud(-99999)) + { + main_scene_.setCloudPose(-99999, pose); + } + else + { + main_scene_.addCloud(-99999, targetPoint_, pcl::IndicesPtr(), pose); + } + + // reset states + removeMeasureClicked_ = false; + addMeasureClicked_ = false; + teleportClicked_ = false; +} + void RTABMapApp::SetCameraType( tango_gl::GestureCamera::CameraType camera_type) { main_scene_.SetCameraType(camera_type); @@ -2337,6 +2839,15 @@ void RTABMapApp::setOrthoCropFactor(float value) } void RTABMapApp::setGridRotation(float value) { + // Update measuring snap axes + rtabmap::Transform rotation(0,0, value * DEGREE_2_RADIANS); + renderingMutex_.lock(); + snapAxes_[0] = rtabmap::util3d::transformPoint(cv::Vec3f(1,0,0), rotation); + snapAxes_[1] = rtabmap::util3d::transformPoint(cv::Vec3f(0,1,0), rotation); + measuresUpdated_ = true; + renderingMutex_.unlock(); + + // Update grid main_scene_.setGridRotation(value); } void RTABMapApp::setLighting(bool enabled) @@ -3492,7 +4003,8 @@ bool RTABMapApp::exportMesh( bool RTABMapApp::postExportation(bool visualize) { LOGI("postExportation(visualize=%d)", visualize?1:0); - optMesh_.reset(new pcl::TextureMesh); + optTextureMesh_.reset(new pcl::TextureMesh); + optMesh_= rtabmap::Mesh(); optTexture_ = cv::Mat(); exportedMeshUpdated_ = false; @@ -3513,8 +4025,9 @@ bool RTABMapApp::postExportation(bool visualize) if(!cloudMat.empty()) { LOGI("postExportation: Found optimized mesh! Visualizing it."); - optMesh_ = rtabmap::util3d::assembleTextureMesh(cloudMat, polygons, texCoords, textures, true); - optTexture_ = textures; + optTextureMesh_ = rtabmap::util3d::assembleTextureMesh(cloudMat, polygons, texCoords, textures, true); + optMesh_ = rtabmap::Mesh(); + optTexture_ = textures; boost::mutex::scoped_lock lock(renderingMutex_); visualizingMesh_ = true; @@ -3799,6 +4312,57 @@ int RTABMapApp::postProcessing(int approach) return returnedValue; } +void RTABMapApp::clearMeasures() +{ + boost::mutex::scoped_lock lockRender(renderingMutex_); + measures_.clear(); + measuresUpdated_ = true; +} + +void RTABMapApp::setMeasuringMode(int mode) +{ + if(visualizingMesh_) + { + measuringMode_ = mode; + } +} + +void RTABMapApp::addMeasureButtonClicked() +{ + if(visualizingMesh_) + { + boost::mutex::scoped_lock lock(renderingMutex_); + addMeasureClicked_ = true; + } +} + +void RTABMapApp::teleportButtonClicked() +{ + if(visualizingMesh_) + { + boost::mutex::scoped_lock lock(renderingMutex_); + teleportClicked_ = true; + } +} + +void RTABMapApp::removeMeasure() +{ + if(visualizingMesh_) + { + boost::mutex::scoped_lock lock(renderingMutex_); + removeMeasureClicked_ = true; + } +} + +void RTABMapApp::setMetricSystem(bool enabled) +{ + metricSystem_ = enabled; + if(measures_.size()) + { + measuresUpdated_ = true; + } +} + void RTABMapApp::postOdometryEvent( rtabmap::Transform pose, float rgb_fx, float rgb_fy, float rgb_cx, float rgb_cy, diff --git a/app/android/jni/RTABMapApp.h b/app/android/jni/RTABMapApp.h index 67a7ab6d..8fcff2ed 100644 --- a/app/android/jni/RTABMapApp.h +++ b/app/android/jni/RTABMapApp.h @@ -48,6 +48,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include "Measure.h" // RTABMapApp handles the application lifecycle and resources. class RTABMapApp : public UEventsHandler { @@ -84,7 +85,7 @@ class RTABMapApp : public UEventsHandler { #ifdef __ANDROID__ bool startCamera(JNIEnv* env, jobject iBinder, jobject context, jobject activity, int driver); #else // __APPLE__ - bool startCamera(); + bool startCamera(); #endif // Allocate OpenGL resources for rendering, mainly for initializing the Scene. void InitializeGLContent(); @@ -180,6 +181,13 @@ class RTABMapApp : public UEventsHandler { bool postExportation(bool visualize); bool writeExportedMesh(const std::string & directory, const std::string & name); int postProcessing(int approach); + void clearMeasures(); + void showMeasures(bool x, bool y, bool z, bool custom); + void setMeasuringMode(int mode); + void addMeasureButtonClicked(); + void teleportButtonClicked(); + void removeMeasure(); + void setMetricSystem(bool enabled); void postOdometryEvent( rtabmap::Transform pose, @@ -203,6 +211,7 @@ class RTABMapApp : public UEventsHandler { private: int updateMeshDecimation(int width, int height); rtabmap::ParametersMap getRtabmapParameters(); + void updateMeasuringState(); bool smoothMesh(int id, rtabmap::Mesh & mesh); void gainCompensation(bool full = false); std::vector filterOrganizedPolygons(const std::vector & polygons, int cloudSize) const; @@ -264,11 +273,26 @@ class RTABMapApp : public UEventsHandler { bool visualizingMesh_; bool exportedMeshUpdated_; - pcl::TextureMesh::Ptr optMesh_; + pcl::TextureMesh::Ptr optTextureMesh_; cv::Mat optTexture_; + rtabmap::Mesh optMesh_; int optRefId_; rtabmap::Transform * optRefPose_; // App crashes when loading native library if not dynamic - + std::list measures_; // In opengl frame + bool measuresUpdated_; + bool metricSystem_; + float snapAxisThr_; + std::vector snapAxes_; + bool markerDetection_; + int measuringMode_; + bool addMeasureClicked_; + bool teleportClicked_; + bool removeMeasureClicked_; + std::vector measuringTmpPts_; // In opengl frame + std::vector measuringTmpNormals_; // In opengl frame + pcl::PointCloud::Ptr targetPoint_; + pcl::PointCloud::Ptr quadSample_; + std::vector quadSamplePolygons_; // main_scene_ includes all drawable object for visualizing Tango device's // movement and point cloud. Scene main_scene_; diff --git a/app/android/jni/quad_color.cpp b/app/android/jni/quad_color.cpp new file mode 100644 index 00000000..f3a0d1e3 --- /dev/null +++ b/app/android/jni/quad_color.cpp @@ -0,0 +1,69 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "quad_color.h" +#include "tango-gl/util.h" + +static const float vertices[] = {-1.0f, -1.0f, 1.0f, -1.0f, + -1.0f, 1.0f, 1.0f, 1.0f}; + +QuadColor::QuadColor(float size) { + SetShader(); + + vertices_.resize(8); + for(int i=0; i<8; ++i) + { + vertices_[i] = vertices[i]*size; + } +} + +QuadColor::QuadColor( + float widthLeft, + float widthRight, + float heightBottom, + float heightTop) { + SetShader(); + + vertices_.resize(8); + vertices_[0] = vertices[0]*widthLeft; + vertices_[1] = vertices[1]*heightBottom; + vertices_[2] = vertices[2]*widthRight; + vertices_[3] = vertices[3]*heightBottom; + vertices_[4] = vertices[4]*widthLeft; + vertices_[5] = vertices[5]*heightTop; + vertices_[6] = vertices[6]*widthRight; + vertices_[7] = vertices[7]*heightTop; +} + +void QuadColor::Render(const glm::mat4& projection_mat, + const glm::mat4& view_mat) const { + glUseProgram(shader_program_); + + // Calculate MVP matrix and pass it to shader. + glm::mat4 model_mat = GetTransformationMatrix(); + glm::mat4 mvp_mat = projection_mat * view_mat * model_mat; + glUniformMatrix4fv(uniform_mvp_mat_, 1, GL_FALSE, glm::value_ptr(mvp_mat)); + + glUniform4f(uniform_color_, red_, green_, blue_, alpha_); + + // Vertice binding + glEnableVertexAttribArray(attrib_vertices_); + glVertexAttribPointer(attrib_vertices_, 2, GL_FLOAT, GL_FALSE, 0, &vertices_[0]); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glUseProgram(0); +} diff --git a/app/android/jni/quad_color.h b/app/android/jni/quad_color.h new file mode 100644 index 00000000..825ffb15 --- /dev/null +++ b/app/android/jni/quad_color.h @@ -0,0 +1,35 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TANGO_GL_QUADCOLOR_H_ +#define TANGO_GL_QUADCOLOR_H_ + +#include "tango-gl/drawable_object.h" + +class QuadColor : public tango_gl::DrawableObject { + public: + QuadColor(float size); + QuadColor(float widthLeft, + float widthRight, + float heightBottom, + float heightTop); + QuadColor(const QuadColor& other) = delete; + QuadColor& operator=(const QuadColor&) = delete; + virtual ~QuadColor() {} + + void Render(const glm::mat4& projection_mat, const glm::mat4& view_mat) const; +}; +#endif // TANGO_GL_QUADCOLOR_H_ diff --git a/app/android/jni/resources/text_atlas.png b/app/android/jni/resources/text_atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..0efda71339df1ed1487a505f19c5ce65b26a9d01 GIT binary patch literal 25668 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 } + clearLines(); + clearQuads(); + clearTexts(); if(trace_) { trace_->ClearVertexArray(); @@ -215,6 +220,31 @@ void Scene::clear() } } +void Scene::clearLines() +{ + for(std::map::iterator iter=lines_.begin(); iter!=lines_.end(); ++iter) + { + delete iter->second; + } + lines_.clear(); +} +void Scene::clearTexts() +{ + for(std::map::iterator iter=texts_.begin(); iter!=texts_.end(); ++iter) + { + delete iter->second; + } + texts_.clear(); +} +void Scene::clearQuads() +{ + for(std::map::iterator iter=quads_.begin(); iter!=quads_.end(); ++iter) + { + delete iter->second; + } + quads_.clear(); +} + //Should only be called in OpenGL thread! void Scene::SetupViewPort(int w, int h) { if (h == 0) { @@ -631,6 +661,15 @@ int Scene::Render(const float * uvsTransformed, glm::mat4 arViewMatrix, glm::mat cloud->Render(projectionMatrix, viewMatrix, meshRendering_, pointSize_, meshRenderingTexture_, lighting_, distanceToCameraSqr, onlineBlending?depthTexture_:0, screenWidth_, screenHeight_, gesture_camera_->getNearClipPlane(), gesture_camera_->getFarClipPlane(), false, wireFrame_); } + + if(quads_.find(55556)!=quads_.end()) + { + glEnable(GL_BLEND); + glDisable(GL_CULL_FACE); + const QuadColor * quad = quads_.at(55556); + quad->Render(projectionMatrix, viewMatrix); + glEnable(GL_CULL_FACE); + } if(onlineBlending) { @@ -643,6 +682,47 @@ int Scene::Render(const float * uvsTransformed, glm::mat4 arViewMatrix, glm::mat glDepthMask(GL_TRUE); } + /////// + glDisable (GL_DEPTH_TEST); + if(lines_.size()) + { + for(std::map::const_iterator iter=lines_.begin(); iter!=lines_.end(); ++iter) + { + const tango_gl::Line * line = iter->second; + line->Render(projectionMatrix, viewMatrix); + } + } + + if(quads_.size()) + { + glEnable(GL_BLEND); + glDisable(GL_CULL_FACE); + for(std::map::const_iterator iter=quads_.begin(); iter!=quads_.end(); ++iter) + { + if(iter->first!=55556) + { + const QuadColor * quad = iter->second; + quad->Render(projectionMatrix, viewMatrix); + } + } + } + if(texts_.size()) + { + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); + + glm::mat4 viewMatrixRotInv = viewMatrix; + viewMatrixRotInv[3][0] = 0; + viewMatrixRotInv[3][1] = 0; + viewMatrixRotInv[3][2] = 0; + viewMatrixRotInv = glm::inverse(viewMatrixRotInv); + for(std::map::const_iterator iter=texts_.begin(); iter!=texts_.end(); ++iter) + { + const TextDrawable * text = iter->second; + text->Render(projectionMatrix, viewMatrix, viewMatrixRotInv); + } + } + //draw markers on foreground for(std::map::const_iterator iter=markers_.begin(); iter!=markers_.end(); ++iter) { @@ -800,12 +880,7 @@ void Scene::addCloud( const rtabmap::Transform & pose) { LOGI("add cloud %d (%d points %d indices)", id, (int)cloud->size(), indices.get()?(int)indices->size():0); - std::map::iterator iter=pointClouds_.find(id); - if(iter != pointClouds_.end()) - { - delete iter->second; - pointClouds_.erase(iter); - } + removeCloudOrMesh(id); //create PointCloudDrawable * drawable = new PointCloudDrawable(cloud, indices); @@ -813,6 +888,16 @@ void Scene::addCloud( pointClouds_.insert(std::make_pair(id, drawable)); } +void Scene::removeCloudOrMesh(int id) +{ + std::map::iterator iter=pointClouds_.find(id); + if(iter != pointClouds_.end()) + { + delete iter->second; + pointClouds_.erase(iter); + } +} + void Scene::addMesh( int id, const rtabmap::Mesh & mesh, @@ -820,12 +905,7 @@ void Scene::addMesh( bool createWireframe) { LOGI("add mesh %d", id); - std::map::iterator iter=pointClouds_.find(id); - if(iter != pointClouds_.end()) - { - delete iter->second; - pointClouds_.erase(iter); - } + removeCloudOrMesh(id); //create PointCloudDrawable * drawable = new PointCloudDrawable(mesh, createWireframe); @@ -885,6 +965,122 @@ void Scene::addMesh( } } +void Scene::addLine( + int id, + const cv::Point3f & pt1, + const cv::Point3f & pt2, + const tango_gl::Color & color) +{ + LOGI("add line %d", id); + removeLine(id); + + //create + tango_gl::Line * line = new tango_gl::Line(2.0f, GL_LINES); + line->SetShader(); + std::vector vertices(2); + vertices[0].x = pt1.x; + vertices[0].y = pt1.y; + vertices[0].z = pt1.z; + vertices[1].x = pt2.x; + vertices[1].y = pt2.y; + vertices[1].z = pt2.z; + line->UpdateLineVertices(vertices); + line->SetColor(color); + lines_.insert(std::make_pair(id, line)); +} + +void Scene::removeLine(int id) +{ + std::map::iterator iter=lines_.find(id); + if(iter != lines_.end()) + { + delete iter->second; + lines_.erase(iter); + } +} + +void Scene::addText( + int id, + const std::string & text, + const rtabmap::Transform & pose, + float size, + const tango_gl::Color & color) +{ + LOGI("add text %d", id); + removeText(id); + + //create + TextDrawable * textD = new TextDrawable(text, pose, size, color); + texts_.insert(std::make_pair(id, textD)); +} +void Scene::removeText(int id) +{ + std::map::iterator iter=texts_.find(id); + if(iter != texts_.end()) + { + delete iter->second; + texts_.erase(iter); + } +} + +void Scene::addQuad( + int id, + float size, + const rtabmap::Transform & pose, + const tango_gl::Color & color, + float alpha) +{ + //LOGI("add quad %d", id); + std::map::iterator iter=quads_.find(id); + if(iter != quads_.end()) + { + delete iter->second; + quads_.erase(iter); + } + + //create + QuadColor * quad = new QuadColor(size); + quad->SetTransformationMatrix(glmFromTransform(pose)); + quad->SetColor(color); + quad->SetAlpha(alpha); + quads_.insert(std::make_pair(id, quad)); +} + +void Scene::addQuad( + int id, + float widthLeft, + float widthRight, + float heightBottom, + float heightTop, + const rtabmap::Transform & pose, + const tango_gl::Color & color, + float alpha) +{ + //LOGI("add quad %d", id); + removeQuad(id); + + //create + QuadColor * quad = new QuadColor(widthLeft, widthRight, heightBottom, heightTop); + quad->SetTransformationMatrix(glmFromTransform(pose)); + quad->SetColor(color); + quad->SetAlpha(alpha); + quads_.insert(std::make_pair(id, quad)); +} + +void Scene::removeQuad(int id) +{ + std::map::iterator iter=quads_.find(id); + if(iter != quads_.end()) + { + delete iter->second; + quads_.erase(iter); + } +} + +bool Scene::hasQuad(int id) const +{ + return quads_.find(id) != quads_.end(); +} void Scene::setCloudPose(int id, const rtabmap::Transform & pose) { diff --git a/app/android/jni/scene.h b/app/android/jni/scene.h index f241c12b..d217f31c 100644 --- a/app/android/jni/scene.h +++ b/app/android/jni/scene.h @@ -41,12 +41,16 @@ #include "graph_drawable.h" #include "bounding_box_drawable.h" #include "background_renderer.h" +#include "text_drawable.h" +#include "quad_color.h" #include #include // Scene provides OpenGL drawable objects and renders them for visualization. class Scene { + public: + static const glm::vec3 kHeightOffset; public: // Constructor and destructor. Scene(); @@ -67,6 +71,9 @@ class Scene { void setScreenRotation(rtabmap::ScreenRotation colorCameraToDisplayRotation) {color_camera_to_display_rotation_ = colorCameraToDisplayRotation;} void clear(); // removed all point clouds + void clearLines(); + void clearTexts(); + void clearQuads(); // Render loop. // @param: cur_pose_transformation, latest pose's transformation. @@ -119,11 +126,42 @@ class Scene { const pcl::PointCloud::Ptr & cloud, const pcl::IndicesPtr & indices, const rtabmap::Transform & pose); + void removeCloudOrMesh(int id); void addMesh( int id, const rtabmap::Mesh & mesh, const rtabmap::Transform & pose, bool createWireframe = false); + void addLine( + int id, + const cv::Point3f & pt1, + const cv::Point3f & pt2, + const tango_gl::Color & color = tango_gl::Color(1.0f, 1.0f, 1.0f)); + void removeLine(int id); + void addText( + int id, + const std::string & text, + const rtabmap::Transform & pose, + float size, + const tango_gl::Color & color); + void removeText(int id); + void addQuad( + int id, + float size, + const rtabmap::Transform & pose, + const tango_gl::Color & color, + float alpha = 1.0f); + void addQuad( + int id, + float widthLeft, + float widthRight, + float heightBottom, + float heightTop, + const rtabmap::Transform & pose, + const tango_gl::Color & color, + float alpha =1.0f); + void removeQuad(int id); + bool hasQuad(int id) const; void setCloudPose(int id, const rtabmap::Transform & pose); void setCloudVisible(int id, bool visible); @@ -188,6 +226,9 @@ class Scene { rtabmap::ScreenRotation color_camera_to_display_rotation_; std::map pointClouds_; + std::map lines_; + std::map texts_; + std::map quads_; rtabmap::Transform * currentPose_; diff --git a/app/android/jni/text_drawable.cpp b/app/android/jni/text_drawable.cpp new file mode 100644 index 00000000..6171a4a7 --- /dev/null +++ b/app/android/jni/text_drawable.cpp @@ -0,0 +1,366 @@ +/* +Copyright (c) 2010-2025, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +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 Universite de Sherbrooke 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 HOLDER 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. +*/ + +#ifdef __ANDROID__ +#include +#endif + +#include +#include +#include +#include "util.h" +#include "text_drawable.h" +#include "text_atlas_png.h" +#include + +const std::string kTextVertexShader = + "uniform mat4 uMVPMatrix;" + "attribute vec4 vPosition;" + "attribute vec2 a_texCoord;" + "varying vec2 v_texCoord;" + "void main() {" + " gl_Position = uMVPMatrix * vPosition;" + " v_texCoord = a_texCoord;" + "}"; +const std::string kTextFragmentShader = + "precision mediump float;" + "uniform vec3 uColor;" + "varying vec2 v_texCoord;" + "uniform sampler2D s_texture;" + "void main() {" + " gl_FragColor = texture2D( s_texture, v_texCoord );" + " gl_FragColor.rgb = uColor;" + "}"; + +const int RI_TEXT_TEXTURE_SIZE = 512; // 512 +const float RI_TEXT_HEIGHT_BASE = 32.0f; +const char RI_TEXT_START = ' '; +const char RI_TEXT_STOP = '~'+1; + +GLuint TextDrawable::textProgram_ = 0; +GLuint TextDrawable::textTextureId_ = 0; +float TextDrawable::textUVWidth_ = 0; +float TextDrawable::textUVHeight_ = 0; +float TextDrawable::textHeight_ = 0; +std::vector TextDrawable::textCharacterWidths_; + +void TextDrawable::createShaderProgram() +{ + releaseShaderProgram(); + + // hard-coded with text_atlas.png resource + textCharacterWidths_.resize(95); + textCharacterWidths_[0]=8; + textCharacterWidths_[1]=9.000000; + textCharacterWidths_[2]=10.000000; + textCharacterWidths_[3]=19.000000; + textCharacterWidths_[4]=18.000000; + textCharacterWidths_[5]=24.000000; + textCharacterWidths_[6]=21.000000; + textCharacterWidths_[7]=5.000000; + textCharacterWidths_[8]=11.000000; + textCharacterWidths_[9]=11.000000; + textCharacterWidths_[10]=15.000000; + textCharacterWidths_[11]=17.000000; + textCharacterWidths_[12]=8.000000; + textCharacterWidths_[13]=12.000000; + textCharacterWidths_[14]=9.000000; + textCharacterWidths_[15]=12.000000; + textCharacterWidths_[16]=18.000000; + textCharacterWidths_[17]=18.000000; + textCharacterWidths_[18]=18.000000; + textCharacterWidths_[19]=18.000000; + textCharacterWidths_[20]=18.000000; + textCharacterWidths_[21]=18.000000; + textCharacterWidths_[22]=18.000000; + textCharacterWidths_[23]=18.000000; + textCharacterWidths_[24]=18.000000; + textCharacterWidths_[25]=18.000000; + textCharacterWidths_[26]=9.000000; + textCharacterWidths_[27]=8.000000; + textCharacterWidths_[28]=16.000000; + textCharacterWidths_[29]=18.000000; + textCharacterWidths_[30]=17.000000; + textCharacterWidths_[31]=16.000000; + textCharacterWidths_[32]=29.000000; + textCharacterWidths_[33]=22.000000; + textCharacterWidths_[34]=20.000000; + textCharacterWidths_[35]=21.000000; + textCharacterWidths_[36]=21.000000; + textCharacterWidths_[37]=18.000000; + textCharacterWidths_[38]=18.000000; + textCharacterWidths_[39]=22.000000; + textCharacterWidths_[40]=23.000000; + textCharacterWidths_[41]=9.000000; + textCharacterWidths_[42]=18.000000; + textCharacterWidths_[43]=20.000000; + textCharacterWidths_[44]=17.000000; + textCharacterWidths_[45]=28.000000; + textCharacterWidths_[46]=23.000000; + textCharacterWidths_[47]=22.000000; + textCharacterWidths_[48]=21.000000; + textCharacterWidths_[49]=22.000000; + textCharacterWidths_[50]=20.000000; + textCharacterWidths_[51]=20.000000; + textCharacterWidths_[52]=20.000000; + textCharacterWidths_[53]=21.000000; + textCharacterWidths_[54]=21.000000; + textCharacterWidths_[55]=28.000000; + textCharacterWidths_[56]=20.000000; + textCharacterWidths_[57]=20.000000; + textCharacterWidths_[58]=19.000000; + textCharacterWidths_[59]=9.000000; + textCharacterWidths_[60]=14.000000; + textCharacterWidths_[61]=9.000000; + textCharacterWidths_[62]=14.000000; + textCharacterWidths_[63]=14.000000; + textCharacterWidths_[64]=11.000000; + textCharacterWidths_[65]=17.000000; + textCharacterWidths_[66]=18.000000; + textCharacterWidths_[67]=17.000000; + textCharacterWidths_[68]=18.000000; + textCharacterWidths_[69]=17.000000; + textCharacterWidths_[70]=11.000000; + textCharacterWidths_[71]=18.000000; + textCharacterWidths_[72]=18.000000; + textCharacterWidths_[73]=8.000000; + textCharacterWidths_[74]=8.000000; + textCharacterWidths_[75]=17.000000; + textCharacterWidths_[76]=8.000000; + textCharacterWidths_[77]=28.000000; + textCharacterWidths_[78]=18.000000; + textCharacterWidths_[79]=18.000000; + textCharacterWidths_[80]=18.000000; + textCharacterWidths_[81]=18.000000; + textCharacterWidths_[82]=12.000000; + textCharacterWidths_[83]=16.000000; + textCharacterWidths_[84]=11.000000; + textCharacterWidths_[85]=18.000000; + textCharacterWidths_[86]=16.000000; + textCharacterWidths_[87]=24.000000; + textCharacterWidths_[88]=16.000000; + textCharacterWidths_[89]=16.000000; + textCharacterWidths_[90]=16.000000; + textCharacterWidths_[91]=11.000000; + textCharacterWidths_[92]=8.000000; + textCharacterWidths_[93]=11.000000; + textCharacterWidths_[94]=21.000000; + textUVWidth_=0.062500; + textUVHeight_=0.082031; + textHeight_=42.000000; + + textProgram_ = tango_gl::util::CreateProgram(kTextVertexShader.c_str(), kTextFragmentShader.c_str()); + UASSERT(textProgram_ != 0); + + glGenTextures(1, &textTextureId_); + UASSERT(textTextureId_); + + // gen texture from image + glBindTexture(GL_TEXTURE_2D, textTextureId_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + std::vector data = uHex2Bytes(rtabmap::TEXT_ATLAS_PNG); + + cv::Mat rgbImage = cv::imdecode(data, cv::IMREAD_UNCHANGED); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, rgbImage.cols, rgbImage.rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgbImage.data); + + GLint error = glGetError(); + UASSERT(error == GL_NO_ERROR); +} + +void TextDrawable::releaseShaderProgram() +{ + if(textProgram_) + { + glDeleteShader(textProgram_); + textProgram_ = 0; + } +} + +TextDrawable::TextDrawable( + const std::string & text, + const rtabmap::Transform & pose, + float textSize, // meters + const tango_gl::Color & color) : + poseGl_(glmFromTransform(pose)), + color_(color) +{ + if(textProgram_ > 0) + { + vertexBuffer_ = std::vector(text.size() * 12); + textureBuffer_ = std::vector(text.size() * 8); + drawListBuffer_ = std::vector(text.size() * 6); + + int index_vecs = 0; + int index_indices = 0; + int index_uvs = 0; + float x=0.0f; + float y=0.0f; + float uniformscale = textSize/textHeight_; + for(unsigned int i=0; i=textCharacterWidths_.size()) { + // unknown character, we will add a space for it to be save. + indx = 0; + } + + int colCount = RI_TEXT_TEXTURE_SIZE/(int)RI_TEXT_HEIGHT_BASE; + + // Calculate the uv parts + int row = indx / colCount; + int col = indx % colCount; + + float v = row * textUVHeight_; + float v2 = v + textUVHeight_; + float u = col * textUVWidth_; + float u2 = u + textCharacterWidths_[indx]/(float)RI_TEXT_TEXTURE_SIZE; + + // Creating the triangle information + std::vector vec(12); + std::vector uv(8); + + vec[0] = x; + vec[1] = y + (textHeight_ * uniformscale); + vec[2] = 0; + vec[3] = x; + vec[4] = y; + vec[5] = 0; + vec[6] = x + (textCharacterWidths_[indx] * uniformscale); + vec[7] = y; + vec[8] = 0; + vec[9] = x + (textCharacterWidths_[indx] * uniformscale); + vec[10] = y + (textHeight_ * uniformscale); + vec[11] = 0; + + // 0.001f = texture bleeding hack/fix + uv[0] = u+0.001f; + uv[1] = v+0.001f; + uv[2] = u+0.001f; + uv[3] = v2-0.001f; + uv[4] = u2-0.001f; + uv[5] = v2-0.001f; + uv[6] = u2-0.001f; + uv[7] = v+0.001f; + + unsigned short inds[6] = {0, 1, 2, 0, 2, 3}; + + // We need a base value because the object has indices related to + // that object and not to this collection so basicly we need to + // translate the indices to align with the vertexlocation in ou + // vecs array of vectors. + short base = (short) (index_vecs / 3); + + // We should add the vec, translating the indices to our saved vector + for(int i=0;iGetParent()->GetRotation()); + glUniformMatrix4fv(mtrxhandle, 1, GL_FALSE, glm::value_ptr(mvp_mat)); + + // get handle to color value + int colorhandle = glGetUniformLocation(textProgram_, "uColor"); + glUniform3f(colorhandle, color_.r, color_.g, color_.b); + + int mSamplerLoc = glGetUniformLocation (textProgram_, "s_texture" ); + + // Texture activate unit 0 + glActiveTexture(GL_TEXTURE0); + // Bind the texture to this unit. + glBindTexture(GL_TEXTURE_2D, textTextureId_); + // Set the sampler texture unit to our selected id + glUniform1i ( mSamplerLoc, 0); + + // Draw the triangle + glDrawElements(GL_TRIANGLES, drawListBuffer_.size(), GL_UNSIGNED_SHORT, &drawListBuffer_[0]); + + // Disable vertex array + glDisableVertexAttribArray(mPositionHandle); + glDisableVertexAttribArray(mTexCoordLoc); +} diff --git a/app/android/jni/text_drawable.h b/app/android/jni/text_drawable.h new file mode 100644 index 00000000..4a6869be --- /dev/null +++ b/app/android/jni/text_drawable.h @@ -0,0 +1,71 @@ +/* +Copyright (c) 2010-2025, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +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 Universite de Sherbrooke 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 HOLDER 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. +*/ + +#ifndef TANGO_TEXT_DRAWABLE_H_ +#define TANGO_TEXT_DRAWABLE_H_ + +#include +#include +#include + +// PointCloudDrawable is responsible for the point cloud rendering. +class TextDrawable { +private: + static GLuint textProgram_; + static GLuint textTextureId_; + static float textUVWidth_; + static float textUVHeight_; + static float textHeight_; + static std::vector textCharacterWidths_; + +public: + static void createShaderProgram(); + static void releaseShaderProgram(); + +public: + TextDrawable( + const std::string & text, + const rtabmap::Transform & pose, + float textSize = 0.1f, // meters + const tango_gl::Color & color = tango_gl::Color(1,0,0)); + + virtual ~TextDrawable() {} + + void Render( + const glm::mat4 & projectionMatrix, + const glm::mat4 & viewMatrix, + const glm::mat4 & viewMatrixRotInv) const; + + private: + std::vector vertexBuffer_; + std::vector textureBuffer_; + std::vector drawListBuffer_; + glm::mat4 poseGl_; + tango_gl::Color color_; +}; + +#endif // TANGO_POINT_CLOUD_POINT_CLOUD_DRAWABLE_H_ diff --git a/app/ios/RTABMapApp.xcodeproj/project.pbxproj b/app/ios/RTABMapApp.xcodeproj/project.pbxproj index e2406206..9fee832a 100644 --- a/app/ios/RTABMapApp.xcodeproj/project.pbxproj +++ b/app/ios/RTABMapApp.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 4E0D83832621F52C00C879AC /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 4E0D83822621F52C00C879AC /* Settings.bundle */; }; + 4E1E9CF72D618E69000CB881 /* quad_color.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4E1E9CF62D618079000CB881 /* quad_color.cpp */; }; + 4E1E9CF82D618E69000CB881 /* text_drawable.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4E1E9CF52D618079000CB881 /* text_drawable.cpp */; }; 4E20B24E266AB94300316EE6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E20B24D266AB94300316EE6 /* Images.xcassets */; }; 4E20B250266AB95600316EE6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E20B24F266AB95600316EE6 /* Images.xcassets */; }; 4E2C516725A63119005CEDBD /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2C516525A63119005CEDBD /* DatabaseView.swift */; }; @@ -156,6 +158,12 @@ /* Begin PBXFileReference section */ 4E0D83822621F52C00C879AC /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 4E1E9CF22D617D08000CB881 /* Measure.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Measure.h; path = ../android/jni/Measure.h; sourceTree = ""; }; + 4E1E9CF32D618079000CB881 /* quad_color.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = quad_color.h; path = ../android/jni/quad_color.h; sourceTree = ""; }; + 4E1E9CF42D618079000CB881 /* text_drawable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = text_drawable.h; path = ../android/jni/text_drawable.h; sourceTree = ""; }; + 4E1E9CF52D618079000CB881 /* text_drawable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = text_drawable.cpp; path = ../android/jni/text_drawable.cpp; sourceTree = ""; }; + 4E1E9CF62D618079000CB881 /* quad_color.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = quad_color.cpp; path = ../android/jni/quad_color.cpp; sourceTree = ""; }; + 4E1E9CF92D619025000CB881 /* text_atlas_png.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = text_atlas_png.h; path = RTABMapApp/text_atlas_png.h; sourceTree = ""; }; 4E20B24D266AB94300316EE6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RTABMapApp/Images.xcassets; sourceTree = SOURCE_ROOT; }; 4E20B24F266AB95600316EE6 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RTABMapApp/Images.xcassets; sourceTree = ""; }; 4E2C516525A63119005CEDBD /* DatabaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseView.swift; path = RTABMapApp/DatabaseView.swift; sourceTree = ""; }; @@ -679,6 +687,12 @@ 4EFD0BB0259D503200575D88 /* NativeWrapper */ = { isa = PBXGroup; children = ( + 4E1E9CF92D619025000CB881 /* text_atlas_png.h */, + 4E1E9CF62D618079000CB881 /* quad_color.cpp */, + 4E1E9CF32D618079000CB881 /* quad_color.h */, + 4E1E9CF52D618079000CB881 /* text_drawable.cpp */, + 4E1E9CF42D618079000CB881 /* text_drawable.h */, + 4E1E9CF22D617D08000CB881 /* Measure.h */, 4EFD0F6E259F847300575D88 /* background_renderer.cc */, 4EFD0F6F259F847300575D88 /* background_renderer.h */, 4EFD0F66259E38DE00575D88 /* NativeWrapper.cpp */, @@ -800,6 +814,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4E1E9CF72D618E69000CB881 /* quad_color.cpp in Sources */, + 4E1E9CF82D618E69000CB881 /* text_drawable.cpp in Sources */, 4EFD0F70259F847300575D88 /* background_renderer.cc in Sources */, 4EFD0BC0259D50A800575D88 /* scene.cpp in Sources */, 4E2C516825A63119005CEDBD /* VerticalScrollerView.swift in Sources */, diff --git a/app/ios/RTABMapApp/Base.lproj/Main.storyboard b/app/ios/RTABMapApp/Base.lproj/Main.storyboard index 3b796cb3..ef737a3a 100644 --- a/app/ios/RTABMapApp/Base.lproj/Main.storyboard +++ b/app/ios/RTABMapApp/Base.lproj/Main.storyboard @@ -18,8 +18,8 @@ - - + + + + @@ -83,17 +109,6 @@ - + + + + + - + - + + + + + @@ -196,17 +264,22 @@ + + + + + @@ -230,7 +303,7 @@