From 404a8c58d23bd9a0f01bc373fcb36b1dcbad352e Mon Sep 17 00:00:00 2001 From: ioes Date: Wed, 21 Oct 2026 11:51:16 +0900 Subject: [PATCH 1/5] Add shared underwater surface optics for Gazebo cameras --- gazebo/posim_gz_rendering/Appearance.hh | 22 ++++ gazebo/posim_gz_rendering/CMakeLists.txt | 31 +++++ gazebo/posim_gz_rendering/README.ko.md | 31 +++++ gazebo/posim_gz_rendering/README.md | 31 +++++ .../posim_gz_rendering/UnderwaterRenderer.cc | 86 +++++++++++++ .../posim_gz_rendering/UnderwaterRenderer.hh | 17 +++ gazebo/posim_gz_rendering/UnderwaterSystem.cc | 121 ++++++++++++++++++ gazebo/posim_gz_rendering/package.xml | 10 ++ .../shaders/Underwater_piece_vs_piece_ps.any | 98 ++++++++++++++ .../tests/AppearanceTest.cc | 9 ++ gazebo/posim_gz_rendering/tests/RenderTest.cc | 62 +++++++++ .../src/UnderwaterCamera.cc | 7 + website/content/en/camera.md | 4 + website/content/ko/camera.md | 4 + 14 files changed, 533 insertions(+) create mode 100644 gazebo/posim_gz_rendering/Appearance.hh create mode 100644 gazebo/posim_gz_rendering/CMakeLists.txt create mode 100644 gazebo/posim_gz_rendering/README.ko.md create mode 100644 gazebo/posim_gz_rendering/README.md create mode 100644 gazebo/posim_gz_rendering/UnderwaterRenderer.cc create mode 100644 gazebo/posim_gz_rendering/UnderwaterRenderer.hh create mode 100644 gazebo/posim_gz_rendering/UnderwaterSystem.cc create mode 100644 gazebo/posim_gz_rendering/package.xml create mode 100644 gazebo/posim_gz_rendering/shaders/Underwater_piece_vs_piece_ps.any create mode 100644 gazebo/posim_gz_rendering/tests/AppearanceTest.cc create mode 100644 gazebo/posim_gz_rendering/tests/RenderTest.cc diff --git a/gazebo/posim_gz_rendering/Appearance.hh b/gazebo/posim_gz_rendering/Appearance.hh new file mode 100644 index 00000000..f8a25fa5 --- /dev/null +++ b/gazebo/posim_gz_rendering/Appearance.hh @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include +#include +#include +namespace posim { +// Stable wire order; schema and revision precede user settings. +using Appearance = std::array; +inline Appearance Defaults(){return {1,0,1,1,0,0,1,1,1,1,0,50,5};} +inline bool Valid(const Appearance &a){ + for(auto v:a)if(!std::isfinite(v))return false; + if(a[0]!=1||a[1]<0||a[1]>9007199254740991.||std::floor(a[1])!=a[1])return false; + for(unsigned i=2;i<=10;++i)if(a[i]!=0&&a[i]!=1)return false; + return a[11]>=1&&a[11]<=500&&a[12]>=1&&a[12]<=50; +} +inline double WaterPath(double cameraZ,double surfaceZ,double distance){ + if(distance<=0)return 0; + if(cameraZ<=0&&surfaceZ<=0)return distance; + if(cameraZ>=0&&surfaceZ>=0)return 0; + return distance*(cameraZ<0?-cameraZ: -surfaceZ)/std::abs(surfaceZ-cameraZ); +} +} diff --git a/gazebo/posim_gz_rendering/CMakeLists.txt b/gazebo/posim_gz_rendering/CMakeLists.txt new file mode 100644 index 00000000..0cf81c00 --- /dev/null +++ b/gazebo/posim_gz_rendering/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.22) +project(posim_gz_rendering LANGUAGES CXX) +find_package(gz-rendering REQUIRED COMPONENTS ogre2) +find_package(gz-sim REQUIRED) +find_package(gz-plugin REQUIRED COMPONENTS register) +find_package(PkgConfig REQUIRED) +pkg_check_modules(OGRE REQUIRED IMPORTED_TARGET OGRE-Next>=2.3) +pkg_check_modules(HLMS REQUIRED IMPORTED_TARGET OGRE-Next-Hlms) +find_path(HLMS_COMMON OgreHlmsTextureBaseClass.h HINTS ${HLMS_INCLUDE_DIRS} ${OGRE_INCLUDE_DIRS} + PATH_SUFFIXES Common ../Common Hlms/Common include/OGRE-Next/Hlms/Common OGRE-Next/Hlms/Common REQUIRED) +add_library(posim_underwater_render SHARED UnderwaterRenderer.cc) +target_compile_features(posim_underwater_render PRIVATE cxx_std_17) +target_include_directories(posim_underwater_render PRIVATE ${HLMS_COMMON}) +target_link_libraries(posim_underwater_render PRIVATE gz-rendering::gz-rendering gz-rendering::ogre2 PkgConfig::OGRE PkgConfig::HLMS) +add_library(posim_underwater SHARED UnderwaterSystem.cc) +target_compile_features(posim_underwater PRIVATE cxx_std_17) +target_link_libraries(posim_underwater PRIVATE gz-sim::gz-sim gz-plugin::register gz-rendering::gz-rendering ${CMAKE_DL_LIBS}) +set_target_properties(posim_underwater posim_underwater_render PROPERTIES PREFIX "") +install(TARGETS posim_underwater posim_underwater_render LIBRARY DESTINATION lib) +install(DIRECTORY shaders/ DESTINATION share/posim/underwater) +include(CTest) +if(BUILD_TESTING) + add_executable(underwater-settings-test tests/AppearanceTest.cc) + target_compile_features(underwater-settings-test PRIVATE cxx_std_17) + target_include_directories(underwater-settings-test PRIVATE .) + add_test(NAME underwater-settings COMMAND underwater-settings-test) + add_executable(underwater-render-test tests/RenderTest.cc) + target_compile_features(underwater-render-test PRIVATE cxx_std_17) + target_include_directories(underwater-render-test PRIVATE .) + target_link_libraries(underwater-render-test PRIVATE posim_underwater_render gz-rendering::gz-rendering) +endif() diff --git a/gazebo/posim_gz_rendering/README.ko.md b/gazebo/posim_gz_rendering/README.ko.md new file mode 100644 index 00000000..a6c180af --- /dev/null +++ b/gazebo/posim_gz_rendering/README.ko.md @@ -0,0 +1,31 @@ +# 수중 표면 렌더링 + +[English](README.md) + +Gazebo Jetty / Ogre2 시스템이 PBR 표면을 그릴 때 수중 경로 안개, RGB 흡수, 수심에 따른 햇빛 감쇠, 움직이는 해저면 햇빛 무늬와 선택 가능한 부유 입자를 적용합니다. RGB 카메라와 RGBD 컬러 영상은 같은 재질 경로를 사용합니다. 수치 깊이와 충돌 형상은 측정값을 유지합니다. Gazebo의 클리핑, 단색 센서 출력과 지형 그림자 기능을 유지합니다. + +수면은 z = 0 m 평면으로 근사합니다. 햇빛 무늬와 부유 입자는 계산량이 적은 절차적 근사입니다. 비조명 객체, 사용자 수면 셰이더와 하늘에는 PBR 효과가 적용되지 않습니다. 로봇 조명의 직접 조명은 유지하며, 표면과 카메라 사이의 물을 통과하는 빛은 감쇠합니다. + +서버와 같은 Gazebo/Ogre SDK에서 CMake로 빌드합니다. 시스템 라이브러리는 Gazebo가 렌더링 장면을 만든 뒤 렌더링 보조 라이브러리를 불러옵니다. 라이브러리는 `lib`, 셰이더는 `share/posim/underwater`에 설치합니다. + +```xml + + /absolute/lib/posim_underwater_render.so + /absolute/share/posim/underwater + unique_session_id + +``` + +macOS 라이브러리 확장자는 `.dylib`입니다. Gazebo 프로세스마다 한 인스턴스를 사용하세요. 렌더러는 엔진의 PBS 리스너를 확장하고 렌더링 종료 시 복원합니다. + +`/posim/appearance//set`은 `gz.msgs.Double_V`를 받습니다. 13개 값의 순서는 스키마(1), 리비전, 수중 광학 효과, 해저면 질감, 격자선, 등고선, 안개, 색상 흡수, 햇빛 감쇠, 햇빛 무늬, 부유 입자, 격자 간격, 등고선 간격입니다. 논리값은 0 또는 1입니다. 리비전은 정확히 1씩 증가하며 같은 요청의 재전송은 중복 적용하지 않습니다. `/state`는 `gz.msgs.Empty`를 받아 13개 값 뒤에 준비 여부, 카메라 수, 렌더러 실패 여부를 반환합니다. 렌더링 후 적용 상태를 확인합니다. + +- 기본 켜짐: 광학 효과, 질감, 안개, 색상 흡수, 햇빛 감쇠, 햇빛 무늬. +- 기본 꺼짐: 격자선, 등고선, 부유 입자. +- 격자 간격: 기본 50 m, 범위 1–500 m. +- 등고선 간격: 기본 5 m, 범위 1–50 m. +- 수중 광학 효과를 꺼도 질감과 측정선 선택은 유지됩니다. + +해저면 시각 요소의 이름은 `seabed` 또는 `shallow`이며 Gazebo 범위 이름도 지원합니다. 지형 내보내기에서 PBR 맵을 제공합니다. 질감과 측정선은 이 시각 요소의 해수면 아래 부분에 적용됩니다. `POSIM_UNDERWATER_PLUGIN`이 설정되면 기존 `UnderwaterCamera` 호환 출력은 네이티브 컬러 영상을 그대로 전달합니다. 호스트는 네이티브 시스템을 설치한 경우에만 이 값을 설정하고 렌더러 실패를 사용자에게 표시해야 합니다. + +`underwater-settings-test`는 설정과 수중 경로 경계를 검사합니다. `underwater-render-test SHADER_DIRECTORY OUTPUT_DIRECTORY SAND_MAP_DIRECTORY`는 RGB·RGBD 비교 영상을 만들고 설정을 바꾸어도 깊이 데이터가 바이트 단위로 같은지 확인합니다. POSIM-Terrain의 모래 맵과 설정된 Gazebo 환경에서 실행하세요. diff --git a/gazebo/posim_gz_rendering/README.md b/gazebo/posim_gz_rendering/README.md new file mode 100644 index 00000000..20528e05 --- /dev/null +++ b/gazebo/posim_gz_rendering/README.md @@ -0,0 +1,31 @@ +# Underwater surface rendering + +[한국어](README.ko.md) + +This Gazebo Jetty / Ogre2 system applies water-path haze, RGB absorption, depth-dependent sunlight, animated seabed caustics and optional suspended specks while PBR surfaces are shaded. RGB cameras and RGBD color use the same material path. Numeric depth and collision geometry retain their measured values. Gazebo's clipping, solid-color sensor output and terrain-shadow hooks are retained. + +The water surface is approximated by the plane z = 0 m. Caustics and specks are inexpensive procedural approximations. Unlit objects, custom water shaders and the sky are outside the PBR effect. Local lamps keep their direct illumination; water between a surface and a camera still attenuates that surface's light. + +Build with CMake against the same Gazebo/Ogre SDK as the server. The system library delays loading its rendering helper until Gazebo creates a rendering scene. Install libraries to `lib` and shader pieces to `share/posim/underwater`. + +```xml + + /absolute/lib/posim_underwater_render.so + /absolute/share/posim/underwater + unique_session_id + +``` + +On macOS the libraries have the `.dylib` suffix. Use one instance per Gazebo process. The renderer extends the engine's PBS listener and restores it during render teardown. + +`/posim/appearance//set` accepts `gz.msgs.Double_V`. Its 13 values are schema (1), revision, water optics, seabed texture, grid, contours, haze, absorption, sunlight fading, caustics, specks, grid spacing and contour spacing. Boolean values are 0 or 1. Revision advances by exactly one; an identical retry is idempotent. `/state` accepts `gz.msgs.Empty` and returns those 13 values plus ready, camera count and renderer failed. Applied state is acknowledged after rendering. + +- Defaults on: optics, texture, haze, absorption, sunlight fading, caustics. +- Defaults off: grid, contours, suspended specks. +- Grid: 50 m, adjustable from 1 to 500 m. +- Contours: 5 m, adjustable from 1 to 50 m. +- Turning water optics off preserves texture and measurement-overlay selections. + +Seabed visuals are named `seabed` or `shallow` (including Gazebo scoped names). Terrain exports supply their PBR maps. Texture detail and measurement overlays affect these visuals below sea level. The legacy `UnderwaterCamera` compatibility output passes native-colored pixels through when `POSIM_UNDERWATER_PLUGIN` is set. Hosts should enable this flag only when they install the native system, and surface renderer failures to users. + +`underwater-settings-test` covers configuration and water-path boundaries. `underwater-render-test SHADER_DIRECTORY OUTPUT_DIRECTORY SAND_MAP_DIRECTORY` renders RGB and RGBD comparisons and requires byte-identical depth across settings. Run it in a configured Gazebo environment with the sand maps distributed by POSIM-Terrain. diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc new file mode 100644 index 00000000..de198bac --- /dev/null +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "UnderwaterRenderer.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace posim { +struct UnderwaterRenderer::Impl : Ogre::HlmsListener { + Ogre::Hlms *pbs=nullptr; + Ogre::ArchiveVec original; + Ogre::HlmsListener *previous=nullptr; + Appearance settings=Defaults(); + double time=0; + unsigned cameras=0; + explicit Impl(const std::string &pieces){ + auto root=gz::rendering::Ogre2RenderEngine::Instance()->OgreRoot(); + pbs=root->getHlmsManager()->getHlms(Ogre::HLMS_PBS); + if(!pbs)throw std::runtime_error("underwater_pbs_unavailable"); + previous=pbs->getListener(); + if(dynamic_cast(previous))throw std::runtime_error("underwater_already_installed"); + original=pbs->getPiecesLibraryAsArchiveVec();auto extended=original; + extended.push_back(Ogre::ArchiveManager::getSingleton().load(pieces,"FileSystem",true)); + pbs->setListener(this); + try {pbs->reloadFrom(pbs->getDataFolder(),&extended);} + catch(...){pbs->setListener(previous);throw;} + } + ~Impl() { + if(pbs&&pbs->getListener()==this){pbs->setListener(previous);pbs->reloadFrom(pbs->getDataFolder(),&original);} + } + void propertiesMergedPreGenerationStep(Ogre::Hlms *h,const Ogre::HlmsCache &c,const Ogre::HlmsPropertyVec &r,const Ogre::PiecesMap p[Ogre::NumShaderTypes],const Ogre::HlmsPropertyVec &v,const Ogre::QueuedRenderable &q) override {if(previous)previous->propertiesMergedPreGenerationStep(h,c,r,p,v,q);} + Ogre::uint16 getNumExtraPassTextures(const Ogre::HlmsPropertyVec &p,bool c) const override {return previous?previous->getNumExtraPassTextures(p,c):0;} + void setupRootLayout(Ogre::RootLayout &r,const Ogre::HlmsPropertyVec &p) const override {if(previous)previous->setupRootLayout(r,p);} + void shaderCacheEntryCreated(const Ogre::String &s,const Ogre::HlmsCache *h,const Ogre::HlmsCache &c,const Ogre::HlmsPropertyVec &p,const Ogre::QueuedRenderable &q) override {if(previous)previous->shaderCacheEntryCreated(s,h,c,p,q);} + void preparePassHash(const Ogre::CompositorShadowNode *s,bool c,bool d,Ogre::SceneManager *m,Ogre::Hlms *h) override {if(previous)previous->preparePassHash(s,c,d,m,h);} + void hlmsTypeChanged(bool c,Ogre::CommandBuffer *b,const Ogre::HlmsDatablock *d,size_t t) override {if(previous)previous->hlmsTypeChanged(c,b,d,t);} + Ogre::uint32 getPassBufferSize(const Ogre::CompositorShadowNode *s,bool c,bool d, + Ogre::SceneManager *m) const override {return (previous?previous->getPassBufferSize(s,c,d,m):0)+(c?0:128);} + float *preparePassBuffer(const Ogre::CompositorShadowNode *s,bool caster,bool d, + Ogre::SceneManager *scene,float *out) override { + if(previous)out=previous->preparePassBuffer(s,caster,d,scene,out); + if(caster)return out; + auto camera=scene->getCamerasInProgress().renderingCamera; + auto inverse=camera->getViewMatrix(true).inverse(); + for(unsigned i=0;i<3;++i)for(unsigned j=0;j<4;++j)*out++=inverse[i][j]; + for(auto i:{2,4,5,3})*out++=settings[i]; + for(auto i:{6,7,8,9})*out++=settings[i]; + *out++=settings[11];*out++=settings[12];*out++=time;*out++=settings[10]; + for(float v:{.18f,.09f,.06f,0.f})*out++=v; + for(float v:{.006f,.055f,.065f,1.f})*out++=v; + return out; + } +}; +UnderwaterRenderer::UnderwaterRenderer(const std::string &pieces):impl(new Impl(pieces)){} +UnderwaterRenderer::~UnderwaterRenderer()=default; +unsigned UnderwaterRenderer::Cameras() const {return impl->cameras;} +void UnderwaterRenderer::Update(const gz::rendering::ScenePtr &scene,const Appearance &settings,double time){ + impl->settings=settings;impl->time=time;impl->cameras=0; + for(unsigned i=0;iSensorCount();++i) + if(std::dynamic_pointer_cast(scene->SensorByIndex(i)))++impl->cameras; + for(unsigned i=0;iVisualCount();++i){ + auto v=scene->VisualByIndex(i);const auto &name=v->Name(); + const bool seabed=name=="seabed"||name.find("::seabed")!=std::string::npos||name.find("::shallow")!=std::string::npos; + if(!seabed)continue; + for(unsigned g=0;gGeometryCount();++g){ + auto m=std::dynamic_pointer_cast(v->GeometryByIndex(g)->Material()); + if(m&&m->Datablock()&&m->Datablock()->getUserValue(0).x!=6421)m->Datablock()->setUserValue(0,Ogre::Vector4(6421,0,0,0)); + } + } +} +} +extern "C" void *posimCreateUnderwater(const char *path){return new posim::UnderwaterRenderer(path);} +extern "C" unsigned posimUpdateUnderwater(void *p,const gz::rendering::ScenePtr &s,const posim::Appearance &a,double t){ + auto r=static_cast(p);r->Update(s,a,t);return r->Cameras(); +} +extern "C" void posimDestroyUnderwater(void *p){delete static_cast(p);} diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.hh b/gazebo/posim_gz_rendering/UnderwaterRenderer.hh new file mode 100644 index 00000000..49d1f77c --- /dev/null +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.hh @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include +#include +#include +#include "Appearance.hh" +namespace posim { +class UnderwaterRenderer { + struct Impl; + std::unique_ptr impl; + public: + explicit UnderwaterRenderer(const std::string &pieces); + ~UnderwaterRenderer(); + void Update(const gz::rendering::ScenePtr &, const Appearance &, double simulationTime); + unsigned Cameras() const; +}; +} diff --git a/gazebo/posim_gz_rendering/UnderwaterSystem.cc b/gazebo/posim_gz_rendering/UnderwaterSystem.cc new file mode 100644 index 00000000..6df2c2c1 --- /dev/null +++ b/gazebo/posim_gz_rendering/UnderwaterSystem.cc @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Appearance.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif +namespace posim { +class Underwater final : public gz::sim::System,public gz::sim::ISystemConfigure,public gz::sim::ISystemPostUpdate { + std::mutex mutex; + Appearance requested=Defaults(),applied=Defaults(),staged=Defaults(); + bool stagedReady=false; + unsigned stagedCameras=0; + double time=0; + unsigned cameras=0; + bool ready=false,failed=false; + void *library=nullptr,*renderer=nullptr; + using Create=void *(*)(const char *); + using Update=unsigned (*)(void *,const gz::rendering::ScenePtr &,const Appearance &,double); + using Destroy=void (*)(void *); + Create create=nullptr;Update update=nullptr;Destroy destroy=nullptr; + std::string path,pieces; + gz::transport::Node node; + gz::common::ConnectionPtr renderConnection,postRenderConnection,teardownConnection; + void Close(){if(renderer&&destroy){destroy(renderer);renderer=nullptr;}} + public: + ~Underwater() override { + renderConnection.reset();postRenderConnection.reset();teardownConnection.reset(); + // The rendering resources are destroyed by RenderTeardown, on their owner thread. + // Keep the helper mapped until process exit if the engine omitted teardown. + if(!renderer&&library){ +#ifdef _WIN32 + FreeLibrary(static_cast(library)); +#else + dlclose(library); +#endif + } + } + void Configure(const gz::sim::Entity &,const std::shared_ptr &sdf, + gz::sim::EntityComponentManager &,gz::sim::EventManager &events) override { + if(!sdf->HasElement("renderer_library")||!sdf->HasElement("shader_directory")||!sdf->HasElement("session_nonce")){ + gzerr<<"Underwater requires renderer_library, shader_directory and session_nonce\n";return; + } + path=sdf->Get("renderer_library");pieces=sdf->Get("shader_directory"); + if(sdf->HasElement("initial_settings")){ + std::istringstream stream(sdf->Get("initial_settings"));Appearance initial; + for(auto &v:initial)if(!(stream>>v))throw std::runtime_error("invalid_initial_appearance"); + if(!Valid(initial))throw std::runtime_error("invalid_initial_appearance"); + requested=applied=initial; + } + auto service="/posim/appearance/"+sdf->Get("session_nonce"); + node.Advertise(service+"/set",&Underwater::Set,this); + node.Advertise(service+"/state",&Underwater::State,this); + renderConnection=events.Connect([this]{Render();}); + postRenderConnection=events.Connect([this]{ + std::lock_guard lock(mutex); + if(stagedReady&&!failed){applied=staged;cameras=stagedCameras;ready=cameras>0;stagedReady=false;} + }); + teardownConnection=events.Connect([this]{Close();std::lock_guard lock(mutex);ready=false;}); + } + bool Set(const gz::msgs::Double_V &request,gz::msgs::Double_V &response){ + std::lock_guard lock(mutex); + if(request.data_size()!=13)return false; + Appearance a;std::copy(request.data().begin(),request.data().end(),a.begin()); + if(!Valid(a)||a[1]requested[1]+1||(a[1]==requested[1]&&a!=requested))return false; + requested=a;for(auto v:requested)response.add_data(v);return true; + } + bool State(const gz::msgs::Empty &,gz::msgs::Double_V &response){ + std::lock_guard lock(mutex); + for(auto v:applied)response.add_data(v); + response.add_data(ready?1:0);response.add_data(cameras);response.add_data(failed?1:0);return true; + } + void PostUpdate(const gz::sim::UpdateInfo &info,const gz::sim::EntityComponentManager &) override { + std::lock_guard lock(mutex);time=std::chrono::duration(info.simTime).count(); + } + void Render(){ + if(failed)return; + auto scene=gz::rendering::sceneFromFirstRenderEngine();if(!scene)return; + try{ + if(scene->Engine()->Name()!="ogre2")throw std::runtime_error("underwater_requires_ogre2"); + if(!renderer){ +#ifdef _WIN32 + library=LoadLibraryA(path.c_str()); + auto symbol=[this](const char *name){return reinterpret_cast(GetProcAddress(static_cast(library),name));}; +#else + library=dlopen(path.c_str(),RTLD_NOW|RTLD_LOCAL); + auto symbol=[this](const char *name){return dlsym(library,name);}; +#endif + if(!library)throw std::runtime_error("underwater_render_library_unavailable"); + create=reinterpret_cast(symbol("posimCreateUnderwater")); + update=reinterpret_cast(symbol("posimUpdateUnderwater")); + destroy=reinterpret_cast(symbol("posimDestroyUnderwater")); + if(!create||!update||!destroy)throw std::runtime_error("underwater_render_abi_mismatch"); + renderer=create(pieces.c_str()); + } + Appearance a;double t; + {std::lock_guard lock(mutex);a=requested;t=time;} + auto count=update(renderer,scene,a,t); + {std::lock_guard lock(mutex);staged=a;stagedCameras=count;stagedReady=true;} + }catch(const std::exception &error){ + std::lock_guard lock(mutex);failed=true;ready=false;gzerr<<"Underwater disabled: "< + + posim_gz_rendering0.1.0 + Shared underwater surface optics for Gazebo Ogre2 cameras. + Woen-Sug Choi + Apache-2.0 + cmake + gz_sim_vendorgz_rendering_vendorgz_plugin_vendor + cmake + diff --git a/gazebo/posim_gz_rendering/shaders/Underwater_piece_vs_piece_ps.any b/gazebo/posim_gz_rendering/shaders/Underwater_piece_vs_piece_ps.any new file mode 100644 index 00000000..5e013b6d --- /dev/null +++ b/gazebo/posim_gz_rendering/shaders/Underwater_piece_vs_piece_ps.any @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// PBR surface optics: RGB and RGBD colour share the same linear radiance path. +@pset( custom_disable_directional_lights, 1 ) +@property( !hlms_shadowcaster ) +@undefpiece( custom_passBuffer ) +@piece( custom_passBuffer ) + @insertpiece( custom_passBuffer_spherical_clip_min_distance ) + @insertpiece( custom_passBuffer_terra ) + float4 uwInverse0; float4 uwInverse1; float4 uwInverse2; + float4 uwControls; float4 uwOptics; float4 uwParams; + float4 uwCoefficients; float4 uwScatter; +@end +@piece( custom_ps_posMaterialLoad ) + float3 uwWorld = float3(dot(passBuf.uwInverse0,float4(inPs.pos,1.0)),dot(passBuf.uwInverse1,float4(inPs.pos,1.0)),dot(passBuf.uwInverse2,float4(inPs.pos,1.0))); + float uwDepth = max(0.0,passBuf.uwCoefficients.w-uwWorld.z); + float uwSeabed = abs(material.userValue[0].x-6421.0)<0.5 && uwDepth>0.0 ? 1.0 : 0.0; +@end +@piece( custom_ps_posSampleNormal ) + if(uwSeabed>0.5){ + if(passBuf.uwControls.w<0.5){ + pixelData.diffuse.xyz=float3(0.32,0.27,0.19)*0.318309886; + @property( normal_map )pixelData.normal=float3(0.0,0.0,1.0);@end + } + float2 cell=uwWorld.xy/passBuf.uwParams.x; + float2 footprint=max(abs(OGRE_ddx(cell))+abs(OGRE_ddy(cell)),float2(0.00001,0.00001)); + float2 edge=abs(fract(cell-0.5)-0.5)/footprint; + float grid=(1.0-smoothstep(0.35,1.2,min(edge.x,edge.y)))*(1.0-smoothstep(0.25,0.6,max(footprint.x,footprint.y))); + float level=uwWorld.z/passBuf.uwParams.y; + float fw=max(abs(OGRE_ddx(level))+abs(OGRE_ddy(level)),0.00001); + float contour=(1.0-smoothstep(0.45*fw,1.35*fw,abs(fract(level-0.5)-0.5)))*(1.0-smoothstep(0.25,0.6,fw)); + pixelData.diffuse.xyz*=1.0-max(grid*passBuf.uwControls.y*0.72,contour*passBuf.uwControls.z*0.88); + } +@end +@undefpiece( custom_ps_preLights ) +@piece( custom_ps_preLights ) + @property( terra_enabled ) + @property( hlms_lights_directional && hlms_num_shadow_map_lights )fShadow *= inPs.terrainShadow; + @else float fShadow = inPs.terrainShadow;@end + @end + float3 uwAmbient=finalColour; + finalColour=float3(0.0,0.0,0.0); + @insertpiece( DoDirectionalLights ) + if(passBuf.uwControls.x>0.5 && uwDepth>0.0){ + if(passBuf.uwOptics.z>0.5) finalColour*=exp(-passBuf.uwCoefficients.xyz*uwDepth); + if(passBuf.uwOptics.w>0.5 && uwSeabed>0.5){ + float2 p=uwWorld.xy*1.4; + float t=passBuf.uwParams.z*0.35; + float a=sin(p.x+sin(p.y*1.3+t)*1.5+t); + float b=sin(p.y+sin(p.x*1.1-t)*1.5-t); + float caustic=pow(max(0.0,1.0-abs(a+b)*0.65),8.0); + finalColour*=1.0+caustic*0.45*exp(-uwDepth*0.12); + } + } + finalColour+=uwAmbient; +@end +@undefpiece( custom_ps_posExecution ) +@piece( custom_ps_posExecution ) + @property( !hlms_render_depth_only && !hlms_prepass && (hlms_normal || hlms_qtangent) ) + if(passBuf.uwControls.x>0.5){ + float cameraZ=passBuf.uwInverse2.w-passBuf.uwCoefficients.w; + float targetZ=uwWorld.z-passBuf.uwCoefficients.w; + float distance=length(inPs.pos); + float fraction=0.0; + if(cameraZ<=0.0 && targetZ<=0.0)fraction=1.0; + else if(cameraZ*targetZ<0.0)fraction=(cameraZ<0.0?-cameraZ:-targetZ)/max(abs(targetZ-cameraZ),0.00001); + float water=distance*fraction; + float3 attenuation=passBuf.uwOptics.y>0.5?passBuf.uwCoefficients.xyz:float3(0.06,0.06,0.06); + if(passBuf.uwOptics.x<0.5 && passBuf.uwOptics.y<0.5)attenuation=float3(0.0,0.0,0.0); + float3 transmission=exp(-water*attenuation); + // Three cheap, world-anchored dust slices; visual approximation, no sensor noise. + float specks=0.0; + if(passBuf.uwParams.w>0.5 && cameraZ<0.0){ + float3 origin=float3(passBuf.uwInverse0.w,passBuf.uwInverse1.w,passBuf.uwInverse2.w); + float3 ray=(uwWorld-origin)/max(distance,0.0001); + for(int layer=1;layer<=3;++layer){ + float travel=float(layer)*0.8; + float3 point=origin+ray*travel; + if(travel0.97?(1.0-smoothstep(0.015,0.015+radius,length(delta)))*0.035:0.0; + } + } + } + float3 radiance=outPs_colour0.xyz; + @property( !hw_gamma_write )radiance*=radiance;@end + radiance=radiance*transmission+passBuf.uwScatter.xyz*(1.0-transmission)*passBuf.uwOptics.x; + radiance+=float3(specks,specks,specks); + @property( !hw_gamma_write )radiance=sqrt(max(radiance,float3(0.0,0.0,0.0)));@end + outPs_colour0.xyz=radiance; + } + @end + @insertpiece( custom_ps_posExecution_solid_color ) +@end +@end diff --git a/gazebo/posim_gz_rendering/tests/AppearanceTest.cc b/gazebo/posim_gz_rendering/tests/AppearanceTest.cc new file mode 100644 index 00000000..4c5dfd37 --- /dev/null +++ b/gazebo/posim_gz_rendering/tests/AppearanceTest.cc @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Appearance.hh" +int main(){ + auto a=posim::Defaults();if(!posim::Valid(a))return 1; + for(unsigned i=2;i<=10;++i){auto bad=a;bad[i]=0.5;if(posim::Valid(bad))return 2;} + for(auto bad:{-1.,0.5,9007199254740992.}){a[1]=bad;if(posim::Valid(a))return 3;} + if(posim::WaterPath(-2,-8,10)!=10||posim::WaterPath(2,8,10)!=0||posim::WaterPath(2,-8,10)!=8||posim::WaterPath(-2,8,10)!=2)return 4; + return 0; +} diff --git a/gazebo/posim_gz_rendering/tests/RenderTest.cc b/gazebo/posim_gz_rendering/tests/RenderTest.cc new file mode 100644 index 00000000..25913a64 --- /dev/null +++ b/gazebo/posim_gz_rendering/tests/RenderTest.cc @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "UnderwaterRenderer.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +int main(int argc,char **argv){ + if(argc!=4)return 2; +#ifdef __APPLE__ + auto engine=gz::rendering::engine("ogre2",{{"metal","1"}}); +#else + auto engine=gz::rendering::engine("ogre2",{{"headless","1"}}); +#endif + if(!engine)return 3; + auto scene=engine->CreateScene("underwater-test"); + scene->SetAmbientLight(.4,.4,.4);scene->SetBackgroundColor(.01,.03,.04); + auto light=scene->CreateDirectionalLight();light->SetDirection(.3,.3,-1);light->SetDiffuseColor(1,1,1);scene->RootVisual()->AddChild(light); + auto seabed=scene->CreateVisual("seabed");seabed->AddGeometry(scene->CreateBox());seabed->SetLocalScale(100,100,.2);seabed->SetLocalPosition(0,0,-8); + auto sand=scene->CreateMaterial();sand->SetDiffuse(.6,.5,.3);sand->SetRoughness(.9);sand->SetTexture(std::string(argv[3])+"/aerial_beach_01_diff_2k.jpg");sand->SetNormalMap(std::string(argv[3])+"/aerial_beach_01_nor_gl_2k.jpg");seabed->SetMaterial(sand);scene->RootVisual()->AddChild(seabed); + auto target=scene->CreateVisual();target->AddGeometry(scene->CreateBox());target->SetLocalPosition(5,0,-6);auto red=scene->CreateMaterial();red->SetDiffuse(1,.05,.05);target->SetMaterial(red);scene->RootVisual()->AddChild(target); + auto camera=scene->CreateCamera();camera->SetImageWidth(640);camera->SetImageHeight(360);camera->SetHFOV(1.05);camera->SetAspectRatio(640./360);camera->SetLocalPosition(-2,0,-3);camera->SetLocalRotation(0,.3,0);scene->RootVisual()->AddChild(camera); + auto depth=scene->CreateDepthCamera();depth->SetImageWidth(640);depth->SetImageHeight(360);depth->SetHFOV(1.05);depth->SetAspectRatio(640./360);depth->SetNearClipPlane(.1);depth->SetFarClipPlane(100);depth->SetLocalPose(camera->LocalPose());scene->RootVisual()->AddChild(depth); + std::vector measured,baseline;std::vector rgb(640*360*3); + auto connection=depth->ConnectNewDepthFrame([&](const float *data,unsigned w,unsigned h,unsigned,const std::string &){measured.assign(data,data+w*h);}); + auto colors=depth->ConnectNewRgbPointCloud([&](const float *data,unsigned w,unsigned h,unsigned,const std::string &){for(unsigned i=0;i>24)&255;rgb[i*3+1]=(c>>16)&255;rgb[i*3+2]=(c>>8)&255;}}); + camera->Update();depth->Update(); + if(measured.empty())return 4;baseline=measured; + { + posim::UnderwaterRenderer renderer(argv[1]);auto settings=posim::Defaults(); + std::vector untreated,waterOnly; + for(int mode=0;mode<5;++mode){ + settings[2]=mode==0?0:1;settings[4]=mode==2?1:0;settings[10]=mode==3?1:0;settings[3]=mode==4?0:1;settings[11]=5; + renderer.Update(scene,settings,10.); + auto image=camera->CreateImage();for(int i=0;i<3;++i)camera->Capture(image); + std::ofstream file(std::string(argv[2])+"/native-"+std::to_string(mode)+".ppm",std::ios::binary);file<<"P6\n640 360\n255\n";file.write(reinterpret_cast(image.Data()),640*360*3); + depth->Update(); + if(measured.size()!=baseline.size()||std::memcmp(measured.data(),baseline.data(),baseline.size()*sizeof(float)))return 5; + auto pixels=image.Data();unsigned long delta=0,agreement=0; + for(size_t i=0;i0)delta+=std::abs(int(pixels[i])-int(mode==1?untreated[i]:waterOnly[i])); + } + if(agreement>rgb.size()*2)return 6; + if(mode==0)untreated.assign(pixels,pixels+rgb.size()); + if(mode==1)waterOnly.assign(pixels,pixels+rgb.size()); + if(mode>0&&delta<100)return 7; + std::ofstream rgbFile(std::string(argv[2])+"/rgbd-"+std::to_string(mode)+".ppm",std::ios::binary);rgbFile<<"P6\n640 360\n255\n";rgbFile.write(reinterpret_cast(rgb.data()),rgb.size()); + auto start=std::chrono::steady_clock::now();for(int i=0;i<40;++i){camera->Capture(image);depth->Update();} + std::cout<<"mode "<(std::chrono::steady_clock::now()-start).count()/40<<" depth unchanged"<DestroyScene(scene);return 0; +} diff --git a/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc b/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc index 26886229..0a167823 100644 --- a/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc +++ b/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc @@ -15,6 +15,7 @@ */ #include "posim_gz_sensor_plugins/UnderwaterCamera.hh" +#include #include #include #include @@ -468,6 +469,12 @@ void UnderwaterCamera::DepthImageCallback(const gz::msgs::Image & msg) cv::Mat UnderwaterCamera::SimulateUnderwater( const cv::Mat & _inputImage, const cv::Mat & _inputDepth, cv::Mat & _outputImage) { + // The scene renderer already applies optics to the raw RGB topic. Preserve + // this compatibility output without attenuating the same pixels twice. + if (std::getenv("POSIM_UNDERWATER_PLUGIN")) { + _inputImage.copyTo(_outputImage); + return _outputImage; + } const float * lutPtr = this->dataPtr->depth2rangeLUT; for (unsigned int row = 0; row < this->dataPtr->height; row++) { diff --git a/website/content/en/camera.md b/website/content/en/camera.md index 23db4dac..db0b92f8 100644 --- a/website/content/en/camera.md +++ b/website/content/en/camera.md @@ -4,6 +4,10 @@ The underwater-camera plugin applies distance-dependent color attenuation to ren ## Run the example +The [POSIM underwater renderer](https://github.com/IOES-Lab/POSIM/tree/main/gazebo/posim_gz_rendering) provides shared scene optics for Ogre2 PBR surfaces. It applies water-path haze, color absorption, depth-dependent sunlight and animated seabed light patterns to RGB cameras and RGBD color. Numeric depth remains unchanged. WWW-POSIM exposes live switches for the Gazebo scene and camera feeds. + +The renderer approximates mean sea level by z = 0 m. Unlit materials, custom water shaders and the sky use their own rendering. When the host enables the native renderer with `POSIM_UNDERWATER_PLUGIN`, the camera plugin passes its already-colored input through to the compatibility output. + ```bash ros2 launch posim_demos posim_sensor.launch.py \ namespace:=underwater_camera world_name:=camera_tutorial \ diff --git a/website/content/ko/camera.md b/website/content/ko/camera.md index a0d6a18d..dd5d062c 100644 --- a/website/content/ko/camera.md +++ b/website/content/ko/camera.md @@ -4,6 +4,10 @@ ## 예제 실행 +[POSIM 수중 렌더러](https://github.com/IOES-Lab/POSIM/tree/main/gazebo/posim_gz_rendering)는 Ogre2 PBR 표면의 광학 효과를 공유합니다. 수중 경로 안개, 색상 흡수, 수심에 따른 햇빛 감쇠와 움직이는 해저면 햇빛 무늬를 RGB 카메라와 RGBD 컬러 영상에 적용합니다. 수치 깊이는 유지합니다. WWW-POSIM에서는 Gazebo 장면과 카메라 영상에 적용할 효과를 실시간 스위치로 바꿉니다. + +렌더러는 평균 해수면을 z = 0 m로 근사합니다. 비조명 재질·사용자 수면 셰이더·하늘은 자체 렌더링을 사용합니다. 호스트에서 `POSIM_UNDERWATER_PLUGIN`으로 네이티브 렌더러를 활성화하면 카메라 플러그인은 이미 색상이 적용된 입력을 호환 출력으로 그대로 전달합니다. + ```bash ros2 launch posim_demos posim_sensor.launch.py \ namespace:=underwater_camera world_name:=camera_tutorial \ From b979dba252703089e931e6c5f9e6d19b0b04ba04 Mon Sep 17 00:00:00 2001 From: ioes Date: Wed, 21 Oct 2026 11:55:39 +0900 Subject: [PATCH 2/5] Match exact seabed visual names including unscoped shallow surfaces --- gazebo/posim_gz_rendering/UnderwaterRenderer.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc index de198bac..70285f06 100644 --- a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc @@ -70,7 +70,9 @@ void UnderwaterRenderer::Update(const gz::rendering::ScenePtr &scene,const Appea if(std::dynamic_pointer_cast(scene->SensorByIndex(i)))++impl->cameras; for(unsigned i=0;iVisualCount();++i){ auto v=scene->VisualByIndex(i);const auto &name=v->Name(); - const bool seabed=name=="seabed"||name.find("::seabed")!=std::string::npos||name.find("::shallow")!=std::string::npos; + const auto scope=name.rfind("::"); + const auto leaf=name.substr(scope==std::string::npos?0:scope+2); + const bool seabed=leaf=="seabed"||leaf=="shallow"; if(!seabed)continue; for(unsigned g=0;gGeometryCount();++g){ auto m=std::dynamic_pointer_cast(v->GeometryByIndex(g)->Material()); From dd678a1ccc255a6849adaa2993a8fb18087cb2cf Mon Sep 17 00:00:00 2001 From: ioes Date: Wed, 21 Oct 2026 12:10:45 +0900 Subject: [PATCH 3/5] Apply repository C++ and CMake formatting to underwater renderer --- gazebo/posim_gz_rendering/Appearance.hh | 53 ++- gazebo/posim_gz_rendering/CMakeLists.txt | 41 ++- .../posim_gz_rendering/UnderwaterRenderer.cc | 274 +++++++++++----- .../posim_gz_rendering/UnderwaterRenderer.hh | 25 +- gazebo/posim_gz_rendering/UnderwaterSystem.cc | 308 ++++++++++++------ .../tests/AppearanceTest.cc | 37 ++- gazebo/posim_gz_rendering/tests/RenderTest.cc | 215 +++++++++--- .../src/UnderwaterCamera.cc | 3 +- 8 files changed, 685 insertions(+), 271 deletions(-) diff --git a/gazebo/posim_gz_rendering/Appearance.hh b/gazebo/posim_gz_rendering/Appearance.hh index f8a25fa5..395e7617 100644 --- a/gazebo/posim_gz_rendering/Appearance.hh +++ b/gazebo/posim_gz_rendering/Appearance.hh @@ -3,20 +3,47 @@ #include #include #include -namespace posim { +namespace posim +{ // Stable wire order; schema and revision precede user settings. using Appearance = std::array; -inline Appearance Defaults(){return {1,0,1,1,0,0,1,1,1,1,0,50,5};} -inline bool Valid(const Appearance &a){ - for(auto v:a)if(!std::isfinite(v))return false; - if(a[0]!=1||a[1]<0||a[1]>9007199254740991.||std::floor(a[1])!=a[1])return false; - for(unsigned i=2;i<=10;++i)if(a[i]!=0&&a[i]!=1)return false; - return a[11]>=1&&a[11]<=500&&a[12]>=1&&a[12]<=50; -} -inline double WaterPath(double cameraZ,double surfaceZ,double distance){ - if(distance<=0)return 0; - if(cameraZ<=0&&surfaceZ<=0)return distance; - if(cameraZ>=0&&surfaceZ>=0)return 0; - return distance*(cameraZ<0?-cameraZ: -surfaceZ)/std::abs(surfaceZ-cameraZ); +inline Appearance Defaults() { return {1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 50, 5}; } +inline bool Valid(const Appearance & a) +{ + for (auto v : a) + { + if (!std::isfinite(v)) + { + return false; + } + } + if (a[0] != 1 || a[1] < 0 || a[1] > 9007199254740991. || std::floor(a[1]) != a[1]) + { + return false; + } + for (unsigned i = 2; i <= 10; ++i) + { + if (a[i] != 0 && a[i] != 1) + { + return false; + } + } + return a[11] >= 1 && a[11] <= 500 && a[12] >= 1 && a[12] <= 50; } +inline double WaterPath(double cameraZ, double surfaceZ, double distance) +{ + if (distance <= 0) + { + return 0; + } + if (cameraZ <= 0 && surfaceZ <= 0) + { + return distance; + } + if (cameraZ >= 0 && surfaceZ >= 0) + { + return 0; + } + return distance * (cameraZ < 0 ? -cameraZ : -surfaceZ) / std::abs(surfaceZ - cameraZ); } +} // namespace posim diff --git a/gazebo/posim_gz_rendering/CMakeLists.txt b/gazebo/posim_gz_rendering/CMakeLists.txt index 0cf81c00..060fb9a3 100644 --- a/gazebo/posim_gz_rendering/CMakeLists.txt +++ b/gazebo/posim_gz_rendering/CMakeLists.txt @@ -6,26 +6,39 @@ find_package(gz-plugin REQUIRED COMPONENTS register) find_package(PkgConfig REQUIRED) pkg_check_modules(OGRE REQUIRED IMPORTED_TARGET OGRE-Next>=2.3) pkg_check_modules(HLMS REQUIRED IMPORTED_TARGET OGRE-Next-Hlms) -find_path(HLMS_COMMON OgreHlmsTextureBaseClass.h HINTS ${HLMS_INCLUDE_DIRS} ${OGRE_INCLUDE_DIRS} - PATH_SUFFIXES Common ../Common Hlms/Common include/OGRE-Next/Hlms/Common OGRE-Next/Hlms/Common REQUIRED) +find_path( + HLMS_COMMON OgreHlmsTextureBaseClass.h + HINTS ${HLMS_INCLUDE_DIRS} ${OGRE_INCLUDE_DIRS} + PATH_SUFFIXES Common ../Common Hlms/Common include/OGRE-Next/Hlms/Common + OGRE-Next/Hlms/Common REQUIRED) add_library(posim_underwater_render SHARED UnderwaterRenderer.cc) target_compile_features(posim_underwater_render PRIVATE cxx_std_17) target_include_directories(posim_underwater_render PRIVATE ${HLMS_COMMON}) -target_link_libraries(posim_underwater_render PRIVATE gz-rendering::gz-rendering gz-rendering::ogre2 PkgConfig::OGRE PkgConfig::HLMS) +target_link_libraries( + posim_underwater_render + PRIVATE gz-rendering::gz-rendering gz-rendering::ogre2 PkgConfig::OGRE + PkgConfig::HLMS) add_library(posim_underwater SHARED UnderwaterSystem.cc) target_compile_features(posim_underwater PRIVATE cxx_std_17) -target_link_libraries(posim_underwater PRIVATE gz-sim::gz-sim gz-plugin::register gz-rendering::gz-rendering ${CMAKE_DL_LIBS}) -set_target_properties(posim_underwater posim_underwater_render PROPERTIES PREFIX "") -install(TARGETS posim_underwater posim_underwater_render LIBRARY DESTINATION lib) +target_link_libraries( + posim_underwater PRIVATE gz-sim::gz-sim gz-plugin::register + gz-rendering::gz-rendering ${CMAKE_DL_LIBS}) +set_target_properties( + posim_underwater posim_underwater_render + PROPERTIES PREFIX "") +install(TARGETS posim_underwater posim_underwater_render + LIBRARY DESTINATION lib) install(DIRECTORY shaders/ DESTINATION share/posim/underwater) include(CTest) if(BUILD_TESTING) - add_executable(underwater-settings-test tests/AppearanceTest.cc) - target_compile_features(underwater-settings-test PRIVATE cxx_std_17) - target_include_directories(underwater-settings-test PRIVATE .) - add_test(NAME underwater-settings COMMAND underwater-settings-test) - add_executable(underwater-render-test tests/RenderTest.cc) - target_compile_features(underwater-render-test PRIVATE cxx_std_17) - target_include_directories(underwater-render-test PRIVATE .) - target_link_libraries(underwater-render-test PRIVATE posim_underwater_render gz-rendering::gz-rendering) + add_executable(underwater-settings-test tests/AppearanceTest.cc) + target_compile_features(underwater-settings-test PRIVATE cxx_std_17) + target_include_directories(underwater-settings-test PRIVATE .) + add_test(NAME underwater-settings COMMAND underwater-settings-test) + add_executable(underwater-render-test tests/RenderTest.cc) + target_compile_features(underwater-render-test PRIVATE cxx_std_17) + target_include_directories(underwater-render-test PRIVATE .) + target_link_libraries( + underwater-render-test PRIVATE posim_underwater_render + gz-rendering::gz-rendering) endif() diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc index 70285f06..9026ce84 100644 --- a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc @@ -1,88 +1,212 @@ // SPDX-License-Identifier: Apache-2.0 #include "UnderwaterRenderer.hh" -#include -#include -#include -#include -#include -#include -#include +#include +#include #include #include #include #include #include #include -#include -#include -namespace posim { -struct UnderwaterRenderer::Impl : Ogre::HlmsListener { - Ogre::Hlms *pbs=nullptr; - Ogre::ArchiveVec original; - Ogre::HlmsListener *previous=nullptr; - Appearance settings=Defaults(); - double time=0; - unsigned cameras=0; - explicit Impl(const std::string &pieces){ - auto root=gz::rendering::Ogre2RenderEngine::Instance()->OgreRoot(); - pbs=root->getHlmsManager()->getHlms(Ogre::HLMS_PBS); - if(!pbs)throw std::runtime_error("underwater_pbs_unavailable"); - previous=pbs->getListener(); - if(dynamic_cast(previous))throw std::runtime_error("underwater_already_installed"); - original=pbs->getPiecesLibraryAsArchiveVec();auto extended=original; - extended.push_back(Ogre::ArchiveManager::getSingleton().load(pieces,"FileSystem",true)); - pbs->setListener(this); - try {pbs->reloadFrom(pbs->getDataFolder(),&extended);} - catch(...){pbs->setListener(previous);throw;} - } - ~Impl() { - if(pbs&&pbs->getListener()==this){pbs->setListener(previous);pbs->reloadFrom(pbs->getDataFolder(),&original);} - } - void propertiesMergedPreGenerationStep(Ogre::Hlms *h,const Ogre::HlmsCache &c,const Ogre::HlmsPropertyVec &r,const Ogre::PiecesMap p[Ogre::NumShaderTypes],const Ogre::HlmsPropertyVec &v,const Ogre::QueuedRenderable &q) override {if(previous)previous->propertiesMergedPreGenerationStep(h,c,r,p,v,q);} - Ogre::uint16 getNumExtraPassTextures(const Ogre::HlmsPropertyVec &p,bool c) const override {return previous?previous->getNumExtraPassTextures(p,c):0;} - void setupRootLayout(Ogre::RootLayout &r,const Ogre::HlmsPropertyVec &p) const override {if(previous)previous->setupRootLayout(r,p);} - void shaderCacheEntryCreated(const Ogre::String &s,const Ogre::HlmsCache *h,const Ogre::HlmsCache &c,const Ogre::HlmsPropertyVec &p,const Ogre::QueuedRenderable &q) override {if(previous)previous->shaderCacheEntryCreated(s,h,c,p,q);} - void preparePassHash(const Ogre::CompositorShadowNode *s,bool c,bool d,Ogre::SceneManager *m,Ogre::Hlms *h) override {if(previous)previous->preparePassHash(s,c,d,m,h);} - void hlmsTypeChanged(bool c,Ogre::CommandBuffer *b,const Ogre::HlmsDatablock *d,size_t t) override {if(previous)previous->hlmsTypeChanged(c,b,d,t);} - Ogre::uint32 getPassBufferSize(const Ogre::CompositorShadowNode *s,bool c,bool d, - Ogre::SceneManager *m) const override {return (previous?previous->getPassBufferSize(s,c,d,m):0)+(c?0:128);} - float *preparePassBuffer(const Ogre::CompositorShadowNode *s,bool caster,bool d, - Ogre::SceneManager *scene,float *out) override { - if(previous)out=previous->preparePassBuffer(s,caster,d,scene,out); - if(caster)return out; - auto camera=scene->getCamerasInProgress().renderingCamera; - auto inverse=camera->getViewMatrix(true).inverse(); - for(unsigned i=0;i<3;++i)for(unsigned j=0;j<4;++j)*out++=inverse[i][j]; - for(auto i:{2,4,5,3})*out++=settings[i]; - for(auto i:{6,7,8,9})*out++=settings[i]; - *out++=settings[11];*out++=settings[12];*out++=time;*out++=settings[10]; - for(float v:{.18f,.09f,.06f,0.f})*out++=v; - for(float v:{.006f,.055f,.065f,1.f})*out++=v; - return out; - } +#include +#include +#include +#include +#include +#include +#include +namespace posim +{ +struct UnderwaterRenderer::Impl : Ogre::HlmsListener +{ + Ogre::Hlms * pbs = nullptr; + Ogre::ArchiveVec original; + Ogre::HlmsListener * previous = nullptr; + Appearance settings = Defaults(); + double time = 0; + unsigned cameras = 0; + explicit Impl(const std::string & pieces) + { + auto root = gz::rendering::Ogre2RenderEngine::Instance()->OgreRoot(); + pbs = root->getHlmsManager()->getHlms(Ogre::HLMS_PBS); + if (!pbs) + { + throw std::runtime_error("underwater_pbs_unavailable"); + } + previous = pbs->getListener(); + if (dynamic_cast(previous)) + { + throw std::runtime_error("underwater_already_installed"); + } + original = pbs->getPiecesLibraryAsArchiveVec(); + auto extended = original; + extended.push_back(Ogre::ArchiveManager::getSingleton().load(pieces, "FileSystem", true)); + pbs->setListener(this); + try + { + pbs->reloadFrom(pbs->getDataFolder(), &extended); + } + catch (...) + { + pbs->setListener(previous); + throw; + } + } + ~Impl() + { + if (pbs && pbs->getListener() == this) + { + pbs->setListener(previous); + pbs->reloadFrom(pbs->getDataFolder(), &original); + } + } + void propertiesMergedPreGenerationStep( + Ogre::Hlms * h, const Ogre::HlmsCache & c, const Ogre::HlmsPropertyVec & r, + const Ogre::PiecesMap p[Ogre::NumShaderTypes], const Ogre::HlmsPropertyVec & v, + const Ogre::QueuedRenderable & q) override + { + if (previous) + { + previous->propertiesMergedPreGenerationStep(h, c, r, p, v, q); + } + } + Ogre::uint16 getNumExtraPassTextures(const Ogre::HlmsPropertyVec & p, bool c) const override + { + return previous ? previous->getNumExtraPassTextures(p, c) : 0; + } + void setupRootLayout(Ogre::RootLayout & r, const Ogre::HlmsPropertyVec & p) const override + { + if (previous) + { + previous->setupRootLayout(r, p); + } + } + void shaderCacheEntryCreated( + const Ogre::String & s, const Ogre::HlmsCache * h, const Ogre::HlmsCache & c, + const Ogre::HlmsPropertyVec & p, const Ogre::QueuedRenderable & q) override + { + if (previous) + { + previous->shaderCacheEntryCreated(s, h, c, p, q); + } + } + void preparePassHash( + const Ogre::CompositorShadowNode * s, bool c, bool d, Ogre::SceneManager * m, + Ogre::Hlms * h) override + { + if (previous) + { + previous->preparePassHash(s, c, d, m, h); + } + } + void hlmsTypeChanged( + bool c, Ogre::CommandBuffer * b, const Ogre::HlmsDatablock * d, size_t t) override + { + if (previous) + { + previous->hlmsTypeChanged(c, b, d, t); + } + } + Ogre::uint32 getPassBufferSize( + const Ogre::CompositorShadowNode * s, bool c, bool d, Ogre::SceneManager * m) const override + { + return (previous ? previous->getPassBufferSize(s, c, d, m) : 0) + (c ? 0 : 128); + } + float * preparePassBuffer( + const Ogre::CompositorShadowNode * s, bool caster, bool d, Ogre::SceneManager * scene, + float * out) override + { + if (previous) + { + out = previous->preparePassBuffer(s, caster, d, scene, out); + } + if (caster) + { + return out; + } + auto camera = scene->getCamerasInProgress().renderingCamera; + auto inverse = camera->getViewMatrix(true).inverse(); + for (unsigned i = 0; i < 3; ++i) + { + for (unsigned j = 0; j < 4; ++j) + { + *out++ = inverse[i][j]; + } + } + for (auto i : {2, 4, 5, 3}) + { + *out++ = settings[i]; + } + for (auto i : {6, 7, 8, 9}) + { + *out++ = settings[i]; + } + *out++ = settings[11]; + *out++ = settings[12]; + *out++ = time; + *out++ = settings[10]; + for (float v : {.18f, .09f, .06f, 0.f}) + { + *out++ = v; + } + for (float v : {.006f, .055f, .065f, 1.f}) + { + *out++ = v; + } + return out; + } }; -UnderwaterRenderer::UnderwaterRenderer(const std::string &pieces):impl(new Impl(pieces)){} -UnderwaterRenderer::~UnderwaterRenderer()=default; -unsigned UnderwaterRenderer::Cameras() const {return impl->cameras;} -void UnderwaterRenderer::Update(const gz::rendering::ScenePtr &scene,const Appearance &settings,double time){ - impl->settings=settings;impl->time=time;impl->cameras=0; - for(unsigned i=0;iSensorCount();++i) - if(std::dynamic_pointer_cast(scene->SensorByIndex(i)))++impl->cameras; - for(unsigned i=0;iVisualCount();++i){ - auto v=scene->VisualByIndex(i);const auto &name=v->Name(); - const auto scope=name.rfind("::"); - const auto leaf=name.substr(scope==std::string::npos?0:scope+2); - const bool seabed=leaf=="seabed"||leaf=="shallow"; - if(!seabed)continue; - for(unsigned g=0;gGeometryCount();++g){ - auto m=std::dynamic_pointer_cast(v->GeometryByIndex(g)->Material()); - if(m&&m->Datablock()&&m->Datablock()->getUserValue(0).x!=6421)m->Datablock()->setUserValue(0,Ogre::Vector4(6421,0,0,0)); +UnderwaterRenderer::UnderwaterRenderer(const std::string & pieces) : impl(new Impl(pieces)) {} +UnderwaterRenderer::~UnderwaterRenderer() = default; +unsigned UnderwaterRenderer::Cameras() const { return impl->cameras; } +void UnderwaterRenderer::Update( + const gz::rendering::ScenePtr & scene, const Appearance & settings, double time) +{ + impl->settings = settings; + impl->time = time; + impl->cameras = 0; + for (unsigned i = 0; i < scene->SensorCount(); ++i) + { + if (std::dynamic_pointer_cast(scene->SensorByIndex(i))) + { + ++impl->cameras; + } + } + for (unsigned i = 0; i < scene->VisualCount(); ++i) + { + auto v = scene->VisualByIndex(i); + const auto & name = v->Name(); + const auto scope = name.rfind("::"); + const auto leaf = name.substr(scope == std::string::npos ? 0 : scope + 2); + const bool seabed = leaf == "seabed" || leaf == "shallow"; + if (!seabed) + { + continue; + } + for (unsigned g = 0; g < v->GeometryCount(); ++g) + { + auto m = + std::dynamic_pointer_cast(v->GeometryByIndex(g)->Material()); + if (m && m->Datablock() && m->Datablock()->getUserValue(0).x != 6421) + { + m->Datablock()->setUserValue(0, Ogre::Vector4(6421, 0, 0, 0)); + } + } } - } } +} // namespace posim +extern "C" void * posimCreateUnderwater(const char * path) +{ + return new posim::UnderwaterRenderer(path); +} +extern "C" unsigned posimUpdateUnderwater( + void * p, const gz::rendering::ScenePtr & s, const posim::Appearance & a, double t) +{ + auto r = static_cast(p); + r->Update(s, a, t); + return r->Cameras(); } -extern "C" void *posimCreateUnderwater(const char *path){return new posim::UnderwaterRenderer(path);} -extern "C" unsigned posimUpdateUnderwater(void *p,const gz::rendering::ScenePtr &s,const posim::Appearance &a,double t){ - auto r=static_cast(p);r->Update(s,a,t);return r->Cameras(); +extern "C" void posimDestroyUnderwater(void * p) +{ + delete static_cast(p); } -extern "C" void posimDestroyUnderwater(void *p){delete static_cast(p);} diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.hh b/gazebo/posim_gz_rendering/UnderwaterRenderer.hh index 49d1f77c..3ae12871 100644 --- a/gazebo/posim_gz_rendering/UnderwaterRenderer.hh +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.hh @@ -1,17 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include #include #include -#include #include "Appearance.hh" -namespace posim { -class UnderwaterRenderer { - struct Impl; - std::unique_ptr impl; - public: - explicit UnderwaterRenderer(const std::string &pieces); - ~UnderwaterRenderer(); - void Update(const gz::rendering::ScenePtr &, const Appearance &, double simulationTime); - unsigned Cameras() const; +namespace posim +{ +class UnderwaterRenderer +{ + struct Impl; + std::unique_ptr impl; + +public: + explicit UnderwaterRenderer(const std::string & pieces); + ~UnderwaterRenderer(); + void Update(const gz::rendering::ScenePtr &, const Appearance &, double simulationTime); + unsigned Cameras() const; }; -} +} // namespace posim diff --git a/gazebo/posim_gz_rendering/UnderwaterSystem.cc b/gazebo/posim_gz_rendering/UnderwaterSystem.cc index 6df2c2c1..159501a4 100644 --- a/gazebo/posim_gz_rendering/UnderwaterSystem.cc +++ b/gazebo/posim_gz_rendering/UnderwaterSystem.cc @@ -1,121 +1,233 @@ // SPDX-License-Identifier: Apache-2.0 -#include "Appearance.hh" +#include +#include #include #include -#include -#include +#include #include -#include -#include +#include #include #include -#include +#include +#include #include -#include -#include -#include +#include +#include +#include "Appearance.hh" #ifdef _WIN32 #include #else #include #endif -namespace posim { -class Underwater final : public gz::sim::System,public gz::sim::ISystemConfigure,public gz::sim::ISystemPostUpdate { - std::mutex mutex; - Appearance requested=Defaults(),applied=Defaults(),staged=Defaults(); - bool stagedReady=false; - unsigned stagedCameras=0; - double time=0; - unsigned cameras=0; - bool ready=false,failed=false; - void *library=nullptr,*renderer=nullptr; - using Create=void *(*)(const char *); - using Update=unsigned (*)(void *,const gz::rendering::ScenePtr &,const Appearance &,double); - using Destroy=void (*)(void *); - Create create=nullptr;Update update=nullptr;Destroy destroy=nullptr; - std::string path,pieces; - gz::transport::Node node; - gz::common::ConnectionPtr renderConnection,postRenderConnection,teardownConnection; - void Close(){if(renderer&&destroy){destroy(renderer);renderer=nullptr;}} - public: - ~Underwater() override { - renderConnection.reset();postRenderConnection.reset();teardownConnection.reset(); - // The rendering resources are destroyed by RenderTeardown, on their owner thread. - // Keep the helper mapped until process exit if the engine omitted teardown. - if(!renderer&&library){ +namespace posim +{ +class Underwater final : public gz::sim::System, + public gz::sim::ISystemConfigure, + public gz::sim::ISystemPostUpdate +{ + std::mutex mutex; + Appearance requested = Defaults(), applied = Defaults(), staged = Defaults(); + bool stagedReady = false; + unsigned stagedCameras = 0; + double time = 0; + unsigned cameras = 0; + bool ready = false, failed = false; + void *library = nullptr, *renderer = nullptr; + using Create = void * (*)(const char *); + using Update = unsigned (*)(void *, const gz::rendering::ScenePtr &, const Appearance &, double); + using Destroy = void (*)(void *); + Create create = nullptr; + Update update = nullptr; + Destroy destroy = nullptr; + std::string path, pieces; + gz::transport::Node node; + gz::common::ConnectionPtr renderConnection, postRenderConnection, teardownConnection; + void Close() + { + if (renderer && destroy) + { + destroy(renderer); + renderer = nullptr; + } + } + +public: + ~Underwater() override + { + renderConnection.reset(); + postRenderConnection.reset(); + teardownConnection.reset(); + // The rendering resources are destroyed by RenderTeardown, on their owner thread. + // Keep the helper mapped until process exit if the engine omitted teardown. + if (!renderer && library) + { #ifdef _WIN32 - FreeLibrary(static_cast(library)); + FreeLibrary(static_cast(library)); #else - dlclose(library); + dlclose(library); #endif + } + } + void Configure( + const gz::sim::Entity &, const std::shared_ptr & sdf, + gz::sim::EntityComponentManager &, gz::sim::EventManager & events) override + { + if ( + !sdf->HasElement("renderer_library") || !sdf->HasElement("shader_directory") || + !sdf->HasElement("session_nonce")) + { + gzerr << "Underwater requires renderer_library, shader_directory and session_nonce\n"; + return; + } + path = sdf->Get("renderer_library"); + pieces = sdf->Get("shader_directory"); + if (sdf->HasElement("initial_settings")) + { + std::istringstream stream(sdf->Get("initial_settings")); + Appearance initial; + for (auto & v : initial) + { + if (!(stream >> v)) + { + throw std::runtime_error("invalid_initial_appearance"); + } + } + if (!Valid(initial)) + { + throw std::runtime_error("invalid_initial_appearance"); + } + requested = applied = initial; + } + auto service = "/posim/appearance/" + sdf->Get("session_nonce"); + node.Advertise(service + "/set", &Underwater::Set, this); + node.Advertise(service + "/state", &Underwater::State, this); + renderConnection = events.Connect([this] { Render(); }); + postRenderConnection = events.Connect( + [this] + { + std::lock_guard lock(mutex); + if (stagedReady && !failed) + { + applied = staged; + cameras = stagedCameras; + ready = cameras > 0; + stagedReady = false; + } + }); + teardownConnection = events.Connect( + [this] + { + Close(); + std::lock_guard lock(mutex); + ready = false; + }); + } + bool Set(const gz::msgs::Double_V & request, gz::msgs::Double_V & response) + { + std::lock_guard lock(mutex); + if (request.data_size() != 13) + { + return false; + } + Appearance a; + std::copy(request.data().begin(), request.data().end(), a.begin()); + if ( + !Valid(a) || a[1] < requested[1] || a[1] > requested[1] + 1 || + (a[1] == requested[1] && a != requested)) + { + return false; + } + requested = a; + for (auto v : requested) + { + response.add_data(v); + } + return true; } - } - void Configure(const gz::sim::Entity &,const std::shared_ptr &sdf, - gz::sim::EntityComponentManager &,gz::sim::EventManager &events) override { - if(!sdf->HasElement("renderer_library")||!sdf->HasElement("shader_directory")||!sdf->HasElement("session_nonce")){ - gzerr<<"Underwater requires renderer_library, shader_directory and session_nonce\n";return; + bool State(const gz::msgs::Empty &, gz::msgs::Double_V & response) + { + std::lock_guard lock(mutex); + for (auto v : applied) + { + response.add_data(v); + } + response.add_data(ready ? 1 : 0); + response.add_data(cameras); + response.add_data(failed ? 1 : 0); + return true; } - path=sdf->Get("renderer_library");pieces=sdf->Get("shader_directory"); - if(sdf->HasElement("initial_settings")){ - std::istringstream stream(sdf->Get("initial_settings"));Appearance initial; - for(auto &v:initial)if(!(stream>>v))throw std::runtime_error("invalid_initial_appearance"); - if(!Valid(initial))throw std::runtime_error("invalid_initial_appearance"); - requested=applied=initial; + void PostUpdate( + const gz::sim::UpdateInfo & info, const gz::sim::EntityComponentManager &) override + { + std::lock_guard lock(mutex); + time = std::chrono::duration(info.simTime).count(); } - auto service="/posim/appearance/"+sdf->Get("session_nonce"); - node.Advertise(service+"/set",&Underwater::Set,this); - node.Advertise(service+"/state",&Underwater::State,this); - renderConnection=events.Connect([this]{Render();}); - postRenderConnection=events.Connect([this]{ - std::lock_guard lock(mutex); - if(stagedReady&&!failed){applied=staged;cameras=stagedCameras;ready=cameras>0;stagedReady=false;} - }); - teardownConnection=events.Connect([this]{Close();std::lock_guard lock(mutex);ready=false;}); - } - bool Set(const gz::msgs::Double_V &request,gz::msgs::Double_V &response){ - std::lock_guard lock(mutex); - if(request.data_size()!=13)return false; - Appearance a;std::copy(request.data().begin(),request.data().end(),a.begin()); - if(!Valid(a)||a[1]requested[1]+1||(a[1]==requested[1]&&a!=requested))return false; - requested=a;for(auto v:requested)response.add_data(v);return true; - } - bool State(const gz::msgs::Empty &,gz::msgs::Double_V &response){ - std::lock_guard lock(mutex); - for(auto v:applied)response.add_data(v); - response.add_data(ready?1:0);response.add_data(cameras);response.add_data(failed?1:0);return true; - } - void PostUpdate(const gz::sim::UpdateInfo &info,const gz::sim::EntityComponentManager &) override { - std::lock_guard lock(mutex);time=std::chrono::duration(info.simTime).count(); - } - void Render(){ - if(failed)return; - auto scene=gz::rendering::sceneFromFirstRenderEngine();if(!scene)return; - try{ - if(scene->Engine()->Name()!="ogre2")throw std::runtime_error("underwater_requires_ogre2"); - if(!renderer){ + void Render() + { + if (failed) + { + return; + } + auto scene = gz::rendering::sceneFromFirstRenderEngine(); + if (!scene) + { + return; + } + try + { + if (scene->Engine()->Name() != "ogre2") + { + throw std::runtime_error("underwater_requires_ogre2"); + } + if (!renderer) + { #ifdef _WIN32 - library=LoadLibraryA(path.c_str()); - auto symbol=[this](const char *name){return reinterpret_cast(GetProcAddress(static_cast(library),name));}; + library = LoadLibraryA(path.c_str()); + auto symbol = [this](const char * name) + { return reinterpret_cast(GetProcAddress(static_cast(library), name)); }; #else - library=dlopen(path.c_str(),RTLD_NOW|RTLD_LOCAL); - auto symbol=[this](const char *name){return dlsym(library,name);}; + library = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + auto symbol = [this](const char * name) { return dlsym(library, name); }; #endif - if(!library)throw std::runtime_error("underwater_render_library_unavailable"); - create=reinterpret_cast(symbol("posimCreateUnderwater")); - update=reinterpret_cast(symbol("posimUpdateUnderwater")); - destroy=reinterpret_cast(symbol("posimDestroyUnderwater")); - if(!create||!update||!destroy)throw std::runtime_error("underwater_render_abi_mismatch"); - renderer=create(pieces.c_str()); - } - Appearance a;double t; - {std::lock_guard lock(mutex);a=requested;t=time;} - auto count=update(renderer,scene,a,t); - {std::lock_guard lock(mutex);staged=a;stagedCameras=count;stagedReady=true;} - }catch(const std::exception &error){ - std::lock_guard lock(mutex);failed=true;ready=false;gzerr<<"Underwater disabled: "<(symbol("posimCreateUnderwater")); + update = reinterpret_cast(symbol("posimUpdateUnderwater")); + destroy = reinterpret_cast(symbol("posimDestroyUnderwater")); + if (!create || !update || !destroy) + { + throw std::runtime_error("underwater_render_abi_mismatch"); + } + renderer = create(pieces.c_str()); + } + Appearance a; + double t; + { + std::lock_guard lock(mutex); + a = requested; + t = time; + } + auto count = update(renderer, scene, a, t); + { + std::lock_guard lock(mutex); + staged = a; + stagedCameras = count; + stagedReady = true; + } + } + catch (const std::exception & error) + { + std::lock_guard lock(mutex); + failed = true; + ready = false; + gzerr << "Underwater disabled: " << error.what() << "\n"; + } } - } }; -} -GZ_ADD_PLUGIN(posim::Underwater,gz::sim::System,posim::Underwater::ISystemConfigure,posim::Underwater::ISystemPostUpdate) -GZ_ADD_PLUGIN_ALIAS(posim::Underwater,"posim::Underwater") +} // namespace posim +GZ_ADD_PLUGIN( + posim::Underwater, gz::sim::System, posim::Underwater::ISystemConfigure, + posim::Underwater::ISystemPostUpdate) +GZ_ADD_PLUGIN_ALIAS(posim::Underwater, "posim::Underwater") diff --git a/gazebo/posim_gz_rendering/tests/AppearanceTest.cc b/gazebo/posim_gz_rendering/tests/AppearanceTest.cc index 4c5dfd37..401f5f31 100644 --- a/gazebo/posim_gz_rendering/tests/AppearanceTest.cc +++ b/gazebo/posim_gz_rendering/tests/AppearanceTest.cc @@ -1,9 +1,34 @@ // SPDX-License-Identifier: Apache-2.0 #include "Appearance.hh" -int main(){ - auto a=posim::Defaults();if(!posim::Valid(a))return 1; - for(unsigned i=2;i<=10;++i){auto bad=a;bad[i]=0.5;if(posim::Valid(bad))return 2;} - for(auto bad:{-1.,0.5,9007199254740992.}){a[1]=bad;if(posim::Valid(a))return 3;} - if(posim::WaterPath(-2,-8,10)!=10||posim::WaterPath(2,8,10)!=0||posim::WaterPath(2,-8,10)!=8||posim::WaterPath(-2,8,10)!=2)return 4; - return 0; +int main() +{ + auto a = posim::Defaults(); + if (!posim::Valid(a)) + { + return 1; + } + for (unsigned i = 2; i <= 10; ++i) + { + auto bad = a; + bad[i] = 0.5; + if (posim::Valid(bad)) + { + return 2; + } + } + for (auto bad : {-1., 0.5, 9007199254740992.}) + { + a[1] = bad; + if (posim::Valid(a)) + { + return 3; + } + } + if ( + posim::WaterPath(-2, -8, 10) != 10 || posim::WaterPath(2, 8, 10) != 0 || + posim::WaterPath(2, -8, 10) != 8 || posim::WaterPath(-2, 8, 10) != 2) + { + return 4; + } + return 0; } diff --git a/gazebo/posim_gz_rendering/tests/RenderTest.cc b/gazebo/posim_gz_rendering/tests/RenderTest.cc index 25913a64..73a6753d 100644 --- a/gazebo/posim_gz_rendering/tests/RenderTest.cc +++ b/gazebo/posim_gz_rendering/tests/RenderTest.cc @@ -1,62 +1,171 @@ // SPDX-License-Identifier: Apache-2.0 -#include "UnderwaterRenderer.hh" -#include -#include +#include +#include +#include #include -#include +#include #include -#include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include #include -int main(int argc,char **argv){ - if(argc!=4)return 2; +#include +#include "UnderwaterRenderer.hh" +int main(int argc, char ** argv) +{ + if (argc != 4) + { + return 2; + } #ifdef __APPLE__ - auto engine=gz::rendering::engine("ogre2",{{"metal","1"}}); + auto engine = gz::rendering::engine("ogre2", {{"metal", "1"}}); #else - auto engine=gz::rendering::engine("ogre2",{{"headless","1"}}); + auto engine = gz::rendering::engine("ogre2", {{"headless", "1"}}); #endif - if(!engine)return 3; - auto scene=engine->CreateScene("underwater-test"); - scene->SetAmbientLight(.4,.4,.4);scene->SetBackgroundColor(.01,.03,.04); - auto light=scene->CreateDirectionalLight();light->SetDirection(.3,.3,-1);light->SetDiffuseColor(1,1,1);scene->RootVisual()->AddChild(light); - auto seabed=scene->CreateVisual("seabed");seabed->AddGeometry(scene->CreateBox());seabed->SetLocalScale(100,100,.2);seabed->SetLocalPosition(0,0,-8); - auto sand=scene->CreateMaterial();sand->SetDiffuse(.6,.5,.3);sand->SetRoughness(.9);sand->SetTexture(std::string(argv[3])+"/aerial_beach_01_diff_2k.jpg");sand->SetNormalMap(std::string(argv[3])+"/aerial_beach_01_nor_gl_2k.jpg");seabed->SetMaterial(sand);scene->RootVisual()->AddChild(seabed); - auto target=scene->CreateVisual();target->AddGeometry(scene->CreateBox());target->SetLocalPosition(5,0,-6);auto red=scene->CreateMaterial();red->SetDiffuse(1,.05,.05);target->SetMaterial(red);scene->RootVisual()->AddChild(target); - auto camera=scene->CreateCamera();camera->SetImageWidth(640);camera->SetImageHeight(360);camera->SetHFOV(1.05);camera->SetAspectRatio(640./360);camera->SetLocalPosition(-2,0,-3);camera->SetLocalRotation(0,.3,0);scene->RootVisual()->AddChild(camera); - auto depth=scene->CreateDepthCamera();depth->SetImageWidth(640);depth->SetImageHeight(360);depth->SetHFOV(1.05);depth->SetAspectRatio(640./360);depth->SetNearClipPlane(.1);depth->SetFarClipPlane(100);depth->SetLocalPose(camera->LocalPose());scene->RootVisual()->AddChild(depth); - std::vector measured,baseline;std::vector rgb(640*360*3); - auto connection=depth->ConnectNewDepthFrame([&](const float *data,unsigned w,unsigned h,unsigned,const std::string &){measured.assign(data,data+w*h);}); - auto colors=depth->ConnectNewRgbPointCloud([&](const float *data,unsigned w,unsigned h,unsigned,const std::string &){for(unsigned i=0;i>24)&255;rgb[i*3+1]=(c>>16)&255;rgb[i*3+2]=(c>>8)&255;}}); - camera->Update();depth->Update(); - if(measured.empty())return 4;baseline=measured; - { - posim::UnderwaterRenderer renderer(argv[1]);auto settings=posim::Defaults(); - std::vector untreated,waterOnly; - for(int mode=0;mode<5;++mode){ - settings[2]=mode==0?0:1;settings[4]=mode==2?1:0;settings[10]=mode==3?1:0;settings[3]=mode==4?0:1;settings[11]=5; - renderer.Update(scene,settings,10.); - auto image=camera->CreateImage();for(int i=0;i<3;++i)camera->Capture(image); - std::ofstream file(std::string(argv[2])+"/native-"+std::to_string(mode)+".ppm",std::ios::binary);file<<"P6\n640 360\n255\n";file.write(reinterpret_cast(image.Data()),640*360*3); - depth->Update(); - if(measured.size()!=baseline.size()||std::memcmp(measured.data(),baseline.data(),baseline.size()*sizeof(float)))return 5; - auto pixels=image.Data();unsigned long delta=0,agreement=0; - for(size_t i=0;i0)delta+=std::abs(int(pixels[i])-int(mode==1?untreated[i]:waterOnly[i])); - } - if(agreement>rgb.size()*2)return 6; - if(mode==0)untreated.assign(pixels,pixels+rgb.size()); - if(mode==1)waterOnly.assign(pixels,pixels+rgb.size()); - if(mode>0&&delta<100)return 7; - std::ofstream rgbFile(std::string(argv[2])+"/rgbd-"+std::to_string(mode)+".ppm",std::ios::binary);rgbFile<<"P6\n640 360\n255\n";rgbFile.write(reinterpret_cast(rgb.data()),rgb.size()); - auto start=std::chrono::steady_clock::now();for(int i=0;i<40;++i){camera->Capture(image);depth->Update();} - std::cout<<"mode "<(std::chrono::steady_clock::now()-start).count()/40<<" depth unchanged"<DestroyScene(scene);return 0; + if (!engine) + { + return 3; + } + auto scene = engine->CreateScene("underwater-test"); + scene->SetAmbientLight(.4, .4, .4); + scene->SetBackgroundColor(.01, .03, .04); + auto light = scene->CreateDirectionalLight(); + light->SetDirection(.3, .3, -1); + light->SetDiffuseColor(1, 1, 1); + scene->RootVisual()->AddChild(light); + auto seabed = scene->CreateVisual("seabed"); + seabed->AddGeometry(scene->CreateBox()); + seabed->SetLocalScale(100, 100, .2); + seabed->SetLocalPosition(0, 0, -8); + auto sand = scene->CreateMaterial(); + sand->SetDiffuse(.6, .5, .3); + sand->SetRoughness(.9); + sand->SetTexture(std::string(argv[3]) + "/aerial_beach_01_diff_2k.jpg"); + sand->SetNormalMap(std::string(argv[3]) + "/aerial_beach_01_nor_gl_2k.jpg"); + seabed->SetMaterial(sand); + scene->RootVisual()->AddChild(seabed); + auto target = scene->CreateVisual(); + target->AddGeometry(scene->CreateBox()); + target->SetLocalPosition(5, 0, -6); + auto red = scene->CreateMaterial(); + red->SetDiffuse(1, .05, .05); + target->SetMaterial(red); + scene->RootVisual()->AddChild(target); + auto camera = scene->CreateCamera(); + camera->SetImageWidth(640); + camera->SetImageHeight(360); + camera->SetHFOV(1.05); + camera->SetAspectRatio(640. / 360); + camera->SetLocalPosition(-2, 0, -3); + camera->SetLocalRotation(0, .3, 0); + scene->RootVisual()->AddChild(camera); + auto depth = scene->CreateDepthCamera(); + depth->SetImageWidth(640); + depth->SetImageHeight(360); + depth->SetHFOV(1.05); + depth->SetAspectRatio(640. / 360); + depth->SetNearClipPlane(.1); + depth->SetFarClipPlane(100); + depth->SetLocalPose(camera->LocalPose()); + scene->RootVisual()->AddChild(depth); + std::vector measured, baseline; + std::vector rgb(640 * 360 * 3); + auto connection = depth->ConnectNewDepthFrame( + [&](const float * data, unsigned w, unsigned h, unsigned, const std::string &) + { measured.assign(data, data + w * h); }); + auto colors = depth->ConnectNewRgbPointCloud( + [&](const float * data, unsigned w, unsigned h, unsigned, const std::string &) + { + for (unsigned i = 0; i < w * h; ++i) + { + uint32_t c; + std::memcpy(&c, data + i * 4 + 3, 4); + rgb[i * 3] = (c >> 24) & 255; + rgb[i * 3 + 1] = (c >> 16) & 255; + rgb[i * 3 + 2] = (c >> 8) & 255; + } + }); + camera->Update(); + depth->Update(); + if (measured.empty()) + { + return 4; + } + baseline = measured; + { + posim::UnderwaterRenderer renderer(argv[1]); + auto settings = posim::Defaults(); + std::vector untreated, waterOnly; + for (int mode = 0; mode < 5; ++mode) + { + settings[2] = mode == 0 ? 0 : 1; + settings[4] = mode == 2 ? 1 : 0; + settings[10] = mode == 3 ? 1 : 0; + settings[3] = mode == 4 ? 0 : 1; + settings[11] = 5; + renderer.Update(scene, settings, 10.); + auto image = camera->CreateImage(); + for (int i = 0; i < 3; ++i) + { + camera->Capture(image); + } + std::ofstream file( + std::string(argv[2]) + "/native-" + std::to_string(mode) + ".ppm", std::ios::binary); + file << "P6\n640 360\n255\n"; + file.write(reinterpret_cast(image.Data()), 640 * 360 * 3); + depth->Update(); + if ( + measured.size() != baseline.size() || + std::memcmp(measured.data(), baseline.data(), baseline.size() * sizeof(float))) + { + return 5; + } + auto pixels = image.Data(); + unsigned long delta = 0, agreement = 0; + for (size_t i = 0; i < rgb.size(); ++i) + { + agreement += std::abs(int(pixels[i]) - int(rgb[i])); + if (mode > 0) + { + delta += std::abs(int(pixels[i]) - int(mode == 1 ? untreated[i] : waterOnly[i])); + } + } + if (agreement > rgb.size() * 2) + { + return 6; + } + if (mode == 0) + { + untreated.assign(pixels, pixels + rgb.size()); + } + if (mode == 1) + { + waterOnly.assign(pixels, pixels + rgb.size()); + } + if (mode > 0 && delta < 100) + { + return 7; + } + std::ofstream rgbFile( + std::string(argv[2]) + "/rgbd-" + std::to_string(mode) + ".ppm", std::ios::binary); + rgbFile << "P6\n640 360\n255\n"; + rgbFile.write(reinterpret_cast(rgb.data()), rgb.size()); + auto start = std::chrono::steady_clock::now(); + for (int i = 0; i < 40; ++i) + { + camera->Capture(image); + depth->Update(); + } + std::cout << "mode " << mode << " RGB + RGBD ms " + << std::chrono::duration( + std::chrono::steady_clock::now() - start) + .count() / + 40 + << " depth unchanged" << std::endl; + } + } + engine->DestroyScene(scene); + return 0; } diff --git a/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc b/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc index 0a167823..864c2096 100644 --- a/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc +++ b/gazebo/posim_gz_sensor_plugins/src/UnderwaterCamera.cc @@ -471,7 +471,8 @@ cv::Mat UnderwaterCamera::SimulateUnderwater( { // The scene renderer already applies optics to the raw RGB topic. Preserve // this compatibility output without attenuating the same pixels twice. - if (std::getenv("POSIM_UNDERWATER_PLUGIN")) { + if (std::getenv("POSIM_UNDERWATER_PLUGIN")) + { _inputImage.copyTo(_outputImage); return _outputImage; } From c1daa95f465fc514e7b7a296c496e9376acd2093 Mon Sep 17 00:00:00 2001 From: ioes Date: Wed, 21 Oct 2026 14:21:28 +0900 Subject: [PATCH 4/5] Attenuate underwater sky in RGB and RGBD cameras --- gazebo/posim_gz_rendering/Appearance.hh | 2 +- gazebo/posim_gz_rendering/README.ko.md | 4 +- gazebo/posim_gz_rendering/README.md | 4 +- .../posim_gz_rendering/UnderwaterRenderer.cc | 155 +++++++++++++++++- gazebo/posim_gz_rendering/tests/RenderTest.cc | 60 +++++++ 5 files changed, 219 insertions(+), 6 deletions(-) diff --git a/gazebo/posim_gz_rendering/Appearance.hh b/gazebo/posim_gz_rendering/Appearance.hh index 395e7617..60b6b549 100644 --- a/gazebo/posim_gz_rendering/Appearance.hh +++ b/gazebo/posim_gz_rendering/Appearance.hh @@ -7,7 +7,7 @@ namespace posim { // Stable wire order; schema and revision precede user settings. using Appearance = std::array; -inline Appearance Defaults() { return {1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 50, 5}; } +inline Appearance Defaults() { return {1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 10, 5}; } inline bool Valid(const Appearance & a) { for (auto v : a) diff --git a/gazebo/posim_gz_rendering/README.ko.md b/gazebo/posim_gz_rendering/README.ko.md index a6c180af..2fd47590 100644 --- a/gazebo/posim_gz_rendering/README.ko.md +++ b/gazebo/posim_gz_rendering/README.ko.md @@ -4,7 +4,7 @@ Gazebo Jetty / Ogre2 시스템이 PBR 표면을 그릴 때 수중 경로 안개, RGB 흡수, 수심에 따른 햇빛 감쇠, 움직이는 해저면 햇빛 무늬와 선택 가능한 부유 입자를 적용합니다. RGB 카메라와 RGBD 컬러 영상은 같은 재질 경로를 사용합니다. 수치 깊이와 충돌 형상은 측정값을 유지합니다. Gazebo의 클리핑, 단색 센서 출력과 지형 그림자 기능을 유지합니다. -수면은 z = 0 m 평면으로 근사합니다. 햇빛 무늬와 부유 입자는 계산량이 적은 절차적 근사입니다. 비조명 객체, 사용자 수면 셰이더와 하늘에는 PBR 효과가 적용되지 않습니다. 로봇 조명의 직접 조명은 유지하며, 표면과 카메라 사이의 물을 통과하는 빛은 감쇠합니다. +수면은 z = 0 m 평면으로 근사합니다. 햇빛 무늬와 부유 입자는 계산량이 적은 절차적 근사입니다. 하늘 배경에도 수중 경로 감쇠가 적용되며 깊이 측정은 유지됩니다. 비조명 객체와 사용자 수면 셰이더는 자체 표현을 사용합니다. 로봇 조명의 직접 조명은 유지하며, 표면과 카메라 사이의 물을 통과하는 빛은 감쇠합니다. 서버와 같은 Gazebo/Ogre SDK에서 CMake로 빌드합니다. 시스템 라이브러리는 Gazebo가 렌더링 장면을 만든 뒤 렌더링 보조 라이브러리를 불러옵니다. 라이브러리는 `lib`, 셰이더는 `share/posim/underwater`에 설치합니다. @@ -22,7 +22,7 @@ macOS 라이브러리 확장자는 `.dylib`입니다. Gazebo 프로세스마다 - 기본 켜짐: 광학 효과, 질감, 안개, 색상 흡수, 햇빛 감쇠, 햇빛 무늬. - 기본 꺼짐: 격자선, 등고선, 부유 입자. -- 격자 간격: 기본 50 m, 범위 1–500 m. +- 격자 간격: 기본 10 m, 범위 1–500 m. - 등고선 간격: 기본 5 m, 범위 1–50 m. - 수중 광학 효과를 꺼도 질감과 측정선 선택은 유지됩니다. diff --git a/gazebo/posim_gz_rendering/README.md b/gazebo/posim_gz_rendering/README.md index 20528e05..15c6c4f3 100644 --- a/gazebo/posim_gz_rendering/README.md +++ b/gazebo/posim_gz_rendering/README.md @@ -4,7 +4,7 @@ This Gazebo Jetty / Ogre2 system applies water-path haze, RGB absorption, depth-dependent sunlight, animated seabed caustics and optional suspended specks while PBR surfaces are shaded. RGB cameras and RGBD color use the same material path. Numeric depth and collision geometry retain their measured values. Gazebo's clipping, solid-color sensor output and terrain-shadow hooks are retained. -The water surface is approximated by the plane z = 0 m. Caustics and specks are inexpensive procedural approximations. Unlit objects, custom water shaders and the sky are outside the PBR effect. Local lamps keep their direct illumination; water between a surface and a camera still attenuates that surface's light. +The water surface is approximated by the plane z = 0 m. Caustics and specks are inexpensive procedural approximations. Sky rays use water-path attenuation without changing depth measurements. Unlit objects and custom water shaders retain their own appearance. Local lamps keep their direct illumination; water between a surface and a camera still attenuates that surface's light. Build with CMake against the same Gazebo/Ogre SDK as the server. The system library delays loading its rendering helper until Gazebo creates a rendering scene. Install libraries to `lib` and shader pieces to `share/posim/underwater`. @@ -22,7 +22,7 @@ On macOS the libraries have the `.dylib` suffix. Use one instance per Gazebo pro - Defaults on: optics, texture, haze, absorption, sunlight fading, caustics. - Defaults off: grid, contours, suspended specks. -- Grid: 50 m, adjustable from 1 to 500 m. +- Grid: 10 m, adjustable from 1 to 500 m. - Contours: 5 m, adjustable from 1 to 50 m. - Turning water optics off preserves texture and measurement-overlay selections. diff --git a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc index 9026ce84..91bb5da9 100644 --- a/gazebo/posim_gz_rendering/UnderwaterRenderer.cc +++ b/gazebo/posim_gz_rendering/UnderwaterRenderer.cc @@ -4,20 +4,30 @@ #include #include #include +#include #include #include +#include +#include +#include #include #include +#include #include #include #include #include #include +#include +#include #include #include +#include +#include +#include namespace posim { -struct UnderwaterRenderer::Impl : Ogre::HlmsListener +struct UnderwaterRenderer::Impl : Ogre::HlmsListener, Ogre::SceneManager::Listener { Ogre::Hlms * pbs = nullptr; Ogre::ArchiveVec original; @@ -25,6 +35,114 @@ struct UnderwaterRenderer::Impl : Ogre::HlmsListener Appearance settings = Defaults(); double time = 0; unsigned cameras = 0; + std::map skyPrograms; + Ogre::HighLevelGpuProgramPtr skyProgram; + bool skyDirty = false; + Ogre::SceneManager * manager = nullptr; + void preFindVisibleObjects( + Ogre::SceneManager * source, Ogre::SceneManager::IlluminationRenderStage stage, + Ogre::Viewport *) override + { + if (stage != Ogre::SceneManager::IRS_NONE) + { + return; + } + auto camera = source->getCamerasInProgress().renderingCamera; + if (!camera) + { + return; + } + for (auto & entry : skyPrograms) + { + auto material = Ogre::MaterialManager::getSingleton().getByName(entry.first); + if (material) + { + material->getTechnique(0)->getPass(0)->getFragmentProgramParameters()->setNamedConstant( + "cameraPosition", camera->getDerivedPosition()); + } + } + } + void Sky(Ogre::Material * material) + { + if (!material || !material->getNumTechniques() || !material->getTechnique(0)->getNumPasses()) + { + return; + } + auto pass = material->getTechnique(0)->getPass(0); + if (!pass->hasFragmentProgram() || !pass->getNumTextureUnitStates()) + { + return; + } + if ( + pass->getFragmentProgramName().find("SkyBox") == std::string::npos && + pass->getFragmentProgramName() != "POSIM/UnderwaterSky") + { + return; + } + if (!skyProgram) + { + const bool metal = + Ogre::Root::getSingleton().getRenderSystem()->getName().find("Metal") != std::string::npos; + skyProgram = Ogre::HighLevelGpuProgramManager::getSingleton().createProgram( + "POSIM/UnderwaterSky", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + metal ? "metal" : "glsl", Ogre::GPT_FRAGMENT_PROGRAM); + if (metal) + { + skyProgram->setSource(R"( +#include +using namespace metal; +struct Input { float3 cameraDir; }; +struct Params { float3 cameraPosition; float4 controls; }; +fragment float4 main_metal(Input input [[stage_in]], constant Params &p [[buffer(PARAMETER_SLOT)]], + texturecube sky [[texture(0)]], sampler state [[sampler(0)]]) { + float3 ray=normalize(input.cameraDir); + float3 color=sky.sample(state,float3(ray.xy,-ray.z)).rgb; + float water=p.cameraPosition.z<0.0 ? (ray.z>0.001 ? -p.cameraPosition.z/ray.z : 200.0) : 0.0; + float3 transmission=exp(-water*float3(.18,.09,.06)*p.controls.x); + return float4(color*transmission+float3(.006,.055,.065)*(1.0-transmission),1.0); +})"); + } + else + { + skyProgram->setSource(R"( +#version 330 +uniform samplerCube skyCubemap; +uniform vec3 cameraPosition; +uniform vec4 controls; +in block { vec3 cameraDir; } inPs; +out vec3 fragColour; +void main() { + vec3 ray=normalize(inPs.cameraDir); + vec3 color=texture(skyCubemap,vec3(ray.xy,-ray.z)).rgb; + float water=cameraPosition.z<0.0 ? (ray.z>0.001 ? -cameraPosition.z/ray.z : 200.0) : 0.0; + vec3 transmission=exp(-water*vec3(.18,.09,.06)*controls.x); + fragColour=color*transmission+vec3(.006,.055,.065)*(1.0-transmission); +})"); + } + if (metal) + { + skyProgram->setParameter("entry_point", "main_metal"); + skyProgram->setParameter("shader_reflection_pair_hint", "SkyBox_vs_Metal"); + } + skyProgram->load(); + } + if (!skyPrograms.count(material->getName())) + { + skyPrograms[material->getName()] = pass->getFragmentProgram(); + } + if (pass->getFragmentProgramName() != skyProgram->getName()) + { + pass->setFragmentProgram(skyProgram->getName()); + skyDirty = true; + } + auto parameters = pass->getFragmentProgramParameters(); + parameters->setNamedConstant("cameraPosition", Ogre::Vector3(0, 0, 0)); + parameters->setNamedConstant("controls", Ogre::Vector4(settings[2] * settings[6], 0, 0, 0)); + if (parameters->_findNamedConstantDefinition("skyCubemap", false)) + { + parameters->setNamedConstant("skyCubemap", 0); + } + } explicit Impl(const std::string & pieces) { auto root = gz::rendering::Ogre2RenderEngine::Instance()->OgreRoot(); @@ -54,6 +172,25 @@ struct UnderwaterRenderer::Impl : Ogre::HlmsListener } ~Impl() { + if (manager) + { + manager->removeListener(this); + } + for (auto & entry : skyPrograms) + { + auto material = Ogre::MaterialManager::getSingleton().getByName(entry.first); + if (material) + { + material->getTechnique(0)->getPass(0)->setFragmentProgram(entry.second->getName()); + } + } + if (skyProgram) + { + Ogre::Root::getSingleton() + .getHlmsManager() + ->getHlms(Ogre::HLMS_LOW_LEVEL) + ->_clearShaderCache(); + } if (pbs && pbs->getListener() == this) { pbs->setListener(previous); @@ -162,9 +299,25 @@ unsigned UnderwaterRenderer::Cameras() const { return impl->cameras; } void UnderwaterRenderer::Update( const gz::rendering::ScenePtr & scene, const Appearance & settings, double time) { + if (!impl->manager) + { + auto native = std::dynamic_pointer_cast(scene); + impl->manager = native->OgreSceneManager(); + impl->manager->addListener(impl.get()); + } impl->settings = settings; impl->time = time; impl->cameras = 0; + auto materials = Ogre::MaterialManager::getSingleton().getResourceIterator(); + while (materials.hasMoreElements()) + { + impl->Sky(dynamic_cast(materials.getNext().get())); + } + if (impl->skyDirty) + { + Ogre::Root::getSingleton().getHlmsManager()->getHlms(Ogre::HLMS_LOW_LEVEL)->_clearShaderCache(); + impl->skyDirty = false; + } for (unsigned i = 0; i < scene->SensorCount(); ++i) { if (std::dynamic_pointer_cast(scene->SensorByIndex(i))) diff --git a/gazebo/posim_gz_rendering/tests/RenderTest.cc b/gazebo/posim_gz_rendering/tests/RenderTest.cc index 73a6753d..3dbafd83 100644 --- a/gazebo/posim_gz_rendering/tests/RenderTest.cc +++ b/gazebo/posim_gz_rendering/tests/RenderTest.cc @@ -29,6 +29,7 @@ int main(int argc, char ** argv) return 3; } auto scene = engine->CreateScene("underwater-test"); + scene->SetSkyEnabled(true); scene->SetAmbientLight(.4, .4, .4); scene->SetBackgroundColor(.01, .03, .04); auto light = scene->CreateDirectionalLight(); @@ -165,6 +166,65 @@ int main(int argc, char ** argv) 40 << " depth unchanged" << std::endl; } + seabed->SetVisible(false); + target->SetVisible(false); + camera->SetLocalRotation(0, 0, 0); + depth->SetLocalPose(camera->LocalPose()); + for (int location = 0; location < 2; ++location) + { + camera->SetLocalPosition(-2, 0, location ? 3 : -3); + depth->SetLocalPose(camera->LocalPose()); + std::vector skyOff; + std::vector skyDepth; + for (int enabled = 0; enabled < 2; ++enabled) + { + settings = posim::Defaults(); + settings[2] = enabled; + renderer.Update(scene, settings, 10.); + auto image = camera->CreateImage(); + camera->Capture(image); + camera->Capture(image); + depth->Update(); + auto pixels = image.Data(); + unsigned long skyAgreement = 0; + for (size_t i = 0; i < rgb.size(); ++i) + { + skyAgreement += std::abs(int(pixels[i]) - int(rgb[i])); + } + if (skyAgreement / double(rgb.size()) > 2) + { + return 10; + } + std::ofstream file( + std::string(argv[2]) + "/sky-" + std::to_string(location) + "-" + + std::to_string(enabled) + ".ppm", + std::ios::binary); + file << "P6\n640 360\n255\n"; + file.write(reinterpret_cast(pixels), 640 * 360 * 3); + if (!enabled) + { + skyOff.assign(pixels, pixels + 640 * 360 * 3); + skyDepth = measured; + } + else + { + unsigned long difference = 0; + for (size_t i = 0; i < skyOff.size(); ++i) + { + difference += std::abs(int(pixels[i]) - int(skyOff[i])); + } + std::cout << "sky location " << location << " delta " << difference << std::endl; + if ((location == 0 && difference < 10000) || (location == 1 && difference != 0)) + { + return 8; + } + if (std::memcmp(skyDepth.data(), measured.data(), measured.size() * sizeof(float))) + { + return 9; + } + } + } + } } engine->DestroyScene(scene); return 0; From a600713034e2fefa431f34c0fe8540961e8c0a5b Mon Sep 17 00:00:00 2001 From: ioes Date: Wed, 21 Oct 2026 16:27:49 +0900 Subject: [PATCH 5/5] Align bilingual camera guides with underwater sky attenuation --- website/content/en/camera.md | 2 +- website/content/ko/camera.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/en/camera.md b/website/content/en/camera.md index db0b92f8..41ee12f7 100644 --- a/website/content/en/camera.md +++ b/website/content/en/camera.md @@ -6,7 +6,7 @@ The underwater-camera plugin applies distance-dependent color attenuation to ren The [POSIM underwater renderer](https://github.com/IOES-Lab/POSIM/tree/main/gazebo/posim_gz_rendering) provides shared scene optics for Ogre2 PBR surfaces. It applies water-path haze, color absorption, depth-dependent sunlight and animated seabed light patterns to RGB cameras and RGBD color. Numeric depth remains unchanged. WWW-POSIM exposes live switches for the Gazebo scene and camera feeds. -The renderer approximates mean sea level by z = 0 m. Unlit materials, custom water shaders and the sky use their own rendering. When the host enables the native renderer with `POSIM_UNDERWATER_PLUGIN`, the camera plugin passes its already-colored input through to the compatibility output. +The renderer approximates mean sea level by z = 0 m. Sky rays use underwater path attenuation. Unlit materials and custom water shaders use their own rendering. When the host enables the native renderer with `POSIM_UNDERWATER_PLUGIN`, the camera plugin passes its already-colored input through to the compatibility output. ```bash ros2 launch posim_demos posim_sensor.launch.py \ diff --git a/website/content/ko/camera.md b/website/content/ko/camera.md index dd5d062c..f65ec23f 100644 --- a/website/content/ko/camera.md +++ b/website/content/ko/camera.md @@ -6,7 +6,7 @@ [POSIM 수중 렌더러](https://github.com/IOES-Lab/POSIM/tree/main/gazebo/posim_gz_rendering)는 Ogre2 PBR 표면의 광학 효과를 공유합니다. 수중 경로 안개, 색상 흡수, 수심에 따른 햇빛 감쇠와 움직이는 해저면 햇빛 무늬를 RGB 카메라와 RGBD 컬러 영상에 적용합니다. 수치 깊이는 유지합니다. WWW-POSIM에서는 Gazebo 장면과 카메라 영상에 적용할 효과를 실시간 스위치로 바꿉니다. -렌더러는 평균 해수면을 z = 0 m로 근사합니다. 비조명 재질·사용자 수면 셰이더·하늘은 자체 렌더링을 사용합니다. 호스트에서 `POSIM_UNDERWATER_PLUGIN`으로 네이티브 렌더러를 활성화하면 카메라 플러그인은 이미 색상이 적용된 입력을 호환 출력으로 그대로 전달합니다. +렌더러는 평균 해수면을 z = 0 m로 근사합니다. 하늘에도 수중 경로 감쇠를 적용합니다. 비조명 재질과 사용자 수면 셰이더는 자체 렌더링을 사용합니다. 호스트에서 `POSIM_UNDERWATER_PLUGIN`으로 네이티브 렌더러를 활성화하면 카메라 플러그인은 이미 색상이 적용된 입력을 호환 출력으로 그대로 전달합니다. ```bash ros2 launch posim_demos posim_sensor.launch.py \